Skip to content

Commit

Permalink
Add '--exclude' option
Browse files Browse the repository at this point in the history
  • Loading branch information
ownik committed Jan 20, 2024
1 parent 2d370be commit d4d22a8
Show file tree
Hide file tree
Showing 7 changed files with 156 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,4 @@ poetry export -f requirements.txt --output requirements.txt
* `--all-extras`: Include all sets of extra dependencies.
* `--without-hashes`: Exclude hashes from the exported file.
* `--with-credentials`: Include credentials for extra indices.
* `--exclude`: The names of dependencies to exclude along with nested dependencies when exporting.
1 change: 1 addition & 0 deletions docs/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,4 @@ poetry export --only test,docs
* `--all-extras`: Include all sets of extra dependencies.
* `--without-hashes`: Exclude hashes from the exported file.
* `--with-credentials`: Include credentials for extra indices.
* `--exclude`: The names of dependencies to exclude along with nested dependencies when exporting.
15 changes: 15 additions & 0 deletions src/poetry_plugin_export/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@ class ExportCommand(GroupCommand):
),
option("all-extras", None, "Include all sets of extra dependencies."),
option("with-credentials", None, "Include credentials for extra indices."),
option(
"exclude",
None,
"The names of dependencies to exclude along with nested dependencies when"
" exporting.",
flag=False,
multiple=True,
),
]

@property
Expand Down Expand Up @@ -116,12 +124,19 @@ def handle(self) -> int:
f"Extra [{', '.join(sorted(invalid_extras))}] is not specified."
)

excludes = {
canonicalize_name(exclude)
for exclude_opt in self.option("exclude")
for exclude in exclude_opt.split()
}

exporter = Exporter(self.poetry, self.io)
exporter.only_groups(list(self.activated_groups))
exporter.with_extras(list(extras))
exporter.with_hashes(not self.option("without-hashes"))
exporter.with_credentials(self.option("with-credentials"))
exporter.with_urls(not self.option("without-urls"))
exporter.with_excludes(list(excludes))
exporter.export(fmt, Path.cwd(), output or self.io)

return 0
7 changes: 7 additions & 0 deletions src/poetry_plugin_export/exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ def __init__(self, poetry: Poetry, io: IO) -> None:
self._with_urls = True
self._extras: Collection[NormalizedName] = ()
self._groups: Iterable[str] = [MAIN_GROUP]
self._excludes: Collection[NormalizedName] = ()

@classmethod
def is_format_supported(cls, fmt: str) -> bool:
Expand Down Expand Up @@ -74,6 +75,11 @@ def with_credentials(self, with_credentials: bool = True) -> Exporter:

return self

def with_excludes(self, excludes: Collection[NormalizedName]) -> Exporter:
self._excludes = excludes

return self

def export(self, fmt: str, cwd: Path, output: IO | str) -> None:
if not self.is_format_supported(fmt):
raise ValueError(f"Invalid export format: {fmt}")
Expand All @@ -99,6 +105,7 @@ def _export_generic_txt(
root_package_name=root.name,
project_python_marker=root.python_marker,
extras=self._extras,
excludes=self._excludes,
):
line = ""

