From a09c47b38a15c2803c8bec159f857258eac80838 Mon Sep 17 00:00:00 2001 From: juhoautio Date: Wed, 8 Jun 2022 00:44:15 +0300 Subject: [PATCH] Extract helpers.requirements, don't inherit InitCommand --- src/poetry/console/commands/init.py | 250 +++++++++------------------- src/poetry/console/commands/new.py | 41 ++++- src/poetry/utils/requirements.py | 123 ++++++++++++++ tests/console/commands/test_init.py | 3 +- 4 files changed, 241 insertions(+), 176 deletions(-) create mode 100644 src/poetry/utils/requirements.py diff --git a/src/poetry/console/commands/init.py b/src/poetry/console/commands/init.py index 5faf78c5d40..ef377731598 100644 --- a/src/poetry/console/commands/init.py +++ b/src/poetry/console/commands/init.py @@ -2,29 +2,25 @@ import sys -from pathlib import Path from typing import TYPE_CHECKING from typing import Any -from typing import Dict -from typing import Mapping -from typing import Union from cleo.helpers import option -from tomlkit import inline_table 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 +from poetry.utils.requirements import determine_requirements_from_list +from poetry.utils.requirements import find_best_version_for_package +from poetry.utils.requirements import format_requirements +from poetry.utils.requirements import parse_requirements if TYPE_CHECKING: from poetry.core.packages.package import Package - from tomlkit.items import InlineTable from poetry.repositories import Pool - -Requirements = Dict[str, Union[str, Mapping[str, Any]]] + from poetry.utils.requirements import Requirements class InitCommand(Command): @@ -168,7 +164,7 @@ def handle(self) -> int: requirements: Requirements = {} if self.option("dependency"): - requirements = self._format_requirements( + requirements = format_requirements( self._determine_requirements(self.option("dependency")) ) @@ -190,15 +186,13 @@ def handle(self) -> int: if self.io.is_interactive(): self.line(help_message) help_displayed = True - requirements.update( - self._format_requirements(self._determine_requirements([])) - ) + requirements.update(format_requirements(self._determine_requirements([]))) if self.io.is_interactive(): self.line("") dev_requirements: Requirements = {} if self.option("dev-dependency"): - dev_requirements = self._format_requirements( + dev_requirements = format_requirements( self._determine_requirements(self.option("dev-dependency")) ) @@ -210,7 +204,7 @@ def handle(self) -> int: self.line(help_message) dev_requirements.update( - self._format_requirements(self._determine_requirements([])) + format_requirements(self._determine_requirements([])) ) if self.io.is_interactive(): self.line("") @@ -263,182 +257,102 @@ def _generate_choice_list( return choices - def _determine_requirements( - self, - requires: list[str], - allow_prereleases: bool = False, - source: str | None = None, - ) -> list[dict[str, Any]]: - if not requires: - result = [] - - package = self.ask( - "Search for package to add (or leave blank to continue):" - ) - while package: - constraint = self._parse_requirements([package])[0] - if ( - "git" in constraint - or "url" in constraint - or "path" in constraint - or "version" in constraint - ): - self.line(f"Adding {package}") - result.append(constraint) - package = self.ask("\nAdd a package:") - continue - - canonicalized_name = canonicalize_name(constraint["name"]) - matches = self._get_pool().search(canonicalized_name) - if not matches: - self.line_error("Unable to find package") - package = False - else: - choices = self._generate_choice_list(matches, canonicalized_name) - - info_string = ( - f"Found {len(matches)} packages matching" - f" {package}" - ) - - if len(matches) > 10: - info_string += "\nShowing the first 10 matches" - - self.line(info_string) - - # Default to an empty value to signal no package was selected - choices.append("") + def _determine_requirements_interactive(self) -> list[dict[str, Any]]: + result = [] - package = self.choice( - "\nEnter package # to add, or the complete package name if it" - " is not listed", - choices, - attempts=3, - default=len(choices) - 1, - ) + package = self.ask("Search for package to add (or leave blank to continue):") + while package: + constraint = parse_requirements([package], self, None)[0] + if ( + "git" in constraint + or "url" in constraint + or "path" in constraint + or "version" in constraint + ): + self.line(f"Adding {package}") + result.append(constraint) + package = self.ask("\nAdd a package:") + continue - if not package: - self.line("No package selected") + canonicalized_name = canonicalize_name(constraint["name"]) + matches = self._get_pool().search(canonicalized_name) + if not matches: + self.line_error("Unable to find package") + package = False + else: + choices = self._generate_choice_list(matches, canonicalized_name) - # package selected by user, set constraint name to package name - if package: - constraint["name"] = package + info_string = ( + f"Found {len(matches)} packages matching" + f" {package}" + ) - # no constraint yet, determine the best version automatically - if package and "version" not in constraint: - question = self.create_question( - "Enter the version constraint to require " - "(or leave blank to use the latest version):" - ) - question.attempts = 3 - question.validator = lambda x: (x or "").strip() or False + if len(matches) > 10: + info_string += "\nShowing the first 10 matches" - package_constraint = self.ask(question) + self.line(info_string) - if package_constraint is None: - _, package_constraint = self._find_best_version_for_package( - package - ) + # Default to an empty value to signal no package was selected + choices.append("") - self.line( - f"Using version {package_constraint} for" - f" {package}" - ) + package = self.choice( + "\nEnter package # to add, or the complete package name if it" + " is not listed", + choices, + attempts=3, + default=len(choices) - 1, + ) - constraint["version"] = package_constraint + if not package: + self.line("No package selected") + # package selected by user, set constraint name to package name if package: - result.append(constraint) + constraint["name"] = package - if self.io.is_interactive(): - package = self.ask("\nAdd a package:") + # no constraint yet, determine the best version automatically + if package and "version" not in constraint: + question = self.create_question( + "Enter the version constraint to require " + "(or leave blank to use the latest version):" + ) + question.attempts = 3 + question.validator = lambda x: (x or "").strip() or False - return result + package_constraint = self.ask(question) - result = [] - for requirement in self._parse_requirements(requires): - if "git" in requirement or "url" in requirement or "path" in requirement: - result.append(requirement) - continue - elif "version" not in requirement: - # determine the best version automatically - name, version = self._find_best_version_for_package( - requirement["name"], - allow_prereleases=allow_prereleases, - source=source, - ) - requirement["version"] = version - requirement["name"] = name + if package_constraint is None: + _, package_constraint = find_best_version_for_package( + self._get_pool(), package + ) - self.line(f"Using version {version} for {name}") - else: - # check that the specified version/constraint exists - # before we proceed - name, _ = self._find_best_version_for_package( - requirement["name"], - requirement["version"], - allow_prereleases=allow_prereleases, - source=source, - ) + self.line( + f"Using version {package_constraint} for" + f" {package}" + ) + + constraint["version"] = package_constraint - requirement["name"] = name + if package: + result.append(constraint) - result.append(requirement) + if self.io.is_interactive(): + package = self.ask("\nAdd a package:") return result - def _find_best_version_for_package( + def _determine_requirements( self, - name: str, - required_version: str | None = None, + requires: list[str], allow_prereleases: bool = False, source: str | None = None, - ) -> tuple[str, str]: - from poetry.version.version_selector import VersionSelector - - selector = VersionSelector(self._get_pool()) - package = selector.find_best_candidate( - name, required_version, allow_prereleases=allow_prereleases, source=source - ) - - if not package: - # TODO: find similar - raise ValueError(f"Could not find a matching version of package {name}") - - return package.pretty_name, selector.find_recommended_require_version(package) - - def _parse_requirements(self, requirements: list[str]) -> list[dict[str, Any]]: - from poetry.core.pyproject.exceptions import PyProjectException - - try: - cwd = self.poetry.file.parent - except (PyProjectException, RuntimeError): - cwd = Path.cwd() - - return [ - parse_dependency_specification( - requirement=requirement, - env=self.env if isinstance(self, EnvCommand) else None, - cwd=cwd, + ) -> list[dict[str, Any]]: + if not requires: + return self._determine_requirements_interactive() + else: + return determine_requirements_from_list( + self, self._get_pool(), requires, allow_prereleases, source ) - for requirement in requirements - ] - - def _format_requirements(self, requirements: list[dict[str, str]]) -> Requirements: - requires: Requirements = {} - for requirement in requirements: - name = requirement.pop("name") - constraint: str | InlineTable - if "version" in requirement and len(requirement) == 1: - constraint = requirement["version"] - else: - constraint = inline_table() - constraint.trivia.trail = "\n" - constraint.update(requirement) - - requires[name] = constraint - - return requires def _validate_author(self, author: str, default: str) -> str | None: from poetry.core.packages.package import AUTHOR_REGEX diff --git a/src/poetry/console/commands/new.py b/src/poetry/console/commands/new.py index a7060e26230..8c283b78aee 100644 --- a/src/poetry/console/commands/new.py +++ b/src/poetry/console/commands/new.py @@ -8,14 +8,18 @@ from cleo.helpers import argument from cleo.helpers import option -from poetry.console.commands.init import InitCommand +from poetry.console.commands.command import Command +from poetry.console.commands.env_command import EnvCommand +from poetry.utils.requirements import determine_requirements_from_list +from poetry.utils.requirements import format_requirements if TYPE_CHECKING: - from poetry.console.commands.init import Requirements + from poetry.repositories import Pool + from poetry.utils.requirements import Requirements -class NewCommand(InitCommand): +class NewCommand(Command): name = "new" description = "Creates a new Python project at ." @@ -53,6 +57,11 @@ class NewCommand(InitCommand): ), ] + def __init__(self) -> None: + super().__init__() + + self._pool: Pool | None = None + def handle(self) -> int: from pathlib import Path @@ -104,14 +113,18 @@ def handle(self) -> int: requirements: Requirements = {} if self.option("dependency"): - requirements = self._format_requirements( - self._determine_requirements(self.option("dependency")) + requirements = format_requirements( + determine_requirements_from_list( + self, self._get_pool(), self.option("dependency") + ) ) dev_requirements: Requirements = {} if self.option("dev-dependency"): - dev_requirements = self._format_requirements( - self._determine_requirements(self.option("dev-dependency")) + dev_requirements = format_requirements( + determine_requirements_from_list( + self, self._get_pool(), self.option("dev-dependency") + ) ) layout_ = layout_cls( @@ -138,3 +151,17 @@ def handle(self) -> int: ) return 0 + + # TODO this code is duplicated with init.py. how to abstract nicely? + def _get_pool(self) -> Pool: + from poetry.repositories import Pool + from poetry.repositories.pypi_repository import PyPiRepository + + if isinstance(self, EnvCommand): + return self.poetry.pool + + if self._pool is None: + self._pool = Pool() + self._pool.add_repository(PyPiRepository()) + + return self._pool diff --git a/src/poetry/utils/requirements.py b/src/poetry/utils/requirements.py new file mode 100644 index 00000000000..4937bcfa919 --- /dev/null +++ b/src/poetry/utils/requirements.py @@ -0,0 +1,123 @@ +from __future__ import annotations + +from pathlib import Path +from typing import TYPE_CHECKING +from typing import Any +from typing import Dict +from typing import Mapping +from typing import Union + +from tomlkit import inline_table + +from poetry.utils.dependency_specification import parse_dependency_specification + + +if TYPE_CHECKING: + from tomlkit.items import InlineTable + + from poetry.console.commands.command import Command + from poetry.repositories import Pool + from poetry.utils.env import Env + + +Requirements = Dict[str, Union[str, Mapping[str, Any]]] + + +def parse_requirements( + requirements: list[str], command: Command, env: Env | None +) -> list[dict[str, Any]]: + from poetry.core.pyproject.exceptions import PyProjectException + + try: + cwd = command.poetry.file.parent + except (PyProjectException, RuntimeError): + cwd = Path.cwd() + + return [ + parse_dependency_specification( + requirement=requirement, + env=env, + cwd=cwd, + ) + for requirement in requirements + ] + + +def format_requirements(requirements: list[dict[str, str]]) -> Requirements: + requires: Requirements = {} + for requirement in requirements: + name = requirement.pop("name") + constraint: str | InlineTable + if "version" in requirement and len(requirement) == 1: + constraint = requirement["version"] + else: + constraint = inline_table() + constraint.trivia.trail = "\n" + constraint.update(requirement) + + requires[name] = constraint + + return requires + + +def find_best_version_for_package( + pool: Pool, + name: str, + required_version: str | None = None, + allow_prereleases: bool = False, + source: str | None = None, +) -> tuple[str, str]: + from poetry.version.version_selector import VersionSelector + + selector = VersionSelector(pool) + package = selector.find_best_candidate( + name, required_version, allow_prereleases=allow_prereleases, source=source + ) + + if not package: + # TODO: find similar + raise ValueError(f"Could not find a matching version of package {name}") + + return package.pretty_name, selector.find_recommended_require_version(package) + + +def determine_requirements_from_list( + command: Command, + pool: Pool, + requires: list[str], + allow_prereleases: bool = False, + source: str | None = None, +) -> list[dict[str, Any]]: + result = [] + for requirement in parse_requirements(requires, command, None): + if "git" in requirement or "url" in requirement or "path" in requirement: + result.append(requirement) + continue + elif "version" not in requirement: + # determine the best version automatically + name, version = find_best_version_for_package( + pool, + requirement["name"], + allow_prereleases=allow_prereleases, + source=source, + ) + requirement["version"] = version + requirement["name"] = name + + command.line(f"Using version {version} for {name}") + else: + # check that the specified version/constraint exists + # before we proceed + name, _ = find_best_version_for_package( + pool, + requirement["name"], + requirement["version"], + allow_prereleases=allow_prereleases, + source=source, + ) + + requirement["name"] = name + + result.append(requirement) + + return result diff --git a/tests/console/commands/test_init.py b/tests/console/commands/test_init.py index c733bba2dfb..64e32d3f038 100644 --- a/tests/console/commands/test_init.py +++ b/tests/console/commands/test_init.py @@ -14,6 +14,7 @@ from poetry.repositories import Pool from poetry.utils._compat import decode from poetry.utils.helpers import canonicalize_name +from poetry.utils.requirements import parse_requirements from tests.helpers import PoetryTestApplication from tests.helpers import get_package @@ -780,7 +781,7 @@ def test_predefined_and_interactive_dev_dependencies( def test_add_package_with_extras_and_whitespace(tester: CommandTester): - result = tester.command._parse_requirements(["databases[postgresql, sqlite]"]) + result = parse_requirements(["databases[postgresql, sqlite]"], tester.command, None) assert result[0]["name"] == "databases" assert len(result[0]["extras"]) == 2