Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor dependency specification parsing logic #5554

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 8 additions & 128 deletions src/poetry/console/commands/init.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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


Expand Down Expand Up @@ -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]]
Expand Down
207 changes: 207 additions & 0 deletions src/poetry/utils/dependency_specification.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
from __future__ import annotations

import contextlib
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 typing import cast

from poetry.core.packages.dependency import Dependency
from poetry.core.packages.vcs_dependency import VCSDependency

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": cast(str, 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 dependency_to_specification(dependency: Dependency) -> DependencySpec:
specification: DependencySpec = {}

if dependency.is_vcs():
dependency = cast(VCSDependency, dependency)
specification[dependency.vcs] = cast(str, dependency.source_url)
if dependency.reference:
specification["rev"] = dependency.reference
elif dependency.is_file() or dependency.is_directory():
specification["path"] = cast(str, dependency.source_url)
elif dependency.is_url():
specification["url"] = cast(str, 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 = dependency_to_specification(dependency)

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}")
Loading