diff --git a/docs/cli.md b/docs/cli.md
index dee328483da..c83b0ea0a46 100644
--- a/docs/cli.md
+++ b/docs/cli.md
@@ -102,6 +102,12 @@ 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" >}})).
* `--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/)
@@ -124,8 +130,8 @@ 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`.
+* `--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`.
diff --git a/src/poetry/console/commands/init.py b/src/poetry/console/commands/init.py
index e2ebf9cf9b7..6fc8e5b0c0e 100644
--- a/src/poetry/console/commands/init.py
+++ b/src/poetry/console/commands/init.py
@@ -2,30 +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 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 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):
@@ -55,7 +50,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 = """\
@@ -162,7 +157,7 @@ def handle(self) -> int:
requirements: Requirements = {}
if self.option("dependency"):
- requirements = self._format_requirements(
+ requirements = format_requirements(
self._determine_requirements(self.option("dependency"))
)
@@ -184,15 +179,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"))
)
@@ -204,7 +197,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("")
@@ -258,185 +251,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 {package}")
- 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("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,
- )
+ 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 {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 (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 {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 (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
@@ -466,9 +381,6 @@ 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())
diff --git a/src/poetry/console/commands/new.py b/src/poetry/console/commands/new.py
index cde571aa202..fc09b421528 100644
--- a/src/poetry/console/commands/new.py
+++ b/src/poetry/console/commands/new.py
@@ -3,11 +3,19 @@
import sys
from contextlib import suppress
+from typing import TYPE_CHECKING
from cleo.helpers import argument
from cleo.helpers import option
from poetry.console.commands.command import Command
+from poetry.utils.requirements import determine_requirements_from_list
+from poetry.utils.requirements import format_requirements
+
+
+if TYPE_CHECKING:
+ from poetry.repositories import Pool
+ from poetry.utils.requirements import Requirements
class NewCommand(Command):
@@ -17,6 +25,19 @@ class NewCommand(Command):
arguments = [argument("path", "The path to create the project at.")]
options = [
option("name", None, "Set the resulting package name.", flag=False),
+ option("description", None, "Description of the package.", flag=False),
+ option("package-version", None, "Set the version of the package.", flag=False),
+ option("author", None, "Author name of the package.", flag=False),
+ option("python", None, "Compatible Python versions.", flag=False),
+ option(
+ "dependency",
+ None,
+ "Package to require, with an optional version constraint, "
+ "e.g. requests:^2.10.0 or requests=2.11.1.",
+ flag=False,
+ multiple=True,
+ ),
+ option("license", "License of the package.", flag=False),
option("src", None, "Use the src layout for the project."),
option(
"readme",
@@ -26,6 +47,11 @@ class NewCommand(Command):
),
]
+ def __init__(self) -> None:
+ super().__init__()
+
+ self._pool: Pool | None = None
+
def handle(self) -> int:
from pathlib import Path
@@ -49,6 +75,10 @@ def handle(self) -> int:
if not name:
name = path.name
+ description = self.option("description") or ""
+ license = self.option("license") or ""
+ version = self.option("package-version") or "0.1.0"
+
if path.exists() and list(path.glob("*")):
# Directory is not empty. Aborting.
raise RuntimeError(
@@ -58,22 +88,36 @@ def handle(self) -> int:
readme_format = self.option("readme") or "md"
config = GitConfig()
- author = None
- if config.get("user.name"):
+ author = self.option("author")
+ if not author and config.get("user.name"):
author = config["user.name"]
author_email = config.get("user.email")
if author_email:
author += f" <{author_email}>"
current_env = SystemEnv(Path(sys.executable))
- default_python = "^" + ".".join(str(v) for v in current_env.version_info[:2])
+
+ python = self.option("python")
+ if not python:
+ python = "^" + ".".join(str(v) for v in current_env.version_info[:2])
+
+ requirements: Requirements = {}
+ if self.option("dependency"):
+ requirements = format_requirements(
+ determine_requirements_from_list(
+ self, self._get_pool(), self.option("dependency")
+ )
+ )
layout_ = layout_cls(
name,
- "0.1.0",
+ version,
+ description=description,
author=author,
+ license=license,
+ python=python,
+ dependencies=requirements,
readme_format=readme_format,
- python=default_python,
)
layout_.create(path)
@@ -88,3 +132,13 @@ def handle(self) -> int:
)
return 0
+
+ def _get_pool(self) -> Pool:
+ from poetry.repositories import Pool
+ from poetry.repositories.pypi_repository import PyPiRepository
+
+ 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 5f176ba0eb9..d5032feb7b0 100644
--- a/tests/console/commands/test_init.py
+++ b/tests/console/commands/test_init.py
@@ -15,6 +15,7 @@
from poetry.console.commands.init import InitCommand
from poetry.repositories import Pool
from poetry.utils._compat import decode
+from poetry.utils.requirements import parse_requirements
from tests.helpers import PoetryTestApplication
from tests.helpers import get_package
@@ -876,7 +877,7 @@ def test_predefined_all_options(tester: CommandTester, repo: TestRepository):
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
diff --git a/tests/console/commands/test_new.py b/tests/console/commands/test_new.py
index 79f09e20f27..b6cb5a5e14f 100644
--- a/tests/console/commands/test_new.py
+++ b/tests/console/commands/test_new.py
@@ -6,12 +6,14 @@
import pytest
from poetry.factory import Factory
+from tests.helpers import get_package
if TYPE_CHECKING:
from cleo.testers.command_tester import CommandTester
from poetry.poetry import Poetry
+ from tests.helpers import TestRepository
from tests.types import CommandTesterFactory
@@ -21,7 +23,10 @@ def tester(command_tester_factory: CommandTesterFactory) -> CommandTester:
def verify_project_directory(
- path: Path, package_name: str, package_path: str, include_from: str | None = None
+ path: Path,
+ package_name: str,
+ package_path: str,
+ include_from: str | None = None,
) -> Poetry:
package_path = Path(package_path)
assert path.is_dir()
@@ -170,3 +175,41 @@ def test_command_new_with_readme(fmt: str | None, tester: CommandTester, tmp_dir
poetry = verify_project_directory(path, package, package, None)
assert poetry.local_config.get("readme") == f"README.{fmt or 'md'}"
+
+
+def test_command_new_with_dependencies(
+ tester: CommandTester, repo: TestRepository, tmp_dir: str
+):
+ repo.add_package(get_package("pendulum", "2.0.0"))
+ repo.add_package(get_package("pytest", "3.6.0"))
+
+ package_path = "custompackage"
+ path = Path(tmp_dir) / package_path
+ options = [
+ path.as_posix(),
+ "--name custompackage",
+ "--package-version 1.2.3",
+ "--description 'My Description'",
+ "--author 'Your Name ' ",
+ "--license 'My License'",
+ "--python '^3.8' ",
+ "--dependency pendulum",
+ ]
+ tester.execute(" ".join(options))
+ poetry = verify_project_directory(path, "custompackage", package_path, None)
+
+ expected = """\
+[tool.poetry]
+name = "custompackage"
+version = "1.2.3"
+description = "My Description"
+authors = ["Your Name "]
+license = "My License"
+readme = "README.md"
+
+[tool.poetry.dependencies]
+python = "^3.8"
+pendulum = "^2.0.0"
+"""
+ # Replacing possible \r\n because as_string adds them on Windows
+ assert expected in poetry.pyproject.data.as_string().replace("\r\n", "\n")