diff --git a/poetry/core/json/schemas/poetry-schema.json b/poetry/core/json/schemas/poetry-schema.json index 10aff39e5..7a558d50c 100644 --- a/poetry/core/json/schemas/poetry-schema.json +++ b/poetry/core/json/schemas/poetry-schema.json @@ -296,6 +296,10 @@ "type": "string", "description": "The revision to checkout." }, + "subdirectory": { + "type": "string", + "description": "path relative to repositories root, where package is located" + }, "python": { "type": "string", "description": "The python versions for which the dependency should be installed." diff --git a/poetry/core/packages/package.py b/poetry/core/packages/package.py index 023b7e7dc..4b683818d 100644 --- a/poetry/core/packages/package.py +++ b/poetry/core/packages/package.py @@ -64,6 +64,7 @@ def __init__(self, name, version, pretty_version=None): self.source_type = "" self.source_reference = "" self.source_url = "" + self.source_subdirectory = "" self.requires = [] self.dev_requires = [] @@ -299,6 +300,7 @@ def add_dependency( rev=constraint.get("rev", None), category=category, optional=optional, + subdirectory=constraint.get("subdirectory", None), ) elif "file" in constraint: file_path = Path(constraint["file"]) diff --git a/poetry/core/packages/vcs_dependency.py b/poetry/core/packages/vcs_dependency.py index 119afb64a..aef60464d 100644 --- a/poetry/core/packages/vcs_dependency.py +++ b/poetry/core/packages/vcs_dependency.py @@ -16,8 +16,9 @@ def __init__( branch=None, tag=None, rev=None, - category="main", optional=False, + category="main", + subdirectory=None, ): self._vcs = vcs self._source = source @@ -29,6 +30,7 @@ def __init__( self._branch = branch self._tag = tag self._rev = rev + self._subdirectory = subdirectory super(VCSDependency, self).__init__( name, "*", category=category, optional=optional, allows_prereleases=True @@ -54,6 +56,10 @@ def tag(self): def rev(self): return self._rev + @property + def subdirectory(self): + return self._subdirectory + @property def reference(self): # type: () -> str return self._branch or self._tag or self._rev @@ -87,6 +93,9 @@ def base_pep_508_name(self): # type: () -> str self._vcs, parsed_url.format(), self.reference ) + if self.subdirectory: + requirement += "#subdirectory={}".format(self.subdirectory) + return requirement def is_vcs(self): # type: () -> bool diff --git a/poetry/core/vcs/git.py b/poetry/core/vcs/git.py index 566a7f6be..bdfb9fa84 100644 --- a/poetry/core/vcs/git.py +++ b/poetry/core/vcs/git.py @@ -14,7 +14,8 @@ "port": r"\d+", "path": r"[\w~.\-/\\]+", "name": r"[\w~.\-]+", - "rev": r"[^@#]+", + "rev": r"[^@#?]+", + "subdir": r"[\w\-/\\]+", } PATTERNS = [ @@ -27,6 +28,7 @@ r"(?P[:/\\]({path}[/\\])?" r"((?P{name}?)(\.git|[/\\])?)?)" r"([@#](?P{rev}))?" + r"(\?subdirectory=(?P{subdir}))?" r"$".format( user=pattern_formats["user"], resource=pattern_formats["resource"], @@ -34,6 +36,7 @@ path=pattern_formats["path"], name=pattern_formats["name"], rev=pattern_formats["rev"], + subdir=pattern_formats["subdir"], ) ), re.compile( @@ -45,6 +48,7 @@ r"(?P({path})" r"(?P{name})(\.git|/)?)" r"([@#](?P{rev}))?" + r"(\?subdirectory=(?P{subdir}))?" r"$".format( protocol=pattern_formats["protocol"], user=pattern_formats["user"], @@ -53,6 +57,7 @@ path=pattern_formats["path"], name=pattern_formats["name"], rev=pattern_formats["rev"], + subdir=pattern_formats["subdir"], ) ), re.compile( @@ -62,6 +67,7 @@ r"(?P([:/]{path}/)" r"(?P{name})(\.git|/)?)" r"([@#](?P{rev}))?" + r"(\?subdirectory=(?P{subdir}))?" r"$".format( user=pattern_formats["user"], resource=pattern_formats["resource"], @@ -69,6 +75,7 @@ path=pattern_formats["path"], name=pattern_formats["name"], rev=pattern_formats["rev"], + subdir=pattern_formats["subdir"], ) ), re.compile( @@ -78,19 +85,23 @@ r"(?P({path})" r"(?P{name})(\.git|/)?)" r"([@#](?P{rev}))?" + r"(\?subdirectory=(?P{subdir}))?" r"$".format( user=pattern_formats["user"], resource=pattern_formats["resource"], path=pattern_formats["path"], name=pattern_formats["name"], rev=pattern_formats["rev"], + subdir=pattern_formats["subdir"], ) ), ] class ParsedUrl: - def __init__(self, protocol, resource, pathname, user, port, name, rev): + def __init__( + self, protocol, resource, pathname, user, port, name, rev, subdirectory + ): self.protocol = protocol self.resource = resource self.pathname = pathname @@ -98,6 +109,7 @@ def __init__(self, protocol, resource, pathname, user, port, name, rev): self.port = port self.name = name self.rev = rev + self.subdirectory = subdirectory @classmethod def parse(cls, url): # type: () -> ParsedUrl @@ -113,6 +125,7 @@ def parse(cls, url): # type: () -> ParsedUrl groups.get("port"), groups.get("name"), groups.get("rev"), + groups.get("subdirectory"), ) raise ValueError('Invalid git url "{}"'.format(url)) @@ -128,13 +141,17 @@ def url(self): # type: () -> str ) def format(self): - return "{}".format(self.url, "#{}".format(self.rev) if self.rev else "",) + return "{}".format( + self.url, + "#{}".format(self.rev) if self.rev else "", + "?subdirectory={}".format(self.subdirectory) if self.subdirectory else "", + ) def __str__(self): # type: () -> str return self.format() -GitUrl = namedtuple("GitUrl", ["url", "revision"]) +GitUrl = namedtuple("GitUrl", ["url", "revision", "subdirectory"]) class GitConfig: @@ -174,7 +191,11 @@ def normalize_url(cls, url): # type: (str) -> GitUrl formatted = re.sub(r"^git\+", "", url) if parsed.rev: - formatted = re.sub(r"[#@]{}$".format(parsed.rev), "", formatted) + formatted = re.sub(r"[#@]{}(\?.*)?$".format(parsed.rev), "", formatted) + if parsed.subdirectory: + formatted = re.sub( + r"\?subdirectory={}$".format(parsed.subdirectory), "", formatted + ) altered = parsed.format() != formatted @@ -186,11 +207,13 @@ def normalize_url(cls, url): # type: (str) -> GitUrl elif re.match(r"^git\+file", url): normalized = re.sub(r"git\+", "", url) else: - normalized = re.sub(r"^(?:git\+)?ssh://", "", url) + normalized = re.sub(r"^(?:git\+)?ssh://", "", formatted) else: normalized = parsed.format() - return GitUrl(re.sub(r"#[^#]*$", "", normalized), parsed.rev) + return GitUrl( + re.sub(r"#[^#]*$", "", normalized), parsed.rev, parsed.subdirectory + ) @property def config(self): # type: () -> GitConfig @@ -229,6 +252,8 @@ def rev_parse(self, rev, folder=None): # type: (...) -> str folder.as_posix(), ] + # We need "^{commit}" to ensure that the commit SHA of the commit the + # tag points to is returned, even in the case of annotated tags. args += ["rev-parse", rev + "^{commit}"] return self.run(*args) diff --git a/tests/packages/test_package.py b/tests/packages/test_package.py index e6eeb7eb3..8edaca206 100644 --- a/tests/packages/test_package.py +++ b/tests/packages/test_package.py @@ -4,6 +4,7 @@ import pytest from poetry.core.packages import Package +from poetry.core.packages import VCSDependency def test_package_authors(): @@ -37,3 +38,18 @@ def test_package_add_dependency_vcs_category_default_main(): "poetry", constraint={"git": "https://github.com/python-poetry/poetry.git"} ) assert dependency.category == "main" + + +def test_package_add_dependency_vcs__with_subdirectory(): + package = Package("foo", "0.1.0") + + dependency = package.add_dependency( + "poetry", + constraint={ + "git": "https://github.com/demo/project_in_subdirectory.git", + "subdirectory": "mypackage", + }, + ) + assert dependency.source == "https://github.com/demo/project_in_subdirectory.git" + assert dependency.subdirectory == "mypackage" + assert isinstance(dependency, VCSDependency) diff --git a/tests/packages/test_vcs_dependency.py b/tests/packages/test_vcs_dependency.py index 94ec591f3..8a2421acf 100644 --- a/tests/packages/test_vcs_dependency.py +++ b/tests/packages/test_vcs_dependency.py @@ -61,6 +61,19 @@ def test_to_pep_508_in_extras(): assert expected == dependency.to_pep_508() +def test_to_pep_508_with_subdirectory(): + dependency = VCSDependency( + "poetry", + "git", + "https://github.com/python-poetry/poetry.git", + subdirectory="mypackage", + ) + + expected = "poetry @ git+https://github.com/python-poetry/poetry.git@master#subdirectory=mypackage" + + assert expected == dependency.to_pep_508() + + @pytest.mark.parametrize("category", ["main", "dev"]) def test_category(category): dependency = VCSDependency( diff --git a/tests/vcs/test_vcs.py b/tests/vcs/test_vcs.py index c2f888fbd..766073d00 100644 --- a/tests/vcs/test_vcs.py +++ b/tests/vcs/test_vcs.py @@ -10,70 +10,106 @@ [ ( "git+ssh://user@hostname:project.git#commit", - GitUrl("user@hostname:project.git", "commit"), + GitUrl("user@hostname:project.git", "commit", None), ), ( "git+http://user@hostname/project/blah.git@commit", - GitUrl("http://user@hostname/project/blah.git", "commit"), + GitUrl("http://user@hostname/project/blah.git", "commit", None), ), ( "git+https://user@hostname/project/blah.git", - GitUrl("https://user@hostname/project/blah.git", None), + GitUrl("https://user@hostname/project/blah.git", None, None), ), ( "git+https://user@hostname/project~_-.foo/blah~_-.bar.git", - GitUrl("https://user@hostname/project~_-.foo/blah~_-.bar.git", None), + GitUrl("https://user@hostname/project~_-.foo/blah~_-.bar.git", None, None), ), ( "git+https://user@hostname:project/blah.git", - GitUrl("https://user@hostname/project/blah.git", None), + GitUrl("https://user@hostname/project/blah.git", None, None), ), ( "git+ssh://git@github.com:sdispater/poetry.git#v1.0.27", - GitUrl("git@github.com:sdispater/poetry.git", "v1.0.27"), + GitUrl("git@github.com:sdispater/poetry.git", "v1.0.27", None), ), ( "git+ssh://git@github.com:/sdispater/poetry.git", - GitUrl("git@github.com:/sdispater/poetry.git", None), + GitUrl("git@github.com:/sdispater/poetry.git", None, None), + ), + ( + "git+ssh://git@github.com:org/repo", + GitUrl("git@github.com:org/repo", None, None), ), - ("git+ssh://git@github.com:org/repo", GitUrl("git@github.com:org/repo", None),), ( "git+ssh://git@github.com/org/repo", - GitUrl("ssh://git@github.com/org/repo", None), + GitUrl("ssh://git@github.com/org/repo", None, None), ), - ("git+ssh://foo:22/some/path", GitUrl("ssh://foo:22/some/path", None)), - ("git@github.com:org/repo", GitUrl("git@github.com:org/repo", None)), + ("git+ssh://foo:22/some/path", GitUrl("ssh://foo:22/some/path", None, None)), + ("git@github.com:org/repo", GitUrl("git@github.com:org/repo", None, None)), ( "git+https://github.com/sdispater/pendulum", - GitUrl("https://github.com/sdispater/pendulum", None), + GitUrl("https://github.com/sdispater/pendulum", None, None), ), ( "git+https://github.com/sdispater/pendulum#7a018f2d075b03a73409e8356f9b29c9ad4ea2c5", GitUrl( "https://github.com/sdispater/pendulum", "7a018f2d075b03a73409e8356f9b29c9ad4ea2c5", + None, ), ), ( "git+ssh://git@git.example.com:b/b.git#v1.0.0", - GitUrl("git@git.example.com:b/b.git", "v1.0.0"), + GitUrl("git@git.example.com:b/b.git", "v1.0.0", None), ), ( "git+ssh://git@github.com:sdispater/pendulum.git#foo/bar", - GitUrl("git@github.com:sdispater/pendulum.git", "foo/bar"), + GitUrl("git@github.com:sdispater/pendulum.git", "foo/bar", None), ), - ("git+file:///foo/bar.git", GitUrl("file:///foo/bar.git", None)), + ("git+file:///foo/bar.git", GitUrl("file:///foo/bar.git", None, None)), ( "git+file://C:\\Users\\hello\\testing.git#zkat/windows-files", - GitUrl("file://C:\\Users\\hello\\testing.git", "zkat/windows-files"), + GitUrl("file://C:\\Users\\hello\\testing.git", "zkat/windows-files", None), ), ( "git+https://git.example.com/sdispater/project/my_repo.git", - GitUrl("https://git.example.com/sdispater/project/my_repo.git", None), + GitUrl("https://git.example.com/sdispater/project/my_repo.git", None, None), ), ( "git+ssh://git@git.example.com:sdispater/project/my_repo.git", - GitUrl("git@git.example.com:sdispater/project/my_repo.git", None), + GitUrl("git@git.example.com:sdispater/project/my_repo.git", None, None), + ), + ( + "git+https://git.example.com/sdispater/project/my_repo.git?subdirectory=path/to/package", + GitUrl( + "https://git.example.com/sdispater/project/my_repo.git", + None, + "path/to/package", + ), + ), + ( + "git+ssh://git@git.example.com:sdispater/project/my_repo.git?subdirectory=path/to/package", + GitUrl( + "git@git.example.com:sdispater/project/my_repo.git", + None, + "path/to/package", + ), + ), + ( + "git+https://git.example.com/sdispater/project/my_repo.git#dev?subdirectory=path/to/package", + GitUrl( + "https://git.example.com/sdispater/project/my_repo.git", + "dev", + "path/to/package", + ), + ), + ( + "git+ssh://git@git.example.com:sdispater/project/my_repo.git#dev?subdirectory=path/to/package", + GitUrl( + "git@git.example.com:sdispater/project/my_repo.git", + "dev", + "path/to/package", + ), ), ], ) @@ -87,19 +123,40 @@ def test_normalize_url(url, normalized): ( "git+ssh://user@hostname:project.git#commit", ParsedUrl( - "ssh", "hostname", ":project.git", "user", None, "project", "commit" + "ssh", + "hostname", + ":project.git", + "user", + None, + "project", + "commit", + None, ), ), ( "git+http://user@hostname/project/blah.git@commit", ParsedUrl( - "http", "hostname", "/project/blah.git", "user", None, "blah", "commit" + "http", + "hostname", + "/project/blah.git", + "user", + None, + "blah", + "commit", + None, ), ), ( "git+https://user@hostname/project/blah.git", ParsedUrl( - "https", "hostname", "/project/blah.git", "user", None, "blah", None + "https", + "hostname", + "/project/blah.git", + "user", + None, + "blah", + None, + None, ), ), ( @@ -112,12 +169,20 @@ def test_normalize_url(url, normalized): None, "blah~_-.bar", None, + None, ), ), ( "git+https://user@hostname:project/blah.git", ParsedUrl( - "https", "hostname", ":project/blah.git", "user", None, "blah", None + "https", + "hostname", + ":project/blah.git", + "user", + None, + "blah", + None, + None, ), ), ( @@ -130,6 +195,7 @@ def test_normalize_url(url, normalized): None, "poetry", "v1.0.27", + None, ), ), ( @@ -142,23 +208,28 @@ def test_normalize_url(url, normalized): None, "poetry", None, + None, ), ), ( "git+ssh://git@github.com:org/repo", - ParsedUrl("ssh", "github.com", ":org/repo", "git", None, "repo", None), + ParsedUrl( + "ssh", "github.com", ":org/repo", "git", None, "repo", None, None + ), ), ( "git+ssh://git@github.com/org/repo", - ParsedUrl("ssh", "github.com", "/org/repo", "git", None, "repo", None), + ParsedUrl( + "ssh", "github.com", "/org/repo", "git", None, "repo", None, None + ), ), ( "git+ssh://foo:22/some/path", - ParsedUrl("ssh", "foo", "/some/path", None, "22", "path", None), + ParsedUrl("ssh", "foo", "/some/path", None, "22", "path", None, None), ), ( "git@github.com:org/repo", - ParsedUrl(None, "github.com", ":org/repo", "git", None, "repo", None), + ParsedUrl(None, "github.com", ":org/repo", "git", None, "repo", None, None), ), ( "git+https://github.com/sdispater/pendulum", @@ -170,6 +241,7 @@ def test_normalize_url(url, normalized): None, "pendulum", None, + None, ), ), ( @@ -182,11 +254,14 @@ def test_normalize_url(url, normalized): None, "pendulum", "7a018f2d075b03a73409e8356f9b29c9ad4ea2c5", + None, ), ), ( "git+ssh://git@git.example.com:b/b.git#v1.0.0", - ParsedUrl("ssh", "git.example.com", ":b/b.git", "git", None, "b", "v1.0.0"), + ParsedUrl( + "ssh", "git.example.com", ":b/b.git", "git", None, "b", "v1.0.0", None + ), ), ( "git+ssh://git@github.com:sdispater/pendulum.git#foo/bar", @@ -198,11 +273,12 @@ def test_normalize_url(url, normalized): None, "pendulum", "foo/bar", + None, ), ), ( "git+file:///foo/bar.git", - ParsedUrl("file", None, "/foo/bar.git", None, None, "bar", None), + ParsedUrl("file", None, "/foo/bar.git", None, None, "bar", None, None), ), ( "git+file://C:\\Users\\hello\\testing.git#zkat/windows-files", @@ -214,6 +290,7 @@ def test_normalize_url(url, normalized): None, "testing", "zkat/windows-files", + None, ), ), ( @@ -226,6 +303,7 @@ def test_normalize_url(url, normalized): None, "my_repo", None, + None, ), ), ( @@ -238,6 +316,59 @@ def test_normalize_url(url, normalized): None, "my_repo", None, + None, + ), + ), + ( + "git+https://git.example.com/sdispater/project/my_repo.git?subdirectory=path/to/package", + ParsedUrl( + "https", + "git.example.com", + "/sdispater/project/my_repo.git", + None, + None, + "my_repo", + None, + "path/to/package", + ), + ), + ( + "git+ssh://git@git.example.com:sdispater/project/my_repo.git?subdirectory=path/to/package", + ParsedUrl( + "ssh", + "git.example.com", + ":sdispater/project/my_repo.git", + "git", + None, + "my_repo", + None, + "path/to/package", + ), + ), + ( + "git+https://git.example.com/sdispater/project/my_repo.git#dev?subdirectory=path/to/package", + ParsedUrl( + "https", + "git.example.com", + "/sdispater/project/my_repo.git", + None, + None, + "my_repo", + "dev", + "path/to/package", + ), + ), + ( + "git+ssh://git@git.example.com:sdispater/project/my_repo.git#dev?subdirectory=path/to/package", + ParsedUrl( + "ssh", + "git.example.com", + ":sdispater/project/my_repo.git", + "git", + None, + "my_repo", + "dev", + "path/to/package", ), ), ], @@ -252,6 +383,7 @@ def test_parse_url(url, parsed): assert parsed.rev == result.rev assert parsed.url == result.url assert parsed.user == result.user + assert parsed.subdirectory == result.subdirectory def test_parse_url_should_fail():