diff --git a/poetry/packages/locker.py b/poetry/packages/locker.py deleted file mode 100644 index d1dbc6c5fc7..00000000000 --- a/poetry/packages/locker.py +++ /dev/null @@ -1,340 +0,0 @@ -import json -import logging -import re - -from hashlib import sha256 -from typing import List - -from tomlkit import comment -from tomlkit import document -from tomlkit import inline_table -from tomlkit import item -from tomlkit import table -from tomlkit.exceptions import TOMLKitError - -import poetry.repositories - -from poetry.core.packages.package import Dependency -from poetry.core.packages.package import Package -from poetry.core.semver import parse_constraint -from poetry.core.semver.version import Version -from poetry.core.version.markers import parse_marker -from poetry.utils._compat import Path -from poetry.utils.toml_file import TomlFile - - -logger = logging.getLogger(__name__) - - -class Locker(object): - - _VERSION = "1.1" - - _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.add(comment("@" + "generated")) - 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) - # We expect the locker to be able to read lock files - # from the same semantic versioning range - accepted_versions = parse_constraint( - "^{}".format(Version(current_version.major, 0)) - ) - 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 diff --git a/src/poetry/packages/locker.py b/src/poetry/packages/locker.py index bcc4821d70e..6c267754785 100644 --- a/src/poetry/packages/locker.py +++ b/src/poetry/packages/locker.py @@ -24,6 +24,7 @@ from poetry.core.version.markers import parse_marker from poetry.core.version.requirements import InvalidRequirement from tomlkit import array +from tomlkit import comment from tomlkit import document from tomlkit import inline_table from tomlkit import item @@ -397,6 +398,7 @@ def set_lock_data(self, root: Package, packages: list[Package]) -> bool: del package["files"] lock = document() + lock.add(comment("@" + "generated")) lock["package"] = package_specs if root.extras: diff --git a/tests/packages/test_locker.py b/tests/packages/test_locker.py index 73cc7d0e603..91587046c0f 100644 --- a/tests/packages/test_locker.py +++ b/tests/packages/test_locker.py @@ -63,6 +63,8 @@ def test_lock_file_data_is_ordered(locker: Locker, root: ProjectPackage): content = f.read() expected = """\ +# @generated + [[package]] name = "A" version = "1.0.0" @@ -172,6 +174,8 @@ def test_locker_properly_loads_extras(locker: Locker): def test_locker_properly_loads_nested_extras(locker: Locker): content = """\ +# @generated + [[package]] name = "a" version = "1.0" @@ -252,6 +256,8 @@ def test_locker_properly_loads_nested_extras(locker: Locker): def test_locker_properly_loads_extras_legacy(locker: Locker): content = """\ +# @generated + [[package]] name = "a" version = "1.0" @@ -309,7 +315,10 @@ def test_lock_packages_with_null_description(locker: Locker, root: ProjectPackag with locker.lock.open(encoding="utf-8") as f: content = f.read() - expected = """[[package]] + expected = """\ +# @generated + +[[package]] name = "A" version = "1.0.0" description = "" @@ -340,7 +349,10 @@ def test_lock_file_should_not_have_mixed_types(locker: Locker, root: ProjectPack locker.set_lock_data(root, [package_a]) - expected = """[[package]] + expected = """\ +# @generated + +[[package]] name = "A" version = "1.0.0" description = "" @@ -373,7 +385,10 @@ def test_lock_file_should_not_have_mixed_types(locker: Locker, root: ProjectPack def test_reading_lock_file_should_raise_an_error_on_invalid_data(locker: Locker): - content = """[[package]] + content = """\ +# @generated + +[[package]] name = "A" version = "1.0.0" description = "" @@ -421,7 +436,10 @@ def test_locking_legacy_repository_package_should_include_source_section( with locker.lock.open(encoding="utf-8") as f: content = f.read() - expected = """[[package]] + expected = """\ +# @generated + +[[package]] name = "A" version = "1.0.0" description = "" @@ -509,7 +527,10 @@ def test_extras_dependencies_are_ordered(locker: Locker, root: ProjectPackage): locker.set_lock_data(root, [package_a]) - expected = """[[package]] + expected = """\ +# @generated + +[[package]] name = "A" version = "1.0.0" description = "" @@ -599,7 +620,9 @@ def test_locker_dumps_dependency_information_correctly( with locker.lock.open(encoding="utf-8") as f: content = f.read() - expected = """[[package]] + expected = """# @generated + +[[package]] name = "A" version = "1.0.0" description = "" @@ -630,6 +653,8 @@ def test_locked_repository_uses_root_dir_of_package( locker: Locker, mocker: MockerFixture ): content = """\ +# @generated + [[package]] name = "lib-a" version = "0.1.0"