Expand Down
4 changes: 4 additions & 0 deletions src/poetry_plugin_export/walker.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ def get_project_dependency_packages(
root_package_name: NormalizedName,
project_python_marker: BaseMarker | None = None,
extras: Collection[NormalizedName] = (),
excludes: Collection[NormalizedName] = (),
) -> Iterator[DependencyPackage]:
# Apply the project python marker to all requirements.
if project_python_marker is not None:
Expand Down Expand Up @@ -92,6 +93,9 @@ def get_project_dependency_packages(
# a package is locked as optional, but is not activated via extras
continue

if excludes and package.name in excludes:
continue

selected.append(dependency)

for package, dependency in get_project_dependencies(
Expand Down
28 changes: 28 additions & 0 deletions tests/command/test_command_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,3 +289,31 @@ def test_export_exports_constraints_txt_with_warnings(

assert develop_warning in tester.io.fetch_error()
assert tester.io.fetch_output() == expected


@pytest.mark.parametrize(
"excludes, expected",
[
(
["foo"],
f"""\
bar==1.1.0 ; {MARKER_PY}
baz==2.0.0 ; {MARKER_PY}
qux==1.2.0 ; {MARKER_PY}
""",
),
(
["foo", "bar"],
f"""\
baz==2.0.0 ; {MARKER_PY}
qux==1.2.0 ; {MARKER_PY}
""",
),
],
)
def test_export_excludes(
tester: CommandTester, do_lock: None, excludes: list[str], expected: str
) -> None:
excludes_opts = " ".join([f"--exclude={exclude}" for exclude in excludes])
tester.execute(f"--format requirements.txt --with=dev --all-extras {excludes_opts}")
assert tester.io.fetch_output() == expected
100 changes: 100 additions & 0 deletions tests/test_exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -831,6 +831,106 @@ def test_exporter_exports_requirements_txt_without_optional_packages(
assert content == expected


@pytest.mark.parametrize(
["packages_data", "skip", "excludes", "lines"],
[
# two package without deps, exclude foo
(
[
{"name": "foo", "version": "1.2.3"},
{"name": "bar", "version": "4.5.6"},
],
None,
["foo"],
[f"bar==4.5.6 ; {MARKER_PY}"],
),
# foo has baz dep, exclude foo with baz dep
(
[
{
"name": "foo",
"version": "1.2.3",
"dependencies": {"baz": ">=7.8.9"},
},
{"name": "bar", "version": "4.5.6"},
{"name": "baz", "version": "7.8.9", "optional": True},
],
{"baz"},
["foo"],
[f"bar==4.5.6 ; {MARKER_PY}"],
),
# foo has baz dep, baz also declared in main group, exclude only foo
(
[
{
"name": "foo",
"version": "1.2.3",
"dependencies": {"baz": ">=7.8.9"},
},
{"name": "bar", "version": "4.5.6"},
{"name": "baz", "version": "7.8.9"},
],
None,
["foo"],
[f"bar==4.5.6 ; {MARKER_PY}", f"baz==7.8.9 ; {MARKER_PY}"],
),
# foo has baz dep, bar also has baz dep, exclude only foo
(
[
{
"name": "foo",
"version": "1.2.3",
"dependencies": {"baz": ">=7.8.9"},
},
{
"name": "bar",
"version": "4.5.6",
"dependencies": {"baz": ">=7.8.9"},
},
{"name": "baz", "version": "7.8.9", "optional": True},
],
{"baz"},
["foo"],
[f"bar==4.5.6 ; {MARKER_PY}", f"baz==7.8.9 ; {MARKER_PY}"],
),
],
)
def test_exporter_exports_requirements_txt_with_excludes(
tmp_path: Path,
poetry: Poetry,
packages_data: list[dict[str, Any]],
excludes: Collection[NormalizedName],
skip: set[str] | None,
lines: list[str],
) -> None:
for package_data in packages_data:
if "optional" not in package_data:
package_data["optional"] = False
if "python-versions" not in package_data:
package_data["python-versions"] = "*"

poetry.locker.mock_lock_data({ # type: ignore[attr-defined]
"package": packages_data,
"metadata": {
"content-hash": "123456789",
"files": {package["name"]: [] for package in packages_data},
},
})
set_package_requires(poetry, skip=skip)

exporter = Exporter(poetry, NullIO())
exporter.with_hashes(False)
exporter.with_excludes(excludes)
exporter.export("requirements.txt", tmp_path, "requirements.txt")

with (tmp_path / "requirements.txt").open(encoding="utf-8") as f:
content = f.read()

expected = "\n".join(lines) + "\n"

assert content == expected


@pytest.mark.parametrize(
["extras", "lines"],
[
Expand Down

0 comments on commit d4d22a8

Please sign in to comment.