def publish(self, repository_name, username, password): if repository_name: self._io.writeln( "Publishing <info>{}</info> (<comment>{}</comment>) " "to <fg=cyan>{}</>".format( self._package.pretty_name, self._package.pretty_version, repository_name, )) else: self._io.writeln( "Publishing <info>{}</info> (<comment>{}</comment>) " "to <fg=cyan>PyPI</>".format(self._package.pretty_name, self._package.pretty_version)) if not repository_name: url = "https://upload.pypi.org/legacy/" repository_name = "pypi" else: # Retrieving config information config_file = TomlFile(Path(CONFIG_DIR) / "config.toml") if not config_file.exists(): raise RuntimeError("Config file does not exist. " "Unable to get repository information") config = config_file.read(raw=True) if ("repositories" not in config or repository_name not in config["repositories"]): raise RuntimeError( "Repository {} is not defined".format(repository_name)) url = config["repositories"][repository_name]["url"] if not (username and password): auth_file = TomlFile(Path(CONFIG_DIR) / "auth.toml") if auth_file.exists(): auth_config = auth_file.read(raw=True) if ("http-basic" in auth_config and repository_name in auth_config["http-basic"]): config = auth_config["http-basic"][repository_name] username = config.get("username") password = config.get("password") # Requesting missing credentials if not username: username = self._io.ask("Username:"******"Password:") # TODO: handle certificates self._uploader.auth(username, password) return self._uploader.upload(url)
def test_activate_activates_non_existing_virtualenv_no_envs_file( mocker, tester, venv_cache, venv_name, venvs_in_cache_config): mocker.patch( "poetry.utils._compat.subprocess.check_output", side_effect=check_output_wrapper(), ) mock_build_env = mocker.patch("poetry.utils.env.EnvManager.build_venv", side_effect=build_venv) tester.execute("3.7") venv_py37 = venv_cache / "{}-py3.7".format(venv_name) mock_build_env.assert_called_with(venv_py37, executable="python3.7") envs_file = TomlFile(venv_cache / "envs.toml") assert envs_file.exists() envs = envs_file.read() assert envs[venv_name]["minor"] == "3.7" assert envs[venv_name]["patch"] == "3.7.1" expected = """\ Creating virtualenv {} in {} Using virtualenv: {} """.format( venv_py37.name, venv_py37.parent, venv_py37, ) assert expected == tester.io.fetch_output()
def test_activate_activates_existing_virtualenv_no_envs_file( tmp_dir, manager, poetry, config, mocker ): if "VIRTUAL_ENV" in os.environ: del os.environ["VIRTUAL_ENV"] venv_name = manager.generate_env_name("simple-project", str(poetry.file.parent)) os.mkdir(os.path.join(tmp_dir, "{}-py3.7".format(venv_name))) config.merge({"virtualenvs": {"path": str(tmp_dir)}}) mocker.patch( "poetry.utils._compat.subprocess.check_output", side_effect=check_output_wrapper(), ) mocker.patch( "poetry.utils._compat.subprocess.Popen.communicate", side_effect=[("/prefix", None)], ) m = mocker.patch("poetry.utils.env.EnvManager.build_venv", side_effect=build_venv) env = manager.activate("python3.7", NullIO()) m.assert_not_called() envs_file = TomlFile(Path(tmp_dir) / "envs.toml") assert envs_file.exists() envs = envs_file.read() assert envs[venv_name]["minor"] == "3.7" assert envs[venv_name]["patch"] == "3.7.1" assert env.path == Path(tmp_dir) / "{}-py3.7".format(venv_name) assert env.base == Path("/prefix")
def get(self, cwd, reload=False): # type: (Path, bool) -> Env if self._env is not None and not reload: return self._env python_minor = ".".join([str(v) for v in sys.version_info[:2]]) venv_path = self._config.setting("settings.virtualenvs.path") if venv_path is None: venv_path = Path(CACHE_DIR) / "virtualenvs" else: venv_path = Path(venv_path) envs_file = TomlFile(venv_path / self.ENVS_FILE) env = None base_env_name = self.generate_env_name(cwd.name, str(cwd)) if envs_file.exists(): envs = envs_file.read() env = envs.get(base_env_name) if env: python_minor = env["minor"] # Check if we are inside a virtualenv or not in_venv = os.environ.get("VIRTUAL_ENV") is not None if not in_venv or env is not None: # Checking if a local virtualenv exists if (cwd / ".venv").exists() and (cwd / ".venv").is_dir(): venv = cwd / ".venv" return VirtualEnv(venv) create_venv = self._config.setting("settings.virtualenvs.create", True) if not create_venv: return SystemEnv(Path(sys.prefix)) venv_path = self._config.setting("settings.virtualenvs.path") if venv_path is None: venv_path = Path(CACHE_DIR) / "virtualenvs" else: venv_path = Path(venv_path) name = "{}-py{}".format(base_env_name, python_minor.strip()) venv = venv_path / name if not venv.exists(): return SystemEnv(Path(sys.prefix)) return VirtualEnv(venv) if os.environ.get("VIRTUAL_ENV") is not None: prefix = Path(os.environ["VIRTUAL_ENV"]) base_prefix = None else: prefix = Path(sys.prefix) base_prefix = self.get_base_prefix() return VirtualEnv(prefix, base_prefix)
def test_pyproject_parsing(fixture): f = TomlFile(fixture.with_name("pyproject.toml")) content = f.read() assert "dependencies" in content["tool"]["poetry"] assert content["tool"]["poetry"]["dependencies"]["python"] == "^3.6"
def test_pyproject_parsing(fixture): f = TomlFile(fixture.with_name('pyproject.toml')) content = f.read() assert 'dependencies' in content['tool']['poetry'] assert content['tool']['poetry']['dependencies']['python'] == '^3.6'
def test_remove_also_deactivates(tmp_dir, config, mocker): config.add_property("settings.virtualenvs.path", str(tmp_dir)) venv_name = EnvManager.generate_env_name("simple_project", str(CWD)) (Path(tmp_dir) / "{}-py3.7".format(venv_name)).mkdir() (Path(tmp_dir) / "{}-py3.6".format(venv_name)).mkdir() mocker.patch( "poetry.utils._compat.subprocess.check_output", side_effect=check_output_wrapper(Version.parse("3.6.6")), ) envs_file = TomlFile(Path(tmp_dir) / "envs.toml") doc = tomlkit.document() doc[venv_name] = {"minor": "3.6", "patch": "3.6.6"} envs_file.write(doc) manager = EnvManager(config) venv = manager.remove("python3.6", CWD) assert (Path(tmp_dir) / "{}-py3.6".format(venv_name)) == venv.path assert not (Path(tmp_dir) / "{}-py3.6".format(venv_name)).exists() envs = envs_file.read() assert venv_name not in envs
def test_activate_activates_non_existing_virtualenv_no_envs_file( tmp_dir, config, mocker): if "VIRTUAL_ENV" in os.environ: del os.environ["VIRTUAL_ENV"] config.add_property("settings.virtualenvs.path", str(tmp_dir)) mocker.patch( "poetry.utils._compat.subprocess.check_output", side_effect=check_output_wrapper(), ) mocker.patch( "poetry.utils._compat.subprocess.Popen.communicate", side_effect=[("/prefix", None), ("/prefix", None)], ) m = mocker.patch("poetry.utils.env.EnvManager.build_venv", side_effect=build_venv) env = EnvManager(config).activate("python3.7", CWD, NullIO()) venv_name = EnvManager.generate_env_name("simple_project", str(CWD)) m.assert_called_with(os.path.join(tmp_dir, "{}-py3.7".format(venv_name)), executable="python3.7") envs_file = TomlFile(Path(tmp_dir) / "envs.toml") assert envs_file.exists() envs = envs_file.read() assert envs[venv_name]["minor"] == "3.7" assert envs[venv_name]["patch"] == "3.7.1" assert env.path == Path(tmp_dir) / "{}-py3.7".format(venv_name) assert env.base == Path("/prefix")
def install_directory(self, package): from poetry.io import NullIO from poetry.masonry.builder import SdistBuilder from poetry.poetry import Poetry from poetry.utils._compat import decode from poetry.utils.env import NullEnv from poetry.utils.toml_file import TomlFile if package.root_dir: req = os.path.join(package.root_dir, package.source_url) else: req = os.path.realpath(package.source_url) args = ["install", "--no-deps", "-U"] pyproject = TomlFile(os.path.join(req, "pyproject.toml")) has_poetry = False has_build_system = False if pyproject.exists(): pyproject_content = pyproject.read() has_poetry = ("tool" in pyproject_content and "poetry" in pyproject_content["tool"]) # Even if there is a build system specified # pip as of right now does not support it fully # TODO: Check for pip version when proper PEP-517 support lands # has_build_system = ("build-system" in pyproject_content) setup = os.path.join(req, "setup.py") has_setup = os.path.exists(setup) if not has_setup and has_poetry and (package.develop or not has_build_system): # We actually need to rely on creating a temporary setup.py # file since pip, as of this comment, does not support # build-system for editable packages # We also need it for non-PEP-517 packages builder = SdistBuilder(Poetry.create(pyproject.parent), NullEnv(), NullIO()) with open(setup, "w", encoding="utf-8") as f: f.write(decode(builder.build_setup())) if package.develop: args.append("-e") args.append(req) if self._vendor_path: args += ["-t", self._vendor_path] try: return self.run(*args) finally: if not has_setup and os.path.exists(setup): os.remove(setup)
def test_activate_activates_recreates_for_different_patch( tmp_dir, manager, poetry, config, mocker ): if "VIRTUAL_ENV" in os.environ: del os.environ["VIRTUAL_ENV"] venv_name = manager.generate_env_name("simple-project", str(poetry.file.parent)) envs_file = TomlFile(Path(tmp_dir) / "envs.toml") doc = tomlkit.document() doc[venv_name] = {"minor": "3.7", "patch": "3.7.0"} envs_file.write(doc) os.mkdir(os.path.join(tmp_dir, "{}-py3.7".format(venv_name))) config.merge({"virtualenvs": {"path": str(tmp_dir)}}) mocker.patch( "poetry.utils._compat.subprocess.check_output", side_effect=check_output_wrapper(), ) mocker.patch( "poetry.utils._compat.subprocess.Popen.communicate", side_effect=[ ("/prefix", None), ('{"version_info": [3, 7, 0]}', None), ("/prefix", None), ("/prefix", None), ("/prefix", None), ], ) build_venv_m = mocker.patch( "poetry.utils.env.EnvManager.build_venv", side_effect=build_venv ) remove_venv_m = mocker.patch( "poetry.utils.env.EnvManager.remove_venv", side_effect=remove_venv ) env = manager.activate("python3.7", NullIO()) build_venv_m.assert_called_with( os.path.join(tmp_dir, "{}-py3.7".format(venv_name)), executable="python3.7" ) remove_venv_m.assert_called_with( os.path.join(tmp_dir, "{}-py3.7".format(venv_name)) ) assert envs_file.exists() envs = envs_file.read() assert envs[venv_name]["minor"] == "3.7" assert envs[venv_name]["patch"] == "3.7.1" assert env.path == Path(tmp_dir) / "{}-py3.7".format(venv_name) assert env.base == Path("/prefix") assert (Path(tmp_dir) / "{}-py3.7".format(venv_name)).exists()
def install_directory(self, package): from poetry.factory import Factory from poetry.utils.toml_file import TomlFile if package.root_dir: req = (package.root_dir / package.source_url).as_posix() else: req = os.path.realpath(package.source_url) args = ["install", "--no-deps", "-U"] pyproject = TomlFile(os.path.join(req, "pyproject.toml")) has_poetry = False has_build_system = False if pyproject.exists(): pyproject_content = pyproject.read() has_poetry = ( "tool" in pyproject_content and "poetry" in pyproject_content["tool"] ) # Even if there is a build system specified # pip as of right now does not support it fully # TODO: Check for pip version when proper PEP-517 support lands # has_build_system = ("build-system" in pyproject_content) setup = os.path.join(req, "setup.py") has_setup = os.path.exists(setup) if has_poetry and (package.develop or not has_build_system): # We actually need to rely on creating a temporary setup.py # file since pip, as of this comment, does not support # build-system for editable packages # We also need it for non-PEP-517 packages from poetry.masonry.builders.editable import EditableBuilder builder = EditableBuilder( Factory().create_poetry(pyproject.parent), self._env, NullIO() ) builder.build() return if package.develop: args.append("-e") args.append(req) try: return self.run(*args) finally: if not has_setup and os.path.exists(setup): os.remove(setup)
def test_validate_fails(): complete = TomlFile(fixtures_dir / "complete.toml") content = complete.read()["tool"]["poetry"] content["this key is not in the schema"] = "" if PY2: expected = ("Additional properties are not allowed " "(u'this key is not in the schema' was unexpected)") else: expected = ("Additional properties are not allowed " "('this key is not in the schema' was unexpected)") assert Factory.validate(content) == {"errors": [expected], "warnings": []}
def _get_poetry_package(path): # type: (Path) -> Optional[ProjectPackage] pyproject = path.joinpath("pyproject.toml") try: # Note: we ignore any setup.py file at this step if pyproject.exists(): # TODO: add support for handling non-poetry PEP-517 builds pyproject = TomlFile(pyproject) pyproject_content = pyproject.read() supports_poetry = ("tool" in pyproject_content and "poetry" in pyproject_content["tool"]) if supports_poetry: return Factory().create_poetry(path).package except RuntimeError: pass
def install_directory(self, package, update=False): from poetry.io import NullIO from poetry.masonry.builder import SdistBuilder from poetry.poetry import Poetry from poetry.utils._compat import decode from poetry.utils.env import NullEnv from poetry.utils.toml_file import TomlFile if package.root_dir: req = os.path.join(package.root_dir, package.source_url) else: req = os.path.realpath(package.source_url) args = ["install", "--no-deps", "-U"] pyproject = TomlFile(os.path.join(req, "pyproject.toml")) has_poetry = False if pyproject.exists(): pyproject_content = pyproject.read() has_poetry = ("tool" in pyproject_content and "poetry" in pyproject_content["tool"]) has_build_system = "build-system" in pyproject_content setup = os.path.join(req, "setup.py") has_setup = os.path.exists(setup) if not has_setup and has_poetry and (package.develop or not has_build_system): # We actually need to rely on creating a temporary setup.py # file since pip, as of this comment, does not support # build-system for editable packages # We also need it for non-PEP-517 packages builder = SdistBuilder(Poetry.create(pyproject.parent), NullEnv(), NullIO()) with open(setup, "w") as f: f.write(decode(builder.build_setup())) if package.develop: args.append("-e") args.append(req) try: return self.run(*args) finally: if not has_setup and os.path.exists(setup): os.remove(setup)
def test_activate_activates_non_existing_virtualenv_no_envs_file( app, tmp_dir, config, mocker): app.poetry._config = config if "VIRTUAL_ENV" in os.environ: del os.environ["VIRTUAL_ENV"] config.add_property("settings.virtualenvs.path", str(tmp_dir)) mocker.patch( "poetry.utils._compat.subprocess.check_output", side_effect=check_output_wrapper(), ) mocker.patch( "poetry.utils._compat.subprocess.Popen.communicate", side_effect=[("/prefix", None), ("/prefix", None)], ) m = mocker.patch("poetry.utils.env.EnvManager.build_venv", side_effect=build_venv) command = app.find("env use") tester = CommandTester(command) tester.execute("3.7") venv_name = EnvManager.generate_env_name("simple_project", str(app.poetry.file.parent)) m.assert_called_with(os.path.join(tmp_dir, "{}-py3.7".format(venv_name)), executable="python3.7") envs_file = TomlFile(Path(tmp_dir) / "envs.toml") assert envs_file.exists() envs = envs_file.read() assert envs[venv_name]["minor"] == "3.7" assert envs[venv_name]["patch"] == "3.7.1" expected = """\ Creating virtualenv {} in {} Using virtualenv: {} """.format( "{}-py3.7".format(venv_name), tmp_dir, os.path.join(tmp_dir, "{}-py3.7".format(venv_name)), ) assert expected == tester.io.fetch_output()
def test_activate_does_not_recreate_when_switching_minor(tmp_dir, config, mocker): if "VIRTUAL_ENV" in os.environ: del os.environ["VIRTUAL_ENV"] venv_name = EnvManager.generate_env_name("simple_project", str(CWD)) envs_file = TomlFile(Path(tmp_dir) / "envs.toml") doc = tomlkit.document() doc[venv_name] = {"minor": "3.7", "patch": "3.7.0"} envs_file.write(doc) os.mkdir(os.path.join(tmp_dir, "{}-py3.7".format(venv_name))) os.mkdir(os.path.join(tmp_dir, "{}-py3.6".format(venv_name))) config.merge({"virtualenvs": {"path": str(tmp_dir)}}) mocker.patch( "poetry.utils._compat.subprocess.check_output", side_effect=check_output_wrapper(Version.parse("3.6.6")), ) mocker.patch( "poetry.utils._compat.subprocess.Popen.communicate", side_effect=[("/prefix", None), ("/prefix", None), ("/prefix", None)], ) build_venv_m = mocker.patch( "poetry.utils.env.EnvManager.build_venv", side_effect=build_venv ) remove_venv_m = mocker.patch( "poetry.utils.env.EnvManager.remove_venv", side_effect=remove_venv ) env = EnvManager(config).activate("python3.6", CWD, NullIO()) build_venv_m.assert_not_called() remove_venv_m.assert_not_called() assert envs_file.exists() envs = envs_file.read() assert envs[venv_name]["minor"] == "3.6" assert envs[venv_name]["patch"] == "3.6.6" assert env.path == Path(tmp_dir) / "{}-py3.6".format(venv_name) assert env.base == Path("/prefix") assert (Path(tmp_dir) / "{}-py3.6".format(venv_name)).exists()
def __init__( self, name, path, # type: Path category="main", # type: str optional=False, # type: bool base=None, # type: Path develop=True, # type: bool ): self._path = path self._base = base self._full_path = path self._develop = develop self._supports_poetry = False if self._base and not self._path.is_absolute(): self._full_path = self._base / self._path if not self._full_path.exists(): raise ValueError("Directory {} does not exist".format(self._path)) if self._full_path.is_file(): raise ValueError("{} is a file, expected a directory".format( self._path)) # Checking content to determine actions setup = self._full_path / "setup.py" pyproject = TomlFile(self._full_path / "pyproject.toml") if pyproject.exists(): pyproject_content = pyproject.read() self._supports_poetry = ("tool" in pyproject_content and "poetry" in pyproject_content["tool"]) if not setup.exists() and not self._supports_poetry: raise ValueError( "Directory {} does not seem to be a Python package".format( self._full_path)) super(DirectoryDependency, self).__init__(name, "*", category=category, optional=optional, allows_prereleases=True)
def deactivate(self, io): # type: (IO) -> None venv_path = self._poetry.config.get("virtualenvs.path") if venv_path is None: venv_path = Path(CACHE_DIR) / "virtualenvs" else: venv_path = Path(venv_path) name = self._poetry.package.name name = self.generate_env_name(name, str(self._poetry.file.parent)) envs_file = TomlFile(venv_path / self.ENVS_FILE) if envs_file.exists(): envs = envs_file.read() env = envs.get(name) if env is not None: io.write_line( "Deactivating virtualenv: <comment>{}</comment>".format( venv_path / (name + "-py{}".format(env["minor"])))) del envs[name] envs_file.write(envs)
def deactivate(self, cwd, io): # type: (Optional[Path], IO) -> None venv_path = self._config.setting("settings.virtualenvs.path") if venv_path is None: venv_path = Path(CACHE_DIR) / "virtualenvs" else: venv_path = Path(venv_path) name = cwd.name name = self.generate_env_name(name, str(cwd)) envs_file = TomlFile(venv_path / self.ENVS_FILE) if envs_file.exists(): envs = envs_file.read() env = envs.get(name) if env is not None: io.write_line( "Deactivating virtualenv: <comment>{}</comment>".format( venv_path / (name + "-py{}".format(env["minor"])))) del envs[name] envs_file.write(envs)
def test_deactivate_activated(tmp_dir, manager, poetry, config, mocker): if "VIRTUAL_ENV" in os.environ: del os.environ["VIRTUAL_ENV"] venv_name = manager.generate_env_name("simple-project", str(poetry.file.parent)) version = Version.parse(".".join(str(c) for c in sys.version_info[:3])) other_version = Version.parse("3.4") if version.major == 2 else version.next_minor ( Path(tmp_dir) / "{}-py{}.{}".format(venv_name, version.major, version.minor) ).mkdir() ( Path(tmp_dir) / "{}-py{}.{}".format(venv_name, other_version.major, other_version.minor) ).mkdir() envs_file = TomlFile(Path(tmp_dir) / "envs.toml") doc = tomlkit.document() doc[venv_name] = { "minor": "{}.{}".format(other_version.major, other_version.minor), "patch": other_version.text, } envs_file.write(doc) config.merge({"virtualenvs": {"path": str(tmp_dir)}}) mocker.patch( "poetry.utils._compat.subprocess.check_output", side_effect=check_output_wrapper(), ) manager.deactivate(NullIO()) env = manager.get() assert env.path == Path(tmp_dir) / "{}-py{}.{}".format( venv_name, version.major, version.minor ) assert Path("/prefix") envs = envs_file.read() assert len(envs) == 0
def test_toml_file(fixture): f = TomlFile(fixture) content = f.read() assert content[""]["title"] == "TOML Example" assert content["owner"]["name"] == "Tom Preston-Werner" assert isinstance(content["owner"], dict) assert isinstance(content["database"]["ports"], list) assert content["database"]["ports"] == [8001, 8001, 8002] assert content["database"]["connection_max"] == 5000 assert content["database"]["enabled"] servers = content["servers"] assert len(servers) == 2 alpha = servers["alpha"] assert len(alpha) == 2 assert alpha["ip"] == "10.0.0.1" assert alpha["dc"] == "eqdc10" beta = servers["beta"] assert len(beta) == 2 assert beta["ip"] == "10.0.0.2" assert beta["dc"] == "eqdc10" clients = content["clients"] assert len(clients["data"]) == 2 assert clients["data"] == [["gamma", "delta"], [1, 2]] assert clients["hosts"] == ["alpha", "omega"] assert clients["str_multiline"] == "Roses are red\nViolets are blue" fruits = content["fruit"] assert len(fruits) == 2 apple = fruits[0] assert len(apple) == 3 banana = fruits[1] assert len(banana["variety"][0]["points"]) == 3
def test_toml_file(fixture): f = TomlFile(fixture) content = f.read() assert content['']['title'] == 'TOML Example' assert content['owner']['name'] == 'Tom Preston-Werner' assert isinstance(content['owner'], dict) assert isinstance(content['database']['ports'], list) assert content['database']['ports'] == [8001, 8001, 8002] assert content['database']['connection_max'] == 5000 assert content['database']['enabled'] servers = content['servers'] assert len(servers) == 2 alpha = servers['alpha'] assert len(alpha) == 2 assert alpha['ip'] == '10.0.0.1' assert alpha['dc'] == 'eqdc10' beta = servers['beta'] assert len(beta) == 2 assert beta['ip'] == '10.0.0.2' assert beta['dc'] == 'eqdc10' clients = content['clients'] assert len(clients['data']) == 2 assert clients['data'] == [['gamma', 'delta'], [1, 2]] assert clients['hosts'] == ['alpha', 'omega'] assert clients['str_multiline'] == 'Roses are red\nViolets are blue' fruits = content['fruit'] assert len(fruits) == 2 apple = fruits[0] assert len(apple) == 3 banana = fruits[1] assert len(banana['variety'][0]['points']) == 3
def test_activate_activates_same_virtualenv_with_envs_file( tmp_dir, config, mocker): if "VIRTUAL_ENV" in os.environ: del os.environ["VIRTUAL_ENV"] venv_name = EnvManager.generate_env_name("simple_project", str(CWD)) envs_file = TomlFile(Path(tmp_dir) / "envs.toml") doc = tomlkit.document() doc[venv_name] = {"minor": "3.7", "patch": "3.7.1"} envs_file.write(doc) os.mkdir(os.path.join(tmp_dir, "{}-py3.7".format(venv_name))) config.add_property("settings.virtualenvs.path", str(tmp_dir)) mocker.patch( "poetry.utils._compat.subprocess.check_output", side_effect=check_output_wrapper(), ) mocker.patch( "poetry.utils._compat.subprocess.Popen.communicate", side_effect=[("/prefix", None)], ) m = mocker.patch("poetry.utils.env.EnvManager.create_venv") env = EnvManager(config).activate("python3.7", CWD, NullIO()) m.assert_not_called() assert envs_file.exists() envs = envs_file.read() assert envs[venv_name]["minor"] == "3.7" assert envs[venv_name]["patch"] == "3.7.1" assert env.path == Path(tmp_dir) / "{}-py3.7".format(venv_name) assert env.base == Path("/prefix")
def search_for_vcs(self, dependency): # type: (VCSDependency) -> List[Package] """ Search for the specifications that match the given VCS dependency. Basically, we clone the repository in a temporary directory and get the information we need by checking out the specified reference. """ if dependency.vcs != "git": raise ValueError("Unsupported VCS dependency {}".format( dependency.vcs)) tmp_dir = Path( mkdtemp(prefix="pypoetry-git-{}".format(dependency.name))) try: git = Git() git.clone(dependency.source, tmp_dir) git.checkout(dependency.reference, tmp_dir) revision = git.rev_parse(dependency.reference, tmp_dir).strip() if dependency.tag or dependency.rev: revision = dependency.reference pyproject = TomlFile(tmp_dir / "pyproject.toml") pyproject_content = None has_poetry = False if pyproject.exists(): pyproject_content = pyproject.read() has_poetry = ("tool" in pyproject_content and "poetry" in pyproject_content["tool"]) if pyproject_content and has_poetry: # If a pyproject.toml file exists # We use it to get the information we need info = pyproject_content["tool"]["poetry"] name = info["name"] version = info["version"] package = Package(name, version, version) package.source_type = dependency.vcs package.source_url = dependency.source package.source_reference = dependency.reference for req_name, req_constraint in info["dependencies"].items(): if req_name == "python": package.python_versions = req_constraint continue package.add_dependency(req_name, req_constraint) else: # We need to use setup.py here # to figure the information we need # We need to place ourselves in the proper # folder for it to work venv = Venv.create(self._io) current_dir = os.getcwd() os.chdir(tmp_dir.as_posix()) try: venv.run("python", "setup.py", "egg_info") # Sometimes pathlib will fail on recursive # symbolic links, so we need to workaround it # and use the glob module instead. # Note that this does not happen with pathlib2 # so it's safe to use it for Python < 3.4. if PY35: egg_info = next( Path(p) for p in glob.glob( os.path.join(str(tmp_dir), "**", "*.egg-info"), recursive=True, )) else: egg_info = next(tmp_dir.glob("**/*.egg-info")) meta = pkginfo.UnpackedSDist(str(egg_info)) if meta.requires_dist: reqs = list(meta.requires_dist) else: reqs = [] requires = egg_info / "requires.txt" if requires.exists(): with requires.open() as f: reqs = parse_requires(f.read()) package = Package(meta.name, meta.version) for req in reqs: dep = dependency_from_pep_508(req) if dep.in_extras: for extra in dep.in_extras: if extra not in package.extras: package.extras[extra] = [] package.extras[extra].append(dep) package.requires.append(dep) except Exception: raise finally: os.chdir(current_dir) package.source_type = "git" package.source_url = dependency.source package.source_reference = revision except Exception: raise finally: shutil.rmtree(tmp_dir.as_posix()) if dependency.name != package.name: # For now, the dependency's name must match the actual package's name raise RuntimeError( "The dependency name for {} does not match the actual package's name: {}" .format(dependency.name, package.name)) if dependency.extras: for extra in dependency.extras: if extra in package.extras: for dep in package.extras[extra]: dep.activate() return [package]
def search_for_vcs(self, dependency): # type: (VCSDependency) -> List[Package] """ Search for the specifications that match the given VCS dependency. Basically, we clone the repository in a temporary directory and get the information we need by checking out the specified reference. """ if dependency.vcs != 'git': raise ValueError('Unsupported VCS dependency {}'.format( dependency.vcs)) tmp_dir = Path( mkdtemp(prefix='pypoetry-git-{}'.format(dependency.name))) try: git = Git() git.clone(dependency.source, tmp_dir) git.checkout(dependency.reference, tmp_dir) revision = git.rev_parse(dependency.reference, tmp_dir).strip() if dependency.tag or dependency.rev: revision = dependency.reference pyproject = TomlFile(tmp_dir / 'pyproject.toml') pyproject_content = None has_poetry = False if pyproject.exists(): pyproject_content = pyproject.read(True) has_poetry = ('tool' in pyproject_content and 'poetry' in pyproject_content['tool']) if pyproject_content and has_poetry: # If a pyproject.toml file exists # We use it to get the information we need info = pyproject_content['tool']['poetry'] name = info['name'] version = info['version'] package = Package(name, version, version) package.source_type = dependency.vcs package.source_url = dependency.source package.source_reference = dependency.reference for req_name, req_constraint in info['dependencies'].items(): if req_name == 'python': package.python_versions = req_constraint continue package.add_dependency(req_name, req_constraint) else: # We need to use setup.py here # to figure the information we need # We need to place ourselves in the proper # folder for it to work venv = Venv.create(self._io) current_dir = os.getcwd() os.chdir(tmp_dir.as_posix()) try: venv.run('python', 'setup.py', 'egg_info') egg_info = list(tmp_dir.glob('*.egg-info'))[0] meta = pkginfo.UnpackedSDist(str(egg_info)) if meta.requires_dist: reqs = list(meta.requires_dist) else: reqs = [] requires = egg_info / 'requires.txt' if requires.exists(): with requires.open() as f: reqs = parse_requires(f.read()) package = Package(meta.name, meta.version) for req in reqs: package.requires.append(dependency_from_pep_508(req)) except Exception: raise finally: os.chdir(current_dir) package.source_type = 'git' package.source_url = dependency.source package.source_reference = revision except Exception: raise finally: shutil.rmtree(tmp_dir.as_posix()) return [package]
def _install_directory(self, operation): from poetry.factory import Factory from poetry.utils.toml_file import TomlFile package = operation.package operation_message = self.get_operation_message(operation) message = " <fg=blue;options=bold>•</> {message}: <info>Building...</info>".format( message=operation_message, ) self._write(operation, message) if package.root_dir: req = os.path.join(str(package.root_dir), package.source_url) else: req = os.path.realpath(package.source_url) args = ["install", "--no-deps", "-U"] pyproject = TomlFile(os.path.join(req, "pyproject.toml")) has_poetry = False has_build_system = False if pyproject.exists(): pyproject_content = pyproject.read() has_poetry = ( "tool" in pyproject_content and "poetry" in pyproject_content["tool"] ) # Even if there is a build system specified # some versions of pip (< 19.0.0) don't understand it # so we need to check the version of pip to know # if we can rely on the build system pip_version = self._env.pip_version pip_version_with_build_system_support = pip_version.__class__(19, 0, 0) has_build_system = ( "build-system" in pyproject_content and pip_version >= pip_version_with_build_system_support ) if has_poetry: package_poetry = Factory().create_poetry(pyproject.parent) if package.develop and not package_poetry.package.build_script: from poetry.masonry.builders.editable import EditableBuilder # This is a Poetry package in editable mode # we can use the EditableBuilder without going through pip # to install it, unless it has a build script. builder = EditableBuilder(package_poetry, self._env, NullIO()) builder.build() return 0 elif not has_build_system or package_poetry.package.build_script: from poetry.core.masonry.builders.sdist import SdistBuilder # We need to rely on creating a temporary setup.py # file since the version of pip does not support # build-systems # We also need it for non-PEP-517 packages builder = SdistBuilder(package_poetry) with builder.setup_py(): if package.develop: args.append("-e") args.append(req) return self.run_pip(*args) if package.develop: args.append("-e") args.append(req) return self.run_pip(*args)
def handle(self): from poetry.config.file_config_source import FileConfigSource from poetry.locations import CONFIG_DIR from poetry.utils._compat import Path from poetry.utils._compat import basestring from poetry.utils.toml_file import TomlFile config = Factory.create_config(self.io) config_file = TomlFile(Path(CONFIG_DIR) / "config.toml") try: local_config_file = TomlFile(self.poetry.file.parent / "poetry.toml") if local_config_file.exists(): config.merge(local_config_file.read()) except RuntimeError: local_config_file = TomlFile(Path.cwd() / "poetry.toml") if self.option("local"): config.set_config_source(FileConfigSource(local_config_file)) if not config_file.exists(): config_file.path.parent.mkdir(parents=True, exist_ok=True) config_file.touch(mode=0o0600) if self.option("list"): self._list_configuration(config.all(), config.raw()) return 0 setting_key = self.argument("key") if not setting_key: return 0 if self.argument("value") and self.option("unset"): raise RuntimeError( "You can not combine a setting value with --unset") # show the value if no value is provided if not self.argument("value") and not self.option("unset"): m = re.match(r"^repos?(?:itories)?(?:\.(.+))?", self.argument("key")) if m: if not m.group(1): value = {} if config.get("repositories") is not None: value = config.get("repositories") else: repo = config.get("repositories.{}".format(m.group(1))) if repo is None: raise ValueError( "There is no {} repository defined".format( m.group(1))) value = repo self.line(str(value)) else: values = self.unique_config_values if setting_key not in values: raise ValueError( "There is no {} setting.".format(setting_key)) value = config.get(setting_key) if not isinstance(value, basestring): value = json.dumps(value) self.line(value) return 0 values = self.argument("value") unique_config_values = self.unique_config_values if setting_key in unique_config_values: if self.option("unset"): return config.config_source.remove_property(setting_key) return self._handle_single_value( config.config_source, setting_key, unique_config_values[setting_key], values, ) # handle repositories m = re.match(r"^repos?(?:itories)?(?:\.(.+))?", self.argument("key")) if m: if not m.group(1): raise ValueError( "You cannot remove the [repositories] section") if self.option("unset"): repo = config.get("repositories.{}".format(m.group(1))) if repo is None: raise ValueError( "There is no {} repository defined".format(m.group(1))) config.config_source.remove_property("repositories.{}".format( m.group(1))) return 0 if len(values) == 1: url = values[0] config.config_source.add_property( "repositories.{}.url".format(m.group(1)), url) return 0 raise ValueError( "You must pass the url. " "Example: poetry config repositories.foo https://bar.com") # handle auth m = re.match(r"^(http-basic|pypi-token)\.(.+)", self.argument("key")) if m: if self.option("unset"): keyring_repository_password_del(config, m.group(2)) config.auth_config_source.remove_property("{}.{}".format( m.group(1), m.group(2))) return 0 if m.group(1) == "http-basic": if len(values) == 1: username = values[0] # Only username, so we prompt for password password = self.secret("Password:"******"Expected one or two arguments " "(username, password), got {}".format( len(values))) else: username = values[0] password = values[1] property_value = dict(username=username) try: keyring_repository_password_set(m.group(2), username, password) except RuntimeError: property_value.update(password=password) config.auth_config_source.add_property( "{}.{}".format(m.group(1), m.group(2)), property_value) elif m.group(1) == "pypi-token": if len(values) != 1: raise ValueError( "Expected only one argument (token), got {}".format( len(values))) token = values[0] config.auth_config_source.add_property( "{}.{}".format(m.group(1), m.group(2)), token) return 0 # handle certs m = re.match(r"(?:certificates)\.([^.]+)\.(cert|client-cert)", self.argument("key")) if m: if self.option("unset"): config.auth_config_source.remove_property( "certificates.{}.{}".format(m.group(1), m.group(2))) return 0 if len(values) == 1: config.auth_config_source.add_property( "certificates.{}.{}".format(m.group(1), m.group(2)), values[0]) else: raise ValueError("You must pass exactly 1 value") return 0 raise ValueError("Setting {} does not exist".format( self.argument("key")))
class Locker(object): _VERSION = "1.0" _relevant_keys = ["dependencies", "dev-dependencies", "source", "extras"] def __init__(self, lock, local_config): # type: (Path, dict) -> None self._lock = TomlFile(lock) self._local_config = local_config self._lock_data = None self._content_hash = self._get_content_hash() @property def lock(self): # type: () -> TomlFile return self._lock @property def lock_data(self): if self._lock_data is None: self._lock_data = self._get_lock_data() return self._lock_data def is_locked(self): # type: () -> bool """ Checks whether the locker has been locked (lockfile found). """ if not self._lock.exists(): return False return "package" in self.lock_data def is_fresh(self): # type: () -> bool """ Checks whether the lock file is still up to date with the current hash. """ lock = self._lock.read() metadata = lock.get("metadata", {}) if "content-hash" in metadata: return self._content_hash == lock["metadata"]["content-hash"] return False def locked_repository(self, with_dev_reqs=False ): # type: (bool) -> poetry.repositories.Repository """ Searches and returns a repository of locked packages. """ if not self.is_locked(): return poetry.repositories.Repository() lock_data = self.lock_data packages = poetry.repositories.Repository() if with_dev_reqs: locked_packages = lock_data["package"] else: locked_packages = [ p for p in lock_data["package"] if p["category"] == "main" ] if not locked_packages: return packages for info in locked_packages: package = Package(info["name"], info["version"], info["version"]) package.description = info.get("description", "") package.category = info["category"] package.optional = info["optional"] if "hashes" in lock_data["metadata"]: # Old lock so we create dummy files from the hashes package.files = [{ "name": h, "hash": h } for h in lock_data["metadata"]["hashes"][info["name"]]] else: package.files = lock_data["metadata"]["files"][info["name"]] package.python_versions = info["python-versions"] extras = info.get("extras", {}) if extras: for name, deps in extras.items(): package.extras[name] = [] for dep in deps: m = re.match(r"^(.+?)(?:\s+\((.+)\))?$", dep) dep_name = m.group(1) constraint = m.group(2) or "*" package.extras[name].append( Dependency(dep_name, constraint)) if "marker" in info: package.marker = parse_marker(info["marker"]) else: # Compatibility for old locks if "requirements" in info: dep = Dependency("foo", "0.0.0") for name, value in info["requirements"].items(): if name == "python": dep.python_versions = value elif name == "platform": dep.platform = value split_dep = dep.to_pep_508(False).split(";") if len(split_dep) > 1: package.marker = parse_marker(split_dep[1].strip()) for dep_name, constraint in info.get("dependencies", {}).items(): if isinstance(constraint, list): for c in constraint: package.add_dependency(dep_name, c) continue package.add_dependency(dep_name, constraint) if "develop" in info: package.develop = info["develop"] if "source" in info: package.source_type = info["source"].get("type", "") package.source_url = info["source"]["url"] package.source_reference = info["source"]["reference"] packages.add_package(package) return packages def set_lock_data(self, root, packages): # type: (...) -> bool files = table() packages = self._lock_packages(packages) # Retrieving hashes for package in packages: if package["name"] not in files: files[package["name"]] = [] for f in package["files"]: file_metadata = inline_table() for k, v in sorted(f.items()): file_metadata[k] = v files[package["name"]].append(file_metadata) if files[package["name"]]: files[package["name"]] = item( files[package["name"]]).multiline(True) del package["files"] lock = document() lock["package"] = packages if root.extras: lock["extras"] = { extra: [dep.pretty_name for dep in deps] for extra, deps in root.extras.items() } lock["metadata"] = { "lock-version": self._VERSION, "python-versions": root.python_versions, "content-hash": self._content_hash, "files": files, } if not self.is_locked() or lock != self.lock_data: self._write_lock_data(lock) return True return False def _write_lock_data(self, data): self.lock.write(data) # Checking lock file data consistency if data != self.lock.read(): raise RuntimeError("Inconsistent lock file data.") self._lock_data = None def _get_content_hash(self): # type: () -> str """ Returns the sha256 hash of the sorted content of the pyproject file. """ content = self._local_config relevant_content = {} for key in self._relevant_keys: relevant_content[key] = content.get(key) content_hash = sha256( json.dumps(relevant_content, sort_keys=True).encode()).hexdigest() return content_hash def _get_lock_data(self): # type: () -> dict if not self._lock.exists(): raise RuntimeError( "No lockfile found. Unable to read locked packages") try: lock_data = self._lock.read() except TOMLKitError as e: raise RuntimeError("Unable to read the lock file ({}).".format(e)) lock_version = Version.parse(lock_data["metadata"].get( "lock-version", "1.0")) current_version = Version.parse(self._VERSION) accepted_versions = parse_constraint("^{}".format( Version(current_version.major, current_version.minor))) lock_version_allowed = accepted_versions.allows(lock_version) if lock_version_allowed and current_version < lock_version: logger.warning( "The lock file might not be compatible with the current version of Poetry.\n" "Upgrade Poetry to ensure the lock file is read properly or, alternatively, " "regenerate the lock file with the `poetry lock` command.") elif not lock_version_allowed: raise RuntimeError( "The lock file is not compatible with the current version of Poetry.\n" "Upgrade Poetry to be able to read the lock file or, alternatively, " "regenerate the lock file with the `poetry lock` command.") return lock_data def _lock_packages( self, packages): # type: (List['poetry.packages.Package']) -> list locked = [] for package in sorted(packages, key=lambda x: x.name): spec = self._dump_package(package) locked.append(spec) return locked def _dump_package(self, package): # type: (Package) -> dict dependencies = {} for dependency in sorted(package.requires, key=lambda d: d.name): if dependency.is_optional() and not dependency.is_activated(): continue if dependency.pretty_name not in dependencies: dependencies[dependency.pretty_name] = [] constraint = inline_table() constraint["version"] = str(dependency.pretty_constraint) if dependency.extras: constraint["extras"] = sorted(dependency.extras) if dependency.is_optional(): constraint["optional"] = True if not dependency.marker.is_any(): constraint["markers"] = str(dependency.marker) dependencies[dependency.pretty_name].append(constraint) # All the constraints should have the same type, # but we want to simplify them if it's possible for dependency, constraints in tuple(dependencies.items()): if all(len(constraint) == 1 for constraint in constraints): dependencies[dependency] = [ constraint["version"] for constraint in constraints ] data = { "name": package.pretty_name, "version": package.pretty_version, "description": package.description or "", "category": package.category, "optional": package.optional, "python-versions": package.python_versions, "files": sorted(package.files, key=lambda x: x["file"]), } if package.extras: extras = {} for name, deps in package.extras.items(): extras[name] = [ str(dep) if not dep.constraint.is_any() else dep.name for dep in deps ] data["extras"] = extras if dependencies: for k, constraints in dependencies.items(): if len(constraints) == 1: dependencies[k] = constraints[0] data["dependencies"] = dependencies if package.source_url: data["source"] = { "url": package.source_url, "reference": package.source_reference, } if package.source_type: data["source"]["type"] = package.source_type if package.source_type == "directory": data["develop"] = package.develop return data
def remove(self, python): # type: (str) -> Env venv_path = self._poetry.config.get("virtualenvs.path") if venv_path is None: venv_path = Path(CACHE_DIR) / "virtualenvs" else: venv_path = Path(venv_path) cwd = self._poetry.file.parent envs_file = TomlFile(venv_path / self.ENVS_FILE) base_env_name = self.generate_env_name(self._poetry.package.name, str(cwd)) if python.startswith(base_env_name): venvs = self.list() for venv in venvs: if venv.path.name == python: # Exact virtualenv name if not envs_file.exists(): self.remove_venv(str(venv.path)) return venv venv_minor = ".".join( str(v) for v in venv.version_info[:2]) base_env_name = self.generate_env_name(cwd.name, str(cwd)) envs = envs_file.read() current_env = envs.get(base_env_name) if not current_env: self.remove_venv(str(venv.path)) return venv if current_env["minor"] == venv_minor: del envs[base_env_name] envs_file.write(envs) self.remove_venv(str(venv.path)) return venv raise ValueError( '<warning>Environment "{}" does not exist.</warning>'.format( python)) try: python_version = Version.parse(python) python = "python{}".format(python_version.major) if python_version.precision > 1: python += ".{}".format(python_version.minor) except ValueError: # Executable in PATH or full executable path pass try: python_version = decode( subprocess.check_output( " ".join([ python, "-c", "\"import sys; print('.'.join([str(s) for s in sys.version_info[:3]]))\"", ]), shell=True, )) except CalledProcessError as e: raise EnvCommandError(e) python_version = Version.parse(python_version.strip()) minor = "{}.{}".format(python_version.major, python_version.minor) name = "{}-py{}".format(base_env_name, minor) venv = venv_path / name if not venv.exists(): raise ValueError( '<warning>Environment "{}" does not exist.</warning>'.format( name)) if envs_file.exists(): envs = envs_file.read() current_env = envs.get(base_env_name) if current_env is not None: current_minor = current_env["minor"] if current_minor == minor: del envs[base_env_name] envs_file.write(envs) self.remove_venv(str(venv)) return VirtualEnv(venv)
def __init__( self, path, # type: Path category="main", # type: str optional=False, # type: bool base=None, # type: Path develop=False, # type: bool ): from . import dependency_from_pep_508 from .package import Package self._path = path self._base = base self._full_path = path self._develop = develop if self._base and not self._path.is_absolute(): self._full_path = self._base / self._path if not self._full_path.exists(): raise ValueError("Directory {} does not exist".format(self._path)) if self._full_path.is_file(): raise ValueError("{} is a file, expected a directory".format(self._path)) # Checking content to dertermine actions setup = self._full_path / "setup.py" pyproject = TomlFile(self._full_path / "pyproject.toml") has_poetry = False if pyproject.exists(): pyproject_content = pyproject.read(True) has_poetry = ( "tool" in pyproject_content and "poetry" in pyproject_content["tool"] ) if not setup.exists() and not has_poetry: raise ValueError( "Directory {} does not seem to be a Python package".format( self._full_path ) ) if has_poetry: from poetry.masonry.builders import SdistBuilder from poetry.poetry import Poetry poetry = Poetry.create(self._full_path) builder = SdistBuilder(poetry, NullVenv(), NullIO()) with setup.open("w") as f: f.write(decode(builder.build_setup())) package = poetry.package self._package = Package(package.pretty_name, package.version) self._package.requires += package.requires self._package.dev_requires += package.dev_requires self._package.python_versions = package.python_versions self._package.platform = package.platform else: # Execute egg_info current_dir = os.getcwd() os.chdir(str(self._full_path)) try: cwd = base venv = Venv.create(NullIO(), cwd=cwd) venv.run("python", "setup.py", "egg_info") finally: os.chdir(current_dir) egg_info = list(self._full_path.glob("*.egg-info"))[0] meta = pkginfo.UnpackedSDist(str(egg_info)) if meta.requires_dist: reqs = list(meta.requires_dist) else: reqs = [] requires = egg_info / "requires.txt" if requires.exists(): with requires.open() as f: reqs = parse_requires(f.read()) package = Package(meta.name, meta.version) package.description = meta.summary for req in reqs: package.requires.append(dependency_from_pep_508(req)) if meta.requires_python: package.python_versions = meta.requires_python if meta.platforms: platforms = [p for p in meta.platforms if p.lower() != "unknown"] if platforms: package.platform = " || ".join(platforms) self._package = package self._package.source_type = "directory" self._package.source_url = self._path.as_posix() super(DirectoryDependency, self).__init__( self._package.name, self._package.version, category=category, optional=optional, allows_prereleases=True, )
def test_check(): complete = TomlFile(fixtures_dir / "complete.toml") content = complete.read(raw=True)["tool"]["poetry"] assert Poetry.check(content)
def get(self, reload=False): # type: (bool) -> Env if self._env is not None and not reload: return self._env python_minor = ".".join([str(v) for v in sys.version_info[:2]]) venv_path = self._poetry.config.get("virtualenvs.path") if venv_path is None: venv_path = Path(CACHE_DIR) / "virtualenvs" else: venv_path = Path(venv_path) cwd = self._poetry.file.parent envs_file = TomlFile(venv_path / self.ENVS_FILE) env = None base_env_name = self.generate_env_name(self._poetry.package.name, str(cwd)) if envs_file.exists(): envs = envs_file.read() env = envs.get(base_env_name) if env: python_minor = env["minor"] # Check if we are inside a virtualenv or not # Conda sets CONDA_PREFIX in its envs, see # https://github.com/conda/conda/issues/2764 env_prefix = os.environ.get("VIRTUAL_ENV", os.environ.get("CONDA_PREFIX")) conda_env_name = os.environ.get("CONDA_DEFAULT_ENV") # It's probably not a good idea to pollute Conda's global "base" env, since # most users have it activated all the time. in_venv = env_prefix is not None and conda_env_name != "base" if not in_venv or env is not None: # Checking if a local virtualenv exists if (cwd / ".venv").exists() and (cwd / ".venv").is_dir(): venv = cwd / ".venv" return VirtualEnv(venv) create_venv = self._poetry.config.get("virtualenvs.create", True) if not create_venv: return SystemEnv(Path(sys.prefix)) venv_path = self._poetry.config.get("virtualenvs.path") if venv_path is None: venv_path = Path(CACHE_DIR) / "virtualenvs" else: venv_path = Path(venv_path) name = "{}-py{}".format(base_env_name, python_minor.strip()) venv = venv_path / name if not venv.exists(): return SystemEnv(Path(sys.prefix)) return VirtualEnv(venv) if env_prefix is not None: prefix = Path(env_prefix) base_prefix = None else: prefix = Path(sys.prefix) base_prefix = self.get_base_prefix() return VirtualEnv(prefix, base_prefix)
class Locker: _relevant_keys = ["dependencies", "dev-dependencies", "source", "extras"] def __init__(self, lock, local_config): # type: (Path, dict) -> None self._lock = TomlFile(lock) self._local_config = local_config self._lock_data = None self._content_hash = self._get_content_hash() @property def lock(self): # type: () -> TomlFile return self._lock @property def lock_data(self): if self._lock_data is None: self._lock_data = self._get_lock_data() return self._lock_data def is_locked(self): # type: () -> bool """ Checks whether the locker has been locked (lockfile found). """ if not self._lock.exists(): return False return "package" in self.lock_data def is_fresh(self): # type: () -> bool """ Checks whether the lock file is still up to date with the current hash. """ lock = self._lock.read() metadata = lock.get("metadata", {}) if "content-hash" in metadata: return self._content_hash == lock["metadata"]["content-hash"] return False def locked_repository( self, with_dev_reqs=False ): # type: (bool) -> poetry.repositories.Repository """ Searches and returns a repository of locked packages. """ if not self.is_locked(): return poetry.repositories.Repository() lock_data = self.lock_data packages = poetry.repositories.Repository() if with_dev_reqs: locked_packages = lock_data["package"] else: locked_packages = [ p for p in lock_data["package"] if p["category"] == "main" ] if not locked_packages: return packages for info in locked_packages: package = poetry.packages.Package( info["name"], info["version"], info["version"] ) package.description = info.get("description", "") package.category = info["category"] package.optional = info["optional"] package.hashes = lock_data["metadata"]["hashes"][info["name"]] package.python_versions = info["python-versions"] for dep_name, constraint in info.get("dependencies", {}).items(): if isinstance(constraint, list): for c in constraint: package.add_dependency(dep_name, c) continue package.add_dependency(dep_name, constraint) if "requirements" in info: package.requirements = info["requirements"] if "source" in info: package.source_type = info["source"]["type"] package.source_url = info["source"]["url"] package.source_reference = info["source"]["reference"] packages.add_package(package) return packages def set_lock_data(self, root, packages): # type: () -> bool hashes = {} packages = self._lock_packages(packages) # Retrieving hashes for package in packages: if package["name"] not in hashes: hashes[package["name"]] = [] hashes[package["name"]] += package["hashes"] del package["hashes"] lock = document() lock["package"] = packages if root.extras: lock["extras"] = { extra: [dep.pretty_name for dep in deps] for extra, deps in root.extras.items() } lock["metadata"] = { "python-versions": root.python_versions, "platform": root.platform, "content-hash": self._content_hash, "hashes": hashes, } if not self.is_locked() or lock != self.lock_data: self._write_lock_data(lock) return True return False def _write_lock_data(self, data): self.lock.write(data) # Checking lock file data consistency if data != self.lock.read(): raise RuntimeError("Inconsistent lock file data.") self._lock_data = None def _get_content_hash(self): # type: () -> str """ Returns the sha256 hash of the sorted content of the composer file. """ content = self._local_config relevant_content = {} for key in self._relevant_keys: relevant_content[key] = content.get(key) content_hash = sha256( json.dumps(relevant_content, sort_keys=True).encode() ).hexdigest() return content_hash def _get_lock_data(self): # type: () -> dict if not self._lock.exists(): raise RuntimeError("No lockfile found. Unable to read locked packages") return self._lock.read() def _lock_packages( self, packages ): # type: (List['poetry.packages.Package']) -> list locked = [] for package in sorted(packages, key=lambda x: x.name): spec = self._dump_package(package) locked.append(spec) return locked def _dump_package(self, package): # type: (poetry.packages.Package) -> dict dependencies = {} for dependency in sorted(package.requires, key=lambda d: d.name): if dependency.is_optional() and not dependency.is_activated(): continue if dependency.pretty_name not in dependencies: dependencies[dependency.pretty_name] = [] constraint = {"version": str(dependency.pretty_constraint)} if not dependency.python_constraint.is_any(): constraint["python"] = str(dependency.python_constraint) if dependency.platform != "*": constraint["platform"] = dependency.platform if len(constraint) == 1: dependencies[dependency.pretty_name].append(constraint["version"]) else: dependencies[dependency.pretty_name].append(constraint) for name, constraints in dependencies.items(): if len(constraints) == 1: dependencies[name] = constraints[0] data = { "name": package.pretty_name, "version": package.pretty_version, "description": package.description, "category": package.category, "optional": package.optional, "python-versions": package.python_versions, "platform": package.platform, "hashes": sorted(package.hashes), } if dependencies: for k, constraints in dependencies.items(): if len(constraints) == 1: dependencies[k] = constraints[0] data["dependencies"] = dependencies if package.source_type: data["source"] = { "type": package.source_type, "url": package.source_url, "reference": package.source_reference, } if package.requirements: data["requirements"] = package.requirements return data
def fixture(name): file = TomlFile(Path(__file__).parent / "fixtures" / "{}.test".format(name)) return file.read()
def search_for_vcs(self, dependency): # type: (VCSDependency) -> List[Package] """ Search for the specifications that match the given VCS dependency. Basically, we clone the repository in a temporary directory and get the information we need by checking out the specified reference. """ if dependency.vcs != "git": raise ValueError("Unsupported VCS dependency {}".format(dependency.vcs)) tmp_dir = Path(mkdtemp(prefix="pypoetry-git-{}".format(dependency.name))) try: git = Git() git.clone(dependency.source, tmp_dir) git.checkout(dependency.reference, tmp_dir) revision = git.rev_parse(dependency.reference, tmp_dir).strip() if dependency.tag or dependency.rev: revision = dependency.reference pyproject = TomlFile(tmp_dir / "pyproject.toml") pyproject_content = None has_poetry = False if pyproject.exists(): pyproject_content = pyproject.read() has_poetry = ( "tool" in pyproject_content and "poetry" in pyproject_content["tool"] ) if pyproject_content and has_poetry: # If a pyproject.toml file exists # We use it to get the information we need info = pyproject_content["tool"]["poetry"] name = info["name"] version = info["version"] package = Package(name, version, version) package.source_type = dependency.vcs package.source_url = dependency.source package.source_reference = dependency.reference for req_name, req_constraint in info["dependencies"].items(): if req_name == "python": package.python_versions = req_constraint continue package.add_dependency(req_name, req_constraint) else: # We need to use setup.py here # to figure the information we need # We need to place ourselves in the proper # folder for it to work venv = Venv.create(self._io) current_dir = os.getcwd() os.chdir(tmp_dir.as_posix()) try: venv.run("python", "setup.py", "egg_info") # Sometimes pathlib will fail on recursive # symbolic links, so we need to workaround it # and use the glob module instead. # Note that this does not happen with pathlib2 # so it's safe to use it for Python < 3.4. if PY35: egg_info = next( Path(p) for p in glob.glob( os.path.join(str(tmp_dir), "**", "*.egg-info"), recursive=True, ) ) else: egg_info = next(tmp_dir.glob("**/*.egg-info")) meta = pkginfo.UnpackedSDist(str(egg_info)) if meta.requires_dist: reqs = list(meta.requires_dist) else: reqs = [] requires = egg_info / "requires.txt" if requires.exists(): with requires.open() as f: reqs = parse_requires(f.read()) package = Package(meta.name, meta.version) for req in reqs: dep = dependency_from_pep_508(req) if dep.in_extras: for extra in dep.in_extras: if extra not in package.extras: package.extras[extra] = [] package.extras[extra].append(dep) package.requires.append(dep) except Exception: raise finally: os.chdir(current_dir) package.source_type = "git" package.source_url = dependency.source package.source_reference = revision except Exception: raise finally: shutil.rmtree(tmp_dir.as_posix()) if dependency.name != package.name: # For now, the dependency's name must match the actual package's name raise RuntimeError( "The dependency name for {} does not match the actual package's name: {}".format( dependency.name, package.name ) ) if dependency.extras: for extra in dependency.extras: if extra in package.extras: for dep in package.extras[extra]: dep.activate() return [package]
def publish(self, repository_name, username, password): if repository_name: self._io.writeln( "Publishing <info>{}</info> (<comment>{}</comment>) " "to <fg=cyan>{}</>".format( self._package.pretty_name, self._package.pretty_version, repository_name, ) ) else: self._io.writeln( "Publishing <info>{}</info> (<comment>{}</comment>) " "to <fg=cyan>PyPI</>".format( self._package.pretty_name, self._package.pretty_version ) ) if not repository_name: url = "https://upload.pypi.org/legacy/" repository_name = "pypi" else: # Retrieving config information config_file = TomlFile(Path(CONFIG_DIR) / "config.toml") if not config_file.exists(): raise RuntimeError( "Config file does not exist. " "Unable to get repository information" ) config = config_file.read() if ( "repositories" not in config or repository_name not in config["repositories"] ): raise RuntimeError( "Repository {} is not defined".format(repository_name) ) url = config["repositories"][repository_name]["url"] if not (username and password): auth = get_http_basic_auth(repository_name) if auth: username = auth[0] password = auth[1] # Requesting missing credentials if not username: username = self._io.ask("Username:"******"Password:") # TODO: handle certificates self._uploader.auth(username, password) return self._uploader.upload(url)
def test_check_fails(): complete = TomlFile(fixtures_dir / "complete.toml") content = complete.read()["tool"]["poetry"] content["this key is not in the schema"] = "" with pytest.raises(InvalidProjectFile): Poetry.check(content)