Skip to content

Commit

Permalink
feat: add an option to exclude packages from freezing (#16)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
ajkerrigan authored Feb 27, 2024
1 parent 9f1ef28 commit 5784857
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 9 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 22 additions & 6 deletions src/poetry_plugin_freeze/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,25 @@
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")
root_dir = self._io and self._io.input.option("directory") or Path.cwd()

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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}"
Expand Down Expand Up @@ -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:
Expand Down
77 changes: 75 additions & 2 deletions tests/test_freeze.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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")
Expand Down Expand Up @@ -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]

0 comments on commit 5784857

Please sign in to comment.