diff --git a/src/poetry/console/commands/plugin/add.py b/src/poetry/console/commands/plugin/add.py index 9e2a53efa7e..f600c97f190 100644 --- a/src/poetry/console/commands/plugin/add.py +++ b/src/poetry/console/commands/plugin/add.py @@ -116,14 +116,16 @@ def handle(self) -> int: break - root_package.python_versions = ".".join( # type: ignore[union-attr] + assert root_package is not None + + root_package.python_versions = ".".join( str(v) for v in system_env.version_info[:3] ) # We create a `pyproject.toml` file based on all the information # we have about the current environment. if not env_dir.joinpath("pyproject.toml").exists(): Factory.create_pyproject_from_package( - root_package, # type: ignore[arg-type] + root_package, env_dir, ) diff --git a/src/poetry/factory.py b/src/poetry/factory.py index ef0ae390efb..d83b62a4b7e 100644 --- a/src/poetry/factory.py +++ b/src/poetry/factory.py @@ -1,5 +1,6 @@ from __future__ import annotations +import contextlib import logging from typing import TYPE_CHECKING @@ -22,10 +23,17 @@ from poetry.poetry import Poetry +try: + from poetry.core.packages.dependency_group import MAIN_GROUP +except ImportError: + MAIN_GROUP = "default" + + if TYPE_CHECKING: from pathlib import Path from cleo.io.io import IO + from poetry.core.packages.package import Package from poetry.repositories.legacy_repository import LegacyRepository @@ -206,23 +214,73 @@ def create_legacy_repository( ) @classmethod - def create_pyproject_from_package(cls, package: ProjectPackage, path: Path) -> None: + def create_pyproject_from_package( + cls, package: Package, path: Path | None = None + ) -> TOMLDocument: import tomlkit - from poetry.layouts.layout import POETRY_DEFAULT + pyproject: dict[str, Any] = tomlkit.document() + + tool_table = tomlkit.table() + tool_table._is_super_table = True + pyproject["tool"] = tool_table - pyproject: dict[str, Any] = tomlkit.loads(POETRY_DEFAULT) - content = pyproject["tool"]["poetry"] + content: dict[str, Any] = tomlkit.table() + pyproject["tool"]["poetry"] = content content["name"] = package.name content["version"] = package.version.text content["description"] = package.description content["authors"] = package.authors + content["license"] = package.license.id if package.license else "" + + if package.classifiers: + content["classifiers"] = package.classifiers + + for key, attr in { + ("documentation", "documentation_url"), + ("repository", "repository_url"), + ("homepage", "homepage"), + ("maintainers", "maintainers"), + ("keywords", "keywords"), + }: + value = getattr(package, attr, None) + if value: + content[key] = value + + readmes = [] + + for readme in package.readmes: + readme_posix_path = readme.as_posix() + + with contextlib.suppress(ValueError): + if package.root_dir: + readme_posix_path = readme.relative_to(package.root_dir).as_posix() + + readmes.append(readme_posix_path) + + if readmes: + content["readme"] = readmes + + optional_dependencies = set() + extras_section = None + + if package.extras: + extras_section = tomlkit.table() - dependency_section = content["dependencies"] + for extra in package.extras: + _dependencies = [] + for dependency in package.extras[extra]: + _dependencies.append(dependency.name) + optional_dependencies.add(dependency.name) + + extras_section[extra] = _dependencies + + optional_dependencies = set(optional_dependencies) + dependency_section = content["dependencies"] = tomlkit.table() dependency_section["python"] = package.python_versions - for dep in package.requires: + for dep in package.all_requires: constraint: dict[str, Any] = tomlkit.inline_table() if dep.is_vcs(): dep = cast(VCSDependency, dep) @@ -241,12 +299,39 @@ def create_pyproject_from_package(cls, package: ProjectPackage, path: Path) -> N if dep.extras: constraint["extras"] = sorted(dep.extras) + if dep.name in optional_dependencies: + constraint["optional"] = True + if len(constraint) == 1 and "version" in constraint: constraint = constraint["version"] - dependency_section[dep.name] = constraint + for group in dep.groups: + if group == MAIN_GROUP: + dependency_section[dep.name] = constraint + else: + if "group" not in content: + _table = tomlkit.table() + _table._is_super_table = True + content["group"] = _table - assert isinstance(pyproject, TOMLDocument) - path.joinpath("pyproject.toml").write_text( - pyproject.as_string(), encoding="utf-8" - ) + if group not in content["group"]: + _table = tomlkit.table() + _table._is_super_table = True + content["group"][group] = _table + + if "dependencies" not in content["group"][group]: + content["group"][group]["dependencies"] = tomlkit.table() + + content["group"][group]["dependencies"][dep.name] = constraint + + if extras_section: + content["extras"] = extras_section + + pyproject.add(tomlkit.nl()) # type: ignore[attr-defined] + + if path: + path.joinpath("pyproject.toml").write_text( + pyproject.as_string(), encoding="utf-8" # type: ignore[attr-defined] + ) + + return cast(TOMLDocument, pyproject) diff --git a/tests/fixtures/simple_project/pyproject.toml b/tests/fixtures/simple_project/pyproject.toml index 41a062fc09a..45a61d43cad 100644 --- a/tests/fixtures/simple_project/pyproject.toml +++ b/tests/fixtures/simple_project/pyproject.toml @@ -7,7 +7,7 @@ authors = [ ] license = "MIT" -readme = "README.rst" +readme = ["README.rst"] homepage = "https://python-poetry.org" repository = "https://github.com/python-poetry/poetry" @@ -31,5 +31,5 @@ fox = "fuz.foo:bar.baz" [build-system] -requires = ["poetry-core>=1.0.2"] +requires = ["poetry-core>=1.1.0a7"] build-backend = "poetry.core.masonry.api" diff --git a/tests/test_factory.py b/tests/test_factory.py index 3513b3034e6..ab360c14576 100644 --- a/tests/test_factory.py +++ b/tests/test_factory.py @@ -5,6 +5,7 @@ import pytest +from deepdiff import DeepDiff from entrypoints import EntryPoint from poetry.core.semver.helpers import parse_constraint from poetry.core.toml.file import TOMLFile @@ -41,10 +42,12 @@ def test_create_poetry(): assert package.description == "Some description." assert package.authors == ["Sébastien Eustace "] assert package.license.id == "MIT" - assert ( - package.readme.relative_to(fixtures_dir).as_posix() - == "sample_project/README.rst" - ) + + for readme in package.readmes: + assert ( + readme.relative_to(fixtures_dir).as_posix() == "sample_project/README.rst" + ) + assert package.homepage == "https://python-poetry.org" assert package.repository_url == "https://github.com/python-poetry/poetry" assert package.keywords == ["packaging", "dependency", "poetry"] @@ -133,6 +136,34 @@ def test_create_poetry(): ] +@pytest.mark.parametrize( + ("project",), + [ + ("simple_project",), + ("project_with_extras",), + ], +) +def test_create_pyproject_from_package(project: str): + poetry = Factory().create_poetry(fixtures_dir / project) + package = poetry.package + + pyproject = Factory.create_pyproject_from_package(package) + + result = pyproject["tool"]["poetry"] + expected = poetry.pyproject.poetry_config + + # packages do not support this at present + expected.pop("scripts", None) + + # remove any empty sections + sections = list(expected.keys()) + for section in sections: + if not expected[section]: + expected.pop(section) + + assert not DeepDiff(expected, result) + + def test_create_poetry_with_packages_and_includes(): poetry = Factory().create_poetry(fixtures_dir / "with-include")