diff --git a/src/poetry/installation/executor.py b/src/poetry/installation/executor.py index fadd1bc2e92..c582b0d9ea7 100644 --- a/src/poetry/installation/executor.py +++ b/src/poetry/installation/executor.py @@ -535,6 +535,9 @@ def _install_directory(self, operation: Install | Update) -> int: else: req = Path(package.source_url).resolve(strict=False) + if package.source_subdirectory: + req /= package.source_subdirectory + pyproject = PyProjectTOML(os.path.join(req, "pyproject.toml")) if pyproject.is_poetry_project(): @@ -762,6 +765,8 @@ def _create_git_url_reference(self, package: Package) -> dict[str, Any]: "commit_id": package.source_resolved_reference, }, } + if package.source_subdirectory: + reference["subdirectory"] = package.source_subdirectory return reference diff --git a/src/poetry/installation/pip_installer.py b/src/poetry/installation/pip_installer.py index 536f6cdce04..21c0e013771 100644 --- a/src/poetry/installation/pip_installer.py +++ b/src/poetry/installation/pip_installer.py @@ -183,6 +183,9 @@ def requirement(self, package: Package, formatted: bool = False) -> str | list[s f"#egg={package.name}" ) + if package.source_subdirectory: + req += f"&subdirectory={package.source_subdirectory}" + if package.develop: return ["-e", req] @@ -217,6 +220,9 @@ def install_directory(self, package: Package) -> str | int: else: req = Path(package.source_url).resolve(strict=False) + if package.source_subdirectory: + req /= package.source_subdirectory + pyproject = PyProjectTOML(os.path.join(req, "pyproject.toml")) if pyproject.is_poetry_project(): @@ -278,9 +284,13 @@ def install_git(self, package: Package) -> None: ) # Now we just need to install from the source directory - pkg = Package(package.name, package.version) - pkg._source_type = "directory" - pkg._source_url = str(source.path) - pkg.develop = package.develop + pkg = Package( + name=package.name, + version=package.version, + source_type="directory", + source_url=str(source.path), + source_subdirectory=package.source_subdirectory, + develop=package.develop, + ) self.install_directory(pkg) diff --git a/src/poetry/packages/locker.py b/src/poetry/packages/locker.py index 25dd14eb522..37be4e606d2 100644 --- a/src/poetry/packages/locker.py +++ b/src/poetry/packages/locker.py @@ -126,6 +126,7 @@ def locked_repository(self) -> Repository: source_url=url, source_reference=source.get("reference"), source_resolved_reference=source.get("resolved_reference"), + source_subdirectory=source.get("subdirectory"), ) package.description = info.get("description", "") package.category = info.get("category", "main") @@ -620,6 +621,9 @@ def _dump_package(self, package: Package) -> dict[str, Any]: if package.source_resolved_reference: data["source"]["resolved_reference"] = package.source_resolved_reference + if package.source_subdirectory: + data["source"]["subdirectory"] = package.source_subdirectory + if package.source_type in ["directory", "git"]: data["develop"] = package.develop diff --git a/src/poetry/repositories/installed_repository.py b/src/poetry/repositories/installed_repository.py index 6cfea588bd7..228df3533f7 100644 --- a/src/poetry/repositories/installed_repository.py +++ b/src/poetry/repositories/installed_repository.py @@ -124,6 +124,7 @@ def create_package_from_distribution( source_url = None source_reference = None source_resolved_reference = None + source_subdirectory = None if is_standard_package: if path.name.endswith(".dist-info"): paths = cls.get_package_paths( @@ -169,6 +170,7 @@ def create_package_from_distribution( source_url=source_url, source_reference=source_reference, source_resolved_reference=source_resolved_reference, + source_subdirectory=source_subdirectory, ) package.description = distribution.metadata.get( # type: ignore[attr-defined] @@ -185,6 +187,7 @@ def create_package_from_pep610(cls, distribution: metadata.Distribution) -> Pack source_url = None source_reference = None source_resolved_reference = None + source_subdirectory = None develop = False url_reference = json.loads( @@ -213,6 +216,7 @@ def create_package_from_pep610(cls, distribution: metadata.Distribution) -> Pack source_reference = url_reference["vcs_info"].get( "requested_revision", source_resolved_reference ) + source_subdirectory = url_reference.get("subdirectory") package = Package( distribution.metadata["name"], @@ -221,6 +225,7 @@ def create_package_from_pep610(cls, distribution: metadata.Distribution) -> Pack source_url=source_url, source_reference=source_reference, source_resolved_reference=source_resolved_reference, + source_subdirectory=source_subdirectory, develop=develop, ) diff --git a/tests/console/commands/self/test_add_plugins.py b/tests/console/commands/self/test_add_plugins.py index 0259342a8fd..e8447a32b13 100644 --- a/tests/console/commands/self/test_add_plugins.py +++ b/tests/console/commands/self/test_add_plugins.py @@ -138,6 +138,53 @@ def test_add_with_git_constraint_with_extras( ) +@pytest.mark.parametrize( + "url, rev", + [ + ("git+https://github.com/demo/poetry-plugin2.git#subdirectory=subdir", None), + ( + "git+https://github.com/demo/poetry-plugin2.git@master#subdirectory=subdir", + "master", + ), + ], +) +def test_add_with_git_constraint_with_subdirectory( + url: str, + rev: str | None, + tester: CommandTester, + repo: TestRepository, +): + repo.add_package(Package("pendulum", "2.0.5")) + + tester.execute(url) + + expected = """ +Updating dependencies +Resolving dependencies... + +Writing lock file + +Package operations: 2 installs, 0 updates, 0 removals + + • Installing pendulum (2.0.5) + • Installing poetry-plugin (0.1.2 9cf87a2) +""" + + constraint = { + "git": "https://github.com/demo/poetry-plugin2.git", + "subdirectory": "subdir", + } + + if rev: + constraint["rev"] = rev + + assert_plugin_add_result( + tester, + expected, + constraint, + ) + + def test_add_existing_plugin_warns_about_no_operation( tester: CommandTester, repo: TestRepository, diff --git a/tests/console/commands/test_add.py b/tests/console/commands/test_add.py index 17a6e6977cf..27f803d3915 100644 --- a/tests/console/commands/test_add.py +++ b/tests/console/commands/test_add.py @@ -374,6 +374,53 @@ def test_add_git_constraint_with_extras( } +@pytest.mark.parametrize( + "url, rev", + [ + ("git+https://github.com/demo/subdirectories.git#subdirectory=two", None), + ( + "git+https://github.com/demo/subdirectories.git@master#subdirectory=two", + "master", + ), + ], +) +def test_add_git_constraint_with_subdirectory( + url: str, + rev: str | None, + app: PoetryTestApplication, + repo: TestRepository, + tester: CommandTester, + env: MockEnv, +): + tester.execute(url) + + expected = """\ +Updating dependencies +Resolving dependencies... + +Writing lock file + +Package operations: 1 install, 0 updates, 0 removals + + • Installing two (2.0.0 9cf87a2) +""" + assert tester.io.fetch_output().strip() == expected.strip() + assert tester.command.installer.executor.installations_count == 1 + + content = app.poetry.file.read()["tool"]["poetry"] + + constraint = { + "git": "https://github.com/demo/subdirectories.git", + "subdirectory": "two", + } + + if rev: + constraint["rev"] = rev + + assert "two" in content["dependencies"] + assert content["dependencies"]["two"] == constraint + + @pytest.mark.parametrize("editable", [False, True]) def test_add_git_ssh_constraint( editable: bool, diff --git a/tests/fixtures/git/github.com/demo/poetry-plugin2/subdir/poetry_plugin/__init__.py b/tests/fixtures/git/github.com/demo/poetry-plugin2/subdir/poetry_plugin/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/fixtures/git/github.com/demo/poetry-plugin2/subdir/pyproject.toml b/tests/fixtures/git/github.com/demo/poetry-plugin2/subdir/pyproject.toml new file mode 100644 index 00000000000..b45d9d976eb --- /dev/null +++ b/tests/fixtures/git/github.com/demo/poetry-plugin2/subdir/pyproject.toml @@ -0,0 +1,18 @@ +[tool.poetry] +name = "poetry-plugin" +version = "0.1.2" +description = "Some description." +authors = [ + "Sébastien Eustace " +] +license = "MIT" + +[tool.poetry.dependencies] +python = "^3.6" +pendulum = "^2.0" +tomlkit = {version = "^0.7.0", optional = true} + +[tool.poetry.extras] +foo = ["tomlkit"] + +[tool.poetry.dev-dependencies] diff --git a/tests/fixtures/git/github.com/demo/subdirectories/one-copy/one/__init__.py b/tests/fixtures/git/github.com/demo/subdirectories/one-copy/one/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/fixtures/git/github.com/demo/subdirectories/one-copy/pyproject.toml b/tests/fixtures/git/github.com/demo/subdirectories/one-copy/pyproject.toml index 39265efe4a3..1548c3a33a1 100644 --- a/tests/fixtures/git/github.com/demo/subdirectories/one-copy/pyproject.toml +++ b/tests/fixtures/git/github.com/demo/subdirectories/one-copy/pyproject.toml @@ -7,3 +7,7 @@ license = "MIT" [tool.poetry.dependencies] python = "^3.7" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/tests/fixtures/git/github.com/demo/subdirectories/one/one/__init__.py b/tests/fixtures/git/github.com/demo/subdirectories/one/one/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/fixtures/git/github.com/demo/subdirectories/one/pyproject.toml b/tests/fixtures/git/github.com/demo/subdirectories/one/pyproject.toml index 39265efe4a3..1548c3a33a1 100644 --- a/tests/fixtures/git/github.com/demo/subdirectories/one/pyproject.toml +++ b/tests/fixtures/git/github.com/demo/subdirectories/one/pyproject.toml @@ -7,3 +7,7 @@ license = "MIT" [tool.poetry.dependencies] python = "^3.7" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/tests/fixtures/git/github.com/demo/subdirectories/two/pyproject.toml b/tests/fixtures/git/github.com/demo/subdirectories/two/pyproject.toml index 58fde435649..6a54d8938ff 100644 --- a/tests/fixtures/git/github.com/demo/subdirectories/two/pyproject.toml +++ b/tests/fixtures/git/github.com/demo/subdirectories/two/pyproject.toml @@ -6,4 +6,8 @@ authors = [] license = "MIT" [tool.poetry.dependencies] -python = "^3.7" +python = "~2.7 || ^3.4" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/tests/fixtures/git/github.com/demo/subdirectories/two/two/__init__.py b/tests/fixtures/git/github.com/demo/subdirectories/two/two/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/installation/test_executor.py b/tests/installation/test_executor.py index 6d67c36770f..7f60905a33c 100644 --- a/tests/installation/test_executor.py +++ b/tests/installation/test_executor.py @@ -553,6 +553,40 @@ def test_executor_should_write_pep610_url_references_for_git( ) +def test_executor_should_write_pep610_url_references_for_git_with_subdirectories( + tmp_venv: VirtualEnv, + pool: Pool, + config: Config, + io: BufferedIO, + mock_file_downloads: None, +): + package = Package( + "two", + "2.0.0", + source_type="git", + source_reference="master", + source_resolved_reference="123456", + source_url="https://github.com/demo/subdirectories.git", + source_subdirectory="two", + ) + + executor = Executor(tmp_venv, pool, config, io) + executor.execute([Install(package)]) + verify_installed_distribution( + tmp_venv, + package, + { + "vcs_info": { + "vcs": "git", + "requested_revision": "master", + "commit_id": "123456", + }, + "url": package.source_url, + "subdirectory": package.source_subdirectory, + }, + ) + + def test_executor_should_use_cached_link_and_hash( tmp_venv: VirtualEnv, pool: Pool, diff --git a/tests/installation/test_pip_installer.py b/tests/installation/test_pip_installer.py index 32a6acc83b1..4763d71bb42 100644 --- a/tests/installation/test_pip_installer.py +++ b/tests/installation/test_pip_installer.py @@ -38,6 +38,20 @@ def package_git() -> Package: return package +@pytest.fixture +def package_git_with_subdirectory() -> Package: + package = Package( + "subdirectories", + "2.0.0", + source_type="git", + source_url="https://github.com/demo/subdirectories.git", + source_reference="master", + source_subdirectory="two", + ) + + return package + + @pytest.fixture def pool() -> Pool: return Pool() @@ -85,6 +99,24 @@ def test_requirement_source_type_url(): assert result == expected +def test_requirement_git_subdirectory( + pool: Pool, package_git_with_subdirectory: Package +) -> None: + null_env = NullEnv() + installer = PipInstaller(null_env, NullIO(), pool) + result = installer.requirement(package_git_with_subdirectory) + expected = ( + "git+https://github.com/demo/subdirectories.git" + "@master#egg=subdirectories&subdirectory=two" + ) + + assert result == expected + installer.install(package_git_with_subdirectory) + assert len(null_env.executed) == 1 + cmd = null_env.executed[0] + assert Path(cmd[-1]).parts[-3:] == ("demo", "subdirectories", "two") + + def test_requirement_git_develop_false(installer: PipInstaller, package_git: Package): package_git.develop = False result = installer.requirement(package_git) diff --git a/tests/packages/test_locker.py b/tests/packages/test_locker.py index 477515f60bb..e0ed720bf94 100644 --- a/tests/packages/test_locker.py +++ b/tests/packages/test_locker.py @@ -57,6 +57,15 @@ def test_lock_file_data_is_ordered(locker: Locker, root: ProjectPackage): source_reference="develop", source_resolved_reference="123456", ) + package_git_with_subdirectory = Package( + "git-package-subdir", + "1.2.3", + source_type="git", + source_url="https://github.com/python-poetry/poetry.git", + source_reference="develop", + source_resolved_reference="123456", + source_subdirectory="subdir", + ) package_url_linux = Package( "url-package", "1.0", @@ -74,6 +83,7 @@ def test_lock_file_data_is_ordered(locker: Locker, root: ProjectPackage): package_a, get_package("B", "1.2"), package_git, + package_git_with_subdirectory, package_url_win32, package_url_linux, ] @@ -126,6 +136,22 @@ def test_lock_file_data_is_ordered(locker: Locker, root: ProjectPackage): reference = "develop" resolved_reference = "123456" +[[package]] +name = "git-package-subdir" +version = "1.2.3" +description = "" +category = "main" +optional = false +python-versions = "*" +develop = false + +[package.source] +type = "git" +url = "https://github.com/python-poetry/poetry.git" +reference = "develop" +resolved_reference = "123456" +subdirectory = "subdir" + [[package]] name = "url-package" version = "1.0" @@ -163,6 +189,7 @@ def test_lock_file_data_is_ordered(locker: Locker, root: ProjectPackage): ] B = [] git-package = [] +git-package-subdir = [] url-package = [] """ @@ -344,6 +371,44 @@ def test_locker_properly_loads_extras_legacy(locker: Locker): assert dependency_b.name == "b" +def test_locker_properly_loads_subdir(locker: Locker) -> None: + content = """\ +[[package]] +name = "git-package-subdir" +version = "1.2.3" +description = "" +category = "main" +optional = false +python-versions = "*" +develop = false + +[package.source] +type = "git" +url = "https://github.com/python-poetry/poetry.git" +reference = "develop" +resolved_reference = "123456" +subdirectory = "subdir" + +[metadata] +lock-version = "1.1" +python-versions = "*" +content-hash = "115cf985d932e9bf5f540555bbdd75decbb62cac81e399375fc19f6277f8c1d8" + +[metadata.files] +git-package-subdir = [] +""" + locker.lock.write(tomlkit.parse(content)) + + repository = locker.locked_repository() + assert len(repository.packages) == 1 + + packages = repository.find_packages(get_dependency("git-package-subdir", "1.2.3")) + assert len(packages) == 1 + + package = packages[0] + assert package.source_subdirectory == "subdir" + + def test_lock_packages_with_null_description(locker: Locker, root: ProjectPackage): package_a = get_package("A", "1.0.0") package_a.description = None @@ -668,6 +733,51 @@ def test_locker_dumps_dependency_information_correctly( assert content == expected +def test_locker_dumps_subdir(locker: Locker, root: ProjectPackage) -> None: + package_git_with_subdirectory = Package( + "git-package-subdir", + "1.2.3", + source_type="git", + source_url="https://github.com/python-poetry/poetry.git", + source_reference="develop", + source_resolved_reference="123456", + source_subdirectory="subdir", + ) + + locker.set_lock_data(root, [package_git_with_subdirectory]) + + with locker.lock.open(encoding="utf-8") as f: + content = f.read() + + expected = """\ +[[package]] +name = "git-package-subdir" +version = "1.2.3" +description = "" +category = "main" +optional = false +python-versions = "*" +develop = false + +[package.source] +type = "git" +url = "https://github.com/python-poetry/poetry.git" +reference = "develop" +resolved_reference = "123456" +subdirectory = "subdir" + +[metadata] +lock-version = "1.1" +python-versions = "*" +content-hash = "115cf985d932e9bf5f540555bbdd75decbb62cac81e399375fc19f6277f8c1d8" + +[metadata.files] +git-package-subdir = [] +""" + + assert content == expected + + def test_locked_repository_uses_root_dir_of_package( locker: Locker, mocker: MockerFixture ): diff --git a/tests/repositories/fixtures/installed/lib/python3.7/site-packages/git_pep_610_subdirectory-1.2.3.dist-info/METADATA b/tests/repositories/fixtures/installed/lib/python3.7/site-packages/git_pep_610_subdirectory-1.2.3.dist-info/METADATA new file mode 100644 index 00000000000..551158eb9ba --- /dev/null +++ b/tests/repositories/fixtures/installed/lib/python3.7/site-packages/git_pep_610_subdirectory-1.2.3.dist-info/METADATA @@ -0,0 +1,6 @@ +Metadata-Version: 2.1 +Name: git-pep-610-subdirectory +Version: 1.2.3 +Summary: Foo +License: MIT +Requires-Python: >=3.6 diff --git a/tests/repositories/fixtures/installed/lib/python3.7/site-packages/git_pep_610_subdirectory-1.2.3.dist-info/direct_url.json b/tests/repositories/fixtures/installed/lib/python3.7/site-packages/git_pep_610_subdirectory-1.2.3.dist-info/direct_url.json new file mode 100644 index 00000000000..6b6c93ab265 --- /dev/null +++ b/tests/repositories/fixtures/installed/lib/python3.7/site-packages/git_pep_610_subdirectory-1.2.3.dist-info/direct_url.json @@ -0,0 +1,9 @@ +{ + "url": "https://github.com/demo/git-pep-610-subdirectory.git", + "vcs_info": { + "vcs": "git", + "requested_revision": "my-branch", + "commit_id": "123456" + }, + "subdirectory": "subdir" +} diff --git a/tests/repositories/test_installed_repository.py b/tests/repositories/test_installed_repository.py index 599c432e858..87dfe183c8a 100644 --- a/tests/repositories/test_installed_repository.py +++ b/tests/repositories/test_installed_repository.py @@ -39,6 +39,9 @@ metadata.PathDistribution( SITE_PURELIB / "git_pep_610_no_requested_version-1.2.3.dist-info" ), + metadata.PathDistribution( + SITE_PURELIB / "git_pep_610_subdirectory-1.2.3.dist-info" + ), metadata.PathDistribution(SITE_PURELIB / "url_pep_610-1.2.3.dist-info"), metadata.PathDistribution(SITE_PURELIB / "file_pep_610-1.2.3.dist-info"), metadata.PathDistribution(SITE_PURELIB / "directory_pep_610-1.2.3.dist-info"), @@ -238,6 +241,20 @@ def test_load_pep_610_compliant_git_packages_no_requested_version( assert package.source_reference == package.source_resolved_reference +def test_load_pep_610_compliant_git_packages_with_subdirectory( + repository: InstalledRepository, +): + package = get_package_from_repository("git-pep-610-subdirectory", repository) + assert package is not None + assert package.name == "git-pep-610-subdirectory" + assert package.version.text == "1.2.3" + assert package.source_type == "git" + assert package.source_url == "https://github.com/demo/git-pep-610-subdirectory.git" + assert package.source_reference == "my-branch" + assert package.source_resolved_reference == "123456" + assert package.source_subdirectory == "subdir" + + def test_load_pep_610_compliant_url_packages(repository: InstalledRepository): package = get_package_from_repository("url-pep-610", repository) diff --git a/tests/utils/test_dependency_specification.py b/tests/utils/test_dependency_specification.py index c536830af25..95cdffea6bf 100644 --- a/tests/utils/test_dependency_specification.py +++ b/tests/utils/test_dependency_specification.py @@ -35,6 +35,15 @@ "git+https://github.com/demo/demo.git@main", {"git": "https://github.com/demo/demo.git", "name": "demo", "rev": "main"}, ), + ( + "git+https://github.com/demo/subdirectories.git@main#subdirectory=two", + { + "git": "https://github.com/demo/subdirectories.git", + "name": "two", + "rev": "main", + "subdirectory": "two", + }, + ), ("demo", {"name": "demo"}), ("demo@1.0.0", {"name": "demo", "version": "1.0.0"}), ("demo@^1.0.0", {"name": "demo", "version": "^1.0.0"}),