From 73a6dbe03c1f96de0b85ab5759854d019a3a95c3 Mon Sep 17 00:00:00 2001 From: Arun Babu Neelicattu Date: Fri, 6 May 2022 23:45:04 +0200 Subject: [PATCH] add: allow specification of PEP 508 dependencies With this change, users can now add dependencies using valid PEP 508 strings. --- src/poetry/utils/dependency_specification.py | 52 +++++++++++++++++++- tests/utils/test_dependency_specification.py | 48 +++++++++++++++++- 2 files changed, 98 insertions(+), 2 deletions(-) diff --git a/src/poetry/utils/dependency_specification.py b/src/poetry/utils/dependency_specification.py index 63e6b50783e..e9bfbe7637d 100644 --- a/src/poetry/utils/dependency_specification.py +++ b/src/poetry/utils/dependency_specification.py @@ -1,5 +1,6 @@ from __future__ import annotations +import contextlib import os import re import urllib.parse @@ -9,6 +10,10 @@ 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 @@ -53,7 +58,7 @@ def _parse_dependency_specification_url( if url_parsed.scheme in ["http", "https"]: package = Provider.get_package_from_url(requirement) - return {"name": package.name, "url": package.source_url} + return {"name": package.name, "url": cast(str, package.source_url)} return None @@ -131,12 +136,57 @@ def _parse_dependency_specification_simple( 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@, 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: diff --git a/tests/utils/test_dependency_specification.py b/tests/utils/test_dependency_specification.py index 655ee5ae324..bed571d20d1 100644 --- a/tests/utils/test_dependency_specification.py +++ b/tests/utils/test_dependency_specification.py @@ -38,6 +38,9 @@ ("demo", {"name": "demo"}), ("demo@1.0.0", {"name": "demo", "version": "1.0.0"}), ("demo@^1.0.0", {"name": "demo", "version": "^1.0.0"}), + ("demo@==1.0.0", {"name": "demo", "version": "==1.0.0"}), + ("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"}), @@ -46,6 +49,47 @@ "https://example.com/packages/demo-0.1.0.tar.gz", {"name": "demo", "url": "https://example.com/packages/demo-0.1.0.tar.gz"}, ), + # PEP 508 inputs + ( + "poetry-core (>=1.0.7,<1.1.0)", + {"name": "poetry-core", "version": ">=1.0.7,<1.1.0"}, + ), + ( + 'requests [security,tests] >= 2.8.1, == 2.8.* ; python_version < "2.7"', + { + "name": "requests", + "markers": 'python_version < "2.7"', + "version": ">=2.8.1,<2.9.0", + "extras": ["security", "tests"], + }, + ), + ("name (>=3,<4)", {"name": "name", "version": ">=3,<4"}), + ( + "name@http://foo.com", + {"name": "name", "url": "http://foo.com"}, + ), + ( + "name [fred,bar] @ http://foo.com ; python_version=='2.7'", + { + "name": "name", + "markers": 'python_version == "2.7"', + "url": "http://foo.com", + # This is commented out as there is a bug in + # Dependency.create_from_pep_508 that leads to incorrect + # URL Dependency creation. + # should be: "extras": ["fred", "bar"], + }, + ), + ( + 'cachecontrol[filecache] (>=0.12.9,<0.13.0); python_version >= "3.6" and' + ' python_version < "4.0"', + { + "version": ">=0.12.9,<0.13.0", + "markers": 'python_version >= "3.6" and python_version < "4.0"', + "extras": ["filecache"], + "name": "cachecontrol", + }, + ), ], ) def test_parse_dependency_specification( @@ -60,4 +104,6 @@ def _mock(self: Path) -> bool: mocker.patch("pathlib.Path.exists", _mock) - assert not DeepDiff(parse_dependency_specification(requirement), specification) + assert not DeepDiff( + parse_dependency_specification(requirement), specification, ignore_order=True + )