From 1b611f557a05bf49f145b3c96c7dbc7864af7064 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Randy=20D=C3=B6ring?=
 <30527984+radoering@users.noreply.github.com>
Date: Sat, 22 Apr 2023 17:17:03 +0200
Subject: [PATCH 1/3] refactor: group functions for requirements parsing into a
 class (#7693)

---
 src/poetry/console/commands/init.py          |  14 +-
 src/poetry/utils/dependency_specification.py | 306 +++++++++----------
 tests/utils/test_dependency_specification.py |   4 +-
 3 files changed, 158 insertions(+), 166 deletions(-)

diff --git a/src/poetry/console/commands/init.py b/src/poetry/console/commands/init.py
index 9204c487f38..a87ac9a0fd1 100644
--- a/src/poetry/console/commands/init.py
+++ b/src/poetry/console/commands/init.py
@@ -13,7 +13,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.dependency_specification import RequirementsParser
 
 
 if TYPE_CHECKING:
@@ -437,14 +437,10 @@ def _parse_requirements(self, requirements: list[str]) -> list[dict[str, Any]]:
         except (PyProjectException, RuntimeError):
             cwd = Path.cwd()
 
-        return [
-            parse_dependency_specification(
-                requirement=requirement,
-                env=self.env if isinstance(self, EnvCommand) else None,
-                cwd=cwd,
-            )
-            for requirement in requirements
-        ]
+        parser = RequirementsParser(
+            self.env if isinstance(self, EnvCommand) else None, cwd
+        )
+        return [parser.parse(requirement) for requirement in requirements]
 
     def _format_requirements(self, requirements: list[dict[str, str]]) -> Requirements:
         requires: Requirements = {}
diff --git a/src/poetry/utils/dependency_specification.py b/src/poetry/utils/dependency_specification.py
index 0598f000f4f..0770019cfcb 100644
--- a/src/poetry/utils/dependency_specification.py
+++ b/src/poetry/utils/dependency_specification.py
@@ -26,128 +26,6 @@
 
 
 DependencySpec = Dict[str, Union[str, bool, 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
-
-    if parsed.subdirectory:
-        pair["subdirectory"] = parsed.subdirectory
-
-    source_root = env.path.joinpath("src") if env else None
-    package = Provider.get_package_from_vcs(
-        "git",
-        url=url.url,
-        rev=pair.get("rev"),
-        subdirectory=parsed.subdirectory,
-        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)
-        assert package.source_url is not None
-        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(" ", 1)
-        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
-
-
 BaseSpec = TypeVar("BaseSpec", DependencySpec, InlineTable)
 
 
@@ -178,49 +56,167 @@ def dependency_to_specification(
     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
+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()
 
-    with contextlib.suppress(ValueError):
-        dependency = Dependency.create_from_pep_508(requirement)
-        specification: DependencySpec = {}
-        specification = dependency_to_specification(dependency, specification)
+        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:
-            specification["name"] = dependency.name
+            if extras and "extras" not in specification:
+                specification["extras"] = extras
             return specification
 
-    return None
+        raise ValueError(f"Invalid dependency specification: {requirement}")
+
+    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 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 parsed.subdirectory:
+            pair["subdirectory"] = parsed.subdirectory
 
-def parse_dependency_specification(
-    requirement: str, env: Env | None = None, cwd: Path | None = None
-) -> DependencySpec:
-    requirement = requirement.strip()
-    cwd = cwd or Path.cwd()
+        source_root = self._env.path.joinpath("src") if self._env else None
+        package = Provider.get_package_from_vcs(
+            "git",
+            url=url.url,
+            rev=pair.get("rev"),
+            subdirectory=parsed.subdirectory,
+            source_root=source_root,
+        )
+        pair["name"] = package.name
+        return pair
 
-    specification = pep508_to_dependency_specification(requirement)
+    def _parse_url(self, requirement: str) -> DependencySpec | None:
+        url_parsed = urllib.parse.urlparse(requirement)
+        if not (url_parsed.scheme and url_parsed.netloc):
+            return None
 
-    if specification is not None:
-        return specification
+        if url_parsed.scheme in ["git+https", "git+ssh"]:
+            return self._parse_git_url(requirement)
 
-    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("[")
+        if url_parsed.scheme in ["http", "https"]:
+            package = Provider.get_package_from_url(requirement)
+            assert package.source_url is not None
+            return {"name": package.name, "url": package.source_url}
 
-    specification = (
-        _parse_dependency_specification_url(requirement, env=env)
-        or _parse_dependency_specification_path(requirement, cwd=cwd)
-        or _parse_dependency_specification_simple(requirement)
-    )
+        return None
+
+    def _parse_path(self, requirement: str) -> DependencySpec | None:
+        if (os.path.sep in requirement or "/" in requirement) and (
+            self._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 = self._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(self._cwd).as_posix()
+                    if not is_absolute
+                    else path.as_posix()
+                ),
+            }
+
+        return None
 
-    if specification:
-        if extras and "extras" not in specification:
-            specification["extras"] = extras
-        return specification
+    def _parse_simple(
+        self,
+        requirement: str,
+    ) -> DependencySpec | None:
+        extras: list[str] = []
+        pair = re.sub(
+            "^([^@=: ]+)(?:@|==|(?<![<>~!])=|:| )(.*)$", "\\1 \\2", requirement
+        )
+        pair = pair.strip()
 
-    raise ValueError(f"Invalid dependency specification: {requirement}")
+        require: DependencySpec = {}
+
+        if " " in pair:
+            name, version = pair.split(" ", 1)
+            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
diff --git a/tests/utils/test_dependency_specification.py b/tests/utils/test_dependency_specification.py
index 8b1c1dfd1b1..89cd2c600a1 100644
--- a/tests/utils/test_dependency_specification.py
+++ b/tests/utils/test_dependency_specification.py
@@ -7,7 +7,7 @@
 
 from deepdiff import DeepDiff
 
-from poetry.utils.dependency_specification import parse_dependency_specification
+from poetry.utils.dependency_specification import RequirementsParser
 
 
 if TYPE_CHECKING:
@@ -116,5 +116,5 @@ def _mock(self: Path) -> bool:
     mocker.patch("pathlib.Path.exists", _mock)
 
     assert not DeepDiff(
-        parse_dependency_specification(requirement), specification, ignore_order=True
+        RequirementsParser().parse(requirement), specification, ignore_order=True
     )

From 5edb992ce2af557ec468ae80acdf899d8680fb81 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Randy=20D=C3=B6ring?=
 <30527984+radoering@users.noreply.github.com>
Date: Sat, 22 Apr 2023 17:38:30 +0200
Subject: [PATCH 2/3] refactor: split off functions to get direct origin
 dependencies from provider (#7693)

---
 src/poetry/packages/direct_origin.py         | 110 +++++++++++++++++++
 src/poetry/puzzle/provider.py                | 108 +-----------------
 src/poetry/utils/dependency_specification.py |  10 +-
 tests/conftest.py                            |   2 +-
 4 files changed, 122 insertions(+), 108 deletions(-)
 create mode 100644 src/poetry/packages/direct_origin.py

diff --git a/src/poetry/packages/direct_origin.py b/src/poetry/packages/direct_origin.py
new file mode 100644
index 00000000000..ce785ea5e12
--- /dev/null
+++ b/src/poetry/packages/direct_origin.py
@@ -0,0 +1,110 @@
+from __future__ import annotations
+
+import functools
+import os
+import tempfile
+import urllib.parse
+
+from pathlib import Path
+from typing import TYPE_CHECKING
+
+from poetry.inspection.info import PackageInfo
+from poetry.inspection.info import PackageInfoError
+from poetry.utils.helpers import download_file
+from poetry.utils.helpers import get_file_hash
+from poetry.vcs.git import Git
+
+
+if TYPE_CHECKING:
+    from poetry.core.packages.package import Package
+
+
+@functools.lru_cache(maxsize=None)
+def _get_package_from_git(
+    url: str,
+    branch: str | None = None,
+    tag: str | None = None,
+    rev: str | None = None,
+    subdirectory: str | None = None,
+    source_root: Path | None = None,
+) -> Package:
+    source = Git.clone(
+        url=url,
+        source_root=source_root,
+        branch=branch,
+        tag=tag,
+        revision=rev,
+        clean=False,
+    )
+    revision = Git.get_revision(source)
+
+    path = Path(source.path)
+    if subdirectory:
+        path = path.joinpath(subdirectory)
+
+    package = DirectOrigin.get_package_from_directory(path)
+    package._source_type = "git"
+    package._source_url = url
+    package._source_reference = rev or tag or branch or "HEAD"
+    package._source_resolved_reference = revision
+    package._source_subdirectory = subdirectory
+
+    return package
+
+
+class DirectOrigin:
+    @classmethod
+    def get_package_from_file(cls, file_path: Path) -> Package:
+        try:
+            package = PackageInfo.from_path(path=file_path).to_package(
+                root_dir=file_path
+            )
+        except PackageInfoError:
+            raise RuntimeError(
+                f"Unable to determine package info from path: {file_path}"
+            )
+
+        return package
+
+    @classmethod
+    def get_package_from_directory(cls, directory: Path) -> Package:
+        return PackageInfo.from_directory(path=directory).to_package(root_dir=directory)
+
+    @classmethod
+    def get_package_from_url(cls, url: str) -> Package:
+        file_name = os.path.basename(urllib.parse.urlparse(url).path)
+        with tempfile.TemporaryDirectory() as temp_dir:
+            dest = Path(temp_dir) / file_name
+            download_file(url, dest)
+            package = cls.get_package_from_file(dest)
+
+            package.files = [
+                {"file": file_name, "hash": "sha256:" + get_file_hash(dest)}
+            ]
+
+        package._source_type = "url"
+        package._source_url = url
+
+        return package
+
+    @staticmethod
+    def get_package_from_vcs(
+        vcs: str,
+        url: str,
+        branch: str | None = None,
+        tag: str | None = None,
+        rev: str | None = None,
+        subdirectory: str | None = None,
+        source_root: Path | None = None,
+    ) -> Package:
+        if vcs != "git":
+            raise ValueError(f"Unsupported VCS dependency {vcs}")
+
+        return _get_package_from_git(
+            url=url,
+            branch=branch,
+            tag=tag,
+            rev=rev,
+            subdirectory=subdirectory,
+            source_root=source_root,
+        )
diff --git a/src/poetry/puzzle/provider.py b/src/poetry/puzzle/provider.py
index facd89c38a3..1c33f833973 100644
--- a/src/poetry/puzzle/provider.py
+++ b/src/poetry/puzzle/provider.py
@@ -1,16 +1,11 @@
 from __future__ import annotations
 
-import functools
 import logging
-import os
 import re
-import tempfile
 import time
-import urllib.parse
 
 from collections import defaultdict
 from contextlib import contextmanager
-from pathlib import Path
 from typing import TYPE_CHECKING
 from typing import cast
 
@@ -22,19 +17,16 @@
 from poetry.core.version.markers import EmptyMarker
 from poetry.core.version.markers import MarkerUnion
 
-from poetry.inspection.info import PackageInfo
-from poetry.inspection.info import PackageInfoError
 from poetry.mixology.incompatibility import Incompatibility
 from poetry.mixology.incompatibility_cause import DependencyCause
 from poetry.mixology.incompatibility_cause import PythonCause
 from poetry.mixology.term import Term
 from poetry.packages import DependencyPackage
+from poetry.packages.direct_origin import DirectOrigin
 from poetry.packages.package_collection import PackageCollection
 from poetry.puzzle.exceptions import OverrideNeeded
 from poetry.repositories.exceptions import PackageNotFound
-from poetry.utils.helpers import download_file
 from poetry.utils.helpers import get_file_hash
-from poetry.vcs.git import Git
 
 
 if TYPE_CHECKING:
@@ -42,6 +34,7 @@
     from collections.abc import Collection
     from collections.abc import Iterable
     from collections.abc import Iterator
+    from pathlib import Path
 
     from cleo.io.io import IO
     from packaging.utils import NormalizedName
@@ -100,39 +93,6 @@ def _formatter_elapsed(self) -> str:
         return f"{elapsed:.1f}s"
 
 
-@functools.lru_cache(maxsize=None)
-def _get_package_from_git(
-    url: str,
-    branch: str | None = None,
-    tag: str | None = None,
-    rev: str | None = None,
-    subdirectory: str | None = None,
-    source_root: Path | None = None,
-) -> Package:
-    source = Git.clone(
-        url=url,
-        source_root=source_root,
-        branch=branch,
-        tag=tag,
-        revision=rev,
-        clean=False,
-    )
-    revision = Git.get_revision(source)
-
-    path = Path(source.path)
-    if subdirectory:
-        path = path.joinpath(subdirectory)
-
-    package = Provider.get_package_from_directory(path)
-    package._source_type = "git"
-    package._source_url = url
-    package._source_reference = rev or tag or branch or "HEAD"
-    package._source_resolved_reference = revision
-    package._source_subdirectory = subdirectory
-
-    return package
-
-
 class Provider:
     UNSAFE_PACKAGES: set[str] = set()
 
@@ -351,7 +311,7 @@ def _search_for_vcs(self, dependency: VCSDependency) -> Package:
         Basically, we clone the repository in a temporary directory
         and get the information we need by checking out the specified reference.
         """
-        package = self.get_package_from_vcs(
+        package = DirectOrigin.get_package_from_vcs(
             dependency.vcs,
             dependency.source,
             branch=dependency.branch,
@@ -368,31 +328,9 @@ def _search_for_vcs(self, dependency: VCSDependency) -> Package:
 
         return package
 
-    @staticmethod
-    def get_package_from_vcs(
-        vcs: str,
-        url: str,
-        branch: str | None = None,
-        tag: str | None = None,
-        rev: str | None = None,
-        subdirectory: str | None = None,
-        source_root: Path | None = None,
-    ) -> Package:
-        if vcs != "git":
-            raise ValueError(f"Unsupported VCS dependency {vcs}")
-
-        return _get_package_from_git(
-            url=url,
-            branch=branch,
-            tag=tag,
-            rev=rev,
-            subdirectory=subdirectory,
-            source_root=source_root,
-        )
-
     def _search_for_file(self, dependency: FileDependency) -> Package:
         dependency.validate(raise_error=True)
-        package = self.get_package_from_file(dependency.full_path)
+        package = DirectOrigin.get_package_from_file(dependency.full_path)
 
         self.validate_package_for_dependency(dependency=dependency, package=package)
 
@@ -408,22 +346,9 @@ def _search_for_file(self, dependency: FileDependency) -> Package:
 
         return package
 
-    @classmethod
-    def get_package_from_file(cls, file_path: Path) -> Package:
-        try:
-            package = PackageInfo.from_path(path=file_path).to_package(
-                root_dir=file_path
-            )
-        except PackageInfoError:
-            raise RuntimeError(
-                f"Unable to determine package info from path: {file_path}"
-            )
-
-        return package
-
     def _search_for_directory(self, dependency: DirectoryDependency) -> Package:
         dependency.validate(raise_error=True)
-        package = self.get_package_from_directory(dependency.full_path)
+        package = DirectOrigin.get_package_from_directory(dependency.full_path)
 
         self.validate_package_for_dependency(dependency=dependency, package=package)
 
@@ -434,12 +359,8 @@ def _search_for_directory(self, dependency: DirectoryDependency) -> Package:
 
         return package
 
-    @classmethod
-    def get_package_from_directory(cls, directory: Path) -> Package:
-        return PackageInfo.from_directory(path=directory).to_package(root_dir=directory)
-
     def _search_for_url(self, dependency: URLDependency) -> Package:
-        package = self.get_package_from_url(dependency.url)
+        package = DirectOrigin.get_package_from_url(dependency.url)
 
         self.validate_package_for_dependency(dependency=dependency, package=package)
 
@@ -453,23 +374,6 @@ def _search_for_url(self, dependency: URLDependency) -> Package:
 
         return package
 
-    @classmethod
-    def get_package_from_url(cls, url: str) -> Package:
-        file_name = os.path.basename(urllib.parse.urlparse(url).path)
-        with tempfile.TemporaryDirectory() as temp_dir:
-            dest = Path(temp_dir) / file_name
-            download_file(url, dest)
-            package = cls.get_package_from_file(dest)
-
-            package.files = [
-                {"file": file_name, "hash": "sha256:" + get_file_hash(dest)}
-            ]
-
-        package._source_type = "url"
-        package._source_url = url
-
-        return package
-
     def _get_dependencies_with_overrides(
         self, dependencies: list[Dependency], package: DependencyPackage
     ) -> list[Dependency]:
diff --git a/src/poetry/utils/dependency_specification.py b/src/poetry/utils/dependency_specification.py
index 0770019cfcb..59893b4b601 100644
--- a/src/poetry/utils/dependency_specification.py
+++ b/src/poetry/utils/dependency_specification.py
@@ -16,7 +16,7 @@
 from poetry.core.packages.dependency import Dependency
 from tomlkit.items import InlineTable
 
-from poetry.puzzle.provider import Provider
+from poetry.packages.direct_origin import DirectOrigin
 
 
 if TYPE_CHECKING:
@@ -120,7 +120,7 @@ def _parse_git_url(self, requirement: str) -> DependencySpec | None:
             pair["subdirectory"] = parsed.subdirectory
 
         source_root = self._env.path.joinpath("src") if self._env else None
-        package = Provider.get_package_from_vcs(
+        package = DirectOrigin.get_package_from_vcs(
             "git",
             url=url.url,
             rev=pair.get("rev"),
@@ -139,7 +139,7 @@ def _parse_url(self, requirement: str) -> DependencySpec | None:
             return self._parse_git_url(requirement)
 
         if url_parsed.scheme in ["http", "https"]:
-            package = Provider.get_package_from_url(requirement)
+            package = DirectOrigin.get_package_from_url(requirement)
             assert package.source_url is not None
             return {"name": package.name, "url": package.source_url}
 
@@ -158,9 +158,9 @@ def _parse_path(self, requirement: str) -> DependencySpec | None:
                 path = self._cwd.joinpath(requirement)
 
             if path.is_file():
-                package = Provider.get_package_from_file(path.resolve())
+                package = DirectOrigin.get_package_from_file(path.resolve())
             else:
-                package = Provider.get_package_from_directory(path.resolve())
+                package = DirectOrigin.get_package_from_directory(path.resolve())
 
             return {
                 "name": package.name,
diff --git a/tests/conftest.py b/tests/conftest.py
index 7e006d30be0..fa0763a7a47 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -239,7 +239,7 @@ def mock_user_config_dir(mocker: MockerFixture, config_dir: Path) -> None:
 def download_mock(mocker: MockerFixture) -> None:
     # Patch download to not download anything but to just copy from fixtures
     mocker.patch("poetry.utils.helpers.download_file", new=mock_download)
-    mocker.patch("poetry.puzzle.provider.download_file", new=mock_download)
+    mocker.patch("poetry.packages.direct_origin.download_file", new=mock_download)
     mocker.patch("poetry.repositories.http_repository.download_file", new=mock_download)
 
 

From e83e29dc1f4b920f825e8977e7b1a95551bb82ae Mon Sep 17 00:00:00 2001
From: ralbertazzi <ra@bendingspoons.com>
Date: Wed, 19 Apr 2023 21:42:13 +0200
Subject: [PATCH 3/3] feat: use ArtifactCache in get_package_from_url (#7693)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Co-authored-by: Randy Döring <30527984+radoering@users.noreply.github.com>
---
 src/poetry/console/commands/debug/resolve.py |  2 +-
 src/poetry/console/commands/init.py          |  6 ++-
 src/poetry/console/commands/show.py          |  2 +-
 src/poetry/factory.py                        | 10 ++--
 src/poetry/installation/installer.py         |  3 +-
 src/poetry/packages/direct_origin.py         | 33 ++++++++----
 src/poetry/poetry.py                         |  2 +-
 src/poetry/puzzle/provider.py                |  9 ++--
 src/poetry/repositories/repository_pool.py   | 12 +++++
 src/poetry/utils/dependency_specification.py | 18 +++++--
 tests/conftest.py                            |  6 +++
 tests/installation/test_chef.py              |  7 +--
 tests/installation/test_executor.py          |  5 --
 tests/packages/test_direct_origin.py         | 56 ++++++++++++++++++++
 tests/test_factory.py                        |  2 +-
 tests/utils/test_dependency_specification.py | 10 +++-
 16 files changed, 139 insertions(+), 44 deletions(-)
 create mode 100644 tests/packages/test_direct_origin.py

diff --git a/src/poetry/console/commands/debug/resolve.py b/src/poetry/console/commands/debug/resolve.py
index 05cf4b15736..ec168f6adf8 100644
--- a/src/poetry/console/commands/debug/resolve.py
+++ b/src/poetry/console/commands/debug/resolve.py
@@ -113,7 +113,7 @@ def handle(self) -> int:
 
         if self.option("install"):
             env = EnvManager(self.poetry).get()
-            pool = RepositoryPool()
+            pool = RepositoryPool(config=self.poetry.config)
             locked_repository = Repository("poetry-locked")
             for op in ops:
                 locked_repository.add_package(op.package)
diff --git a/src/poetry/console/commands/init.py b/src/poetry/console/commands/init.py
index a87ac9a0fd1..54c0af92bd7 100644
--- a/src/poetry/console/commands/init.py
+++ b/src/poetry/console/commands/init.py
@@ -434,11 +434,15 @@ def _parse_requirements(self, requirements: list[str]) -> list[dict[str, Any]]:
 
         try:
             cwd = self.poetry.file.parent
+            artifact_cache = self.poetry.pool.artifact_cache
         except (PyProjectException, RuntimeError):
             cwd = Path.cwd()
+            artifact_cache = self._get_pool().artifact_cache
 
         parser = RequirementsParser(
-            self.env if isinstance(self, EnvCommand) else None, cwd
+            artifact_cache=artifact_cache,
+            env=self.env if isinstance(self, EnvCommand) else None,
+            cwd=cwd,
         )
         return [parser.parse(requirement) for requirement in requirements]
 
diff --git a/src/poetry/console/commands/show.py b/src/poetry/console/commands/show.py
index 03cab868a10..c8c1fbc3595 100644
--- a/src/poetry/console/commands/show.py
+++ b/src/poetry/console/commands/show.py
@@ -213,7 +213,7 @@ def _display_packages_information(
         from poetry.utils.helpers import get_package_version_display_string
 
         locked_packages = locked_repository.packages
-        pool = RepositoryPool(ignore_repository_names=True)
+        pool = RepositoryPool(ignore_repository_names=True, config=self.poetry.config)
         pool.add_repository(locked_repository)
         solver = Solver(
             root,
diff --git a/src/poetry/factory.py b/src/poetry/factory.py
index 68c51bdf2c3..9c2c92c93b2 100644
--- a/src/poetry/factory.py
+++ b/src/poetry/factory.py
@@ -119,7 +119,7 @@ def get_package(cls, name: str, version: str) -> ProjectPackage:
     @classmethod
     def create_pool(
         cls,
-        auth_config: Config,
+        config: Config,
         sources: Iterable[dict[str, Any]] = (),
         io: IO | None = None,
         disable_cache: bool = False,
@@ -133,12 +133,12 @@ def create_pool(
         if disable_cache:
             logger.debug("Disabling source caches")
 
-        pool = RepositoryPool()
+        pool = RepositoryPool(config=config)
 
         explicit_pypi = False
         for source in sources:
             repository = cls.create_package_source(
-                source, auth_config, disable_cache=disable_cache
+                source, config, disable_cache=disable_cache
             )
             priority = Priority[source.get("priority", Priority.PRIMARY.name).upper()]
             if "default" in source or "secondary" in source:
@@ -207,7 +207,7 @@ def create_pool(
 
     @classmethod
     def create_package_source(
-        cls, source: dict[str, str], auth_config: Config, disable_cache: bool = False
+        cls, source: dict[str, str], config: Config, disable_cache: bool = False
     ) -> HTTPRepository:
         from poetry.repositories.exceptions import InvalidSourceError
         from poetry.repositories.legacy_repository import LegacyRepository
@@ -239,7 +239,7 @@ def create_package_source(
         return repository_class(
             name,
             url,
-            config=auth_config,
+            config=config,
             disable_cache=disable_cache,
         )
 
diff --git a/src/poetry/installation/installer.py b/src/poetry/installation/installer.py
index 45c3893bc55..fea3cc96586 100644
--- a/src/poetry/installation/installer.py
+++ b/src/poetry/installation/installer.py
@@ -47,6 +47,7 @@ def __init__(
         self._package = package
         self._locker = locker
         self._pool = pool
+        self._config = config
 
         self._dry_run = False
         self._requires_synchronization = False
@@ -290,7 +291,7 @@ def _do_install(self) -> int:
             )
 
         # We resolve again by only using the lock file
-        pool = RepositoryPool(ignore_repository_names=True)
+        pool = RepositoryPool(ignore_repository_names=True, config=self._config)
 
         # Making a new repo containing the packages
         # newly resolved and the ones from the current lock file
diff --git a/src/poetry/packages/direct_origin.py b/src/poetry/packages/direct_origin.py
index ce785ea5e12..cdc16d55c49 100644
--- a/src/poetry/packages/direct_origin.py
+++ b/src/poetry/packages/direct_origin.py
@@ -2,12 +2,13 @@
 
 import functools
 import os
-import tempfile
 import urllib.parse
 
 from pathlib import Path
 from typing import TYPE_CHECKING
 
+from poetry.core.packages.utils.link import Link
+
 from poetry.inspection.info import PackageInfo
 from poetry.inspection.info import PackageInfoError
 from poetry.utils.helpers import download_file
@@ -18,6 +19,8 @@
 if TYPE_CHECKING:
     from poetry.core.packages.package import Package
 
+    from poetry.utils.cache import ArtifactCache
+
 
 @functools.lru_cache(maxsize=None)
 def _get_package_from_git(
@@ -53,6 +56,9 @@ def _get_package_from_git(
 
 
 class DirectOrigin:
+    def __init__(self, artifact_cache: ArtifactCache) -> None:
+        self._artifact_cache = artifact_cache
+
     @classmethod
     def get_package_from_file(cls, file_path: Path) -> Package:
         try:
@@ -70,17 +76,22 @@ def get_package_from_file(cls, file_path: Path) -> Package:
     def get_package_from_directory(cls, directory: Path) -> Package:
         return PackageInfo.from_directory(path=directory).to_package(root_dir=directory)
 
-    @classmethod
-    def get_package_from_url(cls, url: str) -> Package:
+    def get_package_from_url(self, url: str) -> Package:
         file_name = os.path.basename(urllib.parse.urlparse(url).path)
-        with tempfile.TemporaryDirectory() as temp_dir:
-            dest = Path(temp_dir) / file_name
-            download_file(url, dest)
-            package = cls.get_package_from_file(dest)
-
-            package.files = [
-                {"file": file_name, "hash": "sha256:" + get_file_hash(dest)}
-            ]
+        link = Link(url)
+        artifact = self._artifact_cache.get_cached_archive_for_link(link, strict=True)
+
+        if not artifact:
+            artifact = (
+                self._artifact_cache.get_cache_directory_for_link(link) / file_name
+            )
+            artifact.parent.mkdir(parents=True, exist_ok=True)
+            download_file(url, artifact)
+
+        package = self.get_package_from_file(artifact)
+        package.files = [
+            {"file": file_name, "hash": "sha256:" + get_file_hash(artifact)}
+        ]
 
         package._source_type = "url"
         package._source_url = url
diff --git a/src/poetry/poetry.py b/src/poetry/poetry.py
index 3cb227eaaaa..57c05b3e540 100644
--- a/src/poetry/poetry.py
+++ b/src/poetry/poetry.py
@@ -49,7 +49,7 @@ def __init__(
 
         self._locker = locker
         self._config = config
-        self._pool = RepositoryPool()
+        self._pool = RepositoryPool(config=config)
         self._plugin_manager: PluginManager | None = None
         self._disable_cache = disable_cache
 
diff --git a/src/poetry/puzzle/provider.py b/src/poetry/puzzle/provider.py
index 1c33f833973..190a0e31d66 100644
--- a/src/poetry/puzzle/provider.py
+++ b/src/poetry/puzzle/provider.py
@@ -107,6 +107,7 @@ def __init__(
     ) -> None:
         self._package = package
         self._pool = pool
+        self._direct_origin = DirectOrigin(self._pool.artifact_cache)
         self._io = io
         self._env: Env | None = None
         self._python_constraint = package.python_constraint
@@ -311,7 +312,7 @@ def _search_for_vcs(self, dependency: VCSDependency) -> Package:
         Basically, we clone the repository in a temporary directory
         and get the information we need by checking out the specified reference.
         """
-        package = DirectOrigin.get_package_from_vcs(
+        package = self._direct_origin.get_package_from_vcs(
             dependency.vcs,
             dependency.source,
             branch=dependency.branch,
@@ -330,7 +331,7 @@ def _search_for_vcs(self, dependency: VCSDependency) -> Package:
 
     def _search_for_file(self, dependency: FileDependency) -> Package:
         dependency.validate(raise_error=True)
-        package = DirectOrigin.get_package_from_file(dependency.full_path)
+        package = self._direct_origin.get_package_from_file(dependency.full_path)
 
         self.validate_package_for_dependency(dependency=dependency, package=package)
 
@@ -348,7 +349,7 @@ def _search_for_file(self, dependency: FileDependency) -> Package:
 
     def _search_for_directory(self, dependency: DirectoryDependency) -> Package:
         dependency.validate(raise_error=True)
-        package = DirectOrigin.get_package_from_directory(dependency.full_path)
+        package = self._direct_origin.get_package_from_directory(dependency.full_path)
 
         self.validate_package_for_dependency(dependency=dependency, package=package)
 
@@ -360,7 +361,7 @@ def _search_for_directory(self, dependency: DirectoryDependency) -> Package:
         return package
 
     def _search_for_url(self, dependency: URLDependency) -> Package:
-        package = DirectOrigin.get_package_from_url(dependency.url)
+        package = self._direct_origin.get_package_from_url(dependency.url)
 
         self.validate_package_for_dependency(dependency=dependency, package=package)
 
diff --git a/src/poetry/repositories/repository_pool.py b/src/poetry/repositories/repository_pool.py
index 482a9985503..9b9c5e03260 100644
--- a/src/poetry/repositories/repository_pool.py
+++ b/src/poetry/repositories/repository_pool.py
@@ -8,8 +8,10 @@
 from enum import IntEnum
 from typing import TYPE_CHECKING
 
+from poetry.config.config import Config
 from poetry.repositories.abstract_repository import AbstractRepository
 from poetry.repositories.exceptions import PackageNotFound
+from poetry.utils.cache import ArtifactCache
 
 
 if TYPE_CHECKING:
@@ -40,6 +42,8 @@ def __init__(
         self,
         repositories: list[Repository] | None = None,
         ignore_repository_names: bool = False,
+        *,
+        config: Config | None = None,
     ) -> None:
         super().__init__("poetry-repository-pool")
         self._repositories: OrderedDict[str, PrioritizedRepository] = OrderedDict()
@@ -50,6 +54,10 @@ def __init__(
         for repository in repositories:
             self.add_repository(repository)
 
+        self._artifact_cache = ArtifactCache(
+            cache_dir=(config or Config.create()).artifacts_cache_directory
+        )
+
     @property
     def repositories(self) -> list[Repository]:
         """
@@ -77,6 +85,10 @@ def _sorted_repositories(self) -> list[PrioritizedRepository]:
             self._repositories.values(), key=lambda prio_repo: prio_repo.priority
         )
 
+    @property
+    def artifact_cache(self) -> ArtifactCache:
+        return self._artifact_cache
+
     def has_default(self) -> bool:
         return self._contains_priority(Priority.DEFAULT)
 
diff --git a/src/poetry/utils/dependency_specification.py b/src/poetry/utils/dependency_specification.py
index 59893b4b601..27f3a0da5e8 100644
--- a/src/poetry/utils/dependency_specification.py
+++ b/src/poetry/utils/dependency_specification.py
@@ -22,6 +22,7 @@
 if TYPE_CHECKING:
     from poetry.core.packages.vcs_dependency import VCSDependency
 
+    from poetry.utils.cache import ArtifactCache
     from poetry.utils.env import Env
 
 
@@ -57,7 +58,14 @@ def dependency_to_specification(
 
 
 class RequirementsParser:
-    def __init__(self, env: Env | None = None, cwd: Path | None = None) -> None:
+    def __init__(
+        self,
+        *,
+        artifact_cache: ArtifactCache,
+        env: Env | None = None,
+        cwd: Path | None = None,
+    ) -> None:
+        self._direct_origin = DirectOrigin(artifact_cache)
         self._env = env
         self._cwd = cwd or Path.cwd()
 
@@ -120,7 +128,7 @@ def _parse_git_url(self, requirement: str) -> DependencySpec | None:
             pair["subdirectory"] = parsed.subdirectory
 
         source_root = self._env.path.joinpath("src") if self._env else None
-        package = DirectOrigin.get_package_from_vcs(
+        package = self._direct_origin.get_package_from_vcs(
             "git",
             url=url.url,
             rev=pair.get("rev"),
@@ -139,7 +147,7 @@ def _parse_url(self, requirement: str) -> DependencySpec | None:
             return self._parse_git_url(requirement)
 
         if url_parsed.scheme in ["http", "https"]:
-            package = DirectOrigin.get_package_from_url(requirement)
+            package = self._direct_origin.get_package_from_url(requirement)
             assert package.source_url is not None
             return {"name": package.name, "url": package.source_url}
 
@@ -158,9 +166,9 @@ def _parse_path(self, requirement: str) -> DependencySpec | None:
                 path = self._cwd.joinpath(requirement)
 
             if path.is_file():
-                package = DirectOrigin.get_package_from_file(path.resolve())
+                package = self._direct_origin.get_package_from_file(path.resolve())
             else:
-                package = DirectOrigin.get_package_from_directory(path.resolve())
+                package = self._direct_origin.get_package_from_directory(path.resolve())
 
             return {
                 "name": package.name,
diff --git a/tests/conftest.py b/tests/conftest.py
index fa0763a7a47..f1bc94187b6 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -24,6 +24,7 @@
 from poetry.layouts import layout
 from poetry.repositories import Repository
 from poetry.repositories import RepositoryPool
+from poetry.utils.cache import ArtifactCache
 from poetry.utils.env import EnvManager
 from poetry.utils.env import SystemEnv
 from poetry.utils.env import VirtualEnv
@@ -222,6 +223,11 @@ def config(
     return c
 
 
+@pytest.fixture
+def artifact_cache(config: Config) -> ArtifactCache:
+    return ArtifactCache(cache_dir=config.artifacts_cache_directory)
+
+
 @pytest.fixture()
 def config_dir(tmp_path: Path) -> Path:
     path = tmp_path / "config"
diff --git a/tests/installation/test_chef.py b/tests/installation/test_chef.py
index ca00ba01e73..d134b90f9c0 100644
--- a/tests/installation/test_chef.py
+++ b/tests/installation/test_chef.py
@@ -14,7 +14,6 @@
 from poetry.factory import Factory
 from poetry.installation.chef import Chef
 from poetry.repositories import RepositoryPool
-from poetry.utils.cache import ArtifactCache
 from poetry.utils.env import EnvManager
 from tests.repositories.test_pypi_repository import MockRepository
 
@@ -22,6 +21,7 @@
 if TYPE_CHECKING:
     from pytest_mock import MockerFixture
 
+    from poetry.utils.cache import ArtifactCache
     from tests.conftest import Config
     from tests.types import FixtureDirGetter
 
@@ -40,11 +40,6 @@ def setup(mocker: MockerFixture, pool: RepositoryPool) -> None:
     mocker.patch.object(Factory, "create_pool", return_value=pool)
 
 
-@pytest.fixture
-def artifact_cache(config: Config) -> ArtifactCache:
-    return ArtifactCache(cache_dir=config.artifacts_cache_directory)
-
-
 def test_prepare_sdist(
     config: Config,
     config_cache_dir: Path,
diff --git a/tests/installation/test_executor.py b/tests/installation/test_executor.py
index 602e972b5db..0ec3653ebcf 100644
--- a/tests/installation/test_executor.py
+++ b/tests/installation/test_executor.py
@@ -135,11 +135,6 @@ def pool() -> RepositoryPool:
     return pool
 
 
-@pytest.fixture
-def artifact_cache(config: Config) -> ArtifactCache:
-    return ArtifactCache(cache_dir=config.artifacts_cache_directory)
-
-
 @pytest.fixture
 def mock_file_downloads(
     http: type[httpretty.httpretty], fixture_dir: FixtureDirGetter
diff --git a/tests/packages/test_direct_origin.py b/tests/packages/test_direct_origin.py
new file mode 100644
index 00000000000..ff9548c5fdf
--- /dev/null
+++ b/tests/packages/test_direct_origin.py
@@ -0,0 +1,56 @@
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+from unittest.mock import MagicMock
+
+from poetry.core.packages.utils.link import Link
+
+from poetry.packages.direct_origin import DirectOrigin
+from poetry.utils.cache import ArtifactCache
+
+
+if TYPE_CHECKING:
+    from pathlib import Path
+
+    from pytest_mock import MockerFixture
+
+    from tests.types import FixtureDirGetter
+
+
+def test_direct_origin_get_package_from_file(fixture_dir: FixtureDirGetter) -> None:
+    wheel_path = fixture_dir("distributions") / "demo-0.1.2-py2.py3-none-any.whl"
+    package = DirectOrigin.get_package_from_file(wheel_path)
+    assert package.name == "demo"
+
+
+def test_direct_origin_caches_url_dependency(tmp_path: Path) -> None:
+    artifact_cache = ArtifactCache(cache_dir=tmp_path)
+    direct_origin = DirectOrigin(artifact_cache)
+    url = "https://python-poetry.org/distributions/demo-0.1.0-py2.py3-none-any.whl"
+
+    package = direct_origin.get_package_from_url(url)
+
+    assert package.name == "demo"
+    assert artifact_cache.get_cached_archive_for_link(Link(url), strict=True)
+
+
+def test_direct_origin_does_not_download_url_dependency_when_cached(
+    fixture_dir: FixtureDirGetter, mocker: MockerFixture
+) -> None:
+    artifact_cache = MagicMock()
+    artifact_cache.get_cached_archive_for_link = MagicMock(
+        return_value=fixture_dir("distributions") / "demo-0.1.2-py2.py3-none-any.whl"
+    )
+    direct_origin = DirectOrigin(artifact_cache)
+    url = "https://python-poetry.org/distributions/demo-0.1.0-py2.py3-none-any.whl"
+    mocker.patch(
+        "poetry.packages.direct_origin.download_file",
+        side_effect=Exception("download_file should not be called"),
+    )
+
+    package = direct_origin.get_package_from_url(url)
+
+    assert package.name == "demo"
+    artifact_cache.get_cached_archive_for_link.assert_called_once_with(
+        Link(url), strict=True
+    )
diff --git a/tests/test_factory.py b/tests/test_factory.py
index f64aed75b09..0667c134227 100644
--- a/tests/test_factory.py
+++ b/tests/test_factory.py
@@ -543,7 +543,7 @@ def test_create_package_source_invalid(
     fixture_dir: FixtureDirGetter,
 ) -> None:
     with pytest.raises(InvalidSourceError) as e:
-        Factory.create_package_source(source, auth_config=config)
+        Factory.create_package_source(source, config=config)
         Factory().create_poetry(fixture_dir("with_source_pypi_url"))
 
     assert str(e.value) == expected
diff --git a/tests/utils/test_dependency_specification.py b/tests/utils/test_dependency_specification.py
index 89cd2c600a1..a782a55924e 100644
--- a/tests/utils/test_dependency_specification.py
+++ b/tests/utils/test_dependency_specification.py
@@ -13,6 +13,7 @@
 if TYPE_CHECKING:
     from pytest_mock import MockerFixture
 
+    from poetry.utils.cache import ArtifactCache
     from poetry.utils.dependency_specification import DependencySpec
 
 
@@ -104,7 +105,10 @@
     ],
 )
 def test_parse_dependency_specification(
-    requirement: str, specification: DependencySpec, mocker: MockerFixture
+    requirement: str,
+    specification: DependencySpec,
+    mocker: MockerFixture,
+    artifact_cache: ArtifactCache,
 ) -> None:
     original = Path.exists
 
@@ -116,5 +120,7 @@ def _mock(self: Path) -> bool:
     mocker.patch("pathlib.Path.exists", _mock)
 
     assert not DeepDiff(
-        RequirementsParser().parse(requirement), specification, ignore_order=True
+        RequirementsParser(artifact_cache=artifact_cache).parse(requirement),
+        specification,
+        ignore_order=True,
     )
