From 7d85d60827ca94cd2cf0cc241b624c7239670fdd Mon Sep 17 00:00:00 2001 From: Arun Babu Neelicattu Date: Fri, 6 May 2022 21:45:12 +0200 Subject: [PATCH] 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. --- src/poetry/console/commands/init.py | 136 +--------------- src/poetry/utils/dependency_specification.py | 155 +++++++++++++++++++ tests/utils/test_dependency_specification.py | 63 ++++++++ 3 files changed, 226 insertions(+), 128 deletions(-) create mode 100644 src/poetry/utils/dependency_specification.py create mode 100644 tests/utils/test_dependency_specification.py diff --git a/src/poetry/console/commands/init.py b/src/poetry/console/commands/init.py index 42f8c705210..d59bfe672f2 100644 --- a/src/poetry/console/commands/init.py +++ b/src/poetry/console/commands/init.py @@ -1,9 +1,6 @@ from __future__ import annotations -import os -import re import sys -import urllib.parse from pathlib import Path from typing import TYPE_CHECKING @@ -15,6 +12,7 @@ from poetry.console.commands.command import Command from poetry.console.commands.env_command import EnvCommand +from poetry.utils.dependency_specification import parse_dependency_specification from poetry.utils.helpers import canonicalize_name @@ -402,137 +400,19 @@ def _find_best_version_for_package( def _parse_requirements(self, requirements: list[str]) -> list[dict[str, Any]]: from poetry.core.pyproject.exceptions import PyProjectException - from poetry.puzzle.provider import Provider - - result = [] - try: cwd = self.poetry.file.parent except (PyProjectException, RuntimeError): cwd = Path.cwd() - for requirement in requirements: - requirement = requirement.strip() - 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("[") - - 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( - "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 + return [ + parse_dependency_specification( + requirement=requirement, + env=self.env if isinstance(self, EnvCommand) and self.env else None, + cwd=cwd, ) - 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 + for requirement in requirements + ] def _format_requirements( self, requirements: list[dict[str, str]] diff --git a/src/poetry/utils/dependency_specification.py b/src/poetry/utils/dependency_specification.py new file mode 100644 index 00000000000..d3f582f6a55 --- /dev/null +++ b/src/poetry/utils/dependency_specification.py @@ -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}") diff --git a/tests/utils/test_dependency_specification.py b/tests/utils/test_dependency_specification.py new file mode 100644 index 00000000000..655ee5ae324 --- /dev/null +++ b/tests/utils/test_dependency_specification.py @@ -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"}), + ("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)