Commit bb97ba2e by Arun Babu Neelicattu Committed by Bjorn Neergaard

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.
parent 402ca3b9
from __future__ import annotations from __future__ import annotations
import os
import re
import sys import sys
import urllib.parse
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
...@@ -15,6 +12,7 @@ from tomlkit import inline_table ...@@ -15,6 +12,7 @@ from tomlkit import inline_table
from poetry.console.commands.command import Command from poetry.console.commands.command import Command
from poetry.console.commands.env_command import EnvCommand from poetry.console.commands.env_command import EnvCommand
from poetry.utils.dependency_specification import parse_dependency_specification
from poetry.utils.helpers import canonicalize_name from poetry.utils.helpers import canonicalize_name
...@@ -402,137 +400,19 @@ You can specify a package in the following forms: ...@@ -402,137 +400,19 @@ You can specify a package in the following forms:
def _parse_requirements(self, requirements: list[str]) -> list[dict[str, Any]]: def _parse_requirements(self, requirements: list[str]) -> list[dict[str, Any]]:
from poetry.core.pyproject.exceptions import PyProjectException from poetry.core.pyproject.exceptions import PyProjectException
from poetry.puzzle.provider import Provider
result = []
try: try:
cwd = self.poetry.file.parent cwd = self.poetry.file.parent
except (PyProjectException, RuntimeError): except (PyProjectException, RuntimeError):
cwd = Path.cwd() cwd = Path.cwd()
for requirement in requirements: return [
requirement = requirement.strip() parse_dependency_specification(
extras = [] requirement=requirement,
extras_m = re.search(r"\[([\w\d,-_ ]+)\]$", requirement) env=self.env if isinstance(self, EnvCommand) and self.env else None,
if extras_m: cwd=cwd,
extras = [e.strip() for e in extras_m.group(1).split(",")]
requirement, _ = requirement.split("[")
url_parsed = urllib.parse.urlparse(requirement)
if url_parsed.scheme and url_parsed.netloc:
# Url
if url_parsed.scheme in ["git+https", "git+ssh"]:
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
if extras:
pair["extras"] = extras
source_root = (
self.env.path.joinpath("src")
if isinstance(self, EnvCommand) and self.env
else None
) )
package = Provider.get_package_from_vcs( for requirement in requirements
"git", url=url.url, rev=pair.get("rev"), source_root=source_root
)
pair["name"] = package.name
result.append(pair)
continue
elif url_parsed.scheme in ["http", "https"]:
package = Provider.get_package_from_url(requirement)
pair = {"name": package.name, "url": package.source_url}
if extras:
pair["extras"] = extras
result.append(pair)
continue
elif (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())
result.append(
dict(
[
("name", package.name),
(
"path",
path.relative_to(cwd).as_posix()
if not is_absolute
else path.as_posix(),
),
] ]
+ ([("extras", extras)] if extras else [])
)
)
continue
pair = re.sub(
"^([^@=: ]+)(?:@|==|(?<![<>~!])=|:| )(.*)$", "\\1 \\2", requirement
)
pair = pair.strip()
require: dict[str, str] = {}
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
result.append(require)
return result
def _format_requirements( def _format_requirements(
self, requirements: list[dict[str, str]] self, requirements: list[dict[str, str]]
......
from __future__ import annotations
import os
import re
import urllib.parse
from pathlib import Path
from typing import TYPE_CHECKING
from typing import Dict
from typing import List
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}")
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"}),
("demo@1.0.0", {"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)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment