From b2b2cce056580c5e84eda98ce9fdf219650266c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Randy=20D=C3=B6ring?= <30527984+radoering@users.noreply.github.com> Date: Sat, 10 Sep 2022 14:33:04 +0200 Subject: [PATCH] support for file dependencies with subdirectories (analogous to git and url dependencies) --- src/poetry/core/factory.py | 5 ++ .../core/json/schemas/poetry-schema.json | 8 +++ src/poetry/core/packages/dependency.py | 13 ++++- src/poetry/core/packages/file_dependency.py | 16 ++++++ src/poetry/core/packages/package.py | 1 + src/poetry/core/packages/path_dependency.py | 2 + .../distributions/demo-0.1.0-in-subdir.zip | Bin 0 -> 1717 bytes .../README.rst | 2 + .../pyproject.toml | 40 ++++++++++++++ tests/packages/test_directory_dependency.py | 12 +++++ tests/packages/test_file_dependency.py | 10 +++- tests/packages/test_package.py | 2 + tests/test_factory.py | 50 ++++++++++++++++++ 13 files changed, 158 insertions(+), 3 deletions(-) create mode 100644 tests/fixtures/distributions/demo-0.1.0-in-subdir.zip create mode 100644 tests/fixtures/project_with_dependencies_with_subdirectory/README.rst create mode 100644 tests/fixtures/project_with_dependencies_with_subdirectory/pyproject.toml diff --git a/src/poetry/core/factory.py b/src/poetry/core/factory.py index a1d92fcb2..c70175b6c 100644 --- a/src/poetry/core/factory.py +++ b/src/poetry/core/factory.py @@ -290,6 +290,7 @@ def create_dependency( dependency = FileDependency( name, file_path, + directory=constraint.get("subdirectory", None), groups=groups, base=root_dir, extras=constraint.get("extras", []), @@ -306,12 +307,16 @@ def create_dependency( dependency = FileDependency( name, path, + directory=constraint.get("subdirectory", None), groups=groups, optional=optional, base=root_dir, extras=constraint.get("extras", []), ) else: + subdirectory = constraint.get("subdirectory", None) + if subdirectory: + path = path / subdirectory dependency = DirectoryDependency( name, path, diff --git a/src/poetry/core/json/schemas/poetry-schema.json b/src/poetry/core/json/schemas/poetry-schema.json index a7fc59eee..564559070 100644 --- a/src/poetry/core/json/schemas/poetry-schema.json +++ b/src/poetry/core/json/schemas/poetry-schema.json @@ -429,6 +429,10 @@ "type": "string", "description": "The path to the file." }, + "subdirectory": { + "type": "string", + "description": "The relative path to the directory where the package is located." + }, "python": { "type": "string", "description": "The python versions for which the dependency should be installed." @@ -465,6 +469,10 @@ "type": "string", "description": "The path to the dependency." }, + "subdirectory": { + "type": "string", + "description": "The relative path to the directory where the package is located." + }, "python": { "type": "string", "description": "The python versions for which the dependency should be installed." diff --git a/src/poetry/core/packages/dependency.py b/src/poetry/core/packages/dependency.py index 1e2a77865..3f59d5e75 100644 --- a/src/poetry/core/packages/dependency.py +++ b/src/poetry/core/packages/dependency.py @@ -441,7 +441,11 @@ def create_from_pep_508( # handle RFC 8089 references path = url_to_path(req.url) dep = _make_file_or_dir_dep( - name=name, path=path, base=relative_to, extras=req.extras + name=name, + path=path, + base=relative_to, + subdirectory=link.subdirectory_fragment, + extras=req.extras, ) else: with suppress(ValueError): @@ -502,6 +506,7 @@ def _make_file_or_dir_dep( name: str, path: Path, base: Path | None = None, + subdirectory: str | None = None, extras: list[str] | None = None, ) -> FileDependency | DirectoryDependency | None: """ @@ -517,8 +522,12 @@ def _make_file_or_dir_dep( _path = Path(base) / path if _path.is_file(): - return FileDependency(name, path, base=base, extras=extras) + return FileDependency( + name, path, base=base, directory=subdirectory, extras=extras + ) elif _path.is_dir(): + if subdirectory: + path = path / subdirectory return DirectoryDependency(name, path, base=base, extras=extras) return None diff --git a/src/poetry/core/packages/file_dependency.py b/src/poetry/core/packages/file_dependency.py index e2a94d0b2..b6dacfffd 100644 --- a/src/poetry/core/packages/file_dependency.py +++ b/src/poetry/core/packages/file_dependency.py @@ -19,6 +19,8 @@ def __init__( self, name: str, path: Path, + *, + directory: str | None = None, groups: Iterable[str] | None = None, optional: bool = False, base: Path | None = None, @@ -31,9 +33,23 @@ def __init__( groups=groups, optional=optional, base=base, + subdirectory=directory, extras=extras, ) + @property + def directory(self) -> str | None: + return self.source_subdirectory + + @property + def base_pep_508_name(self) -> str: + requirement = super().base_pep_508_name + + if self.directory: + requirement += f"#subdirectory={self.directory}" + + return requirement + def _validate(self) -> str: message = super()._validate() if message: diff --git a/src/poetry/core/packages/package.py b/src/poetry/core/packages/package.py index 0a8399861..7a544fca5 100644 --- a/src/poetry/core/packages/package.py +++ b/src/poetry/core/packages/package.py @@ -538,6 +538,7 @@ def to_dependency(self) -> Dependency: dep = FileDependency( self._name, Path(self._source_url), + directory=self.source_subdirectory, groups=list(self._dependency_groups.keys()), optional=self.optional, base=self.root_dir, diff --git a/src/poetry/core/packages/path_dependency.py b/src/poetry/core/packages/path_dependency.py index f82c95532..8a1cbcbae 100644 --- a/src/poetry/core/packages/path_dependency.py +++ b/src/poetry/core/packages/path_dependency.py @@ -29,6 +29,7 @@ def __init__( groups: Iterable[str] | None = None, optional: bool = False, base: Path | None = None, + subdirectory: str | None = None, extras: Iterable[str] | None = None, ) -> None: assert source_type in ("file", "directory") @@ -47,6 +48,7 @@ def __init__( allows_prereleases=True, source_type=source_type, source_url=self._full_path.as_posix(), + source_subdirectory=subdirectory, extras=extras, ) # cache validation result to avoid unnecessary file system access diff --git a/tests/fixtures/distributions/demo-0.1.0-in-subdir.zip b/tests/fixtures/distributions/demo-0.1.0-in-subdir.zip new file mode 100644 index 0000000000000000000000000000000000000000..a4c0bc00eacda201d8d35f606a4afd1092513f99 GIT binary patch literal 1717 zcmWIWW@Zs#00G9Hq);#eO0Waz;?ks)%p&~&oQil5ic(T@^T8^)zy^SfYvL+=DF)<$ zusE8^`1s7c%#!$cy@Ja4__EZZ;>`TK_;>|d1$6^GLp=j^E|{epV3&33o_6PBWMJ6D z#K6FhFe<>?UDwmk&421dUoU1yf!6P}T}Ilxe#Z4y7mOS)HgNMEZ20Z9%*f2o|M)+_ zVwLj`kG_#`cZ&J?F7SQzSFTqRWz1d}RH`K}eaHI5CfPVdWr5<-oA+&=O8VbCq}`gP z-EcSC^(5PEhI1wUZ;tI2&t=isXE!}#?h?6W_pD-g!<@KM)el@+U+-Hz-}~&m7iYbl z&1U8^OTSVpJ-52pVkP&yPaoq_noqC?elXnq_CnOoX(FedyKQHiI;obM|HgK^JM2nl z>bJcMkvsia%P8>3r#(G>F)r$Qi-kqGuk6k_6ueCS+=2$xS@oN@+Ev_}yT$LoD&<<% zEuR;zOB9)K(Qxy^eIMtB7g(({j{EfK&5VMm()E{Q^|<~jf8#|By6V0+e{KSUtO^)> zqKKd?s4OVT&q_@$(JRT%&FP(B>&I*;(E8i5^{G(f)CnFYDvKVnOu6V*R3?Au(Cf&X zk2^XJ)LO8MACB9`q2zk%d*zq9J%VZZY;8xJoezEs&|Yf&Wld)P44HPWw6wgmSyoHT zb~&o8I+<4aa7NdS7t7zTpX>Wb;7YPzQuK=7Eu6Lw#3hes-xZkqY!)B0Q~L$0C2eia zA7%d9=bxQxC3m$faoVg`zTbAP*MEKWj*fPY?T&`;lh=j+iu!ot;;%)jX*ClH-p9|4 zDf+ct`ku1eGVWrC-9`2BnGgshNkJ;<_ zyu>v;SMJBQSu;7>e5#l2P6@lZ=+U${YBOY}yxJ1e@G7h1Z|YU?%c-;fcWkhC*(7Ci z{k!GMt^OC%qm&)YSP$@1Rr=f|AZ z{CLbhar(;>YOxpEIrn)^FL9e}H{*DMV6&?4p9Xc2eJ}Mq_4dD(`wI+oMkad(T;+-i zFcE@)0EB{-GF%J_U;#!335N6buNU5UcY_f|b6sVg1LET|n1=z`U{#=5(BcbRG9jCu zJmn_X^y^F?!8D8yF&$U=1v4GwN`ExdkxDac7Qrll(Fku~E8UPSnuV|kR@RYY0csh5 wY{6AvZh(dpYKcIOHK^GO*_wyI3" +] +license = "MIT" + +readme = "README.rst" + +homepage = "https://python-poetry.org" +repository = "https://github.com/python-poetry/poetry" +documentation = "https://python-poetry.org/docs" + +keywords = ["packaging", "dependency", "poetry"] + +classifiers = [ + "Topic :: Software Development :: Build Tools", + "Topic :: Software Development :: Libraries :: Python Modules" +] + +# Requirements +[tool.poetry.dependencies] +python = "^3.6" + +# Git dependency with subdirectory +pendulum = { git = "https://github.com/sdispater/pendulum.git", subdirectory = "sub", branch = "2.0" } + +# File dependency with subdirectory +demo = [ + { path = "../distributions/demo-0.1.0-in-subdir.zip", subdirectory = "sub", platform = "linux" }, + { file = "../distributions/demo-0.1.0-in-subdir.zip", subdirectory = "sub", platform = "win32" } +] + +# Dir dependency with subdirectory (same as path "../simple_project" without subdirectory) +simple-project = { path = "..", subdirectory = "simple_project" } + +# Url dependency with subdirectory +foo = { url = "https://example.com/foo.zip", subdirectory = "sub" } diff --git a/tests/packages/test_directory_dependency.py b/tests/packages/test_directory_dependency.py index 776593ab1..3d3533633 100644 --- a/tests/packages/test_directory_dependency.py +++ b/tests/packages/test_directory_dependency.py @@ -119,6 +119,18 @@ def test_directory_dependency_pep_508_local_relative() -> None: _test_directory_dependency_pep_508("demo", path, requirement, expected) +def test_directory_dependency_pep_508_with_subdirectory() -> None: + path = ( + Path(__file__).parent.parent + / "fixtures" + / "project_with_multi_constraints_dependency" + ) + expected = f"demo @ {path.as_uri()}" + + requirement = f"demo @ file://{path.parent.as_posix()}#subdirectory={path.name}" + _test_directory_dependency_pep_508("demo", path, requirement, expected) + + def test_directory_dependency_pep_508_extras() -> None: path = ( Path(__file__).parent.parent diff --git a/tests/packages/test_file_dependency.py b/tests/packages/test_file_dependency.py index 124792258..694ff367a 100644 --- a/tests/packages/test_file_dependency.py +++ b/tests/packages/test_file_dependency.py @@ -177,7 +177,15 @@ def test_file_dependency_pep_508_local_file_relative_path( _test_file_dependency_pep_508(mocker, "demo", path, requirement, expected) -def test_absolute_file_dependency_to_pep_508_with_marker(mocker: MockerFixture) -> None: +def test_file_dependency_pep_508_with_subdirectory(mocker: MockerFixture) -> None: + path = DIST_PATH / "demo.zip" + expected = f"demo @ {path.as_uri()}#subdirectory=sub" + + requirement = f"demo @ file://{path.as_posix()}#subdirectory=sub" + _test_file_dependency_pep_508(mocker, "demo", path, requirement, expected) + + +def test_to_pep_508_with_marker(mocker: MockerFixture) -> None: wheel = "demo-0.1.0-py2.py3-none-any.whl" abs_path = DIST_PATH / wheel diff --git a/tests/packages/test_package.py b/tests/packages/test_package.py index bf4ca8638..4763c79a7 100644 --- a/tests/packages/test_package.py +++ b/tests/packages/test_package.py @@ -368,6 +368,7 @@ def test_to_dependency_for_file() -> None: "1.2.3", source_type="file", source_url=path.as_posix(), + source_subdirectory="qux", features=["baz", "bar"], ) dep = package.to_dependency() @@ -380,6 +381,7 @@ def test_to_dependency_for_file() -> None: assert dep.path == path assert dep.source_type == "file" assert dep.source_url == path.as_posix() + assert dep.source_subdirectory == "qux" def test_to_dependency_for_url() -> None: diff --git a/tests/test_factory.py b/tests/test_factory.py index fdf944af3..fb820df04 100644 --- a/tests/test_factory.py +++ b/tests/test_factory.py @@ -20,6 +20,8 @@ from _pytest.logging import LogCaptureFixture from poetry.core.packages.dependency import Dependency + from poetry.core.packages.directory_dependency import DirectoryDependency + from poetry.core.packages.file_dependency import FileDependency from poetry.core.packages.vcs_dependency import VCSDependency @@ -151,6 +153,54 @@ def test_create_poetry() -> None: ] +def test_create_poetry_with_dependencies_with_subdirectory() -> None: + poetry = Factory().create_poetry( + fixtures_dir / "project_with_dependencies_with_subdirectory" + ) + package = poetry.package + dependencies = {str(dep.name): dep for dep in package.requires} + + # git dependency + pendulum = dependencies["pendulum"] + assert pendulum.is_vcs() + assert pendulum.pretty_constraint == "branch 2.0" + pendulum = cast("VCSDependency", pendulum) + assert pendulum.source == "https://github.com/sdispater/pendulum.git" + assert pendulum.directory == "sub" + + # file dependency + demo = dependencies["demo"] + assert demo.is_file() + assert demo.pretty_constraint == "*" + demo = cast("FileDependency", demo) + assert demo.path == Path("../distributions/demo-0.1.0-in-subdir.zip") + assert demo.directory == "sub" + demo_dependencies = [dep for dep in package.requires if dep.name == "demo"] + assert len(demo_dependencies) == 2 + assert demo_dependencies[0] == demo_dependencies[1] + assert {str(dep.marker) for dep in demo_dependencies} == { + 'sys_platform == "win32"', + 'sys_platform == "linux"', + } + + # directory dependency + simple_project = dependencies["simple-project"] + assert simple_project.is_directory() + assert simple_project.pretty_constraint == "*" + simple_project = cast("DirectoryDependency", simple_project) + assert simple_project.path == Path("../simple_project") + with pytest.raises(AttributeError): + _ = simple_project.directory # type: ignore[attr-defined] + + # url dependency + foo = dependencies["foo"] + assert foo.is_url() + assert foo.pretty_constraint == "*" + foo = cast("URLDependency", foo) + assert foo.url == "https://example.com/foo.zip" + assert foo.directory == "sub" + + def test_create_poetry_with_packages_and_includes() -> None: poetry = Factory().create_poetry( fixtures_dir.parent / "masonry" / "builders" / "fixtures" / "with-include"