Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add '--exclude' option #255

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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