-
Notifications
You must be signed in to change notification settings - Fork 2.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor dependency specification parsing logic
This change moves, cleans up and refactors dependency specification parsing logic from `InitCommand` to `poetry.utils.dependency_specification`. This is done to improve usability and maintainability of this logic.
- Loading branch information
Showing
3 changed files
with
226 additions
and
128 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
from __future__ import annotations | ||
|
||
import os | ||
import re | ||
import urllib.parse | ||
|
||
from pathlib import Path | ||
from typing import TYPE_CHECKING | ||
from typing import Union | ||
|
||
from poetry.puzzle.provider import Provider | ||
|
||
|
||
if TYPE_CHECKING: | ||
from poetry.utils.env import Env | ||
|
||
|
||
DependencySpec = dict[str, Union[str, dict[str, Union[str, bool]], list[str]]] | ||
|
||
|
||
def _parse_dependency_specification_git_url( | ||
requirement: str, env: Env | None = None | ||
) -> DependencySpec | None: | ||
from poetry.core.vcs.git import Git | ||
from poetry.core.vcs.git import ParsedUrl | ||
|
||
parsed = ParsedUrl.parse(requirement) | ||
url = Git.normalize_url(requirement) | ||
|
||
pair = {"name": parsed.name, "git": url.url} | ||
if parsed.rev: | ||
pair["rev"] = url.revision | ||
|
||
source_root = env.path.joinpath("src") if env else None | ||
package = Provider.get_package_from_vcs( | ||
"git", url=url.url, rev=pair.get("rev"), source_root=source_root | ||
) | ||
pair["name"] = package.name | ||
return pair | ||
|
||
|
||
def _parse_dependency_specification_url( | ||
requirement: str, env: Env | None = None | ||
) -> DependencySpec | None: | ||
url_parsed = urllib.parse.urlparse(requirement) | ||
if not (url_parsed.scheme and url_parsed.netloc): | ||
return None | ||
|
||
if url_parsed.scheme in ["git+https", "git+ssh"]: | ||
return _parse_dependency_specification_git_url(requirement, env) | ||
|
||
if url_parsed.scheme in ["http", "https"]: | ||
package = Provider.get_package_from_url(requirement) | ||
return {"name": package.name, "url": package.source_url} | ||
|
||
return None | ||
|
||
|
||
def _parse_dependency_specification_path( | ||
requirement: str, cwd: Path | ||
) -> DependencySpec | None: | ||
if (os.path.sep in requirement or "/" in requirement) and ( | ||
cwd.joinpath(requirement).exists() | ||
or Path(requirement).expanduser().exists() | ||
and Path(requirement).expanduser().is_absolute() | ||
): | ||
path = Path(requirement).expanduser() | ||
is_absolute = path.is_absolute() | ||
|
||
if not path.is_absolute(): | ||
path = cwd.joinpath(requirement) | ||
|
||
if path.is_file(): | ||
package = Provider.get_package_from_file(path.resolve()) | ||
else: | ||
package = Provider.get_package_from_directory(path.resolve()) | ||
|
||
return { | ||
"name": package.name, | ||
"path": path.relative_to(cwd).as_posix() | ||
if not is_absolute | ||
else path.as_posix(), | ||
} | ||
|
||
return None | ||
|
||
|
||
def _parse_dependency_specification_simple( | ||
requirement: str, | ||
) -> DependencySpec | None: | ||
extras: list[str] = [] | ||
pair = re.sub("^([^@=: ]+)(?:@|==|(?<![<>~!])=|:| )(.*)$", "\\1 \\2", requirement) | ||
pair = pair.strip() | ||
|
||
require: DependencySpec = {} | ||
|
||
if " " in pair: | ||
name, version = pair.split(" ", 2) | ||
extras_m = re.search(r"\[([\w\d,-_]+)\]$", name) | ||
if extras_m: | ||
extras = [e.strip() for e in extras_m.group(1).split(",")] | ||
name, _ = name.split("[") | ||
|
||
require["name"] = name | ||
if version != "latest": | ||
require["version"] = version | ||
else: | ||
m = re.match(r"^([^><=!: ]+)((?:>=|<=|>|<|!=|~=|~|\^).*)$", requirement.strip()) | ||
if m: | ||
name, constraint = m.group(1), m.group(2) | ||
extras_m = re.search(r"\[([\w\d,-_]+)\]$", name) | ||
if extras_m: | ||
extras = [e.strip() for e in extras_m.group(1).split(",")] | ||
name, _ = name.split("[") | ||
|
||
require["name"] = name | ||
require["version"] = constraint | ||
else: | ||
extras_m = re.search(r"\[([\w\d,-_]+)\]$", pair) | ||
if extras_m: | ||
extras = [e.strip() for e in extras_m.group(1).split(",")] | ||
pair, _ = pair.split("[") | ||
|
||
require["name"] = pair | ||
|
||
if extras: | ||
require["extras"] = extras | ||
|
||
return require | ||
|
||
|
||
def parse_dependency_specification( | ||
requirement: str, env: Env | None = None, cwd: Path | None = None | ||
) -> DependencySpec: | ||
requirement = requirement.strip() | ||
cwd = cwd or Path.cwd() | ||
|
||
extras = [] | ||
extras_m = re.search(r"\[([\w\d,-_ ]+)\]$", requirement) | ||
if extras_m: | ||
extras = [e.strip() for e in extras_m.group(1).split(",")] | ||
requirement, _ = requirement.split("[") | ||
|
||
specification = ( | ||
_parse_dependency_specification_url(requirement, env=env) | ||
or _parse_dependency_specification_path(requirement, cwd=cwd) | ||
or _parse_dependency_specification_simple(requirement) | ||
) | ||
|
||
if specification: | ||
if extras and "extras" not in specification: | ||
specification["extras"] = extras | ||
return specification | ||
|
||
raise ValueError(f"Invalid dependency specification: {requirement}") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
from __future__ import annotations | ||
|
||
from pathlib import Path | ||
from typing import TYPE_CHECKING | ||
|
||
import pytest | ||
|
||
from deepdiff import DeepDiff | ||
|
||
from poetry.utils.dependency_specification import parse_dependency_specification | ||
|
||
|
||
if TYPE_CHECKING: | ||
from pytest_mock import MockerFixture | ||
|
||
from poetry.utils.dependency_specification import DependencySpec | ||
|
||
|
||
@pytest.mark.parametrize( | ||
("requirement", "specification"), | ||
[ | ||
( | ||
"git+https://github.com/demo/demo.git", | ||
{"git": "https://github.com/demo/demo.git", "name": "demo"}, | ||
), | ||
( | ||
"git+ssh://github.com/demo/demo.git", | ||
{"git": "ssh://github.com/demo/demo.git", "name": "demo"}, | ||
), | ||
( | ||
"git+https://github.com/demo/demo.git#main", | ||
{"git": "https://github.com/demo/demo.git", "name": "demo", "rev": "main"}, | ||
), | ||
( | ||
"git+https://github.com/demo/demo.git@main", | ||
{"git": "https://github.com/demo/demo.git", "name": "demo", "rev": "main"}, | ||
), | ||
("demo", {"name": "demo"}), | ||
("[email protected]", {"name": "demo", "version": "1.0.0"}), | ||
("demo@^1.0.0", {"name": "demo", "version": "^1.0.0"}), | ||
("demo[a,b]@1.0.0", {"name": "demo", "version": "1.0.0", "extras": ["a", "b"]}), | ||
("demo[a,b]", {"name": "demo", "extras": ["a", "b"]}), | ||
("../demo", {"name": "demo", "path": "../demo"}), | ||
("../demo/demo.whl", {"name": "demo", "path": "../demo/demo.whl"}), | ||
( | ||
"https://example.com/packages/demo-0.1.0.tar.gz", | ||
{"name": "demo", "url": "https://example.com/packages/demo-0.1.0.tar.gz"}, | ||
), | ||
], | ||
) | ||
def test_parse_dependency_specification( | ||
requirement: str, specification: DependencySpec, mocker: MockerFixture | ||
) -> None: | ||
original = Path.exists | ||
|
||
def _mock(self: Path) -> bool: | ||
if "/" in requirement and self == Path.cwd().joinpath(requirement): | ||
return True | ||
return original(self) | ||
|
||
mocker.patch("pathlib.Path.exists", _mock) | ||
|
||
assert not DeepDiff(parse_dependency_specification(requirement), specification) |