def test_get_prefers_explicitly_activated_virtualenvs_over_env_var( tmp_dir, config, mocker): os.environ["VIRTUAL_ENV"] = "/environment/prefix" venv_name = EnvManager.generate_env_name("simple_project", str(CWD)) config.add_property("settings.virtualenvs.path", str(tmp_dir)) (Path(tmp_dir) / "{}-py3.7".format(venv_name)).mkdir() 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) 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)], ) env = EnvManager(config).get(CWD) assert env.path == Path(tmp_dir) / "{}-py3.7".format(venv_name) assert env.base == Path("/prefix")
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_activated(app, tmp_dir, config): app.poetry._config = config config.add_property("settings.virtualenvs.path", str(tmp_dir)) venv_name = EnvManager.generate_env_name("simple_project", str(app.poetry.file.parent)) (Path(tmp_dir) / "{}-py3.7".format(venv_name)).mkdir() (Path(tmp_dir) / "{}-py3.6".format(venv_name)).mkdir() 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) command = app.find("env list") tester = CommandTester(command) tester.execute() expected = """\ {}-py3.6 {}-py3.7 (Activated) """.format(venv_name, venv_name) assert expected == tester.io.fetch_output()
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 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 test_get_prefers_explicitly_activated_virtualenvs_over_env_var( app, tmp_dir, mocker ): mocker.stopall() os.environ["VIRTUAL_ENV"] = "/environment/prefix" venv_name = EnvManager.generate_env_name( "simple-project", str(app.poetry.file.parent) ) current_python = sys.version_info[:3] python_minor = ".".join(str(v) for v in current_python[:2]) python_patch = ".".join(str(v) for v in current_python) app.poetry.config.merge({"virtualenvs": {"path": str(tmp_dir)}}) (Path(tmp_dir) / "{}-py{}".format(venv_name, python_minor)).mkdir() envs_file = TomlFile(Path(tmp_dir) / "envs.toml") doc = tomlkit.document() doc[venv_name] = {"minor": python_minor, "patch": python_patch} envs_file.write(doc) mocker.patch( "poetry.utils._compat.subprocess.check_output", side_effect=check_output_wrapper(Version(*current_python)), ) mocker.patch( "poetry.utils._compat.subprocess.Popen.communicate", side_effect=[("/prefix", None), ("/prefix", None), ("/prefix", None)], ) command = app.find("env use") tester = CommandTester(command) tester.execute(python_minor) expected = """\ Using virtualenv: {} """.format( os.path.join(tmp_dir, "{}-py{}".format(venv_name, python_minor)) ) assert expected == tester.io.fetch_output()
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_get_prefers_explicitly_activated_virtualenvs_over_env_var( tester, current_python, venv_cache, venv_name, venvs_in_cache_config): os.environ["VIRTUAL_ENV"] = "/environment/prefix" python_minor = ".".join(str(v) for v in current_python[:2]) python_patch = ".".join(str(v) for v in current_python[:3]) venv_dir = venv_cache / "{}-py{}".format(venv_name, python_minor) venv_dir.mkdir(parents=True, exist_ok=True) envs_file = TomlFile(venv_cache / "envs.toml") doc = tomlkit.document() doc[venv_name] = {"minor": python_minor, "patch": python_patch} envs_file.write(doc) tester.execute(python_minor) expected = """\ Using virtualenv: {} """.format(venv_dir) assert expected == tester.io.fetch_output()
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_activate_activates_different_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(Version.parse("3.6.6")), ) mocker.patch( "poetry.utils._compat.subprocess.Popen.communicate", side_effect=[("/prefix", None), ("/prefix", None), ("/prefix", None)], ) m = mocker.patch("poetry.utils.env.EnvManager.build_venv", side_effect=build_venv) env = EnvManager(config).activate("python3.6", CWD, NullIO()) m.assert_called_with(os.path.join(tmp_dir, "{}-py3.6".format(venv_name)), executable="python3.6") 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")
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 activate(self, python, io): # type: (str, IO) -> 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) 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) patch = python_version.text create = False envs = tomlkit.document() base_env_name = self.generate_env_name(self._poetry.package.name, str(cwd)) 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"] current_patch = current_env["patch"] if current_minor == minor and current_patch != patch: # We need to recreate create = True name = "{}-py{}".format(base_env_name, minor) venv = venv_path / name # Create if needed if not venv.exists() or venv.exists() and create: in_venv = os.environ.get("VIRTUAL_ENV") is not None if in_venv or not venv.exists(): create = True if venv.exists(): # We need to check if the patch version is correct _venv = VirtualEnv(venv) current_patch = ".".join( str(v) for v in _venv.version_info[:3]) if patch != current_patch: create = True self.create_venv(io, executable=python, force=create) # Activate envs[base_env_name] = {"minor": minor, "patch": patch} envs_file.write(envs) return self.get(reload=True)
class Locker: _relevant_keys = [ 'name', 'version', 'python-versions', 'platform', 'dependencies', 'dev-dependencies', 'source', ] def __init__(self, lock: Path, local_config: dict): self._lock = TomlFile(lock) self._local_config = local_config self._lock_data = None self._content_hash = self._get_content_hash() @property def lock(self) -> 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) -> 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) -> bool: """ Checks whether the lock file is still up to date with the current hash. """ lock = self._lock.read(True) 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: bool = False) -> Repository: """ Searches and returns a repository of locked packages. """ if not self.is_locked(): return Repository() lock_data = self.lock_data packages = 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(): package.add_dependency(dep_name, constraint) 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) -> bool: hashes = {} packages = self._lock_packages(packages) # Retrieving hashes for package in packages: hashes[package['name']] = package['hashes'] del package['hashes'] lock = { 'package': packages, 'metadata': { 'python-versions': root.python_versions, 'platform': root.platform, 'content-hash': self._content_hash, 'hashes': hashes, } } if root.extras: lock['extras'] = { extra: [dep.pretty_name for dep in deps] for extra, deps in root.extras.items() } 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) self._lock_data = None def _get_content_hash(self) -> 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) -> dict: if not self._lock.exists(): raise RuntimeError( 'No lockfile found. Unable to read locked packages' ) return self._lock.read(True) def _lock_packages(self, packages: 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: 'poetry.packages.Package') -> dict: dependencies = {} for dependency in package.requires: if dependency.is_optional(): continue dependencies[dependency.pretty_name] = dependency.pretty_constraint 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': package.hashes, '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
class Locker: _relevant_keys = ["dependencies", "dev-dependencies", "source"] 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(True) 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(): 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: hashes[package["name"]] = package["hashes"] del package["hashes"] lock = { "package": packages, "metadata": { "python-versions": root.python_versions, "platform": root.platform, "content-hash": self._content_hash, "hashes": hashes, }, } if root.extras: lock["extras"] = { extra: [dep.pretty_name for dep in deps] for extra, deps in root.extras.items() } 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) 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(True) 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 package.requires: if dependency.is_optional() and not dependency.is_activated(): continue dependencies[dependency.pretty_name] = str( dependency.pretty_constraint) 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": package.hashes, "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 venv_activate_37(venv_cache, venv_name): envs_file = TomlFile(venv_cache / "envs.toml") doc = tomlkit.document() doc[venv_name] = {"minor": "3.7", "patch": "3.7.0"} envs_file.write(doc)