diff --git a/src/poetry/core/factory.py b/src/poetry/core/factory.py index 98e44631b..de4ae8a4b 100644 --- a/src/poetry/core/factory.py +++ b/src/poetry/core/factory.py @@ -286,6 +286,7 @@ def create_dependency( dependency = FileDependency( name, file_path, + directory=constraint.get("subdirectory", None), groups=groups, base=root_dir, extras=constraint.get("extras", []), @@ -302,12 +303,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 790367fe2..7eaffe9b8 100644 --- a/src/poetry/core/json/schemas/poetry-schema.json +++ b/src/poetry/core/json/schemas/poetry-schema.json @@ -428,6 +428,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." @@ -464,6 +468,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 6f97f2ad0..9706c511c 100644 --- a/src/poetry/core/packages/dependency.py +++ b/src/poetry/core/packages/dependency.py @@ -447,7 +447,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): @@ -511,6 +515,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: """ @@ -526,7 +531,9 @@ 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(): return DirectoryDependency(name, path, base=base, extras=extras) diff --git a/src/poetry/core/packages/file_dependency.py b/src/poetry/core/packages/file_dependency.py index a5dade981..0835ab1c6 100644 --- a/src/poetry/core/packages/file_dependency.py +++ b/src/poetry/core/packages/file_dependency.py @@ -15,6 +15,8 @@ def __init__( self, name: str, path: Path, + *, + directory: str | None = None, groups: Iterable[str] | None = None, optional: bool = False, base: Path | None = None, @@ -23,6 +25,7 @@ def __init__( self._path = path self._base = base or Path.cwd() self._full_path = path + self._directory = directory if not self._path.is_absolute(): try: @@ -44,6 +47,7 @@ def __init__( allows_prereleases=True, source_type="file", source_url=self._full_path.as_posix(), + source_subdirectory=directory, extras=extras, ) @@ -59,6 +63,10 @@ def path(self) -> Path: def full_path(self) -> Path: return self._full_path + @property + def directory(self) -> str | None: + return self._directory + def is_file(self) -> bool: return True @@ -83,4 +91,7 @@ def base_pep_508_name(self) -> str: ) requirement += f" @ {path}" + if self.directory: + requirement += f"#subdirectory={self.directory}" + return requirement diff --git a/src/poetry/core/packages/package.py b/src/poetry/core/packages/package.py index 9c87eda9a..cf36b30f8 100644 --- a/src/poetry/core/packages/package.py +++ b/src/poetry/core/packages/package.py @@ -495,6 +495,7 @@ def to_dependency(self) -> Dependency: dep = FileDependency( self._name, Path(cast(str, self._source_url)), + directory=self.source_subdirectory, groups=list(self._dependency_groups.keys()), optional=self.optional, base=self.root_dir, 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..661936083 --- /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-py2.py3-none-any.whl", subdirectory = "sub", platform = "linux" }, + { file = "../distributions/demo-0.1.0-py2.py3-none-any.whl", 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_file_dependency.py b/tests/packages/test_file_dependency.py index ecbf06fb7..97ac3326f 100644 --- a/tests/packages/test_file_dependency.py +++ b/tests/packages/test_file_dependency.py @@ -149,7 +149,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 610870a57..19e31c1c8 100644 --- a/tests/packages/test_package.py +++ b/tests/packages/test_package.py @@ -365,6 +365,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() @@ -377,6 +378,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 3849d4d4d..61ee1410f 100644 --- a/tests/test_factory.py +++ b/tests/test_factory.py @@ -8,6 +8,8 @@ import pytest 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.packages.vcs_dependency import VCSDependency from poetry.core.semver.helpers import parse_constraint @@ -146,6 +148,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-py2.py3-none-any.whl") + 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"