Skip to content

Commit 7399348

Browse files
committed
Add '--exclude' option
1 parent 2d370be commit 7399348

File tree

6 files changed

+128
-0
lines changed

6 files changed

+128
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,4 @@ poetry export -f requirements.txt --output requirements.txt
5353
* `--all-extras`: Include all sets of extra dependencies.
5454
* `--without-hashes`: Exclude hashes from the exported file.
5555
* `--with-credentials`: Include credentials for extra indices.
56+
* `--exclude`: The names of dependencies to exclude along with nested dependencies when exporting.

docs/_index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,4 @@ poetry export --only test,docs
7676
* `--all-extras`: Include all sets of extra dependencies.
7777
* `--without-hashes`: Exclude hashes from the exported file.
7878
* `--with-credentials`: Include credentials for extra indices.
79+
* `--exclude`: The names of dependencies to exclude along with nested dependencies when exporting.

src/poetry_plugin_export/command.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,13 @@ class ExportCommand(GroupCommand):
5151
),
5252
option("all-extras", None, "Include all sets of extra dependencies."),
5353
option("with-credentials", None, "Include credentials for extra indices."),
54+
option(
55+
"exclude",
56+
None,
57+
"The names of dependencies to exclude along with nested dependencies when exporting.",
58+
flag=False,
59+
multiple=True,
60+
),
5461
]
5562

5663
@property
@@ -116,12 +123,19 @@ def handle(self) -> int:
116123
f"Extra [{', '.join(sorted(invalid_extras))}] is not specified."
117124
)
118125

126+
excludes = {
127+
canonicalize_name(exclude)
128+
for exclude_opt in self.option("exclude")
129+
for exclude in exclude_opt.split()
130+
}
131+
119132
exporter = Exporter(self.poetry, self.io)
120133
exporter.only_groups(list(self.activated_groups))
121134
exporter.with_extras(list(extras))
122135
exporter.with_hashes(not self.option("without-hashes"))
123136
exporter.with_credentials(self.option("with-credentials"))
124137
exporter.with_urls(not self.option("without-urls"))
138+
exporter.with_excludes(list(excludes))
125139
exporter.export(fmt, Path.cwd(), output or self.io)
126140

127141
return 0

src/poetry_plugin_export/exporter.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ def __init__(self, poetry: Poetry, io: IO) -> None:
4444
self._with_urls = True
4545
self._extras: Collection[NormalizedName] = ()
4646
self._groups: Iterable[str] = [MAIN_GROUP]
47+
self._excludes: Collection[NormalizedName] = ()
4748

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

7576
return self
7677

78+
def with_excludes(self, excludes: Collection[NormalizedName]) -> Exporter:
79+
self._excludes = excludes
80+
81+
return self
82+
7783
def export(self, fmt: str, cwd: Path, output: IO | str) -> None:
7884
if not self.is_format_supported(fmt):
7985
raise ValueError(f"Invalid export format: {fmt}")
@@ -99,6 +105,7 @@ def _export_generic_txt(
99105
root_package_name=root.name,
100106
project_python_marker=root.python_marker,
101107
extras=self._extras,
108+
excludes=self._excludes,
102109
):
103110
line = ""
104111

src/poetry_plugin_export/walker.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ def get_project_dependency_packages(
5555
root_package_name: NormalizedName,
5656
project_python_marker: BaseMarker | None = None,
5757
extras: Collection[NormalizedName] = (),
58+
excludes: Collection[NormalizedName] = (),
5859
) -> Iterator[DependencyPackage]:
5960
# Apply the project python marker to all requirements.
6061
if project_python_marker is not None:
@@ -92,6 +93,9 @@ def get_project_dependency_packages(
9293
# a package is locked as optional, but is not activated via extras
9394
continue
9495

96+
if excludes and package.name in excludes:
97+
continue
98+
9599
selected.append(dependency)
96100

97101
for package, dependency in get_project_dependencies(

tests/test_exporter.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -831,6 +831,107 @@ def test_exporter_exports_requirements_txt_without_optional_packages(
831831
assert content == expected
832832

833833

834+
@pytest.mark.parametrize(
835+
["packages_data", "skip", "excludes", "lines"],
836+
[
837+
# two package without deps, exclude foo
838+
(
839+
[
840+
{"name": "foo", "version": "1.2.3"},
841+
{"name": "bar", "version": "4.5.6"},
842+
],
843+
None,
844+
["foo"],
845+
[f"bar==4.5.6 ; {MARKER_PY}"],
846+
),
847+
# foo has baz dep, exclude foo with baz dep
848+
(
849+
[
850+
{
851+
"name": "foo",
852+
"version": "1.2.3",
853+
"dependencies": {"baz": ">=7.8.9"},
854+
},
855+
{"name": "bar", "version": "4.5.6"},
856+
{"name": "baz", "version": "7.8.9", "optional": True},
857+
],
858+
{"baz"},
859+
["foo"],
860+
[f"bar==4.5.6 ; {MARKER_PY}"],
861+
862+
),
863+
# foo has baz dep, baz also declared in main group, exclude only foo
864+
(
865+
[
866+
{
867+
"name": "foo",
868+
"version": "1.2.3",
869+
"dependencies": {"baz": ">=7.8.9"},
870+
},
871+
{"name": "bar", "version": "4.5.6"},
872+
{"name": "baz", "version": "7.8.9"},
873+
],
874+
None,
875+
["foo"],
876+
[f"bar==4.5.6 ; {MARKER_PY}", f"baz==7.8.9 ; {MARKER_PY}"],
877+
),
878+
# foo has baz dep, bar also has baz dep, exclude only foo
879+
(
880+
[
881+
{
882+
"name": "foo",
883+
"version": "1.2.3",
884+
"dependencies": {"baz": ">=7.8.9"},
885+
},
886+
{
887+
"name": "bar",
888+
"version": "4.5.6",
889+
"dependencies": {"baz": ">=7.8.9"},
890+
},
891+
{"name": "baz", "version": "7.8.9", "optional": True},
892+
],
893+
{"baz"},
894+
["foo"],
895+
[f"bar==4.5.6 ; {MARKER_PY}", f"baz==7.8.9 ; {MARKER_PY}"],
896+
),
897+
],
898+
)
899+
def test_exporter_exports_requirements_txt_with_excludes(
900+
tmp_path: Path,
901+
poetry: Poetry,
902+
packages_data: list[dict[str, Any]],
903+
excludes: Collection[NormalizedName],
904+
skip: set[str] | None,
905+
lines: list[str],
906+
) -> None:
907+
for package_data in packages_data:
908+
if "optional" not in package_data:
909+
package_data["optional"] = False
910+
if "python-versions" not in package_data:
911+
package_data["python-versions"] = "*"
912+
913+
poetry.locker.mock_lock_data({ # type: ignore[attr-defined]
914+
"package": packages_data,
915+
"metadata": {
916+
"content-hash": "123456789",
917+
"files": {package["name"]: [] for package in packages_data},
918+
},
919+
})
920+
set_package_requires(poetry, skip=skip)
921+
922+
exporter = Exporter(poetry, NullIO())
923+
exporter.with_hashes(False)
924+
exporter.with_excludes(excludes)
925+
exporter.export("requirements.txt", tmp_path, "requirements.txt")
926+
927+
with (tmp_path / "requirements.txt").open(encoding="utf-8") as f:
928+
content = f.read()
929+
930+
expected = "\n".join(lines) + "\n"
931+
932+
assert content == expected
933+
934+
834935
@pytest.mark.parametrize(
835936
["extras", "lines"],
836937
[

0 commit comments

Comments
 (0)