Commit 3248fe17 by Randy Döring

refactor: group functions for requirements parsing into a class (#7693)

parent 65486660
...@@ -13,7 +13,7 @@ from tomlkit import inline_table ...@@ -13,7 +13,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.dependency_specification import RequirementsParser
if TYPE_CHECKING: if TYPE_CHECKING:
...@@ -437,14 +437,10 @@ You can specify a package in the following forms: ...@@ -437,14 +437,10 @@ You can specify a package in the following forms:
except (PyProjectException, RuntimeError): except (PyProjectException, RuntimeError):
cwd = Path.cwd() cwd = Path.cwd()
return [ parser = RequirementsParser(
parse_dependency_specification( self.env if isinstance(self, EnvCommand) else None, cwd
requirement=requirement,
env=self.env if isinstance(self, EnvCommand) else None,
cwd=cwd,
) )
for requirement in requirements return [parser.parse(requirement) for requirement in requirements]
]
def _format_requirements(self, requirements: list[dict[str, str]]) -> Requirements: def _format_requirements(self, requirements: list[dict[str, str]]) -> Requirements:
requires: Requirements = {} requires: Requirements = {}
......
...@@ -26,11 +26,85 @@ if TYPE_CHECKING: ...@@ -26,11 +26,85 @@ if TYPE_CHECKING:
DependencySpec = Dict[str, Union[str, bool, Dict[str, Union[str, bool]], List[str]]] DependencySpec = Dict[str, Union[str, bool, Dict[str, Union[str, bool]], List[str]]]
BaseSpec = TypeVar("BaseSpec", DependencySpec, InlineTable)
def dependency_to_specification(
dependency: Dependency, specification: BaseSpec
) -> BaseSpec:
if dependency.is_vcs():
dependency = cast("VCSDependency", dependency)
assert dependency.source_url is not None
specification[dependency.vcs] = dependency.source_url
if dependency.reference:
specification["rev"] = dependency.reference
elif dependency.is_file() or dependency.is_directory():
assert dependency.source_url is not None
specification["path"] = dependency.source_url
elif dependency.is_url():
assert dependency.source_url is not None
specification["url"] = dependency.source_url
elif dependency.pretty_constraint != "*" and not dependency.constraint.is_empty():
specification["version"] = dependency.pretty_constraint
if not dependency.marker.is_any():
specification["markers"] = str(dependency.marker)
if dependency.extras:
specification["extras"] = sorted(dependency.extras)
return specification
class RequirementsParser:
def __init__(self, env: Env | None = None, cwd: Path | None = None) -> None:
self._env = env
self._cwd = cwd or Path.cwd()
def parse(self, requirement: str) -> DependencySpec:
requirement = requirement.strip()
specification = self._parse_pep508(requirement)
if specification is not None:
return specification
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 = (
self._parse_url(requirement)
or self._parse_path(requirement)
or self._parse_simple(requirement)
)
if specification:
if extras and "extras" not in specification:
specification["extras"] = extras
return specification
def _parse_dependency_specification_git_url( raise ValueError(f"Invalid dependency specification: {requirement}")
requirement: str, env: Env | None = None
) -> DependencySpec | None: def _parse_pep508(self, requirement: str) -> DependencySpec | None:
if " ; " not in requirement and re.search(r"@[\^~!=<>\d]", requirement):
# this is of the form package@<semver>, do not attempt to parse it
return None
with contextlib.suppress(ValueError):
dependency = Dependency.create_from_pep_508(requirement)
specification: DependencySpec = {}
specification = dependency_to_specification(dependency, specification)
if specification:
specification["name"] = dependency.name
return specification
return None
def _parse_git_url(self, requirement: str) -> DependencySpec | None:
from poetry.core.vcs.git import Git from poetry.core.vcs.git import Git
from poetry.core.vcs.git import ParsedUrl from poetry.core.vcs.git import ParsedUrl
...@@ -45,7 +119,7 @@ def _parse_dependency_specification_git_url( ...@@ -45,7 +119,7 @@ def _parse_dependency_specification_git_url(
if parsed.subdirectory: if parsed.subdirectory:
pair["subdirectory"] = parsed.subdirectory pair["subdirectory"] = parsed.subdirectory
source_root = env.path.joinpath("src") if env else None source_root = self._env.path.joinpath("src") if self._env else None
package = Provider.get_package_from_vcs( package = Provider.get_package_from_vcs(
"git", "git",
url=url.url, url=url.url,
...@@ -56,16 +130,13 @@ def _parse_dependency_specification_git_url( ...@@ -56,16 +130,13 @@ def _parse_dependency_specification_git_url(
pair["name"] = package.name pair["name"] = package.name
return pair return pair
def _parse_url(self, requirement: str) -> DependencySpec | None:
def _parse_dependency_specification_url(
requirement: str, env: Env | None = None
) -> DependencySpec | None:
url_parsed = urllib.parse.urlparse(requirement) url_parsed = urllib.parse.urlparse(requirement)
if not (url_parsed.scheme and url_parsed.netloc): if not (url_parsed.scheme and url_parsed.netloc):
return None return None
if url_parsed.scheme in ["git+https", "git+ssh"]: if url_parsed.scheme in ["git+https", "git+ssh"]:
return _parse_dependency_specification_git_url(requirement, env) return self._parse_git_url(requirement)
if url_parsed.scheme in ["http", "https"]: if url_parsed.scheme in ["http", "https"]:
package = Provider.get_package_from_url(requirement) package = Provider.get_package_from_url(requirement)
...@@ -74,12 +145,9 @@ def _parse_dependency_specification_url( ...@@ -74,12 +145,9 @@ def _parse_dependency_specification_url(
return None return None
def _parse_path(self, requirement: str) -> DependencySpec | None:
def _parse_dependency_specification_path(
requirement: str, cwd: Path
) -> DependencySpec | None:
if (os.path.sep in requirement or "/" in requirement) and ( if (os.path.sep in requirement or "/" in requirement) and (
cwd.joinpath(requirement).exists() self._cwd.joinpath(requirement).exists()
or Path(requirement).expanduser().exists() or Path(requirement).expanduser().exists()
and Path(requirement).expanduser().is_absolute() and Path(requirement).expanduser().is_absolute()
): ):
...@@ -87,7 +155,7 @@ def _parse_dependency_specification_path( ...@@ -87,7 +155,7 @@ def _parse_dependency_specification_path(
is_absolute = path.is_absolute() is_absolute = path.is_absolute()
if not path.is_absolute(): if not path.is_absolute():
path = cwd.joinpath(requirement) path = self._cwd.joinpath(requirement)
if path.is_file(): if path.is_file():
package = Provider.get_package_from_file(path.resolve()) package = Provider.get_package_from_file(path.resolve())
...@@ -97,18 +165,22 @@ def _parse_dependency_specification_path( ...@@ -97,18 +165,22 @@ def _parse_dependency_specification_path(
return { return {
"name": package.name, "name": package.name,
"path": ( "path": (
path.relative_to(cwd).as_posix() if not is_absolute else path.as_posix() path.relative_to(self._cwd).as_posix()
if not is_absolute
else path.as_posix()
), ),
} }
return None return None
def _parse_simple(
def _parse_dependency_specification_simple( self,
requirement: str, requirement: str,
) -> DependencySpec | None: ) -> DependencySpec | None:
extras: list[str] = [] extras: list[str] = []
pair = re.sub("^([^@=: ]+)(?:@|==|(?<![<>~!])=|:| )(.*)$", "\\1 \\2", requirement) pair = re.sub(
"^([^@=: ]+)(?:@|==|(?<![<>~!])=|:| )(.*)$", "\\1 \\2", requirement
)
pair = pair.strip() pair = pair.strip()
require: DependencySpec = {} require: DependencySpec = {}
...@@ -124,7 +196,9 @@ def _parse_dependency_specification_simple( ...@@ -124,7 +196,9 @@ def _parse_dependency_specification_simple(
if version != "latest": if version != "latest":
require["version"] = version require["version"] = version
else: else:
m = re.match(r"^([^><=!: ]+)((?:>=|<=|>|<|!=|~=|~|\^).*)$", requirement.strip()) m = re.match(
r"^([^><=!: ]+)((?:>=|<=|>|<|!=|~=|~|\^).*)$", requirement.strip()
)
if m: if m:
name, constraint = m.group(1), m.group(2) name, constraint = m.group(1), m.group(2)
extras_m = re.search(r"\[([\w\d,-_]+)\]$", name) extras_m = re.search(r"\[([\w\d,-_]+)\]$", name)
...@@ -146,81 +220,3 @@ def _parse_dependency_specification_simple( ...@@ -146,81 +220,3 @@ def _parse_dependency_specification_simple(
require["extras"] = extras require["extras"] = extras
return require return require
BaseSpec = TypeVar("BaseSpec", DependencySpec, InlineTable)
def dependency_to_specification(
dependency: Dependency, specification: BaseSpec
) -> BaseSpec:
if dependency.is_vcs():
dependency = cast("VCSDependency", dependency)
assert dependency.source_url is not None
specification[dependency.vcs] = dependency.source_url
if dependency.reference:
specification["rev"] = dependency.reference
elif dependency.is_file() or dependency.is_directory():
assert dependency.source_url is not None
specification["path"] = dependency.source_url
elif dependency.is_url():
assert dependency.source_url is not None
specification["url"] = dependency.source_url
elif dependency.pretty_constraint != "*" and not dependency.constraint.is_empty():
specification["version"] = dependency.pretty_constraint
if not dependency.marker.is_any():
specification["markers"] = str(dependency.marker)
if dependency.extras:
specification["extras"] = sorted(dependency.extras)
return specification
def pep508_to_dependency_specification(requirement: str) -> DependencySpec | None:
if " ; " not in requirement and re.search(r"@[\^~!=<>\d]", requirement):
# this is of the form package@<semver>, do not attempt to parse it
return None
with contextlib.suppress(ValueError):
dependency = Dependency.create_from_pep_508(requirement)
specification: DependencySpec = {}
specification = dependency_to_specification(dependency, specification)
if specification:
specification["name"] = dependency.name
return specification
return None
def parse_dependency_specification(
requirement: str, env: Env | None = None, cwd: Path | None = None
) -> DependencySpec:
requirement = requirement.strip()
cwd = cwd or Path.cwd()
specification = pep508_to_dependency_specification(requirement)
if specification is not None:
return specification
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}")
...@@ -7,7 +7,7 @@ import pytest ...@@ -7,7 +7,7 @@ import pytest
from deepdiff import DeepDiff from deepdiff import DeepDiff
from poetry.utils.dependency_specification import parse_dependency_specification from poetry.utils.dependency_specification import RequirementsParser
if TYPE_CHECKING: if TYPE_CHECKING:
...@@ -116,5 +116,5 @@ def test_parse_dependency_specification( ...@@ -116,5 +116,5 @@ def test_parse_dependency_specification(
mocker.patch("pathlib.Path.exists", _mock) mocker.patch("pathlib.Path.exists", _mock)
assert not DeepDiff( assert not DeepDiff(
parse_dependency_specification(requirement), specification, ignore_order=True RequirementsParser().parse(requirement), specification, ignore_order=True
) )
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