From 578485715377881104115fe31bdae5d0d90623bf Mon Sep 17 00:00:00 2001 From: AJ Kerrigan Date: Tue, 27 Feb 2024 18:09:07 -0500 Subject: [PATCH] feat: add an option to exclude packages from freezing (#16) * Add an exclude example to the README * Confirm that command-level options feed into IcedPoet attributes * And that default/unset options come through as expected --- README.md | 5 ++- src/poetry_plugin_freeze/app.py | 28 +++++++++--- tests/test_freeze.py | 77 ++++++++++++++++++++++++++++++++- 3 files changed, 101 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index e371fdf..679b111 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,10 @@ poetry build # add freeze step poetry freeze-wheel -# Note we can't use poetry to publish because it uses metadata from pyproject.toml instead +# avoid freezing specific packages +poetry freeze-wheel --exclude boto3 -e attrs + +# Note we can't use poetry to publish because it uses metadata from pyproject.toml instead # of frozen wheel metadata. # publish per normal diff --git a/src/poetry_plugin_freeze/app.py b/src/poetry_plugin_freeze/app.py index be3fe3c..e80f1cc 100644 --- a/src/poetry_plugin_freeze/app.py +++ b/src/poetry_plugin_freeze/app.py @@ -27,7 +27,17 @@ class FreezeCommand(Command): name = "freeze-wheel" - options = [option("wheel-dir", None, "Sub-directory containing wheels")] + options = [ + option("wheel-dir", None, "Sub-directory containing wheels", default="dist", flag=False), + option( + "exclude", + short_name="-e", + description="A package name to exclude from freezing", + flag=False, + value_required=False, + multiple=True, + ), + ] def handle(self) -> int: self.line("freezing wheels") @@ -35,7 +45,7 @@ def handle(self) -> int: fridge = {} for project_root in project_roots(root_dir): - iced = IcedPoet(project_root) + iced = IcedPoet(project_root, self.option("wheel-dir"), self.option("exclude")) iced.check() fridge[iced.name] = iced @@ -71,12 +81,13 @@ def get_sha256_digest(content: bytes): class IcedPoet: factory = Factory() - def __init__(self, project_dir, wheel_dir="dist"): + def __init__(self, project_dir, wheel_dir="dist", exclude_packages=()): self.project_dir = project_dir self.wheel_dir = wheel_dir self.poetry = self.factory.create_poetry(project_dir) self.meta = Metadata.from_package(self.poetry.package) self.fridge = None + self.exclude_packages = exclude_packages def set_fridge(self, fridge): self.fridge = fridge @@ -197,17 +208,22 @@ def compact_markers(self, dependency): new_marker = MultiMarker(new_marker, extra_markers) dependency.marker = new_marker - def get_frozen_deps(self, dep_packages): + def get_frozen_deps(self, dep_packages, exclude_packages=None): lines = [] dependency_sources = self.get_dependency_sources() for pkg_name, dep_package in dep_packages.items(): self.compact_markers(dep_package.dependency) - require_dist = "%s (==%s)" % (pkg_name, dep_package.package.version) # Freeze extra markers for dependencies which were pulled in via extras # Don't freeze markers if a dependency is also part of the base # dependency tree. freeze_extras = "base" not in dependency_sources.get(dep_package.dependency.name, set()) requirement = dep_package.dependency.to_pep_508(with_extras=freeze_extras) + + if dep_package.package.name in exclude_packages: + lines.append(requirement) + continue + + require_dist = "%s (==%s)" % (pkg_name, dep_package.package.version) if ";" in requirement: markers = requirement.split(";", 1)[1].strip() require_dist += f" ; {markers}" @@ -277,7 +293,7 @@ def freeze_wheel(self, wheel_path, dep_packages): dist_meta = Parser().parsestr(md_text) deps = self.get_path_deps(MAIN_GROUP) deps.update(dep_packages) - dep_lines = self.get_frozen_deps(deps) + dep_lines = self.get_frozen_deps(deps, self.exclude_packages) self.replace_deps(dist_meta, dep_lines) with source_whl.open(record_path) as record_fh: diff --git a/tests/test_freeze.py b/tests/test_freeze.py index 1174df8..38fef6d 100644 --- a/tests/test_freeze.py +++ b/tests/test_freeze.py @@ -1,8 +1,13 @@ import csv +import zipfile from email.parser import Parser from io import StringIO -import zipfile -from poetry_plugin_freeze.app import IcedPoet, project_roots, get_sha256_digest + +from cleo.io.null_io import NullIO +from cleo.testers.command_tester import CommandTester +from poetry.console.application import Application +from poetry.factory import Factory +from poetry_plugin_freeze.app import IcedPoet, get_sha256_digest, project_roots def test_project_roots(fixture_root): @@ -21,6 +26,37 @@ def parse_record(record_text: bytes): return list(csv.reader(StringIO(record_text.decode("utf8")))) +def test_freeze_command_options(fixture_root, monkeypatch): + poet_options = {} + + def mock_check(self): + poet_options["wheel_dir"] = self.wheel_dir + poet_options["exclude_packages"] = self.exclude_packages + return True + + def mock_freeze(self): + return [] + + monkeypatch.setattr(IcedPoet, "check", mock_check) + monkeypatch.setattr(IcedPoet, "freeze", mock_freeze) + + poetry = Factory().create_poetry(fixture_root) + app = Application() + app._poetry = poetry + app._load_plugins(NullIO()) + + cmd = app.find("freeze-wheel") + tester = CommandTester(cmd) + + tester.execute("--exclude boto3 -e attrs --wheel-dir mydir") + assert poet_options["wheel_dir"] == "mydir" + assert poet_options["exclude_packages"] == ["boto3", "attrs"] + + tester.execute() + assert poet_options["wheel_dir"] == "dist" + assert poet_options["exclude_packages"] == [] + + def test_freeze_nested(fixture_root, fixture_copy): package = fixture_copy(fixture_root / "nested_packages") sub_package = fixture_copy(fixture_root / "nested_packages" / "others" / "app_c") @@ -162,3 +198,40 @@ def test_freeze_extras(fixture_root, fixture_copy): 'extra == "toml"' not in md_requirements["tomli"], ] ) + + +def test_freeze_exclude_packages(fixture_root, fixture_copy): + package = fixture_copy(fixture_root / "nested_packages") + + iced_pkg = IcedPoet(package, exclude_packages=["pytest", "ruff"]) + iced_pkg.set_fridge({iced_pkg.name: iced_pkg}) + wheels = iced_pkg.freeze() + assert len(wheels) == 1 + + wheel = zipfile.ZipFile(wheels[0]) + + md = parse_md( + wheel.open(f"{iced_pkg.distro_name}-{iced_pkg.version}.dist-info/METADATA").read() + ) + + md_requirements = {} + for header_type, header_value in md._headers: + if header_type != "Requires-Dist": + continue + pkg_name, requirements = header_value.split(maxsplit=1) + md_requirements[pkg_name] = requirements + + for package, expected_version_constraint in [ + # Excluded packages should not have frozen versions + ("pytest", "(>=7.1,<8.0)"), + ("ruff", "(>=0.0.259,<0.0.260)"), + # ...but other packages should + ("attrs", "(==22.2.0)"), + ("colorama", "(==0.4.6)"), + ("exceptiongroup", "(==1.1.0)"), + ("iniconfig", "(==2.0.0)"), + ("packaging", "(==23.0)"), + ("pluggy", "(==1.0.0)"), + ("tomli", "(==2.0.1)"), + ]: + assert expected_version_constraint in md_requirements[package]