From d2500d97112fbff1ddf224998b32ebecb923264b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Randy=20D=C3=B6ring?= <30527984+radoering@users.noreply.github.com> Date: Sun, 10 Mar 2024 13:48:49 +0100 Subject: [PATCH 1/9] add support for PEP 621: use poetry-core from main branch and bump version to 2.0.0.dev0 (#9135) --- poetry.lock | 27 ++++++++++++++++----------- pyproject.toml | 4 ++-- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/poetry.lock b/poetry.lock index 1b43ad8b9ec..91fc217135f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -20,7 +20,7 @@ name = "build" version = "1.2.1" description = "A simple, correct Python build frontend" optional = false -python-versions = ">= 3.8" +python-versions = ">=3.8" files = [ {file = "build-1.2.1-py3-none-any.whl", hash = "sha256:75e10f767a433d9a86e50d83f418e83efc18ede923ee5ff7df93b6cb0306c5d4"}, {file = "build-1.2.1.tar.gz", hash = "sha256:526263f4870c26f26c433545579475377b2b7588b6f1eac76a001e873ae3e19d"}, @@ -814,6 +814,7 @@ files = [ {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fbb160554e319f7b22ecf530a80a3ff496d38e8e07ae763b9e82fadfe96f273"}, {file = "msgpack-1.0.8-cp39-cp39-win32.whl", hash = "sha256:f9af38a89b6a5c04b7d18c492c8ccf2aee7048aff1ce8437c4683bb5a1df893d"}, {file = "msgpack-1.0.8-cp39-cp39-win_amd64.whl", hash = "sha256:ed59dd52075f8fc91da6053b12e8c89e37aa043f8986efd89e61fae69dc1b011"}, + {file = "msgpack-1.0.8-py3-none-any.whl", hash = "sha256:24f727df1e20b9876fa6e95f840a2a2651e34c0ad147676356f4bf5fbb0206ca"}, {file = "msgpack-1.0.8.tar.gz", hash = "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3"}, ] @@ -880,7 +881,7 @@ name = "nodeenv" version = "1.9.1" description = "Node.js virtual environment builder" optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, @@ -972,21 +973,25 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "poetry-core" -version = "1.9.0" +version = "2.0.0.dev0" description = "Poetry PEP 517 Build Backend" optional = false -python-versions = ">=3.8,<4.0" -files = [ - {file = "poetry_core-1.9.0-py3-none-any.whl", hash = "sha256:4e0c9c6ad8cf89956f03b308736d84ea6ddb44089d16f2adc94050108ec1f5a1"}, - {file = "poetry_core-1.9.0.tar.gz", hash = "sha256:fa7a4001eae8aa572ee84f35feb510b321bd652e5cf9293249d62853e1f935a2"}, -] +python-versions = "^3.8" +files = [] +develop = false + +[package.source] +type = "git" +url = "https://github.com/python-poetry/poetry-core.git" +reference = "main" +resolved_reference = "b57e32c1bc558031dbae371ec85894e941bf039e" [[package]] name = "poetry-plugin-export" version = "1.8.0" description = "Poetry plugin to export the dependencies to various formats" optional = false -python-versions = ">=3.8,<4.0" +python-versions = "<4.0,>=3.8" files = [ {file = "poetry_plugin_export-1.8.0-py3-none-any.whl", hash = "sha256:adbe232cfa0cc04991ea3680c865cf748bff27593b9abcb1f35fb50ed7ba2c22"}, {file = "poetry_plugin_export-1.8.0.tar.gz", hash = "sha256:1fa6168a85d59395d835ca564bc19862a7c76061e60c3e7dfaec70d50937fc61"}, @@ -1019,7 +1024,7 @@ name = "psutil" version = "6.0.0" description = "Cross-platform lib for process and system monitoring in Python." optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ {file = "psutil-6.0.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a021da3e881cd935e64a3d0a20983bda0bb4cf80e4f74fa9bfcb1bc5785360c6"}, {file = "psutil-6.0.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:1287c2b95f1c0a364d23bc6f2ea2365a8d4d9b726a3be7294296ff7ba97c17f0"}, @@ -1622,4 +1627,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "8e40cd7dad5d45e8b7038be044dfdce858d38cdbc62727fabcc70f5ce9ccdd04" +content-hash = "816bcb3532fd7484005946146373ddb848e7405a72f10a8dc345b9e4596427f7" diff --git a/pyproject.toml b/pyproject.toml index f27b526d089..f9fe2d313e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "poetry" -version = "1.9.0.dev0" +version = "2.0.0.dev0" description = "Python dependency management and packaging made easy." authors = ["Sébastien Eustace "] maintainers = [ @@ -31,7 +31,7 @@ Changelog = "https://python-poetry.org/history/" [tool.poetry.dependencies] python = "^3.8" -poetry-core = "1.9.0" +poetry-core = { git = "https://github.com/python-poetry/poetry-core.git", branch = "main" } poetry-plugin-export = "^1.8.0" build = "^1.2.1" cachecontrol = { version = "^0.14.0", extras = ["filecache"] } From e67ce70a7b25d408d315608a7064bdef11e539c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Randy=20D=C3=B6ring?= <30527984+radoering@users.noreply.github.com> Date: Sun, 10 Mar 2024 07:55:46 +0100 Subject: [PATCH 2/9] add support for PEP 621: basic compatibility with upstream changes in poetry-core (#9135) - update some pyproject.toml files to avoid deprecation warnings in tests for `poetry check` - add explicit test for deprecation warnings that should be printed when running `poetry check` with a legacy project --- src/poetry/console/commands/check.py | 18 +++-- src/poetry/console/commands/version.py | 8 ++- src/poetry/factory.py | 20 +++--- tests/console/commands/test_check.py | 70 +++++++++++++++---- .../fixtures/invalid_pyproject/pyproject.toml | 12 ++-- tests/fixtures/no_name_project/pyproject.toml | 2 +- tests/fixtures/outdated_lock/pyproject.toml | 14 ++-- .../fixtures/private_pyproject/pyproject.toml | 10 +-- tests/fixtures/simple_project/pyproject.toml | 24 +++---- .../fixtures/simple_project_legacy/README.rst | 2 + .../simple_project_legacy/pyproject.toml | 35 ++++++++++ .../simple_project/__init__.py} | 0 tests/fixtures/up_to_date_lock/poetry.lock | 2 +- tests/fixtures/up_to_date_lock/pyproject.toml | 14 ++-- tests/installation/test_chef.py | 4 +- tests/json/test_schema_sources.py | 12 ++-- .../masonry/builders/test_editable_builder.py | 12 +++- tests/puzzle/test_solver.py | 23 ++++-- tests/test_factory.py | 35 +++------- tests/utils/env/test_env_manager.py | 6 +- 20 files changed, 205 insertions(+), 118 deletions(-) create mode 100644 tests/fixtures/simple_project_legacy/README.rst create mode 100644 tests/fixtures/simple_project_legacy/pyproject.toml rename tests/fixtures/{private_pyproject/README.md => simple_project_legacy/simple_project/__init__.py} (100%) diff --git a/src/poetry/console/commands/check.py b/src/poetry/console/commands/check.py index ba533f992ee..bd672409a81 100644 --- a/src/poetry/console/commands/check.py +++ b/src/poetry/console/commands/check.py @@ -130,21 +130,27 @@ def handle(self) -> int: # Load poetry config and display errors, if any poetry_file = self.poetry.file.path - config = PyProjectTOML(poetry_file).poetry_config - check_result = Factory.validate(config, strict=True) + toml_data = PyProjectTOML(poetry_file).data + check_result = Factory.validate(toml_data, strict=True) + + project = toml_data.get("project", {}) + poetry_config = toml_data["tool"]["poetry"] # Validate trove classifiers - project_classifiers = set(config.get("classifiers", [])) + project_classifiers = set( + project.get("classifiers") or poetry_config.get("classifiers", []) + ) errors, warnings = self._validate_classifiers(project_classifiers) check_result["errors"].extend(errors) check_result["warnings"].extend(warnings) # Validate readme (files must exist) - if "readme" in config: - errors = self._validate_readme(config["readme"], poetry_file) + # TODO: consider [project.readme] as well + if "readme" in poetry_config: + errors = self._validate_readme(poetry_config["readme"], poetry_file) check_result["errors"].extend(errors) - check_result["errors"] += self._validate_dependencies_source(config) + check_result["errors"] += self._validate_dependencies_source(poetry_config) # Verify that lock file is consistent if self.option("lock") and not self.poetry.locker.is_locked(): diff --git a/src/poetry/console/commands/version.py b/src/poetry/console/commands/version.py index 3968557a8aa..85c97d4c28c 100644 --- a/src/poetry/console/commands/version.py +++ b/src/poetry/console/commands/version.py @@ -69,8 +69,12 @@ def handle(self) -> int: if not self.option("dry-run"): content: dict[str, Any] = self.poetry.file.read() - poetry_content = content["tool"]["poetry"] - poetry_content["version"] = version.text + project_content = content.get("project", {}) + if "version" in project_content: + project_content["version"] = version.text + poetry_content = content.get("tool", {}).get("poetry", {}) + if "version" in poetry_content: + poetry_content["version"] = version.text assert isinstance(content, TOMLDocument) self.poetry.file.write(content) diff --git a/src/poetry/factory.py b/src/poetry/factory.py index 4cfcd91d060..f016dcf88e6 100644 --- a/src/poetry/factory.py +++ b/src/poetry/factory.py @@ -73,7 +73,7 @@ def create_poetry( # Load local sources repositories = {} existing_repositories = config.get("repositories", {}) - for source in base_poetry.pyproject.poetry_config.get("source", []): + for source in base_poetry.local_config.get("source", []): name = source.get("name") url = source.get("url") if name and url and name not in existing_repositories: @@ -340,22 +340,26 @@ def create_pyproject_from_package(cls, package: Package) -> TOMLDocument: @classmethod def validate( - cls, config: dict[str, Any], strict: bool = False + cls, toml_data: dict[str, Any], strict: bool = False ) -> dict[str, list[str]]: - results = super().validate(config, strict) + results = super().validate(toml_data, strict) + poetry_config = toml_data["tool"]["poetry"] - results["errors"].extend(validate_object(config)) + results["errors"].extend(validate_object(poetry_config)) # A project should not depend on itself. - dependencies = set(config.get("dependencies", {}).keys()) - dependencies.update(config.get("dev-dependencies", {}).keys()) - groups = config.get("group", {}).values() + # TODO: consider [project.dependencies] and [project.optional-dependencies] + dependencies = set(poetry_config.get("dependencies", {}).keys()) + dependencies.update(poetry_config.get("dev-dependencies", {}).keys()) + groups = poetry_config.get("group", {}).values() for group in groups: dependencies.update(group.get("dependencies", {}).keys()) dependencies = {canonicalize_name(d) for d in dependencies} - project_name = config.get("name") + project_name = toml_data.get("project", {}).get("name") or poetry_config.get( + "name" + ) if project_name is not None and canonicalize_name(project_name) in dependencies: results["errors"].append( f"Project name ({project_name}) is same as one of its dependencies" diff --git a/tests/console/commands/test_check.py b/tests/console/commands/test_check.py index 67cfef8410b..1496ebcd544 100644 --- a/tests/console/commands/test_check.py +++ b/tests/console/commands/test_check.py @@ -22,8 +22,8 @@ @pytest.fixture -def poetry_sample_project(set_project_context: SetProjectContext) -> Iterator[Poetry]: - with set_project_context("sample_project", in_place=False) as cwd: +def poetry_simple_project(set_project_context: SetProjectContext) -> Iterator[Poetry]: + with set_project_context("simple_project", in_place=False) as cwd: yield Factory().create_poetry(cwd) @@ -45,9 +45,9 @@ def poetry_with_up_to_date_lockfile( @pytest.fixture() def tester( - command_tester_factory: CommandTesterFactory, poetry_sample_project: Poetry + command_tester_factory: CommandTesterFactory, poetry_simple_project: Poetry ) -> CommandTester: - return command_tester_factory("check", poetry=poetry_sample_project) + return command_tester_factory("check", poetry=poetry_simple_project) def test_check_valid(tester: CommandTester) -> None: @@ -60,6 +60,57 @@ def test_check_valid(tester: CommandTester) -> None: assert tester.io.fetch_output() == expected +def test_check_valid_legacy( + mocker: MockerFixture, tester: CommandTester, fixture_dir: FixtureDirGetter +) -> None: + mocker.patch( + "poetry.poetry.Poetry.file", + return_value=TOMLFile(fixture_dir("simple_project_legacy") / "pyproject.toml"), + new_callable=mocker.PropertyMock, + ) + tester.execute() + + expected = ( + "Warning: [tool.poetry.name] is deprecated. Use [project.name] instead.\n" + "Warning: [tool.poetry.version] is set but 'version' is not in " + "[project.dynamic]. If it is static use [project.version]. If it is dynamic, " + "add 'version' to [project.dynamic].\n" + "If you want to set the version dynamically via `poetry build " + "--local-version` or you are using a plugin, which sets the version " + "dynamically, you should define the version in [tool.poetry] and add " + "'version' to [project.dynamic].\n" + "Warning: [tool.poetry.description] is deprecated. Use [project.description] " + "instead.\n" + "Warning: [tool.poetry.readme] is set but 'readme' is not in " + "[project.dynamic]. If it is static use [project.readme]. If it is dynamic, " + "add 'readme' to [project.dynamic].\n" + "If you want to define multiple readmes, you should define them in " + "[tool.poetry] and add 'readme' to [project.dynamic].\n" + "Warning: [tool.poetry.license] is deprecated. Use [project.license] instead.\n" + "Warning: [tool.poetry.authors] is deprecated. Use [project.authors] instead.\n" + "Warning: [tool.poetry.keywords] is deprecated. Use [project.keywords] " + "instead.\n" + "Warning: [tool.poetry.classifiers] is set but 'classifiers' is not in " + "[project.dynamic]. If it is static use [project.classifiers]. If it is " + "dynamic, add 'classifiers' to [project.dynamic].\n" + "ATTENTION: Per default Poetry determines classifiers for supported Python " + "versions and license automatically. If you define classifiers in [project], " + "you disable the automatic enrichment. In other words, you have to define all " + "classifiers manually. If you want to use Poetry's automatic enrichment of " + "classifiers, you should define them in [tool.poetry] and add 'classifiers' " + "to [project.dynamic].\n" + "Warning: [tool.poetry.homepage] is deprecated. Use [project.urls] instead.\n" + "Warning: [tool.poetry.repository] is deprecated. Use [project.urls] instead.\n" + "Warning: [tool.poetry.documentation] is deprecated. Use [project.urls] " + "instead.\n" + "Warning: Defining console scripts in [tool.poetry.scripts] is deprecated. " + "Use [project.scripts] instead. ([tool.poetry.scripts] should only be used " + "for scripts of type 'file').\n" + ) + + assert tester.io.fetch_error() == expected + + def test_check_invalid( mocker: MockerFixture, tester: CommandTester, fixture_dir: FixtureDirGetter ) -> None: @@ -71,10 +122,7 @@ def test_check_invalid( tester.execute("--lock") - fastjsonschema_error = "data must contain ['description'] properties" - custom_error = "The fields ['description'] are required in package mode." - expected_template = """\ -Error: {schema_error} + expected = """\ Error: Project name (invalid) is same as one of its dependencies Error: Unrecognized classifiers: ['Intended Audience :: Clowns']. Error: Declared README file does not exist: never/exists.md @@ -91,12 +139,8 @@ def test_check_invalid( 'Topic :: Communications :: Chat :: AOL Instant Messenger'.\ Must be removed. """ - expected = { - expected_template.format(schema_error=schema_error) - for schema_error in (fastjsonschema_error, custom_error) - } - assert tester.io.fetch_error() in expected + assert tester.io.fetch_error() == expected def test_check_private( diff --git a/tests/fixtures/invalid_pyproject/pyproject.toml b/tests/fixtures/invalid_pyproject/pyproject.toml index 94c7d9fb4d5..b830211ac6e 100644 --- a/tests/fixtures/invalid_pyproject/pyproject.toml +++ b/tests/fixtures/invalid_pyproject/pyproject.toml @@ -1,17 +1,17 @@ -[tool.poetry] +[project] name = "invalid" version = "1.0.0" -authors = [ - "Foo " -] -readme = "never/exists.md" -license = "INVALID" +license = { text = "INVALID" } classifiers = [ "Environment :: Console", "Intended Audience :: Clowns", "Natural Language :: Ukranian", "Topic :: Communications :: Chat :: AOL Instant Messenger", ] +dynamic = [ "readme", "dependencies", "requires-python" ] + +[tool.poetry] +readme = "never/exists.md" [tool.poetry.dependencies] python = "*" diff --git a/tests/fixtures/no_name_project/pyproject.toml b/tests/fixtures/no_name_project/pyproject.toml index f18fa403c06..10d8f3f3f3d 100644 --- a/tests/fixtures/no_name_project/pyproject.toml +++ b/tests/fixtures/no_name_project/pyproject.toml @@ -1,5 +1,5 @@ [tool.poetry] -name = "" +package-mode = false version = "1.2.3" description = "This project has no name" authors = [ diff --git a/tests/fixtures/outdated_lock/pyproject.toml b/tests/fixtures/outdated_lock/pyproject.toml index 257fbe6ea74..79dd46973fe 100644 --- a/tests/fixtures/outdated_lock/pyproject.toml +++ b/tests/fixtures/outdated_lock/pyproject.toml @@ -1,14 +1,10 @@ -[tool.poetry] +[project] name = "foobar" version = "0.1.0" -description = "" -authors = ["Poetry Developer "] - -[tool.poetry.dependencies] -python = "^3.8" -docker = "4.3.1" - -[tool.poetry.group.dev.dependencies] +requires-python = ">=3.8,<4.0" +dependencies = [ + "docker>=4.3.1", +] [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/tests/fixtures/private_pyproject/pyproject.toml b/tests/fixtures/private_pyproject/pyproject.toml index a572e83c8ff..f3cc460cf0e 100644 --- a/tests/fixtures/private_pyproject/pyproject.toml +++ b/tests/fixtures/private_pyproject/pyproject.toml @@ -1,17 +1,11 @@ -[tool.poetry] +[project] name = "private" version = "0.1.0" -description = "" -authors = ["Your Name "] -readme = "README.md" +requires-python = ">=3.7,<4.0" classifiers = [ "Private :: Do Not Upload", ] - -[tool.poetry.dependencies] -python = "^3.7" - [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" diff --git a/tests/fixtures/simple_project/pyproject.toml b/tests/fixtures/simple_project/pyproject.toml index 45a61d43cad..e85c7a74a20 100644 --- a/tests/fixtures/simple_project/pyproject.toml +++ b/tests/fixtures/simple_project/pyproject.toml @@ -1,20 +1,26 @@ -[tool.poetry] +[project] name = "simple-project" version = "1.2.3" description = "Some description." authors = [ - "Sébastien Eustace " + { name = "Sébastien Eustace", email = "sebastien@eustace.io" } ] -license = "MIT" - -readme = ["README.rst"] +license = { text = "MIT" } +readme = "README.rst" +keywords = ["packaging", "dependency", "poetry"] +dynamic = [ "classifiers", "dependencies", "requires-python" ] +[project.urls] homepage = "https://python-poetry.org" repository = "https://github.com/python-poetry/poetry" documentation = "https://python-poetry.org/docs" -keywords = ["packaging", "dependency", "poetry"] +[project.scripts] +foo = "foo:bar" +baz = "bar:baz.boom.bim" +fox = "fuz.foo:bar.baz" +[tool.poetry] classifiers = [ "Topic :: Software Development :: Build Tools", "Topic :: Software Development :: Libraries :: Python Modules" @@ -24,12 +30,6 @@ classifiers = [ [tool.poetry.dependencies] python = "~2.7 || ^3.4" -[tool.poetry.scripts] -foo = "foo:bar" -baz = "bar:baz.boom.bim" -fox = "fuz.foo:bar.baz" - - [build-system] requires = ["poetry-core>=1.1.0a7"] build-backend = "poetry.core.masonry.api" diff --git a/tests/fixtures/simple_project_legacy/README.rst b/tests/fixtures/simple_project_legacy/README.rst new file mode 100644 index 00000000000..f7fe15470f9 --- /dev/null +++ b/tests/fixtures/simple_project_legacy/README.rst @@ -0,0 +1,2 @@ +My Package +========== diff --git a/tests/fixtures/simple_project_legacy/pyproject.toml b/tests/fixtures/simple_project_legacy/pyproject.toml new file mode 100644 index 00000000000..45a61d43cad --- /dev/null +++ b/tests/fixtures/simple_project_legacy/pyproject.toml @@ -0,0 +1,35 @@ +[tool.poetry] +name = "simple-project" +version = "1.2.3" +description = "Some description." +authors = [ + "Sébastien Eustace " +] +license = "MIT" + +readme = ["README.rst"] + +homepage = "https://python-poetry.org" +repository = "https://github.com/python-poetry/poetry" +documentation = "https://python-poetry.org/docs" + +keywords = ["packaging", "dependency", "poetry"] + +classifiers = [ + "Topic :: Software Development :: Build Tools", + "Topic :: Software Development :: Libraries :: Python Modules" +] + +# Requirements +[tool.poetry.dependencies] +python = "~2.7 || ^3.4" + +[tool.poetry.scripts] +foo = "foo:bar" +baz = "bar:baz.boom.bim" +fox = "fuz.foo:bar.baz" + + +[build-system] +requires = ["poetry-core>=1.1.0a7"] +build-backend = "poetry.core.masonry.api" diff --git a/tests/fixtures/private_pyproject/README.md b/tests/fixtures/simple_project_legacy/simple_project/__init__.py similarity index 100% rename from tests/fixtures/private_pyproject/README.md rename to tests/fixtures/simple_project_legacy/simple_project/__init__.py diff --git a/tests/fixtures/up_to_date_lock/poetry.lock b/tests/fixtures/up_to_date_lock/poetry.lock index ad184f3353c..c3b04ddf2a3 100644 --- a/tests/fixtures/up_to_date_lock/poetry.lock +++ b/tests/fixtures/up_to_date_lock/poetry.lock @@ -140,4 +140,4 @@ six = "*" [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "ff2489c48d3c858a11c1ce7463ae5dc1524d9d457826c1bf16fd687a7bc1e819" +content-hash = "115cf985d932e9bf5f540555bbdd75decbb62cac81e399375fc19f6277f8c1d8" diff --git a/tests/fixtures/up_to_date_lock/pyproject.toml b/tests/fixtures/up_to_date_lock/pyproject.toml index adaafb9481a..79dd46973fe 100644 --- a/tests/fixtures/up_to_date_lock/pyproject.toml +++ b/tests/fixtures/up_to_date_lock/pyproject.toml @@ -1,14 +1,10 @@ -[tool.poetry] +[project] name = "foobar" version = "0.1.0" -description = "" -authors = ["Poetry Developer "] - -[tool.poetry.dependencies] -python = "^3.8" -docker = ">=4.3.1" - -[tool.poetry.group.dev.dependencies] +requires-python = ">=3.8,<4.0" +dependencies = [ + "docker>=4.3.1", +] [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/tests/installation/test_chef.py b/tests/installation/test_chef.py index ffe0e06b3bd..9a659bc0a2a 100644 --- a/tests/installation/test_chef.py +++ b/tests/installation/test_chef.py @@ -69,7 +69,7 @@ def test_prepare_directory( chef = Chef( artifact_cache, EnvManager.get_system_env(), Factory.create_pool(config) ) - archive = fixture_dir("simple_project").resolve() + archive = fixture_dir("simple_project_legacy").resolve() wheel = chef.prepare(archive) @@ -111,7 +111,7 @@ def test_prepare_directory_editable( chef = Chef( artifact_cache, EnvManager.get_system_env(), Factory.create_pool(config) ) - archive = fixture_dir("simple_project").resolve() + archive = fixture_dir("simple_project_legacy").resolve() wheel = chef.prepare(archive, editable=True) diff --git a/tests/json/test_schema_sources.py b/tests/json/test_schema_sources.py index f0a998276db..9e5d6fca50b 100644 --- a/tests/json/test_schema_sources.py +++ b/tests/json/test_schema_sources.py @@ -12,22 +12,19 @@ def test_pyproject_toml_valid_legacy() -> None: toml: dict[str, Any] = TOMLFile(FIXTURE_DIR / "complete_valid_legacy.toml").read() - content = toml["tool"]["poetry"] - assert Factory.validate(content) == {"errors": [], "warnings": []} + assert Factory.validate(toml) == {"errors": [], "warnings": []} def test_pyproject_toml_valid() -> None: toml: dict[str, Any] = TOMLFile(FIXTURE_DIR / "complete_valid.toml").read() - content = toml["tool"]["poetry"] - assert Factory.validate(content) == {"errors": [], "warnings": []} + assert Factory.validate(toml) == {"errors": [], "warnings": []} def test_pyproject_toml_invalid_priority() -> None: toml: dict[str, Any] = TOMLFile( FIXTURE_DIR / "complete_invalid_priority.toml" ).read() - content = toml["tool"]["poetry"] - assert Factory.validate(content) == { + assert Factory.validate(toml) == { "errors": [ "data.source[0].priority must be one of ['primary', 'default', " "'secondary', 'supplemental', 'explicit']" @@ -40,8 +37,7 @@ def test_pyproject_toml_invalid_priority_legacy_and_new() -> None: toml: dict[str, Any] = TOMLFile( FIXTURE_DIR / "complete_invalid_priority_legacy_and_new.toml" ).read() - content = toml["tool"]["poetry"] - assert Factory.validate(content) == { + assert Factory.validate(toml) == { "errors": ["data.source[0] must NOT match a disallowed definition"], "warnings": [], } diff --git a/tests/masonry/builders/test_editable_builder.py b/tests/masonry/builders/test_editable_builder.py index 6e2e332fdbb..18551c5af6e 100644 --- a/tests/masonry/builders/test_editable_builder.py +++ b/tests/masonry/builders/test_editable_builder.py @@ -108,9 +108,19 @@ def expected_metadata_version() -> str: return metadata.metadata_version +@pytest.mark.parametrize("project", ("simple_project", "simple_project_legacy")) def test_builder_installs_proper_files_for_standard_packages( - simple_poetry: Poetry, tmp_venv: VirtualEnv + project: str, + simple_poetry: Poetry, + tmp_path: Path, + fixture_dir: FixtureDirGetter, ) -> None: + simple_poetry = Factory().create_poetry(fixture_dir(project)) + env_manager = EnvManager(simple_poetry) + venv_path = tmp_path / "venv" + env_manager.build_venv(venv_path) + tmp_venv = VirtualEnv(venv_path) + builder = EditableBuilder(simple_poetry, tmp_venv, NullIO()) builder.build() diff --git a/tests/puzzle/test_solver.py b/tests/puzzle/test_solver.py index 36f41d88b05..5c2fc8496ce 100644 --- a/tests/puzzle/test_solver.py +++ b/tests/puzzle/test_solver.py @@ -13,6 +13,8 @@ from cleo.io.null_io import NullIO from packaging.utils import canonicalize_name from poetry.core.packages.dependency import Dependency +from poetry.core.packages.dependency_group import MAIN_GROUP +from poetry.core.packages.dependency_group import DependencyGroup from poetry.core.packages.package import Package from poetry.core.packages.project_package import ProjectPackage from poetry.core.packages.vcs_dependency import VCSDependency @@ -3118,17 +3120,30 @@ def test_solver_chooses_from_correct_repository_if_forced( assert ops[0].package.source_url == legacy_repository.url +@pytest.mark.parametrize("project_dependencies", [True, False]) def test_solver_chooses_from_correct_repository_if_forced_and_transitive_dependency( package: ProjectPackage, io: NullIO, legacy_repository: LegacyRepository, pypi_repository: PyPiRepository, + project_dependencies: bool, ) -> None: package.python_versions = "^3.7" - package.add_dependency(Factory.create_dependency("foo", "^1.0")) - package.add_dependency( - Factory.create_dependency("tomlkit", {"version": "^0.5", "source": "legacy"}) - ) + if project_dependencies: + main_group = DependencyGroup(MAIN_GROUP) + package.add_dependency_group(main_group) + main_group.add_dependency(Factory.create_dependency("foo", "^1.0")) + main_group.add_dependency(Factory.create_dependency("tomlkit", "^0.5")) + main_group.add_poetry_dependency( + Factory.create_dependency("tomlkit", {"source": "legacy"}) + ) + else: + package.add_dependency(Factory.create_dependency("foo", "^1.0")) + package.add_dependency( + Factory.create_dependency( + "tomlkit", {"version": "^0.5", "source": "legacy"} + ) + ) repo = Repository("repo") foo = get_package("foo", "1.0.0") diff --git a/tests/test_factory.py b/tests/test_factory.py index 5cc433bb3c0..dee66ba6a9f 100644 --- a/tests/test_factory.py +++ b/tests/test_factory.py @@ -153,7 +153,7 @@ def test_create_poetry(fixture_dir: FixtureDirGetter) -> None: @pytest.mark.parametrize( ("project",), [ - ("simple_project",), + ("simple_project_legacy",), ("project_with_extras",), ], ) @@ -502,23 +502,21 @@ def test_poetry_with_two_default_sources( def test_validate(fixture_dir: FixtureDirGetter) -> None: complete = TOMLFile(fixture_dir("complete.toml")) pyproject: dict[str, Any] = complete.read() - content = pyproject["tool"]["poetry"] - assert Factory.validate(content) == {"errors": [], "warnings": []} + assert Factory.validate(pyproject) == {"errors": [], "warnings": []} def test_validate_fails(fixture_dir: FixtureDirGetter) -> None: complete = TOMLFile(fixture_dir("complete.toml")) pyproject: dict[str, Any] = complete.read() - content = pyproject["tool"]["poetry"] - content["this key is not in the schema"] = "" + pyproject["tool"]["poetry"]["this key is not in the schema"] = "" expected = ( "Additional properties are not allowed " "('this key is not in the schema' was unexpected)" ) - assert Factory.validate(content) == {"errors": [expected], "warnings": []} + assert Factory.validate(pyproject) == {"errors": [expected], "warnings": []} def test_create_poetry_fails_on_invalid_configuration( @@ -527,20 +525,12 @@ def test_create_poetry_fails_on_invalid_configuration( with pytest.raises(RuntimeError) as e: Factory().create_poetry(fixture_dir("invalid_pyproject")) - fastjsonschema_error = "data must contain ['description'] properties" - custom_error = "The fields ['description'] are required in package mode." - - expected_template = """\ + expected = """\ The Poetry configuration is invalid: - - {schema_error} - Project name (invalid) is same as one of its dependencies """ - expected = { - expected_template.format(schema_error=schema_error) - for schema_error in (fastjsonschema_error, custom_error) - } - assert str(e.value) in expected + assert str(e.value) == expected def test_create_poetry_fails_on_nameless_project( @@ -549,19 +539,12 @@ def test_create_poetry_fails_on_nameless_project( with pytest.raises(RuntimeError) as e: Factory().create_poetry(fixture_dir("nameless_pyproject")) - fastjsonschema_error = "data must contain ['name'] properties" - custom_error = "The fields ['name'] are required in package mode." - - expected_template = """\ + expected = """\ The Poetry configuration is invalid: - - {schema_error} + - Either [project.name] or [tool.poetry.name] is required in package mode. """ - expected = { - expected_template.format(schema_error=schema_error) - for schema_error in (fastjsonschema_error, custom_error) - } - assert str(e.value) in expected + assert str(e.value) == expected def test_create_poetry_with_local_config(fixture_dir: FixtureDirGetter) -> None: diff --git a/tests/utils/env/test_env_manager.py b/tests/utils/env/test_env_manager.py index a7b02c54513..88d973da3c7 100644 --- a/tests/utils/env/test_env_manager.py +++ b/tests/utils/env/test_env_manager.py @@ -1156,7 +1156,9 @@ def test_create_venv_project_name_empty_sets_correct_prompt( manager = EnvManager(poetry) poetry.package.python_versions = "^3.7" - venv_name = manager.generate_env_name("", str(poetry.file.path.parent)) + venv_name = manager.generate_env_name( + "non-package-mode", str(poetry.file.path.parent) + ) mocker.patch("sys.version_info", (2, 7, 16)) mocker.patch("shutil.which", side_effect=lambda py: f"/usr/bin/{py}") @@ -1179,7 +1181,7 @@ def test_create_venv_project_name_empty_sets_correct_prompt( "no-pip": False, "no-setuptools": False, }, - prompt="virtualenv-py3.7", + prompt="non-package-mode-py3.7", ) From 45dcc2255235b3353d0198d5d6d8b4f86262a97e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Randy=20D=C3=B6ring?= <30527984+radoering@users.noreply.github.com> Date: Sun, 10 Mar 2024 11:30:28 +0100 Subject: [PATCH 3/9] add support for PEP 621: consider relevant project sections when calculating the content hash for the lock file (#9135) --- src/poetry/packages/locker.py | 36 +++++++++++++++++++--- tests/fixtures/up_to_date_lock/poetry.lock | 2 +- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/poetry/packages/locker.py b/src/poetry/packages/locker.py index 80177f16dd3..5d40c782921 100644 --- a/src/poetry/packages/locker.py +++ b/src/poetry/packages/locker.py @@ -59,6 +59,11 @@ class Locker: "dev-dependencies", ] _relevant_keys: ClassVar[list[str]] = [*_legacy_keys, "group"] + _relevant_project_keys: ClassVar[list[str]] = [ + "requires-python", + "dependencies", + "optional-dependencies", + ] def __init__(self, lock: Path, pyproject_data: dict[str, Any]) -> None: self._lock = lock @@ -324,16 +329,37 @@ def _get_content_hash(self) -> str: """ Returns the sha256 hash of the sorted content of the pyproject file. """ - content = self._pyproject_data.get("tool", {}).get("poetry", {}) + project_content = self._pyproject_data.get("project", {}) + tool_poetry_content = self._pyproject_data.get("tool", {}).get("poetry", {}) - relevant_content = {} + relevant_project_content = {} + for key in self._relevant_project_keys: + data = project_content.get(key) + if data is not None: + relevant_project_content[key] = data + + relevant_poetry_content = {} for key in self._relevant_keys: - data = content.get(key) + data = tool_poetry_content.get(key) - if data is None and key not in self._legacy_keys: + if data is None and ( + # Special handling for legacy keys is just for backwards compatibility, + # and thereby not required if there is relevant content in [project]. + key not in self._legacy_keys or relevant_project_content + ): continue - relevant_content[key] = data + relevant_poetry_content[key] = data + + if relevant_project_content: + relevant_content = { + "project": relevant_project_content, + "tool": {"poetry": relevant_poetry_content}, + } + else: + # For backwards compatibility, we have to put the relevant content + # of the [tool.poetry] section at top level! + relevant_content = relevant_poetry_content return sha256(json.dumps(relevant_content, sort_keys=True).encode()).hexdigest() diff --git a/tests/fixtures/up_to_date_lock/poetry.lock b/tests/fixtures/up_to_date_lock/poetry.lock index c3b04ddf2a3..b4dd4fd91ac 100644 --- a/tests/fixtures/up_to_date_lock/poetry.lock +++ b/tests/fixtures/up_to_date_lock/poetry.lock @@ -140,4 +140,4 @@ six = "*" [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "115cf985d932e9bf5f540555bbdd75decbb62cac81e399375fc19f6277f8c1d8" +content-hash = "8f975cfcda1d3c938f9e7013b2e24cb2e43e6a2a573f0c6867acad407b0fb0d9" From ee7bb20af1a9001590e5522d7357d5d716df5a91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Randy=20D=C3=B6ring?= <30527984+radoering@users.noreply.github.com> Date: Sun, 10 Mar 2024 13:20:26 +0100 Subject: [PATCH 4/9] add support for PEP 621: document `project` fields (#9135) - add notes about `dynamic` use cases - deprecate `tool.poetry` fields that can be completely replaced by `project` fields - add hint about `poetry check` --- docs/pyproject.md | 450 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 417 insertions(+), 33 deletions(-) diff --git a/docs/pyproject.md b/docs/pyproject.md index 75ad139623c..6c52ee172d7 100644 --- a/docs/pyproject.md +++ b/docs/pyproject.md @@ -11,11 +11,355 @@ menu: # The `pyproject.toml` file +In package mode, the only required fields are `name` and `version` +(either in the `project` section or in the `tool.poetry` section). +Other fields are optional. +In non-package mode, all fields are optional. + +{{% note %}} +Run `poetry check` to print warnings about deprecated fields. +{{% /note %}} + + +## The `project` section + +The `project` section of the `pyproject.toml` file according to the +[specification of the PyPA](https://packaging.python.org/en/latest/specifications/pyproject-toml/#declaring-project-metadata-the-project-table). + +### name + +The name of the package. **Required in package mode** + +This should be a valid name as defined by [PEP 508](https://peps.python.org/pep-0508/#names). + + +```toml +name = "my-package" +``` + +### version + +The version of the package. **Required in package mode** + +This should be a valid [PEP 440](https://peps.python.org/pep-0440/) string. + +```toml +version = "0.1.0" +``` + +If you want to set the version dynamically via `poetry build --local-version` +or you are using a plugin, which sets the version dynamically, you should add `version` +to dynamic and define the base version in the `tool.poetry` section, for example: + +```toml +[project] +name = "my-package" +dynamic = [ "version" ] + +[tool.poetry] +version = "1.0" # base version +``` + +### description + +A short description of the package. + +```toml +description = "A short description of the package." +``` + +### license + +The license of the package. + +The recommended notation for the most common licenses is (alphabetical): + +* Apache-2.0 +* BSD-2-Clause +* BSD-3-Clause +* BSD-4-Clause +* GPL-2.0-only +* GPL-2.0-or-later +* GPL-3.0-only +* GPL-3.0-or-later +* LGPL-2.1-only +* LGPL-2.1-or-later +* LGPL-3.0-only +* LGPL-3.0-or-later +* MIT + +Optional, but it is highly recommended to supply this. +More identifiers are listed at the [SPDX Open Source License Registry](https://spdx.org/licenses/). + +```toml +license = { text = "MIT" } +``` +{{% note %}} +If your project is proprietary and does not use a specific licence, you can set this value as `Proprietary`. +{{% /note %}} + +You can also specify a license file. However, when doing this the complete license text +will be added to the metadata and the License classifier cannot be determined +automatically so that you have to add it manually. + +```toml +license = { file = "LICENSE" } +``` + +### readme + +A path to the README file or the content. + +```toml +[tool.poetry] +# ... +readme = "README.md" +``` + +{{% note %}} +If you want to define multiple README files, you have to add `readme` to `dynamic` +and define them in the `tool.poetry` section. +{{% /note %}} + +```toml +[project] +# ... +dynamic = [ "readme" ] + +[tool.poetry] +# ... +readme = ["docs/README1.md", "docs/README2.md"] +``` + +### requires-python + +The Python version requirements of the project. + +```toml +requires-python = ">=3.8" +``` + +{{% note %}} +If you need an upper bound for locking, but do not want to define an upper bound +in your package metadata, you can omit the upper bound in the `requires-python` field +and add it in the `tool.poetry.dependencies` section. +{{% /note %}} + +```toml +[project] +# ... +requires-python = ">=3.8" + +[tool.poetry.dependencies] +python = ">=3.8,<4.0" +``` + +### authors + +The authors of the package. + +This is a list of authors and should contain at least one author. + +```toml +authors = [ + { name = "Sébastien Eustace", email = "sebastien@eustace.io" }, +] +``` + +### maintainers + +The maintainers of the package. + +This is a list of maintainers and should be distinct from authors. + +```toml +maintainers = [ + { name = "John Smith", email = "johnsmith@example.org" }, + { name = "Jane Smith", email = "janesmith@example.org" }, +] +``` + +### keywords + +A list of keywords that the package is related to. + +```toml +keywords = [ "packaging", "poetry" ] +``` + +### classifiers + +A list of PyPI [trove classifiers](https://pypi.org/classifiers/) that describe the project. + +```toml +classifiers = [ + "Topic :: Software Development :: Build Tools", + "Topic :: Software Development :: Libraries :: Python Modules" +] +``` + +{{% warning %}} +Note that suitable classifiers based on your `python` requirement and `license` +are **not** automatically added for you if you define classifiers statically +in the `project` section. + +If you want to enrich classifiers automatically, you should add `classifiers` to `dynamic` +and use the `tool.poetry` section instead. +{{% /warning %}} + +```toml +[project] +# ... +dynamic = [ "classifiers" ] + +[tool.poetry] +# ... +classifiers = [ + "Topic :: Software Development :: Build Tools", + "Topic :: Software Development :: Libraries :: Python Modules" +] +``` + +### urls + +The URLs of the project. + +```toml +[tool.poetry.urls] +homepage = "https://python-poetry.org/" +repository = "https://github.com/python-poetry/poetry" +documentation = "https://python-poetry.org/docs/" +"Bug Tracker" = "https://github.com/python-poetry/poetry/issues" +``` + +If you publish your package on PyPI, they will appear in the `Project Links` section. + +### scripts + +This section describes the console scripts that will be installed when installing the package. + +```toml +[project.scripts] +my_package_cli = 'my_package.console:run' +``` + +Here, we will have the `my_package_cli` script installed which will execute the `run` function in the `console` module in the `my_package` package. + +{{% note %}} +When a script is added or updated, run `poetry install` to make them available in the project's virtualenv. +{{% /note %}} + +### gui-scripts + +This section describes the GUI scripts that will be installed when installing the package. + +```toml +[project.scripts] +my_package_gui = 'my_package.gui:run' +``` + +Here, we will have the `my_package_gui` script installed which will execute the `run` function in the `gui` module in the `my_package` package. + +{{% note %}} +When a script is added or updated, run `poetry install` to make them available in the project's virtualenv. +{{% /note %}} + +### entry-points + +Entry points can be used to define plugins for your package. + +Poetry supports arbitrary plugins, which are exposed as the ecosystem-standard +[entry points](https://packaging.python.org/en/latest/specifications/entry-points/) +and discoverable using `importlib.metadata`. +This is similar to (and compatible with) the entry points feature of `setuptools`. +The syntax for registering a plugin is: + +```toml +[project.entry-points] # Optional super table + +[project.entry-points."A"] +B = "C:D" +``` +Which are: + +- `A` - type of the plugin, for example `poetry.plugin` or `flake8.extension` +- `B` - name of the plugin +- `C` - python module import path +- `D` - the entry point of the plugin (a function or class) + +Example (from [`poetry-plugin-export`](http://github.com/python-poetry/poetry-plugin-export)): + +```toml +[project.entry-points."poetry.application.plugin"] +export = "poetry_plugin_export.plugins:ExportApplicationPlugin" +``` + +### dependencies + +The `dependencies` of the project. + +```toml +dependencies = [ + "requests>=2.13.0", +] +``` + +These are the dependencies that will be declared when building an sdist or a wheel. + +If you want to define additional information that is not required for building +but only for locking (for example an explicit source), you can enrich dependency +information in the `tool.poetry` section. + +```toml +[project] +# ... +dependencies = [ + "requests>=2.13.0", +] + +[tool.poetry.dependencies] +requests = { source = "private-source" } +``` + +Alternatively, you can add `dependencies` to `dynamic` and define your dependencies +completely in the `tool.poetry` section. Using only the `tool.poetry` section might +make sense in non-package mode when you will not build an sdist or a wheel. + +```toml +[project] +# ... +dynamic = [ "dependencies" ] + +[tool.poetry.dependencies] +requests = { version = ">=2.13.0", source = "private-source" } +``` + +### optional-dependencies + +The optional dependencies of the project (also known as extras). + +```toml +[project.optional-dependencies] +mysql = [ "mysqlclient>=1.3,<2.0" ] +pgsql = [ "psycopg2>=2.9,<3.0" ] +databases = [ "mysqlclient>=1.3,<2.0", "psycopg2>=2.9,<3.0" ] +``` + +{{% note %}} + +You can enrich optional dependencies for locking in the `tool.poetry` section +analogous to `dependencies`. + +{{% /note %}} + + +## The `tool.poetry` section + The `tool.poetry` section of the `pyproject.toml` file is composed of multiple sections. -## package-mode +### package-mode -Whether Poetry operates in package mode (default) or not. **Optional** +Whether Poetry operates in package mode (default) or not. See [basic usage]({{< relref "basic-usage#operating-modes" >}}) for more information. @@ -23,9 +367,11 @@ See [basic usage]({{< relref "basic-usage#operating-modes" >}}) for more informa package-mode = false ``` -## name +### name -The name of the package. **Required in package mode** +**Deprecated**: Use `project.name` instead. + +The name of the package. **Required in package mode if not defined in the project section** This should be a valid name as defined by [PEP 508](https://peps.python.org/pep-0508/#names). @@ -34,9 +380,15 @@ This should be a valid name as defined by [PEP 508](https://peps.python.org/pep- name = "my-package" ``` -## version +### version -The version of the package. **Required in package mode** +{{% note %}} +If you do not want to set the version dynamically via `poetry build --local-version` +and you are not using a plugin, which sets the version dynamically, +prefer `project.version` over this setting. +{{% /note %}} + +The version of the package. **Required in package mode if not defined in the project section** This should be a valid [PEP 440](https://peps.python.org/pep-0440/) string. @@ -51,15 +403,19 @@ If you would like to use semantic versioning for your project, please see {{% /note %}} -## description +### description -A short description of the package. **Required in package mode** +**Deprecated**: Use `project.description` instead. + +A short description of the package. ```toml description = "A short description of the package." ``` -## license +### license + +**Deprecated**: Use `project.license` instead. The license of the package. @@ -89,9 +445,11 @@ license = "MIT" If your project is proprietary and does not use a specific licence, you can set this value as `Proprietary`. {{% /note %}} -## authors +### authors -The authors of the package. **Required in package mode** +**Deprecated**: Use `project.authors` instead. + +The authors of the package. This is a list of authors and should contain at least one author. Authors must be in the form `name `. @@ -101,9 +459,11 @@ authors = [ ] ``` -## maintainers +### maintainers + +**Deprecated**: Use `project.maintainers` instead. -The maintainers of the package. **Optional** +The maintainers of the package. This is a list of maintainers and should be distinct from authors. Maintainers may contain an email and be in the form `name `. @@ -114,10 +474,13 @@ maintainers = [ ] ``` -## readme +### readme + +{{% note %}} +If you do not want to set multiple README files, prefer `project.readme` over this setting. +{{% /note %}} A path, or list of paths corresponding to the README file(s) of the package. -**Optional** The file(s) can be of any format, but if you intend to publish to PyPI keep the [recommendations for a PyPI-friendly README]( @@ -147,41 +510,49 @@ readme = "README.md" readme = ["docs/README1.md", "docs/README2.md"] ``` -## homepage +### homepage + +**Deprecated**: Use `project.urls` instead. -An URL to the website of the project. **Optional** +An URL to the website of the project. ```toml homepage = "https://python-poetry.org/" ``` -## repository +### repository -An URL to the repository of the project. **Optional** +**Deprecated**: Use `project.urls` instead. + +An URL to the repository of the project. ```toml repository = "https://github.com/python-poetry/poetry" ``` -## documentation +### documentation + +**Deprecated**: Use `project.urls` instead. -An URL to the documentation of the project. **Optional** +An URL to the documentation of the project. ```toml documentation = "https://python-poetry.org/docs/" ``` -## keywords +### keywords -A list of keywords that the package is related to. **Optional** +**Deprecated**: Use `project.keywords` instead. + +A list of keywords that the package is related to. ```toml keywords = ["packaging", "poetry"] ``` -## classifiers +### classifiers -A list of PyPI [trove classifiers](https://pypi.org/classifiers/) that describe the project. **Optional** +A list of PyPI [trove classifiers](https://pypi.org/classifiers/) that describe the project. ```toml [tool.poetry] @@ -193,12 +564,17 @@ classifiers = [ ``` {{% note %}} -Note that Python classifiers are still automatically added for you and are determined by your `python` requirement. +Note that Python classifiers are automatically added for you +and are determined by your `python` requirement. The `license` property will also set the License classifier automatically. + +If you do not want Poetry to automatically add suitable classifiers +based on the `python` requirement and `license` property, +use `project.classifiers` instead of this setting. {{% /note %}} -## packages +### packages A list of packages and modules to include in the final distribution. @@ -271,7 +647,7 @@ Poetry is clever enough to detect Python subpackages. Thus, you only have to specify the directory where your root package resides. {{% /note %}} -## include and exclude +### include and exclude A list of patterns that will be included in the final package. @@ -309,7 +685,7 @@ In contrast, `exclude` defaults to both `sdist` and `wheel`. exclude = ["my_package/excluded.py"] ``` -## dependencies and dependency groups +### dependencies and dependency groups Poetry is configured to look for dependencies on [PyPI](https://pypi.org) by default. Only the name and a version string are required in this case. @@ -360,7 +736,9 @@ See [Dependency groups]({{< relref "managing-dependencies#dependency-groups" >}} at how to manage dependency groups and [Dependency specification]({{< relref "dependency-specification" >}}) for more information on other keys and specifying version ranges. -## `scripts` +### scripts + +**Deprecated**: Use `project.scripts` instead. This section describes the scripts or executables that will be installed when installing the package @@ -375,7 +753,9 @@ Here, we will have the `my_package_cli` script installed which will execute the When a script is added or updated, run `poetry install` to make them available in the project's virtualenv. {{% /note %}} -## `extras` +### extras + +**Deprecated**: Use `project.optional-dependencies` instead. Poetry supports extras to allow expression of: @@ -447,7 +827,9 @@ Dependencies listed in [dependency groups]({{< relref "managing-dependencies#dep {{% /note %}} -## `plugins` +### plugins + +**Deprecated**: Use `project.entry-points` instead. Poetry supports arbitrary plugins, which are exposed as the ecosystem-standard [entry points](https://packaging.python.org/en/latest/specifications/entry-points/) and discoverable using `importlib.metadata`. This is similar to (and compatible with) the entry points feature of `setuptools`. The syntax for registering a plugin is: @@ -472,7 +854,9 @@ Example (from [`poetry-plugin-export`](http://github.com/python-poetry/poetry-pl export = "poetry_plugin_export.plugins:ExportApplicationPlugin" ``` -## `urls` +### urls + +**Deprecated**: Use `project.urls` instead. In addition to the basic urls (`homepage`, `repository` and `documentation`), you can specify any custom url in the `urls` section. From dca71728d4fd1e0496aa27c4a426fe823c5ffe36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Randy=20D=C3=B6ring?= <30527984+radoering@users.noreply.github.com> Date: Fri, 15 Mar 2024 17:53:52 +0100 Subject: [PATCH 5/9] add support for PEP 621: poetry remove (#9135) --- src/poetry/console/commands/remove.py | 73 +++++---- tests/console/commands/test_remove.py | 140 ++++++++++++++--- .../up_to_date_lock_non_package/poetry.lock | 143 ++++++++++++++++++ .../pyproject.toml | 10 ++ 4 files changed, 313 insertions(+), 53 deletions(-) create mode 100644 tests/fixtures/up_to_date_lock_non_package/poetry.lock create mode 100644 tests/fixtures/up_to_date_lock_non_package/pyproject.toml diff --git a/src/poetry/console/commands/remove.py b/src/poetry/console/commands/remove.py index 718a7d59903..dcd7ffff08c 100644 --- a/src/poetry/console/commands/remove.py +++ b/src/poetry/console/commands/remove.py @@ -7,6 +7,7 @@ from cleo.helpers import argument from cleo.helpers import option from packaging.utils import canonicalize_name +from poetry.core.packages.dependency import Dependency from poetry.core.packages.dependency_group import MAIN_GROUP from tomlkit.toml_document import TOMLDocument @@ -66,39 +67,45 @@ def handle(self) -> int: group = self.option("group", self.default_group) content: dict[str, Any] = self.poetry.file.read() - poetry_content = content["tool"]["poetry"] + project_content = content.get("project", {}) + poetry_content = content.get("tool", {}).get("poetry", {}) if group is None: - removed = [] + # remove from all groups + removed = set() group_sections = [ - (group_name, group_section.get("dependencies", {})) - for group_name, group_section in poetry_content.get("group", {}).items() + ( + MAIN_GROUP, + project_content.get("dependencies", []), + poetry_content.get("dependencies", {}), + ) ] + group_sections.extend( + (group_name, [], group_section.get("dependencies", {})) + for group_name, group_section in poetry_content.get("group", {}).items() + ) - for group_name, section in [ - (MAIN_GROUP, poetry_content["dependencies"]), - *group_sections, - ]: - removed += self._remove_packages(packages, section, group_name) - if group_name != MAIN_GROUP: - if not section: - del poetry_content["group"][group_name] - else: - poetry_content["group"][group_name]["dependencies"] = section + for group_name, project_section, poetry_section in group_sections: + removed |= self._remove_packages( + packages, project_section, poetry_section, group_name + ) + if group_name != MAIN_GROUP and not poetry_section: + del poetry_content["group"][group_name] elif group == "dev" and "dev-dependencies" in poetry_content: # We need to account for the old `dev-dependencies` section removed = self._remove_packages( - packages, poetry_content["dev-dependencies"], "dev" + packages, [], poetry_content["dev-dependencies"], "dev" ) if not poetry_content["dev-dependencies"]: del poetry_content["dev-dependencies"] else: - removed = [] + removed = set() if "group" in poetry_content: if group in poetry_content["group"]: removed = self._remove_packages( packages, + [], poetry_content["group"][group].get("dependencies", {}), group, ) @@ -109,15 +116,13 @@ def handle(self) -> int: if "group" in poetry_content and not poetry_content["group"]: del poetry_content["group"] - removed_set = set(removed) - not_found = set(packages).difference(removed_set) + not_found = set(packages).difference(removed) if not_found: raise ValueError( "The following packages were not found: " + ", ".join(sorted(not_found)) ) # Refresh the locker - content["tool"]["poetry"] = poetry_content self.poetry.locker.set_pyproject_data(content) self.installer.set_locker(self.poetry.locker) self.installer.set_package(self.poetry.package) @@ -125,7 +130,7 @@ def handle(self) -> int: self.installer.verbose(self.io.is_verbose()) self.installer.update(True) self.installer.execute_operations(not self.option("lock")) - self.installer.whitelist(removed_set) + self.installer.whitelist(removed) status = self.installer.run() @@ -136,17 +141,27 @@ def handle(self) -> int: return status def _remove_packages( - self, packages: list[str], section: dict[str, Any], group_name: str - ) -> list[str]: - removed = [] + self, + packages: list[str], + project_section: list[str], + poetry_section: dict[str, Any], + group_name: str, + ) -> set[str]: + removed = set() group = self.poetry.package.dependency_group(group_name) - section_keys = list(section.keys()) for package in packages: - for existing_package in section_keys: - if canonicalize_name(existing_package) == canonicalize_name(package): - del section[existing_package] - removed.append(package) - group.remove_dependency(package) + normalized_name = canonicalize_name(package) + for requirement in project_section.copy(): + if Dependency.create_from_pep_508(requirement).name == normalized_name: + project_section.remove(requirement) + removed.add(package) + for existing_package in list(poetry_section): + if canonicalize_name(existing_package) == normalized_name: + del poetry_section[existing_package] + removed.add(package) + + for package in removed: + group.remove_dependency(package) return removed diff --git a/tests/console/commands/test_remove.py b/tests/console/commands/test_remove.py index 6991045f82b..fb67774a50e 100644 --- a/tests/console/commands/test_remove.py +++ b/tests/console/commands/test_remove.py @@ -2,6 +2,7 @@ from typing import TYPE_CHECKING from typing import Any +from typing import Callable from typing import cast import pytest @@ -31,18 +32,21 @@ @pytest.fixture def poetry_with_up_to_date_lockfile( project_factory: ProjectFactory, fixture_dir: FixtureDirGetter -) -> Poetry: - source = fixture_dir("up_to_date_lock") +) -> Callable[[str], Poetry]: + def get_poetry(fixture_name: str) -> Poetry: + source = fixture_dir(fixture_name) - poetry = project_factory( - name="foobar", - pyproject_content=(source / "pyproject.toml").read_text(encoding="utf-8"), - poetry_lock_content=(source / "poetry.lock").read_text(encoding="utf-8"), - ) + poetry = project_factory( + name="foobar", + pyproject_content=(source / "pyproject.toml").read_text(encoding="utf-8"), + poetry_lock_content=(source / "poetry.lock").read_text(encoding="utf-8"), + ) + + assert isinstance(poetry.locker, TestLocker) + poetry.locker.locked(True) + return poetry - assert isinstance(poetry.locker, TestLocker) - poetry.locker.locked(True) - return poetry + return get_poetry @pytest.fixture() @@ -50,6 +54,81 @@ def tester(command_tester_factory: CommandTesterFactory) -> CommandTester: return command_tester_factory("remove") +def test_remove_from_project_and_poetry( + tester: CommandTester, + app: PoetryTestApplication, + repo: TestRepository, + installed: Repository, +) -> None: + repo.add_package(Package("foo", "2.0.0")) + repo.add_package(Package("bar", "1.0.0")) + + pyproject: dict[str, Any] = app.poetry.file.read() + + project_dependencies: dict[str, Any] = tomlkit.parse( + """\ +[project] +dependencies = [ + "foo>=2.0", + "bar>=1.0", +] +""" + ) + + poetry_dependencies: dict[str, Any] = tomlkit.parse( + """\ +[tool.poetry.dependencies] +foo = "^2.0.0" +bar = "^1.0.0" + +""" + ) + + pyproject["project"]["dependencies"] = project_dependencies["project"][ + "dependencies" + ] + pyproject["tool"]["poetry"]["dependencies"] = poetry_dependencies["tool"]["poetry"][ + "dependencies" + ] + pyproject = cast("TOMLDocument", pyproject) + app.poetry.file.write(pyproject) + + app.poetry.package.add_dependency(Factory.create_dependency("foo", "^2.0.0")) + app.poetry.package.add_dependency(Factory.create_dependency("bar", "^1.0.0")) + + tester.execute("foo") + + pyproject = app.poetry.file.read() + pyproject = cast("dict[str, Any]", pyproject) + project_dependencies = pyproject["project"]["dependencies"] + assert "foo>=2.0" not in project_dependencies + assert "bar>=1.0" in project_dependencies + poetry_dependencies = pyproject["tool"]["poetry"]["dependencies"] + assert "foo" not in poetry_dependencies + assert "bar" in poetry_dependencies + + expected_project_string = """\ +dependencies = [ + "bar>=1.0", +] +""" + expected_poetry_string = """\ + +[tool.poetry.dependencies] +bar = "^1.0.0" + +""" + pyproject = cast("TOMLDocument", pyproject) + string_content = pyproject.as_string() + if "\r\n" in string_content: + # consistent line endings + expected_project_string = expected_project_string.replace("\n", "\r\n") + expected_poetry_string = expected_poetry_string.replace("\n", "\r\n") + + assert expected_project_string in string_content + assert expected_poetry_string in string_content + + def test_remove_without_specific_group_removes_from_all_groups( tester: CommandTester, app: PoetryTestApplication, @@ -110,7 +189,7 @@ def test_remove_without_specific_group_removes_from_all_groups( assert expected in string_content -def test_remove_without_specific_group_removes_from_specific_groups( +def test_remove_with_specific_group_removes_from_specific_groups( tester: CommandTester, app: PoetryTestApplication, repo: TestRepository, @@ -169,7 +248,7 @@ def test_remove_without_specific_group_removes_from_specific_groups( assert expected in string_content -def test_remove_does_not_live_empty_groups( +def test_remove_does_not_keep_empty_groups( tester: CommandTester, app: PoetryTestApplication, repo: TestRepository, @@ -299,33 +378,41 @@ def test_remove_command_should_not_write_changes_upon_installer_errors( assert app.poetry.file.read().as_string() == original_content +@pytest.mark.parametrize( + "fixture_name", ["up_to_date_lock", "up_to_date_lock_non_package"] +) def test_remove_with_dry_run_keep_files_intact( - poetry_with_up_to_date_lockfile: Poetry, + fixture_name: str, + poetry_with_up_to_date_lockfile: Callable[[str], Poetry], repo: TestRepository, command_tester_factory: CommandTesterFactory, ) -> None: - tester = command_tester_factory("remove", poetry=poetry_with_up_to_date_lockfile) + poetry = poetry_with_up_to_date_lockfile(fixture_name) + tester = command_tester_factory("remove", poetry=poetry) - original_pyproject_content = poetry_with_up_to_date_lockfile.file.read() - original_lockfile_content = poetry_with_up_to_date_lockfile._locker.lock_data + original_pyproject_content = poetry.file.read() + original_lockfile_content = poetry._locker.lock_data repo.add_package(get_package("docker", "4.3.1")) tester.execute("docker --dry-run") - assert poetry_with_up_to_date_lockfile.file.read() == original_pyproject_content - assert ( - poetry_with_up_to_date_lockfile._locker.lock_data == original_lockfile_content - ) + assert poetry.file.read() == original_pyproject_content + assert poetry._locker.lock_data == original_lockfile_content +@pytest.mark.parametrize( + "fixture_name", ["up_to_date_lock", "up_to_date_lock_non_package"] +) def test_remove_performs_uninstall_op( - poetry_with_up_to_date_lockfile: Poetry, + fixture_name: str, + poetry_with_up_to_date_lockfile: Callable[[str], Poetry], command_tester_factory: CommandTesterFactory, installed: Repository, ) -> None: installed.add_package(get_package("docker", "4.3.1")) - tester = command_tester_factory("remove", poetry=poetry_with_up_to_date_lockfile) + poetry = poetry_with_up_to_date_lockfile(fixture_name) + tester = command_tester_factory("remove", poetry=poetry) tester.execute("docker") @@ -343,13 +430,18 @@ def test_remove_performs_uninstall_op( assert tester.io.fetch_output() == expected +@pytest.mark.parametrize( + "fixture_name", ["up_to_date_lock", "up_to_date_lock_non_package"] +) def test_remove_with_lock_does_not_perform_uninstall_op( - poetry_with_up_to_date_lockfile: Poetry, + fixture_name: str, + poetry_with_up_to_date_lockfile: Callable[[str], Poetry], command_tester_factory: CommandTesterFactory, installed: Repository, ) -> None: installed.add_package(get_package("docker", "4.3.1")) - tester = command_tester_factory("remove", poetry=poetry_with_up_to_date_lockfile) + poetry = poetry_with_up_to_date_lockfile(fixture_name) + tester = command_tester_factory("remove", poetry=poetry) tester.execute("docker --lock") diff --git a/tests/fixtures/up_to_date_lock_non_package/poetry.lock b/tests/fixtures/up_to_date_lock_non_package/poetry.lock new file mode 100644 index 00000000000..c3b04ddf2a3 --- /dev/null +++ b/tests/fixtures/up_to_date_lock_non_package/poetry.lock @@ -0,0 +1,143 @@ +# This file is automatically @generated by Poetry 1.5.0.dev0 and should not be changed by hand. + +[[package]] +name = "certifi" +version = "2020.12.5" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = "*" +files = [ + {file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"}, + {file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"}, +] + +[[package]] +name = "chardet" +version = "4.0.0" +description = "Universal encoding detector for Python 2 and 3" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, + {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, +] + +[[package]] +name = "docker" +version = "4.3.1" +description = "A Python library for the Docker Engine API." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "docker-4.3.1-py2.py3-none-any.whl", hash = "sha256:13966471e8bc23b36bfb3a6fb4ab75043a5ef1dac86516274777576bed3b9828"}, + {file = "docker-4.3.1.tar.gz", hash = "sha256:bad94b8dd001a8a4af19ce4becc17f41b09f228173ffe6a4e0355389eef142f2"}, +] + +[package.dependencies] +pywin32 = {version = "227", markers = "sys_platform == \"win32\""} +requests = ">=2.14.2,<2.18.0 || >2.18.0" +six = ">=1.4.0" +websocket-client = ">=0.32.0" + +[package.extras] +ssh = ["paramiko (>=2.4.2)"] +tls = ["cryptography (>=1.3.4)", "idna (>=2.0.0)", "pyOpenSSL (>=17.5.0)"] + +[[package]] +name = "idna" +version = "2.10" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, + {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, +] + +[[package]] +name = "pywin32" +version = "227" +description = "Python for Window Extensions" +optional = false +python-versions = "*" +files = [ + {file = "pywin32-227-cp27-cp27m-win32.whl", hash = "sha256:371fcc39416d736401f0274dd64c2302728c9e034808e37381b5e1b22be4a6b0"}, + {file = "pywin32-227-cp27-cp27m-win_amd64.whl", hash = "sha256:4cdad3e84191194ea6d0dd1b1b9bdda574ff563177d2adf2b4efec2a244fa116"}, + {file = "pywin32-227-cp35-cp35m-win32.whl", hash = "sha256:f4c5be1a293bae0076d93c88f37ee8da68136744588bc5e2be2f299a34ceb7aa"}, + {file = "pywin32-227-cp35-cp35m-win_amd64.whl", hash = "sha256:a929a4af626e530383a579431b70e512e736e9588106715215bf685a3ea508d4"}, + {file = "pywin32-227-cp36-cp36m-win32.whl", hash = "sha256:300a2db938e98c3e7e2093e4491439e62287d0d493fe07cce110db070b54c0be"}, + {file = "pywin32-227-cp36-cp36m-win_amd64.whl", hash = "sha256:9b31e009564fb95db160f154e2aa195ed66bcc4c058ed72850d047141b36f3a2"}, + {file = "pywin32-227-cp37-cp37m-win32.whl", hash = "sha256:47a3c7551376a865dd8d095a98deba954a98f326c6fe3c72d8726ca6e6b15507"}, + {file = "pywin32-227-cp37-cp37m-win_amd64.whl", hash = "sha256:31f88a89139cb2adc40f8f0e65ee56a8c585f629974f9e07622ba80199057511"}, + {file = "pywin32-227-cp38-cp38-win32.whl", hash = "sha256:7f18199fbf29ca99dff10e1f09451582ae9e372a892ff03a28528a24d55875bc"}, + {file = "pywin32-227-cp38-cp38-win_amd64.whl", hash = "sha256:7c1ae32c489dc012930787f06244426f8356e129184a02c25aef163917ce158e"}, + {file = "pywin32-227-cp39-cp39-win32.whl", hash = "sha256:c054c52ba46e7eb6b7d7dfae4dbd987a1bb48ee86debe3f245a2884ece46e295"}, + {file = "pywin32-227-cp39-cp39-win_amd64.whl", hash = "sha256:f27cec5e7f588c3d1051651830ecc00294f90728d19c3bf6916e6dba93ea357c"}, +] + +[[package]] +name = "requests" +version = "2.25.1" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, + {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +chardet = ">=3.0.2,<5" +idna = ">=2.5,<3" +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +security = ["cryptography (>=1.3.4)", "pyOpenSSL (>=0.14)"] +socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] + +[[package]] +name = "six" +version = "1.15.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, + {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, +] + +[[package]] +name = "urllib3" +version = "1.26.3" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" +files = [ + {file = "urllib3-1.26.3-py2.py3-none-any.whl", hash = "sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80"}, + {file = "urllib3-1.26.3.tar.gz", hash = "sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73"}, +] + +[package.extras] +brotli = ["brotlipy (>=0.6.0)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[[package]] +name = "websocket-client" +version = "0.58.0" +description = "WebSocket client for Python with low level API options" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "websocket_client-0.58.0-py2.py3-none-any.whl", hash = "sha256:44b5df8f08c74c3d82d28100fdc81f4536809ce98a17f0757557813275fbb663"}, + {file = "websocket_client-0.58.0.tar.gz", hash = "sha256:63509b41d158ae5b7f67eb4ad20fecbb4eee99434e73e140354dc3ff8e09716f"}, +] + +[package.dependencies] +six = "*" + +[metadata] +lock-version = "2.0" +python-versions = "^3.8" +content-hash = "115cf985d932e9bf5f540555bbdd75decbb62cac81e399375fc19f6277f8c1d8" diff --git a/tests/fixtures/up_to_date_lock_non_package/pyproject.toml b/tests/fixtures/up_to_date_lock_non_package/pyproject.toml new file mode 100644 index 00000000000..6760fde5449 --- /dev/null +++ b/tests/fixtures/up_to_date_lock_non_package/pyproject.toml @@ -0,0 +1,10 @@ +[tool.poetry] +package-mode = false + +[tool.poetry.dependencies] +python = "^3.8" +docker = ">=4.3.1" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" From 2f0e1af10dabcbd2bf844b8136f087811c2951f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Randy=20D=C3=B6ring?= <30527984+radoering@users.noreply.github.com> Date: Tue, 26 Mar 2024 10:51:22 +0100 Subject: [PATCH 6/9] add support for PEP 621: poetry add (#9135) --- src/poetry/console/commands/add.py | 115 ++++++++++--- tests/console/commands/test_add.py | 262 +++++++++++++++++++++++++---- 2 files changed, 320 insertions(+), 57 deletions(-) diff --git a/src/poetry/console/commands/add.py b/src/poetry/console/commands/add.py index 330b29c951c..303aca542da 100644 --- a/src/poetry/console/commands/add.py +++ b/src/poetry/console/commands/add.py @@ -9,6 +9,7 @@ from cleo.helpers import argument from cleo.helpers import option from packaging.utils import canonicalize_name +from poetry.core.packages.dependency import Dependency from poetry.core.packages.dependency_group import MAIN_GROUP from tomlkit.toml_document import TOMLDocument @@ -17,8 +18,11 @@ if TYPE_CHECKING: + from collections.abc import Collection + from cleo.io.inputs.argument import Argument from cleo.io.inputs.option import Option + from packaging.utils import NormalizedName class AddCommand(InstallerCommand, InitCommand): @@ -111,6 +115,7 @@ class AddCommand(InstallerCommand, InitCommand): def handle(self) -> int: from poetry.core.constraints.version import parse_constraint + from tomlkit import array from tomlkit import inline_table from tomlkit import nl from tomlkit import table @@ -135,16 +140,29 @@ def handle(self) -> int: # tomlkit types are awkward to work with, treat content as a mostly untyped # dictionary. content: dict[str, Any] = self.poetry.file.read() - poetry_content = content["tool"]["poetry"] + project_content = content.get("project", table()) + poetry_content = content.get("tool", {}).get("poetry", table()) project_name = ( - canonicalize_name(name) if (name := poetry_content.get("name")) else None + canonicalize_name(name) + if (name := project_content.get("name", poetry_content.get("name"))) + else None ) + use_project_section = False + project_dependency_names = [] if group == MAIN_GROUP: - if "dependencies" not in poetry_content: - poetry_content["dependencies"] = table() - - section = poetry_content["dependencies"] + if ( + "dependencies" in project_content + or "optional-dependencies" in project_content + ): + use_project_section = True + project_dependency_names = [ + Dependency.create_from_pep_508(dep).name + for dep in project_content.get("dependencies", {}) + ] + + poetry_section = poetry_content.get("dependencies", table()) + project_section = project_content.get("dependencies", array()) else: if "group" not in poetry_content: poetry_content["group"] = table(is_super_table=True) @@ -160,9 +178,12 @@ def handle(self) -> int: if "dependencies" not in this_group: this_group["dependencies"] = table() - section = this_group["dependencies"] + poetry_section = this_group["dependencies"] + project_section = [] - existing_packages = self.get_existing_packages_from_input(packages, section) + existing_packages = self.get_existing_packages_from_input( + packages, poetry_section, project_dependency_names + ) if existing_packages: self.notify_about_existing_packages(existing_packages) @@ -187,11 +208,11 @@ def handle(self) -> int: parse_constraint(version) constraint: dict[str, Any] = inline_table() - for name, value in _constraint.items(): - if name == "name": + for key, value in _constraint.items(): + if key == "name": continue - constraint[name] = value + constraint[key] = value if self.option("optional"): constraint["optional"] = True @@ -244,28 +265,61 @@ def handle(self) -> int: self.line_error("\nNo changes were applied.") return 1 - for key in section: - if canonicalize_name(key) == canonical_constraint_name: - section[key] = constraint - break - else: - section[constraint_name] = constraint - with contextlib.suppress(ValueError): self.poetry.package.dependency_group(group).remove_dependency( constraint_name ) - self.poetry.package.add_dependency( - Factory.create_dependency( - constraint_name, - constraint, - groups=[group], - root_dir=self.poetry.file.path.parent, - ) + dependency = Factory.create_dependency( + constraint_name, + constraint, + groups=[group], + root_dir=self.poetry.file.path.parent, ) + self.poetry.package.add_dependency(dependency) + + if use_project_section: + try: + index = project_dependency_names.index(canonical_constraint_name) + except ValueError: + project_section.append(dependency.to_pep_508()) + else: + project_section[index] = dependency.to_pep_508() + + # create a second constraint for tool.poetry.dependencies with keys + # that cannot be stored in the project section + poetry_constraint: dict[str, Any] = inline_table() + if not isinstance(constraint, str): + for key in ["optional", "allow-prereleases", "develop", "source"]: + if value := constraint.get(key): + poetry_constraint[key] = value + if poetry_constraint: + # add marker related keys to avoid ambiguity + for key in ["python", "platform"]: + if value := constraint.get(key): + poetry_constraint[key] = value + else: + poetry_constraint = constraint + + if poetry_constraint: + for key in poetry_section: + if canonicalize_name(key) == canonical_constraint_name: + poetry_section[key] = poetry_constraint + break + else: + poetry_section[constraint_name] = poetry_constraint # Refresh the locker + if project_section and "dependencies" not in project_content: + assert group == MAIN_GROUP + project_content["dependencies"] = project_section + if poetry_section: + if "tool" not in content: + content["tool"] = table() + if "poetry" not in content["tool"]: + content["tool"]["poetry"] = poetry_content + if group == MAIN_GROUP and "dependencies" not in poetry_content: + poetry_content["dependencies"] = poetry_section self.poetry.locker.set_pyproject_data(content) self.installer.set_locker(self.poetry.locker) @@ -289,13 +343,20 @@ def handle(self) -> int: return status def get_existing_packages_from_input( - self, packages: list[str], section: dict[str, Any] + self, + packages: list[str], + section: dict[str, Any], + project_dependencies: Collection[NormalizedName], ) -> list[str]: existing_packages = [] for name in packages: + normalized_name = canonicalize_name(name) + if normalized_name in project_dependencies: + existing_packages.append(name) + continue for key in section: - if canonicalize_name(key) == canonicalize_name(name): + if normalized_name == canonicalize_name(key): existing_packages.append(name) return existing_packages diff --git a/tests/console/commands/test_add.py b/tests/console/commands/test_add.py index 73ba0241018..2289bfb8ba9 100644 --- a/tests/console/commands/test_add.py +++ b/tests/console/commands/test_add.py @@ -1010,6 +1010,34 @@ def test_add_to_section_that_does_not_exist_yet( assert expected in string_content +def test_add_creating_poetry_section_does_not_remove_existing_tools( + repo: TestRepository, + project_factory: ProjectFactory, + command_tester_factory: CommandTesterFactory, +) -> None: + repo.add_package(get_package("cachy", "0.2.0")) + + poetry = project_factory( + name="foobar", + pyproject_content=( + '[project]\nname = "foobar"\nversion="0"\n' '[tool.foo]\nkey = "value"\n' + ), + ) + tester = command_tester_factory("add", poetry=poetry) + tester.execute("--group dev cachy") + + assert isinstance(tester.command, InstallerCommand) + assert tester.command.installer.executor.installations_count == 2 + + pyproject: dict[str, Any] = poetry.file.read() + content = pyproject["tool"]["poetry"] + + assert "cachy" in content["group"]["dev"]["dependencies"] + assert content["group"]["dev"]["dependencies"]["cachy"] == "^0.2.0" + assert "foo" in pyproject["tool"] + assert pyproject["tool"]["foo"]["key"] == "value" + + def test_add_to_dev_section_deprecated( app: PoetryTestApplication, tester: CommandTester ) -> None: @@ -1074,11 +1102,18 @@ def test_add_should_not_select_prereleases( assert content["dependencies"]["pyyaml"] == "^3.13" +@pytest.mark.parametrize("project_dependencies", [True, False]) def test_add_should_skip_when_adding_existing_package_with_no_constraint( - app: PoetryTestApplication, repo: TestRepository, tester: CommandTester + app: PoetryTestApplication, + repo: TestRepository, + tester: CommandTester, + project_dependencies: bool, ) -> None: pyproject: dict[str, Any] = app.poetry.file.read() - pyproject["tool"]["poetry"]["dependencies"]["foo"] = "^1.0" + if project_dependencies: + pyproject["project"]["dependencies"] = ["foo>1"] + else: + pyproject["tool"]["poetry"]["dependencies"]["foo"] = "^1.0" pyproject = cast("TOMLDocument", pyproject) app.poetry.file.write(pyproject) @@ -1099,11 +1134,18 @@ def test_add_should_skip_when_adding_existing_package_with_no_constraint( assert expected in tester.io.fetch_output() +@pytest.mark.parametrize("project_dependencies", [True, False]) def test_add_should_skip_when_adding_canonicalized_existing_package_with_no_constraint( - app: PoetryTestApplication, repo: TestRepository, tester: CommandTester + app: PoetryTestApplication, + repo: TestRepository, + tester: CommandTester, + project_dependencies: bool, ) -> None: pyproject: dict[str, Any] = app.poetry.file.read() - pyproject["tool"]["poetry"]["dependencies"]["foo-bar"] = "^1.0" + if project_dependencies: + pyproject["project"]["dependencies"] = ["foo-bar>1"] + else: + pyproject["tool"]["poetry"]["dependencies"]["foo-bar"] = "^1.0" pyproject = cast("TOMLDocument", pyproject) app.poetry.file.write(pyproject) @@ -1136,49 +1178,72 @@ def test_add_should_fail_circular_dependency( assert expected in tester.io.fetch_error() +@pytest.mark.parametrize("project_dependencies", [True, False]) def test_add_latest_should_not_create_duplicate_keys( project_factory: ProjectFactory, repo: TestRepository, command_tester_factory: CommandTesterFactory, + project_dependencies: bool, ) -> None: - pyproject_content = """\ - [tool.poetry] - name = "simple-project" - version = "1.2.3" - description = "Some description." - authors = [ - "Python Poetry " - ] - license = "MIT" - readme = "README.md" - - [tool.poetry.dependencies] - python = "^3.6" - Foo = "^0.6" - """ + if project_dependencies: + pyproject_content = """\ + [project] + name = "simple-project" + version = "1.2.3" + dependencies = [ + "Foo >= 0.6,<0.7", + ] + """ + else: + pyproject_content = """\ + [tool.poetry] + name = "simple-project" + version = "1.2.3" + + [tool.poetry.dependencies] + python = "^3.6" + Foo = "^0.6" + """ poetry = project_factory(name="simple-project", pyproject_content=pyproject_content) pyproject: dict[str, Any] = poetry.file.read() - assert "Foo" in pyproject["tool"]["poetry"]["dependencies"] - assert pyproject["tool"]["poetry"]["dependencies"]["Foo"] == "^0.6" - assert "foo" not in pyproject["tool"]["poetry"]["dependencies"] + if project_dependencies: + assert "tool" not in pyproject + assert pyproject["project"]["dependencies"] == ["Foo >= 0.6,<0.7"] + else: + assert "project" not in pyproject + assert "Foo" in pyproject["tool"]["poetry"]["dependencies"] + assert pyproject["tool"]["poetry"]["dependencies"]["Foo"] == "^0.6" + assert "foo" not in pyproject["tool"]["poetry"]["dependencies"] tester = command_tester_factory("add", poetry=poetry) repo.add_package(get_package("foo", "1.1.2")) tester.execute("foo@latest") updated_pyproject: dict[str, Any] = poetry.file.read() - assert "Foo" in updated_pyproject["tool"]["poetry"]["dependencies"] - assert updated_pyproject["tool"]["poetry"]["dependencies"]["Foo"] == "^1.1.2" - assert "foo" not in updated_pyproject["tool"]["poetry"]["dependencies"] + if project_dependencies: + assert "tool" not in updated_pyproject + assert updated_pyproject["project"]["dependencies"] == ["foo (>=1.1.2,<2.0.0)"] + else: + assert "project" not in updated_pyproject + assert "Foo" in updated_pyproject["tool"]["poetry"]["dependencies"] + assert updated_pyproject["tool"]["poetry"]["dependencies"]["Foo"] == "^1.1.2" + assert "foo" not in updated_pyproject["tool"]["poetry"]["dependencies"] +@pytest.mark.parametrize("project_dependencies", [True, False]) def test_add_should_work_when_adding_existing_package_with_latest_constraint( - app: PoetryTestApplication, repo: TestRepository, tester: CommandTester + app: PoetryTestApplication, + repo: TestRepository, + tester: CommandTester, + project_dependencies: bool, ) -> None: pyproject: dict[str, Any] = app.poetry.file.read() - pyproject["tool"]["poetry"]["dependencies"]["foo"] = "^1.0" + if project_dependencies: + pyproject["project"]["dependencies"] = ["foo>1"] + else: + pyproject["tool"]["poetry"]["dependencies"]["foo"] = "^1.0" pyproject = cast("TOMLDocument", pyproject) app.poetry.file.write(pyproject) @@ -1202,10 +1267,16 @@ def test_add_should_work_when_adding_existing_package_with_latest_constraint( assert expected in tester.io.fetch_output() pyproject2: dict[str, Any] = app.poetry.file.read() - content = pyproject2["tool"]["poetry"] + project_content = pyproject2["project"] + poetry_content = pyproject2["tool"]["poetry"] - assert "foo" in content["dependencies"] - assert content["dependencies"]["foo"] == "^1.1.2" + if project_dependencies: + assert "foo" not in poetry_content["dependencies"] + assert project_content["dependencies"] == ["foo (>=1.1.2,<2.0.0)"] + else: + assert "dependencies" not in project_content + assert "foo" in poetry_content["dependencies"] + assert poetry_content["dependencies"]["foo"] == "^1.1.2" def test_add_chooses_prerelease_if_only_prereleases_are_available( @@ -1466,3 +1537,134 @@ def test_add_does_not_update_locked_dependencies( p for p in lock_data["package"] if p["name"] == "docker" ) assert docker_locked_after_command["version"] == expected_docker + + +def test_add_creates_dependencies_array_if_necessary( + project_factory: ProjectFactory, + repo: TestRepository, + command_tester_factory: CommandTesterFactory, +) -> None: + pyproject_content = """\ + [project] + name = "simple-project" + version = "1.2.3" + + [project.optional-dependencies] + test = ["foo"] + """ + + poetry = project_factory(name="simple-project", pyproject_content=pyproject_content) + + repo.add_package(get_package("foo", "2.0")) + repo.add_package(get_package("bar", "2.0")) + + tester = command_tester_factory("add", poetry=poetry) + tester.execute("bar>=1.0") + + updated_pyproject: dict[str, Any] = poetry.file.read() + assert updated_pyproject["project"]["dependencies"] == ["bar (>=1.0)"] + + +@pytest.mark.parametrize("has_poetry_section", [True, False]) +def test_add_does_not_add_poetry_dependencies_if_not_necessary( + project_factory: ProjectFactory, + repo: TestRepository, + command_tester_factory: CommandTesterFactory, + has_poetry_section: bool, +) -> None: + pyproject_content = """\ + [project] + name = "simple-project" + version = "1.2.3" + dependencies = [ + "foo >= 1.0", + ] + """ + if has_poetry_section: + pyproject_content += """\ + [tool.poetry] + packages = [ { include = "simple" } ] + """ + + poetry = project_factory(name="simple-project", pyproject_content=pyproject_content) + pyproject: dict[str, Any] = poetry.file.read() + + if has_poetry_section: + assert "dependencies" not in pyproject["tool"]["poetry"] + else: + assert "tool" not in pyproject + + repo.add_package(get_package("foo", "2.0")) + repo.add_package(get_package("bar", "2.0")) + + tester = command_tester_factory("add", poetry=poetry) + tester.execute("bar>=1.0 --platform linux") + + updated_pyproject: dict[str, Any] = poetry.file.read() + if has_poetry_section: + assert "dependencies" not in pyproject["tool"]["poetry"] + else: + assert "tool" not in pyproject + assert updated_pyproject["project"]["dependencies"] == [ + "foo >= 1.0", + 'bar (>=1.0) ; sys_platform == "linux"', + ] + + +@pytest.mark.parametrize("has_poetry_section", [True, False]) +def test_add_poetry_dependencies_if_necessary( + project_factory: ProjectFactory, + repo: TestRepository, + command_tester_factory: CommandTesterFactory, + mocker: MockerFixture, + has_poetry_section: bool, +) -> None: + pyproject_content = """\ + [project] + name = "simple-project" + version = "1.2.3" + dependencies = [ + "foo >= 1.0", + ] + """ + if has_poetry_section: + pyproject_content += """\ + [tool.poetry] + packages = [ { include = "simple" } ] + """ + + poetry = project_factory(name="simple-project", pyproject_content=pyproject_content) + pyproject: dict[str, Any] = poetry.file.read() + + if has_poetry_section: + assert "dependencies" not in pyproject["tool"]["poetry"] + else: + assert "tool" not in pyproject + + repo.add_package(get_package("foo", "2.0")) + other_repo = LegacyRepository(name="my-index", url="https://my-index.fake") + poetry.pool.add_repository(other_repo) + package = get_package("bar", "2.0") + mocker.patch.object(other_repo, "package", return_value=package) + mocker.patch.object(other_repo, "_find_packages", wraps=lambda _, name: [package]) + repo.add_package(package) + + tester = command_tester_factory("add", poetry=poetry) + tester.execute("bar>=1.0 --platform linux --allow-prereleases --source my-index") + + updated_pyproject: dict[str, Any] = poetry.file.read() + if has_poetry_section: + assert "dependencies" not in pyproject["tool"]["poetry"] + else: + assert "tool" not in pyproject + assert updated_pyproject["project"]["dependencies"] == [ + "foo >= 1.0", + 'bar (>=1.0) ; sys_platform == "linux"', + ] + assert updated_pyproject["tool"]["poetry"]["dependencies"] == { + "bar": { + "platform": "linux", + "source": "my-index", + "allow-prereleases": True, + } + } From 7be26f027fcf69c55c7e3d240c72a31c990a4486 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Randy=20D=C3=B6ring?= <30527984+radoering@users.noreply.github.com> Date: Wed, 27 Mar 2024 11:04:34 +0100 Subject: [PATCH 7/9] add support for PEP 621: poetry init and poetry new (#9135) --- src/poetry/console/commands/init.py | 6 +- src/poetry/layouts/layout.py | 66 ++++-- tests/console/commands/conftest.py | 14 +- tests/console/commands/test_init.py | 331 ++++++++++++++++------------ tests/console/commands/test_new.py | 9 +- 5 files changed, 252 insertions(+), 174 deletions(-) diff --git a/src/poetry/console/commands/init.py b/src/poetry/console/commands/init.py index 6725008daaf..595b53fc232 100644 --- a/src/poetry/console/commands/init.py +++ b/src/poetry/console/commands/init.py @@ -105,8 +105,8 @@ def _init_pyproject( if pyproject.file.exists(): if pyproject.is_poetry_project(): self.line_error( - "A pyproject.toml file with a poetry section already" - " exists." + "A pyproject.toml file with a project and/or" + " a poetry section already exists." ) return 1 @@ -255,7 +255,7 @@ def _init_pyproject( if create_layout: layout_.create(project_path, with_pyproject=False) - content = layout_.generate_poetry_content() + content = layout_.generate_project_content() for section, item in content.items(): pyproject.data.append(section, item) diff --git a/src/poetry/layouts/layout.py b/src/poetry/layouts/layout.py index f5174ba3951..1fbd897f4a8 100644 --- a/src/poetry/layouts/layout.py +++ b/src/poetry/layouts/layout.py @@ -5,12 +5,14 @@ from typing import Any from packaging.utils import canonicalize_name +from poetry.core.packages.package import AUTHOR_REGEX from poetry.core.utils.helpers import module_name from tomlkit import inline_table from tomlkit import loads from tomlkit import table from tomlkit.toml_document import TOMLDocument +from poetry.factory import Factory from poetry.pyproject.toml import PyProjectTOML @@ -21,16 +23,20 @@ POETRY_DEFAULT = """\ -[tool.poetry] +[project] name = "" version = "" description = "" -authors = [] -license = "" +authors = [ +] +license = {} readme = "" -packages = [] +requires-python = "" +dependencies = [ +] -[tool.poetry.dependencies] +[tool.poetry] +packages = [] [tool.poetry.group.dev.dependencies] """ @@ -48,7 +54,7 @@ def __init__( readme_format: str = "md", author: str | None = None, license: str | None = None, - python: str = "*", + python: str | None = None, dependencies: Mapping[str, str | Mapping[str, Any]] | None = None, dev_dependencies: Mapping[str, str | Mapping[str, Any]] | None = None, ) -> None: @@ -117,34 +123,49 @@ def create( if with_pyproject: self._write_poetry(path) - def generate_poetry_content(self) -> TOMLDocument: + def generate_project_content(self) -> TOMLDocument: template = POETRY_DEFAULT content: dict[str, Any] = loads(template) - poetry_content = content["tool"]["poetry"] - poetry_content["name"] = self._project - poetry_content["version"] = self._version - poetry_content["description"] = self._description - poetry_content["authors"].append(self._author) + project_content = content["project"] + project_content["name"] = self._project + project_content["version"] = self._version + project_content["description"] = self._description + m = AUTHOR_REGEX.match(self._author) + if m is None: + # This should not happen because author has been validated before. + raise ValueError(f"Invalid author: {self._author}") + else: + author = {"name": m.group("name")} + if email := m.group("email"): + author["email"] = email + project_content["authors"].append(author) if self._license: - poetry_content["license"] = self._license + project_content["license"]["text"] = self._license + else: + project_content.remove("license") + + project_content["readme"] = f"README.{self._readme_format}" + + if self._python: + project_content["requires-python"] = self._python else: - poetry_content.remove("license") + project_content.remove("requires-python") + + for dep_name, dep_constraint in self._dependencies.items(): + dependency = Factory.create_dependency(dep_name, dep_constraint) + project_content["dependencies"].append(dependency.to_pep_508()) + + poetry_content = content["tool"]["poetry"] - poetry_content["readme"] = f"README.{self._readme_format}" packages = self.get_package_include() if packages: poetry_content["packages"].append(packages) else: poetry_content.remove("packages") - poetry_content["dependencies"]["python"] = self._python - - for dep_name, dep_constraint in self._dependencies.items(): - poetry_content["dependencies"][dep_name] = dep_constraint - if self._dev_dependencies: for dep_name, dep_constraint in self._dev_dependencies.items(): poetry_content["group"]["dev"]["dependencies"][dep_name] = ( @@ -153,6 +174,9 @@ def generate_poetry_content(self) -> TOMLDocument: else: del poetry_content["group"] + if not poetry_content: + del content["tool"]["poetry"] + # Add build system build_system = table() build_system_version = "" @@ -194,7 +218,7 @@ def _create_tests(path: Path) -> None: def _write_poetry(self, path: Path) -> None: pyproject = PyProjectTOML(path / "pyproject.toml") - content = self.generate_poetry_content() + content = self.generate_project_content() for section, item in content.items(): pyproject.data.append(section, item) pyproject.save() diff --git a/tests/console/commands/conftest.py b/tests/console/commands/conftest.py index 8c095a6bb81..b96f5a07e8b 100644 --- a/tests/console/commands/conftest.py +++ b/tests/console/commands/conftest.py @@ -12,7 +12,7 @@ def init_basic_inputs() -> str: "This is a description", # Description "n", # Author "MIT", # License - "~2.7 || ^3.6", # Python + ">=3.6", # Python "n", # Interactive packages "n", # Interactive dev packages "\n", # Generate @@ -23,14 +23,14 @@ def init_basic_inputs() -> str: @pytest.fixture() def init_basic_toml() -> str: return """\ -[tool.poetry] +[project] name = "my-package" version = "1.2.3" description = "This is a description" -authors = ["Your Name "] -license = "MIT" +authors = [ + {name = "Your Name",email = "you@example.com"} +] +license = {text = "MIT"} readme = "README.md" - -[tool.poetry.dependencies] -python = "~2.7 || ^3.6" +requires-python = ">=3.6" """ diff --git a/tests/console/commands/test_init.py b/tests/console/commands/test_init.py index 80aee46e048..ec85d321f59 100644 --- a/tests/console/commands/test_init.py +++ b/tests/console/commands/test_init.py @@ -93,7 +93,7 @@ def test_noninteractive( toml_content = (tmp_path / "pyproject.toml").read_text(encoding="utf-8") assert 'name = "my-package"' in toml_content - assert 'pytest = "^3.6.0"' in toml_content + assert '"pytest (>=3.6.0,<4.0.0)"' in toml_content def test_interactive_with_dependencies( @@ -110,7 +110,7 @@ def test_interactive_with_dependencies( "This is a description", # Description "n", # Author "MIT", # License - "~2.7 || ^3.6", # Python + ">=3.6", # Python "", # Interactive packages "pendulu", # Search for package "1", # Second option is pendulum @@ -129,18 +129,22 @@ def test_interactive_with_dependencies( tester.execute(inputs="\n".join(inputs)) expected = """\ -[tool.poetry] +[project] name = "my-package" version = "1.2.3" description = "This is a description" -authors = ["Your Name "] -license = "MIT" +authors = [ + {name = "Your Name",email = "you@example.com"} +] +license = {text = "MIT"} readme = "README.md" +requires-python = ">=3.6" +dependencies = [ + "pendulum (>=2.0.0,<3.0.0)", + "flask (>=2.0.0,<3.0.0)" +] -[tool.poetry.dependencies] -python = "~2.7 || ^3.6" -pendulum = "^2.0.0" -flask = "^2.0.0" +[tool.poetry] [tool.poetry.group.dev.dependencies] pytest = "^3.6.0" @@ -163,7 +167,7 @@ def test_interactive_with_dependencies_and_no_selection( "This is a description", # Description "n", # Author "MIT", # License - "~2.7 || ^3.6", # Python + ">=3.6", # Python "", # Interactive packages "pendulu", # Search for package "", # Do not select an option @@ -177,16 +181,16 @@ def test_interactive_with_dependencies_and_no_selection( ] tester.execute(inputs="\n".join(inputs)) expected = """\ -[tool.poetry] +[project] name = "my-package" version = "1.2.3" description = "This is a description" -authors = ["Your Name "] -license = "MIT" +authors = [ + {name = "Your Name",email = "you@example.com"} +] +license = {text = "MIT"} readme = "README.md" - -[tool.poetry.dependencies] -python = "~2.7 || ^3.6" +requires-python = ">=3.6" """ assert expected in tester.io.fetch_output() @@ -208,15 +212,15 @@ def test_empty_license(tester: CommandTester) -> None: python = ".".join(str(c) for c in sys.version_info[:2]) expected = f"""\ -[tool.poetry] +[project] name = "my-package" version = "1.2.3" description = "" -authors = ["Your Name "] +authors = [ + {{name = "Your Name",email = "you@example.com"}} +] readme = "README.md" - -[tool.poetry.dependencies] -python = ">={python}" +requires-python = ">={python}" """ assert expected in tester.io.fetch_output() @@ -233,7 +237,7 @@ def test_interactive_with_git_dependencies( "This is a description", # Description "n", # Author "MIT", # License - "~2.7 || ^3.6", # Python + ">=3.6", # Python "", # Interactive packages "git+https://github.com/demo/demo.git", # Search for package "", # Stop searching for packages @@ -247,17 +251,21 @@ def test_interactive_with_git_dependencies( tester.execute(inputs="\n".join(inputs)) expected = """\ -[tool.poetry] +[project] name = "my-package" version = "1.2.3" description = "This is a description" -authors = ["Your Name "] -license = "MIT" +authors = [ + {name = "Your Name",email = "you@example.com"} +] +license = {text = "MIT"} readme = "README.md" +requires-python = ">=3.6" +dependencies = [ + "demo @ git+https://github.com/demo/demo.git" +] -[tool.poetry.dependencies] -python = "~2.7 || ^3.6" -demo = {git = "https://github.com/demo/demo.git"} +[tool.poetry] [tool.poetry.group.dev.dependencies] pytest = "^3.6.0" @@ -325,7 +333,7 @@ def test_interactive_with_git_dependencies_with_reference( "This is a description", # Description "n", # Author "MIT", # License - "~2.7 || ^3.6", # Python + ">=3.6", # Python "", # Interactive packages "git+https://github.com/demo/demo.git@develop", # Search for package "", # Stop searching for packages @@ -339,17 +347,21 @@ def test_interactive_with_git_dependencies_with_reference( tester.execute(inputs="\n".join(inputs)) expected = """\ -[tool.poetry] +[project] name = "my-package" version = "1.2.3" description = "This is a description" -authors = ["Your Name "] -license = "MIT" +authors = [ + {name = "Your Name",email = "you@example.com"} +] +license = {text = "MIT"} readme = "README.md" +requires-python = ">=3.6" +dependencies = [ + "demo @ git+https://github.com/demo/demo.git@develop" +] -[tool.poetry.dependencies] -python = "~2.7 || ^3.6" -demo = {git = "https://github.com/demo/demo.git", rev = "develop"} +[tool.poetry] [tool.poetry.group.dev.dependencies] pytest = "^3.6.0" @@ -370,7 +382,7 @@ def test_interactive_with_git_dependencies_and_other_name( "This is a description", # Description "n", # Author "MIT", # License - "~2.7 || ^3.6", # Python + ">=3.6", # Python "", # Interactive packages "git+https://github.com/demo/pyproject-demo.git", # Search for package "", # Stop searching for packages @@ -384,17 +396,21 @@ def test_interactive_with_git_dependencies_and_other_name( tester.execute(inputs="\n".join(inputs)) expected = """\ -[tool.poetry] +[project] name = "my-package" version = "1.2.3" description = "This is a description" -authors = ["Your Name "] -license = "MIT" +authors = [ + {name = "Your Name",email = "you@example.com"} +] +license = {text = "MIT"} readme = "README.md" +requires-python = ">=3.6" +dependencies = [ + "demo @ git+https://github.com/demo/pyproject-demo.git" +] -[tool.poetry.dependencies] -python = "~2.7 || ^3.6" -demo = {git = "https://github.com/demo/pyproject-demo.git"} +[tool.poetry] [tool.poetry.group.dev.dependencies] pytest = "^3.6.0" @@ -421,7 +437,7 @@ def test_interactive_with_directory_dependency( "This is a description", # Description "n", # Author "MIT", # License - "~2.7 || ^3.6", # Python + ">=3.6", # Python "", # Interactive packages "./demo", # Search for package "", # Stop searching for packages @@ -434,18 +450,23 @@ def test_interactive_with_directory_dependency( ] tester.execute(inputs="\n".join(inputs)) - expected = """\ -[tool.poetry] + demo_uri = (Path.cwd() / "demo").as_uri() + expected = f"""\ +[project] name = "my-package" version = "1.2.3" description = "This is a description" -authors = ["Your Name "] -license = "MIT" +authors = [ + {{name = "Your Name",email = "you@example.com"}} +] +license = {{text = "MIT"}} readme = "README.md" +requires-python = ">=3.6" +dependencies = [ + "demo @ {demo_uri}" +] -[tool.poetry.dependencies] -python = "~2.7 || ^3.6" -demo = {path = "demo"} +[tool.poetry] [tool.poetry.group.dev.dependencies] pytest = "^3.6.0" @@ -471,7 +492,7 @@ def test_interactive_with_directory_dependency_and_other_name( "This is a description", # Description "n", # Author "MIT", # License - "~2.7 || ^3.6", # Python + ">=3.6", # Python "", # Interactive packages "./pyproject-demo", # Search for package "", # Stop searching for packages @@ -484,18 +505,23 @@ def test_interactive_with_directory_dependency_and_other_name( ] tester.execute(inputs="\n".join(inputs)) - expected = """\ -[tool.poetry] + demo_uri = (Path.cwd() / "pyproject-demo").as_uri() + expected = f"""\ +[project] name = "my-package" version = "1.2.3" description = "This is a description" -authors = ["Your Name "] -license = "MIT" +authors = [ + {{name = "Your Name",email = "you@example.com"}} +] +license = {{text = "MIT"}} readme = "README.md" +requires-python = ">=3.6" +dependencies = [ + "demo @ {demo_uri}" +] -[tool.poetry.dependencies] -python = "~2.7 || ^3.6" -demo = {path = "pyproject-demo"} +[tool.poetry] [tool.poetry.group.dev.dependencies] pytest = "^3.6.0" @@ -522,7 +548,7 @@ def test_interactive_with_file_dependency( "This is a description", # Description "n", # Author "MIT", # License - "~2.7 || ^3.6", # Python + ">=3.6", # Python "", # Interactive packages "./demo-0.1.0-py2.py3-none-any.whl", # Search for package "", # Stop searching for packages @@ -535,18 +561,23 @@ def test_interactive_with_file_dependency( ] tester.execute(inputs="\n".join(inputs)) - expected = """\ -[tool.poetry] + demo_uri = (Path.cwd() / "demo-0.1.0-py2.py3-none-any.whl").as_uri() + expected = f"""\ +[project] name = "my-package" version = "1.2.3" description = "This is a description" -authors = ["Your Name "] -license = "MIT" +authors = [ + {{name = "Your Name",email = "you@example.com"}} +] +license = {{text = "MIT"}} readme = "README.md" +requires-python = ">=3.6" +dependencies = [ + "demo @ {demo_uri}" +] -[tool.poetry.dependencies] -python = "~2.7 || ^3.6" -demo = {path = "demo-0.1.0-py2.py3-none-any.whl"} +[tool.poetry] [tool.poetry.group.dev.dependencies] pytest = "^3.6.0" @@ -564,7 +595,7 @@ def test_interactive_with_wrong_dependency_inputs( "This is a description", # Description "n", # Author "MIT", # License - "^3.8", # Python + ">=3.8", # Python "", # Interactive packages "foo 1.19.2", "pendulum 2.0.0 foo", # Package name and constraint (invalid) @@ -580,18 +611,22 @@ def test_interactive_with_wrong_dependency_inputs( tester.execute(inputs="\n".join(inputs)) expected = """\ -[tool.poetry] +[project] name = "my-package" version = "1.2.3" description = "This is a description" -authors = ["Your Name "] -license = "MIT" +authors = [ + {name = "Your Name",email = "you@example.com"} +] +license = {text = "MIT"} readme = "README.md" +requires-python = ">=3.8" +dependencies = [ + "foo (==1.19.2)", + "pendulum (>=2.0.0,<3.0.0)" +] -[tool.poetry.dependencies] -python = "^3.8" -foo = "1.19.2" -pendulum = "^2.0.0" +[tool.poetry] [tool.poetry.group.dev.dependencies] pytest = "3.6.0" @@ -611,19 +646,19 @@ def test_python_option(tester: CommandTester) -> None: "n", # Interactive dev packages "\n", # Generate ] - tester.execute("--python '~2.7 || ^3.6'", inputs="\n".join(inputs)) + tester.execute("--python '>=3.6'", inputs="\n".join(inputs)) expected = """\ -[tool.poetry] +[project] name = "my-package" version = "1.2.3" description = "This is a description" -authors = ["Your Name "] -license = "MIT" +authors = [ + {name = "Your Name",email = "you@example.com"} +] +license = {text = "MIT"} readme = "README.md" - -[tool.poetry.dependencies] -python = "~2.7 || ^3.6" +requires-python = ">=3.6" """ assert expected in tester.io.fetch_output() @@ -638,7 +673,7 @@ def test_predefined_dependency(tester: CommandTester, repo: TestRepository) -> N "This is a description", # Description "n", # Author "MIT", # License - "~2.7 || ^3.6", # Python + ">=3.6", # Python "n", # Interactive packages "n", # Interactive dev packages "\n", # Generate @@ -646,17 +681,19 @@ def test_predefined_dependency(tester: CommandTester, repo: TestRepository) -> N tester.execute("--dependency pendulum", inputs="\n".join(inputs)) expected = """\ -[tool.poetry] +[project] name = "my-package" version = "1.2.3" description = "This is a description" -authors = ["Your Name "] -license = "MIT" +authors = [ + {name = "Your Name",email = "you@example.com"} +] +license = {text = "MIT"} readme = "README.md" - -[tool.poetry.dependencies] -python = "~2.7 || ^3.6" -pendulum = "^2.0.0" +requires-python = ">=3.6" +dependencies = [ + "pendulum (>=2.0.0,<3.0.0)" +] """ assert expected in tester.io.fetch_output() @@ -674,7 +711,7 @@ def test_predefined_and_interactive_dependencies( "This is a description", # Description "n", # Author "MIT", # License - "~2.7 || ^3.6", # Python + ">=3.6", # Python "", # Interactive packages "pyramid", # Search for package "0", # First option @@ -687,21 +724,22 @@ def test_predefined_and_interactive_dependencies( tester.execute("--dependency pendulum", inputs="\n".join(inputs)) expected = """\ -[tool.poetry] +[project] name = "my-package" version = "1.2.3" description = "This is a description" -authors = ["Your Name "] -license = "MIT" +authors = [ + {name = "Your Name",email = "you@example.com"} +] +license = {text = "MIT"} readme = "README.md" - -[tool.poetry.dependencies] -python = "~2.7 || ^3.6" +requires-python = ">=3.6" +dependencies = [ + "pendulum (>=2.0.0,<3.0.0)", + "pyramid (>=1.10,<2.0)" +] """ - output = tester.io.fetch_output() - assert expected in output - assert 'pendulum = "^2.0.0"' in output - assert 'pyramid = "^1.10"' in output + assert expected in tester.io.fetch_output() def test_predefined_dev_dependency(tester: CommandTester, repo: TestRepository) -> None: @@ -713,7 +751,7 @@ def test_predefined_dev_dependency(tester: CommandTester, repo: TestRepository) "This is a description", # Description "n", # Author "MIT", # License - "~2.7 || ^3.6", # Python + ">=3.6", # Python "n", # Interactive packages "n", # Interactive dev packages "\n", # Generate @@ -722,16 +760,20 @@ def test_predefined_dev_dependency(tester: CommandTester, repo: TestRepository) tester.execute("--dev-dependency pytest", inputs="\n".join(inputs)) expected = """\ -[tool.poetry] +[project] name = "my-package" version = "1.2.3" description = "This is a description" -authors = ["Your Name "] -license = "MIT" +authors = [ + {name = "Your Name",email = "you@example.com"} +] +license = {text = "MIT"} readme = "README.md" +requires-python = ">=3.6" +dependencies = [ +] -[tool.poetry.dependencies] -python = "~2.7 || ^3.6" +[tool.poetry] [tool.poetry.group.dev.dependencies] pytest = "^3.6.0" @@ -752,7 +794,7 @@ def test_predefined_and_interactive_dev_dependencies( "This is a description", # Description "n", # Author "MIT", # License - "~2.7 || ^3.6", # Python + ">=3.6", # Python "n", # Interactive packages "", # Interactive dev packages "pytest-requests", # Search for package @@ -765,16 +807,20 @@ def test_predefined_and_interactive_dev_dependencies( tester.execute("--dev-dependency pytest", inputs="\n".join(inputs)) expected = """\ -[tool.poetry] +[project] name = "my-package" version = "1.2.3" description = "This is a description" -authors = ["Your Name "] -license = "MIT" +authors = [ + {name = "Your Name",email = "you@example.com"} +] +license = {text = "MIT"} readme = "README.md" +requires-python = ">=3.6" +dependencies = [ +] -[tool.poetry.dependencies] -python = "~2.7 || ^3.6" +[tool.poetry] [tool.poetry.group.dev.dependencies] pytest = "^3.6.0" @@ -803,7 +849,7 @@ def test_predefined_all_options(tester: CommandTester, repo: TestRepository) -> "--name my-package " "--description 'This is a description' " "--author 'Foo Bar ' " - "--python '^3.8' " + "--python '>=3.8' " "--license MIT " "--dependency pendulum " "--dev-dependency pytest", @@ -811,17 +857,21 @@ def test_predefined_all_options(tester: CommandTester, repo: TestRepository) -> ) expected = """\ -[tool.poetry] +[project] name = "my-package" version = "1.2.3" description = "This is a description" -authors = ["Foo Bar "] -license = "MIT" +authors = [ + {name = "Foo Bar",email = "foo@example.com"} +] +license = {text = "MIT"} readme = "README.md" +requires-python = ">=3.8" +dependencies = [ + "pendulum (>=2.0.0,<3.0.0)" +] -[tool.poetry.dependencies] -python = "^3.8" -pendulum = "^2.0.0" +[tool.poetry] [tool.poetry.group.dev.dependencies] pytest = "^3.6.0" @@ -900,22 +950,24 @@ def test_init_non_interactive_existing_pyproject_add_dependency( tester.execute( "--author 'Your Name ' " "--name 'my-package' " - "--python '^3.6' " + "--python '>=3.6' " "--dependency foo", interactive=False, ) expected = """\ -[tool.poetry] +[project] name = "my-package" version = "0.1.0" description = "" -authors = ["Your Name "] +authors = [ + {name = "Your Name",email = "you@example.com"} +] readme = "README.md" - -[tool.poetry.dependencies] -python = "^3.6" -foo = "^1.19.2" +requires-python = ">=3.6" +dependencies = [ + "foo (>=1.19.2,<2.0.0)" +] """ assert f"{existing_section}\n{expected}" in pyproject_file.read_text( encoding="utf-8" @@ -1005,7 +1057,7 @@ def test_package_include( "", # Description "poetry", # Author "", # License - "^3.10", # Python + ">=3.10", # Python "n", # Interactive packages "n", # Interactive dev packages "\n", # Generate @@ -1015,19 +1067,21 @@ def test_package_include( packages = "" if include and module_name(package_name) != include: - packages = f'packages = [{{include = "{include}"}}]\n' + packages = f'\n[tool.poetry]\npackages = [{{include = "{include}"}}]\n' expected = ( - f"[tool.poetry]\n" + "[project]\n" f'name = "{package_name.replace(".", "-")}"\n' # canonicalized - f'version = "0.1.0"\n' - f'description = ""\n' - f'authors = ["poetry"]\n' - f'readme = "README.md"\n' - f"{packages}" # This line is optional. Thus no newline here. - f"\n" - f"[tool.poetry.dependencies]\n" - f'python = "^3.10"\n' + 'version = "0.1.0"\n' + 'description = ""\n' + 'authors = [\n' + ' {name = "poetry"}\n' + ']\n' + 'readme = "README.md"\n' + 'requires-python = ">=3.10"\n' + 'dependencies = [\n' + ']\n' + f"{packages}" # This line is optional. Thus, no newline here. ) assert expected in tester.io.fetch_output() @@ -1069,8 +1123,7 @@ def mock_check_output(cmd: str, *_: Any, **__: Any) -> str: ) expected = f"""\ -[tool.poetry.dependencies] -python = ">={python}" +requires-python = ">={python}" """ assert expected in pyproject_file.read_text(encoding="utf-8") diff --git a/tests/console/commands/test_new.py b/tests/console/commands/test_new.py index dba738f4e8c..72cb5654a66 100644 --- a/tests/console/commands/test_new.py +++ b/tests/console/commands/test_new.py @@ -57,7 +57,7 @@ def verify_project_directory( else: package_include = {"include": package_path.parts[0]} - name = poetry.local_config.get("name", "") + name = poetry.package.name packages = poetry.local_config.get("packages") if not packages: @@ -183,7 +183,9 @@ def test_command_new_with_readme( tester.execute(" ".join(options)) poetry = verify_project_directory(path, package, package, None) - assert poetry.local_config.get("readme") == f"README.{fmt or 'md'}" + project_section = poetry.pyproject.data["project"] + assert isinstance(project_section, dict) + assert project_section["readme"] == f"README.{fmt or 'md'}" @pytest.mark.parametrize( @@ -224,8 +226,7 @@ def mock_check_output(cmd: str, *_: Any, **__: Any) -> str: pyproject_file = path / "pyproject.toml" expected = f"""\ -[tool.poetry.dependencies] -python = ">={python}" +requires-python = ">={python}" """ assert expected in pyproject_file.read_text(encoding="utf-8") From 1aaabd6d4364aadfcf23b16337e2b02e3241a1d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Randy=20D=C3=B6ring?= <30527984+radoering@users.noreply.github.com> Date: Wed, 27 Mar 2024 13:24:10 +0100 Subject: [PATCH 8/9] add support for PEP 621: explain the difference between `project.dependencies` and `tool.poetry.dependencies`, update examples (#9135) --- docs/basic-usage.md | 27 +++-- docs/dependency-specification.md | 171 ++++++++++++++++++++++++++++++- docs/faq.md | 12 +++ docs/managing-dependencies.md | 26 ++++- docs/plugins.md | 12 +-- docs/pyproject.md | 29 +----- 6 files changed, 235 insertions(+), 42 deletions(-) diff --git a/docs/basic-usage.md b/docs/basic-usage.md index 425742e9fc2..c7fa444bc66 100644 --- a/docs/basic-usage.md +++ b/docs/basic-usage.md @@ -38,16 +38,18 @@ The `pyproject.toml` file is what is the most important here. This will orchestr your project and its dependencies. For now, it looks like this: ```toml -[tool.poetry] +[project] name = "poetry-demo" version = "0.1.0" description = "" -authors = ["Sébastien Eustace "] +authors = [ + {name = "Sébastien Eustace", email = "sebastien@eustace.io"} +] readme = "README.md" -packages = [{include = "poetry_demo"}] +requires-python = ">=3.8" -[tool.poetry.dependencies] -python = "^3.7" +[tool.poetry] +packages = [{include = "poetry_demo"}] [build-system] @@ -122,7 +124,20 @@ In the [pyproject section]({{< relref "pyproject" >}}) you can see which fields ### Specifying dependencies -If you want to add dependencies to your project, you can specify them in the `tool.poetry.dependencies` section. +If you want to add dependencies to your project, you can specify them in the +`project` or `tool.poetry.dependencies` section. +See the [Dependency specification]({{< relref "dependency-specification" >}}) +for more information. + +```toml +[project] +# ... +dependencies = [ + "pendulum (>=2.1,<3.0)" +] +``` + +or ```toml [tool.poetry.dependencies] diff --git a/docs/dependency-specification.md b/docs/dependency-specification.md index f8aaf57a866..efb9a93af31 100644 --- a/docs/dependency-specification.md +++ b/docs/dependency-specification.md @@ -14,10 +14,87 @@ menu: Dependencies for a project can be specified in various forms, which depend on the type of the dependency and on the optional constraints that might be needed for it to be installed. +## `project.dependencies` and `tool.poetry.dependencies` + +Prior Poetry 2.0, dependencies had to be declared in the `tool.poetry.dependencies` +section of the `pyproject.toml` file. + +```toml +[tool.poetry.dependencies] +requests = "^2.13.0" +``` + +With Poetry 2.0, you should consider using the `project.dependencies` section instead. + +```toml +[project] +# ... +dependencies = [ + "requests (>=2.23.0,<3.0.0)" +] +``` + +While dependencies in `tool.poetry.dependencies` are specified using toml tables, +dependencies in `project.dependencies` are specified as strings according +to [PEP 508](https://peps.python.org/pep-0508/). + +In many cases, `tool.poetry.dependencies` can be replaced with `project.dependencies`. +However, there are some cases where you might still need to use `tool.poetry.dependencies`. +For example, if you want to define additional information that is not required for building +but only for locking (for example an explicit source), you can enrich dependency +information in the `tool.poetry` section. + +```toml +[project] +# ... +dependencies = [ + "requests>=2.13.0", +] + +[tool.poetry.dependencies] +requests = { source = "private-source" } +``` + +When both are specified, `project.dependencies` are used for metadata when building the project, +`tool.poetry.dependencies` is only used to enrich `project.dependencies` for locking. + +Alternatively, you can add `dependencies` to `dynamic` and define your dependencies +completely in the `tool.poetry` section. Using only the `tool.poetry` section might +make sense in non-package mode when you will not build an sdist or a wheel. + +```toml +[project] +# ... +dynamic = [ "dependencies" ] + +[tool.poetry.dependencies] +requests = { version = ">=2.13.0", source = "private-source" } +``` + +{{% note %}} +Another use case for `tool.poetry.dependencies` are relative path dependencies +since `project.dependencies` only support absolute paths. +{{% /note %}} + +{{% note %}} +Only main dependencies can be specified in the `project` section. +Other [Dependency groups]({{< relref "managing-dependencies#dependency-groups" >}}) +must still be specified in the `tool.poetry` section. +{{% /note %}} + ## Version constraints +{{% warning %}} +Some of the following constraints can only be used in `tool.poetry.dependencies` and not in `project.dependencies`. +When using `poetry add` such constraints are automatically converted into an equivalent constraint. +{{% /warning %}} + ### Caret requirements +{{% warning %}} +Not supported in `project.dependencies`. +{{% /warning %}} + **Caret requirements** allow [SemVer](https://semver.org/) compatible updates to a specified version. An update is allowed if the new version number does not modify the left-most non-zero digit in the major, minor, patch grouping. For instance, if we previously ran `poetry add requests@^2.13.0` and wanted to update the library and ran `poetry update requests`, poetry would update us to version `2.14.0` if it was available, but would not update us to `3.0.0`. If instead we had specified the version string as `^0.1.13`, poetry would update to `0.1.14` but not `0.2.0`. `0.0.x` is not considered compatible with any other version. Here are some more examples of caret requirements and the versions that would be allowed with them: @@ -34,6 +111,10 @@ Here are some more examples of caret requirements and the versions that would be ### Tilde requirements +{{% warning %}} +Not supported in `project.dependencies`. +{{% /warning %}} + **Tilde requirements** specify a minimal version with some ability to update. If you specify a major, minor, and patch version or only a major and minor version, only patch-level changes are allowed. If you only specify a major version, then minor- and patch-level changes are allowed. @@ -131,6 +212,16 @@ the minimum information you need to specify is the location of the repository wi requests = { git = "https://github.com/requests/requests.git" } ``` +or in the `project` section: + +```toml +[project] +# ... +dependencies = [ + "requests @ git+https://github.com/requests/requests.git" +] +``` + Since we haven’t specified any other information, Poetry assumes that we intend to use the latest commit on the `main` branch to build our project. @@ -149,6 +240,18 @@ flask = { git = "https://github.com/pallets/flask.git", rev = "38eb5d3b" } numpy = { git = "https://github.com/numpy/numpy.git", tag = "v0.13.2" } ``` +or in the `project` section: + +```toml +[project] +# ... +dependencies = [ + "requests @ git+https://github.com/requests/requests.git@next", + "flask @ git+https://github.com/pallets/flask.git@38eb5d3b", + "numpy @ git+https://github.com/numpy/numpy.git@v0.13.2", +] +``` + In cases where the package you want to install is located in a subdirectory of the VCS repository, you can use the `subdirectory` option, similarly to what [pip](https://pip.pypa.io/en/stable/topics/vcs-support/#url-fragments) provides: ```toml @@ -212,6 +315,16 @@ my-package = { path = "../my-package/", develop = false } my-package = { path = "../my-package/dist/my-package-0.1.0.tar.gz" } ``` +In the `project` section, you can only use absolute paths: + +```toml +[project] +# ... +dependencies = [ + "my-package @ file:///absolute/path/to/my-package/dist/my-package-0.1.0.tar.gz" +] +``` + {{% note %}} Before poetry 1.1 directory path dependencies were installed in editable mode by default. You should set the `develop` attribute explicitly, to make sure the behavior is the same for all poetry versions. @@ -228,6 +341,16 @@ you can use the `url` property: my-package = { url = "https://example.com/my-package-0.1.0.tar.gz" } ``` +or in the `project` section: + +```toml +[project] +# ... +dependencies = [ + "my-package @ https://example.com/my-package-0.1.0.tar.gz" +] +``` + with the corresponding `add` call: ```bash @@ -244,6 +367,16 @@ for a dependency as shown here. gunicorn = { version = "^20.1", extras = ["gevent"] } ``` +or in the `project` section: + +```toml +[project] +# ... +dependencies = [ + "gunicorn[gevent] (>=20.1,<21.0)" +] +``` + {{% note %}} These activate extra defined for the dependency, to configure an optional dependency for extras in your project refer to [`extras`]({{< relref "pyproject#extras" >}}). @@ -275,6 +408,10 @@ In this example, we expect `foo` to be configured correctly. See [using a privat for further information. {{% /note %}} +{{% note %}} +It is not possible to define source dependencies in the `project` section. +{{% /note %}} + ## Python restricted dependencies You can also specify that a dependency should be installed only for specific Python versions: @@ -286,7 +423,18 @@ tomli = { version = "^2.0.1", python = "<3.11" } ```toml [tool.poetry.dependencies] -pathlib2 = { version = "^2.2", python = "^3.2" } +pathlib2 = { version = "^2.2", python = "^3.9" } +``` + +or in the `project` section: + +```toml +[project] +# ... +dependencies = [ + "tomli (>=2.0.1,<3.11) ; python_version < '3.11'", + "pathlib2 (>=2.2,<3.0) ; python_version >= '3.9' and python_version < '4.0'" +] ``` ## Using environment markers @@ -300,6 +448,16 @@ via the `markers` property: pathlib2 = { version = "^2.2", markers = "python_version <= '3.4' or sys_platform == 'win32'" } ``` +or in the `project` section: + +```toml +[project] +# ... +dependencies = [ + "pathlib2 (>=2.2,<3.0) ; python_version <= '3.4' or sys_platform == 'win32'" +] +``` + ## Multiple constraints dependencies Sometimes, one of your dependency may have different version ranges depending @@ -317,6 +475,17 @@ foo = [ ] ``` +or in the `project` section: + +```toml +[project] +# ... +dependencies = [ + "foo (<=1.9) ; python_version >= '3.6' and python_version < '3.8'", + "foo (>=2.0,<3.0) ; python_version >= '3.8'" +] +``` + {{% note %}} The constraints **must** have different requirements (like `python`) otherwise it will cause an error when resolving dependencies. diff --git a/docs/faq.md b/docs/faq.md index 57843f71ffa..c35a92326a6 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -211,6 +211,18 @@ Usually you will want to match the supported Python range of your project with t Alternatively you can tell Poetry to install this dependency [only for a specific range of Python versions]({{< relref "dependency-specification#multiple-constraints-dependencies" >}}), if you know that it's not needed in all versions. +If you do not want to set an upper bound in the metadata when building your project, +you can omit it in the `project` section and only set it in `tool.poetry.dependencies`: + +```toml +[project] +# ... +requires-python = ">=3.7" # used for metadata when building the project + +[tool.poetry.dependencies] +python = ">=3.7,<3.11" # used for locking dependencies +``` + ### Why does Poetry enforce PEP 440 versions? diff --git a/docs/managing-dependencies.md b/docs/managing-dependencies.md index 14932d43df9..67a5bb42de1 100644 --- a/docs/managing-dependencies.md +++ b/docs/managing-dependencies.md @@ -11,6 +11,14 @@ type: docs # Managing dependencies +{{% note %}} +Since Poetry 2.0, main dependencies can be specified in `project.dependencies` +instead of `tool.poetry.dependencies`. +See [Dependency specification]({{< relref "dependency-specification" >}}) for more information. +Only main dependencies can be specified in the `project` section. +Other groups must still be specified in the `tool.poetry` section. +{{% /note %}} + ## Dependency groups Poetry provides a way to **organize** your dependencies by **groups**. For instance, you might have @@ -37,7 +45,22 @@ the dependencies logically. {{% /note %}} {{% note %}} -The dependencies declared in `tool.poetry.dependencies` are part of an implicit `main` group. +The dependencies declared in `project.dependencies` respectively `tool.poetry.dependencies` +are part of an implicit `main` group. +{{% /note %}} + +```toml +[project] +# ... +dependencies = [ # main dependency group + "httpx", + "pendulum", +] + +[tool.poetry.group.test.dependencies] +pytest = "^6.0.0" +pytest-mock = "*" +``` ```toml [tool.poetry.dependencies] # main dependency group @@ -48,7 +71,6 @@ pendulum = "*" pytest = "^6.0.0" pytest-mock = "*" ``` -{{% /note %}} {{% note %}} Dependency groups, other than the implicit `main` group, must only contain dependencies you need in your development diff --git a/docs/plugins.md b/docs/plugins.md index 9f133b32024..adcda0ba076 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -32,16 +32,16 @@ The plugin package must depend on Poetry and declare a proper [plugin]({{< relref "pyproject#plugins" >}}) in the `pyproject.toml` file. ```toml -[tool.poetry] +[project] name = "my-poetry-plugin" version = "1.0.0" - # ... -[tool.poetry.dependencies] -python = "^3.7" -poetry = "^1.2" +requires-python = ">=3.7" +dependencies = [ + "poetry (>=1.2,<2.0)", +] -[tool.poetry.plugins."poetry.plugin"] +[project.entry-points."poetry.plugin"] demo = "poetry_demo_plugin.plugin:MyPlugin" ``` diff --git a/docs/pyproject.md b/docs/pyproject.md index 6c52ee172d7..1d0f3a7c271 100644 --- a/docs/pyproject.md +++ b/docs/pyproject.md @@ -306,33 +306,8 @@ dependencies = [ These are the dependencies that will be declared when building an sdist or a wheel. -If you want to define additional information that is not required for building -but only for locking (for example an explicit source), you can enrich dependency -information in the `tool.poetry` section. - -```toml -[project] -# ... -dependencies = [ - "requests>=2.13.0", -] - -[tool.poetry.dependencies] -requests = { source = "private-source" } -``` - -Alternatively, you can add `dependencies` to `dynamic` and define your dependencies -completely in the `tool.poetry` section. Using only the `tool.poetry` section might -make sense in non-package mode when you will not build an sdist or a wheel. - -```toml -[project] -# ... -dynamic = [ "dependencies" ] - -[tool.poetry.dependencies] -requests = { version = ">=2.13.0", source = "private-source" } -``` +See [Dependency specification]({{< relref "dependency-specification" >}}) for more information +about the relation between `project.dependencies` and `tool.poetry.dependencies`. ### optional-dependencies From 28bbbf3ee21d5e9ff82f001a85b9aa803a6a5fa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Randy=20D=C3=B6ring?= <30527984+radoering@users.noreply.github.com> Date: Sun, 7 Apr 2024 14:47:54 +0200 Subject: [PATCH 9/9] add support for PEP 621: poetry add - change "--optional" to require an extra the optional dependency is added to (#9135) --- docs/cli.md | 2 +- src/poetry/console/commands/add.py | 44 ++++++++++++++---- tests/console/commands/test_add.py | 71 ++++++++++++++++++++++++++---- 3 files changed, 99 insertions(+), 18 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index 6ff3f48664a..085f1fe86a9 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -456,7 +456,7 @@ about dependency groups. * `--dev (-D)`: Add package as development dependency. (**Deprecated**, use `-G dev` instead) * `--editable (-e)`: Add vcs/path dependencies as editable. * `--extras (-E)`: Extras to activate for the dependency. (multiple values allowed) -* `--optional`: Add as an optional dependency. +* `--optional`: Add as an optional dependency to an extra. * `--python`: Python version for which the dependency must be installed. * `--platform`: Platforms for which the dependency must be installed. * `--source`: Name of the source to use to install the package. diff --git a/src/poetry/console/commands/add.py b/src/poetry/console/commands/add.py index 303aca542da..57d4d5ff2f0 100644 --- a/src/poetry/console/commands/add.py +++ b/src/poetry/console/commands/add.py @@ -54,7 +54,12 @@ class AddCommand(InstallerCommand, InitCommand): flag=False, multiple=True, ), - option("optional", None, "Add as an optional dependency."), + option( + "optional", + None, + "Add as an optional dependency to an extra.", + flag=False, + ), option( "python", None, @@ -137,6 +142,10 @@ def handle(self) -> int: "You can only specify one package when using the --extras option" ) + optional = self.option("optional") + if optional and group != MAIN_GROUP: + raise ValueError("You can only add optional dependencies to the main group") + # tomlkit types are awkward to work with, treat content as a mostly untyped # dictionary. content: dict[str, Any] = self.poetry.file.read() @@ -156,13 +165,19 @@ def handle(self) -> int: or "optional-dependencies" in project_content ): use_project_section = True + if optional: + project_section = project_content.get( + "optional-dependencies", {} + ).get(optional, array()) + else: + project_section = project_content.get("dependencies", array()) project_dependency_names = [ - Dependency.create_from_pep_508(dep).name - for dep in project_content.get("dependencies", {}) + Dependency.create_from_pep_508(dep).name for dep in project_section ] + else: + project_section = array() poetry_section = poetry_content.get("dependencies", table()) - project_section = project_content.get("dependencies", array()) else: if "group" not in poetry_content: poetry_content["group"] = table(is_super_table=True) @@ -194,6 +209,13 @@ def handle(self) -> int: self.line("Nothing to add.") return 0 + if optional and not use_project_section: + self.line_error( + "Optional dependencies will not be added to extras" + " in legacy mode. Consider converting your project to use the [project]" + " section." + ) + requirements = self._determine_requirements( packages, allow_prereleases=self.option("allow-prereleases"), @@ -214,7 +236,7 @@ def handle(self) -> int: constraint[key] = value - if self.option("optional"): + if optional: constraint["optional"] = True if self.option("allow-prereleases"): @@ -290,7 +312,7 @@ def handle(self) -> int: # that cannot be stored in the project section poetry_constraint: dict[str, Any] = inline_table() if not isinstance(constraint, str): - for key in ["optional", "allow-prereleases", "develop", "source"]: + for key in ["allow-prereleases", "develop", "source"]: if value := constraint.get(key): poetry_constraint[key] = value if poetry_constraint: @@ -310,9 +332,15 @@ def handle(self) -> int: poetry_section[constraint_name] = poetry_constraint # Refresh the locker - if project_section and "dependencies" not in project_content: + if project_section: assert group == MAIN_GROUP - project_content["dependencies"] = project_section + if optional: + if "optional-dependencies" not in project_content: + project_content["optional-dependencies"] = table() + if optional not in project_content["optional-dependencies"]: + project_content["optional-dependencies"][optional] = project_section + elif "dependencies" not in project_content: + project_content["dependencies"] = project_section if poetry_section: if "tool" not in content: content["tool"] = table() diff --git a/tests/console/commands/test_add.py b/tests/console/commands/test_add.py index 2289bfb8ba9..ebdea972688 100644 --- a/tests/console/commands/test_add.py +++ b/tests/console/commands/test_add.py @@ -795,10 +795,40 @@ def test_add_url_constraint_wheel_with_extras( } +@pytest.mark.parametrize("project_dependencies", [True, False]) +@pytest.mark.parametrize( + ("existing_extras", "expected_extras"), + [ + (None, {"my-extra": ["cachy (==0.2.0)"]}), + ( + {"other": ["foo>2"]}, + {"other": ["foo>2"], "my-extra": ["cachy (==0.2.0)"]}, + ), + ({"my-extra": ["foo>2"]}, {"my-extra": ["foo>2", "cachy (==0.2.0)"]}), + ( + {"my-extra": ["foo>2", "cachy (==0.1.0)", "bar>1"]}, + {"my-extra": ["foo>2", "cachy (==0.2.0)", "bar>1"]}, + ), + ], +) def test_add_constraint_with_optional( - app: PoetryTestApplication, tester: CommandTester + app: PoetryTestApplication, + tester: CommandTester, + project_dependencies: bool, + existing_extras: dict[str, list[str]] | None, + expected_extras: dict[str, list[str]], ) -> None: - tester.execute("cachy=0.2.0 --optional") + pyproject: dict[str, Any] = app.poetry.file.read() + if project_dependencies: + pyproject["project"]["dependencies"] = ["foo>1"] + if existing_extras: + pyproject["project"]["optional-dependencies"] = existing_extras + else: + pyproject["tool"]["poetry"]["dependencies"]["foo"] = "^1.0" + pyproject = cast("TOMLDocument", pyproject) + app.poetry.file.write(pyproject) + + tester.execute("cachy=0.2.0 --optional my-extra") expected = """\ Updating dependencies @@ -813,14 +843,37 @@ def test_add_constraint_with_optional( assert isinstance(tester.command, InstallerCommand) assert tester.command.installer.executor.installations_count == 0 - pyproject: dict[str, Any] = app.poetry.file.read() - content = pyproject["tool"]["poetry"] + pyproject2: dict[str, Any] = app.poetry.file.read() + project_content = pyproject2["project"] + poetry_content = pyproject2["tool"]["poetry"] - assert "cachy" in content["dependencies"] - assert content["dependencies"]["cachy"] == { - "version": "0.2.0", - "optional": True, - } + if project_dependencies: + assert "cachy" not in poetry_content["dependencies"] + assert "cachy" not in project_content["dependencies"] + assert "my-extra" in project_content["optional-dependencies"] + assert project_content["optional-dependencies"] == expected_extras + assert not tester.io.fetch_error() + else: + assert "dependencies" not in project_content + assert "optional-dependencies" not in project_content + assert "cachy" in poetry_content["dependencies"] + assert poetry_content["dependencies"]["cachy"] == { + "version": "0.2.0", + "optional": True, + } + assert ( + "Optional dependencies will not be added to extras in legacy mode." + in tester.io.fetch_error() + ) + + +def test_add_constraint_with_optional_not_main_group( + app: PoetryTestApplication, tester: CommandTester +) -> None: + with pytest.raises(ValueError) as e: + tester.execute("cachy=0.2.0 --group dev --optional my-extra") + + assert str(e.value) == "You can only add optional dependencies to the main group" def test_add_constraint_with_python(