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 f6d6adac1..55a3230d2 100644 --- a/src/poetry/core/packages/package.py +++ b/src/poetry/core/packages/package.py @@ -536,6 +536,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 000000000..a4c0bc00e Binary files /dev/null and b/tests/fixtures/distributions/demo-0.1.0-in-subdir.zip differ diff --git a/tests/fixtures/project_with_dependencies_with_subdirectory/README.rst b/tests/fixtures/project_with_dependencies_with_subdirectory/README.rst new file mode 100644 index 000000000..f7fe15470 --- /dev/null +++ b/tests/fixtures/project_with_dependencies_with_subdirectory/README.rst @@ -0,0 +1,2 @@ +My Package +========== diff --git a/tests/fixtures/project_with_dependencies_with_subdirectory/pyproject.toml b/tests/fixtures/project_with_dependencies_with_subdirectory/pyproject.toml new file mode 100644 index 000000000..0eab12a6f --- /dev/null +++ b/tests/fixtures/project_with_dependencies_with_subdirectory/pyproject.toml @@ -0,0 +1,40 @@ +[tool.poetry] +name = "my-package" +version = "1.2.3" +description = "Some description." +authors = [ + "SeĢbastien Eustace " +] +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 = "~2.7 || ^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 5ee406968..2baa73c60 100644 --- a/tests/packages/test_package.py +++ b/tests/packages/test_package.py @@ -371,6 +371,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() @@ -383,6 +384,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..bab41d0d5 100644 --- a/tests/test_factory.py +++ b/tests/test_factory.py @@ -11,6 +11,8 @@ from poetry.core.constraints.version import parse_constraint from poetry.core.factory import Factory +from poetry.core.packages.directory_dependency import DirectoryDependency +from poetry.core.packages.file_dependency import FileDependency from poetry.core.packages.url_dependency import URLDependency from poetry.core.utils._compat import tomllib from poetry.core.version.markers import SingleMarker @@ -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"