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

new command to support more options #5710

Closed
wants to merge 23 commits into from
Closed
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
f5e625c
new command to support more options
juhoautio May 27, 2022
e4e036d
Fix mypy error
juhoautio May 30, 2022
5e82630
Fix the new test
juhoautio May 30, 2022
73c0cfb
Docs: add a missing bracket
juhoautio May 30, 2022
cd0bee9
Docs: add a missing bracket
juhoautio May 30, 2022
125e301
Docs: add a missing colon
juhoautio May 30, 2022
f4a190a
Try to fix the test on Windows
juhoautio May 31, 2022
7d578a1
Drop -l as a short version of --license
juhoautio Jun 7, 2022
a09c47b
Extract helpers.requirements, don't inherit InitCommand
juhoautio Jun 7, 2022
16caa17
Merge branch 'master' into new_command_args
juhoautio Jun 7, 2022
03e272c
Merge branch 'master' into new_command_args
juhoautio Sep 12, 2022
16d52ee
Merge branch 'master' into new_command_args
juhoautio Sep 12, 2022
efb1336
Extract function: create_pool
juhoautio Sep 12, 2022
ad7e353
Drop --dev-dependencies from `new`
juhoautio Sep 24, 2022
d2a1baf
Merge branch 'master' into new_command_args
juhoautio Sep 24, 2022
e5a6ea9
Merge branch 'master' into new_command_args
juhoautio Sep 24, 2022
c456461
Merge branch 'master' into new_command_args
juhoautio Oct 10, 2022
865c652
Move pool creation inline
juhoautio Oct 10, 2022
b3e5364
Pool -> AbstractRepository
juhoautio Oct 10, 2022
b5ccc78
Merge branch 'master' into new_command_args
juhoautio Oct 10, 2022
4341009
Revert "Pool -> AbstractRepository"
juhoautio Oct 10, 2022
b9ba428
Merge branch 'master' into new_command_args
juhoautio Oct 11, 2022
f3b66b3
Merge branch 'master' into new_command_args
juhoautio Oct 17, 2022
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
13 changes: 10 additions & 3 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,13 @@ my-package
### Options

* `--name`: Set the resulting package name.
* `--description`: Description of the package.
* `--package-version`: Set the version of the package.
* `--author`: Author of the package.
* `--python`: Compatible Python versions.
* `--license`: License of the package.
* `--dependency`: Package to require with an optional version constraint, e.g. `requests:^2.10.0` or `requests=2.11.1`. (see [add]({{< relref "#add" >}})).
* `--dev-dependency`: Development requirements, see `--dependency`.
* `--src`: Use the src layout for the project.
* `--readme`: Specify the readme file extension. Default is `md`. If you intend to publish to PyPI
keep the [recommendations for a PyPI-friendly README](https://packaging.python.org/en/latest/guides/making-a-pypi-friendly-readme/)
Expand All @@ -124,9 +131,9 @@ poetry init
* `--name`: Name of the package.
* `--description`: Description of the package.
* `--author`: Author of the package.
* `--python` Compatible Python versions.
* `--dependency`: Package to require with a version constraint. Should be in format `foo:1.0.0`.
* `--dev-dependency`: Development requirements, see `--require`.
* `--python`: Compatible Python versions.
* `--dependency`: Package to require with an optional version constraint, e.g. `requests:^2.10.0` or `requests=2.11.1`. (see [add]({{< relref "#add" >}})).
* `--dev-dependency`: Development requirements, see `--dependency`.


## install
Expand Down
267 changes: 90 additions & 177 deletions src/poetry/console/commands/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,27 @@

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 packaging.utils import canonicalize_name
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.requirements import create_pool
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 packaging.utils import NormalizedName
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):
Expand Down Expand Up @@ -55,7 +52,7 @@ class InitCommand(Command):
flag=False,
multiple=True,
),
option("license", "l", "License of the package.", flag=False),
option("license", "License of the package.", flag=False),
]

help = """\
Expand Down Expand Up @@ -162,7 +159,7 @@ def handle(self) -> int:

requirements: Requirements = {}
if self.option("dependency"):
requirements = self._format_requirements(
requirements = format_requirements(
self._determine_requirements(self.option("dependency"))
)

Expand All @@ -184,15 +181,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"))
)

Expand All @@ -204,7 +199,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("")
Expand Down Expand Up @@ -258,185 +253,107 @@ 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 = []

question = self.create_question(
"Package to add or search for (leave blank to skip):"
)
question.set_validator(self._validate_package)

package = self.ask(question)
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 <info>{package}</info>")
result.append(constraint)
package = self.ask("\nAdd a package (leave blank to skip):")
continue

canonicalized_name = canonicalize_name(constraint["name"])
matches = self._get_pool().search(canonicalized_name)
if not matches:
self.line_error("<error>Unable to find package</error>")
package = False
else:
choices = self._generate_choice_list(matches, canonicalized_name)

info_string = (
f"Found <info>{len(matches)}</info> packages matching"
f" <c1>{package}</c1>"
)

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,
)
question = self.create_question(
"Package to add or search for (leave blank to skip):"
)
question.set_validator(self._validate_package)

package = self.ask(question)
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 <info>{package}</info>")
result.append(constraint)
package = self.ask("\nAdd a package:")
continue

if not package:
self.line("<warning>No package selected</warning>")
canonicalized_name = canonicalize_name(constraint["name"])
matches = self._get_pool().search(canonicalized_name)
if not matches:
self.line_error("<error>Unable to find package</error>")
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 <info>{len(matches)}</info> packages matching"
f" <c1>{package}</c1>"
)

# 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 <b>{package_constraint}</b> for"
f" <c1>{package}</c1>"
)
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("<warning>No package selected</warning>")

# 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 (leave blank to skip):")
# 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 <b>{version}</b> for <c1>{name}</c1>")
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 <b>{package_constraint}</b> for"
f" <c1>{package}</c1>"
)

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 (leave blank to skip):")

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
Expand All @@ -463,14 +380,10 @@ def _validate_package(package: str | None) -> str | None:
return package

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())
self._pool = create_pool()

return self._pool
Loading