Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: do not ignore dynamic project dependencies via tool.poetry.dependencies if project.optional-dependencies are defined #811

Merged
merged 2 commits into from
Jan 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/poetry/core/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,10 +290,16 @@ def _configure_package_dependencies(

dependencies = project.get("dependencies", {})
optional_dependencies = project.get("optional-dependencies", {})
dynamic = project.get("dynamic", [])

package_extras: dict[NormalizedName, list[Dependency]]
if dependencies or optional_dependencies:
group = DependencyGroup(MAIN_GROUP)
group = DependencyGroup(
MAIN_GROUP,
mixed_dynamic=(
"dependencies" in dynamic or "optional-dependencies" in dynamic
),
)
package.add_dependency_group(group)

for constraint in dependencies:
Expand Down
23 changes: 20 additions & 3 deletions src/poetry/core/packages/dependency_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@


class DependencyGroup:
def __init__(self, name: str, optional: bool = False) -> None:
def __init__(
self, name: str, *, optional: bool = False, mixed_dynamic: bool = False
) -> None:
self._name: str = name
self._optional: bool = optional
self._mixed_dynamic = mixed_dynamic
self._dependencies: list[Dependency] = []
self._poetry_dependencies: list[Dependency] = []

Expand All @@ -26,7 +29,21 @@ def name(self) -> str:

@property
def dependencies(self) -> list[Dependency]:
return self._dependencies or self._poetry_dependencies
if not self._dependencies:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess I understood what's going on here 💪 And it looks like it should work.

As far as I see, we don't take into account the dynamic key of the project section anywhere in our codebase. Instead we are doing educated guesses. Wouldn't it be easier to rely on this field?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At first, I thought it is redundant. However, there is at least one use case where it is necessary:

Projects that do not have any mandatory dependencies but optional dependencies and use tool.poetry.dependencies to define sources for these optional dependencies. Without taking project.dynamic into account the dependencies in tool.poetry.dependencies would have been considered mandatory dependencies in this case.

I added a second commit that takes project.dynamic into account to fix this use case.

# legacy mode
return self._poetry_dependencies
if self._mixed_dynamic and self._poetry_dependencies:
if all(dep.is_optional() for dep in self._dependencies):
return [
*self._dependencies,
*(d for d in self._poetry_dependencies if not d.is_optional()),
]
if all(not dep.is_optional() for dep in self._dependencies):
return [
*self._dependencies,
*(d for d in self._poetry_dependencies if d.is_optional()),
]
return self._dependencies

@property
def dependencies_for_locking(self) -> list[Dependency]:
Expand All @@ -40,7 +57,7 @@ def dependencies_for_locking(self) -> list[Dependency]:
poetry_dependencies_by_name[dep.name].append(dep)

dependencies = []
for dep in self._dependencies:
for dep in self.dependencies:
if dep.name in poetry_dependencies_by_name:
enriched = False
dep_marker = dep.marker
Expand Down
2 changes: 2 additions & 0 deletions tests/fixtures/sample_project_dynamic/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
My Package
==========
83 changes: 83 additions & 0 deletions tests/fixtures/sample_project_dynamic/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
[project]
name = "my-package"
version = "1.2.3"
description = "Some description."
readme = "README.rst"
requires-python = ">=3.6"
license = { text = "MIT" }
keywords = ["packaging", "dependency", "poetry"]
authors = [
{ name = "Sébastien Eustace", email = "[email protected]" }
]
maintainers = [
{ name = "Sébastien Eustace", email = "[email protected]" }
]
dynamic = [ "version", "readme", "dependencies", "classifiers" ]

[project.optional-dependencies]
db = [
"orator ~=0.9"
]
network = [
"requests[security] ~=2.18"
]

[project.urls]
homepage = "https://python-poetry.org"
repository = "https://github.com/python-poetry/poetry"
documentation = "https://python-poetry.org/docs"

[project.scripts]
my-script = "my_package:main"

[tool.poetry]
version = "1.2.3"
readme = "README.rst"
classifiers = [
"Topic :: Software Development :: Build Tools",
"Topic :: Software Development :: Libraries :: Python Modules"
]

# Requirements
[tool.poetry.dependencies]
python = ">=3.6"
cleo = "^0.6"
pendulum = { git = "https://github.com/sdispater/pendulum.git", branch = "2.0" }
tomlkit = { git = "https://github.com/sdispater/tomlkit.git", rev = "3bff550", develop = true }
pathlib2 = { version = "^2.2", python = "~2.7" }

# File dependency
demo = { path = "../distributions/demo-0.1.0-py2.py3-none-any.whl" }

# Dir dependency with setup.py
my-package = { path = "../project_with_setup/" }

# Dir dependency with pyproject.toml
simple-project = { path = "../simple_project/" }

# Dependency with markers
functools32 = { version = "^3.2.3", markers = "python_version ~= '2.7' and sys_platform == 'win32' or python_version in '3.4 3.5'" }

# Dependency with python constraint
dataclasses = { version = "^0.7", python = ">=3.6.1,<3.7" }


# Non-regression test for https://github.com/python-poetry/poetry-core/pull/492.
# The underlying issue occurred because `tomlkit` can either return a TOML table as `Table` instance or an
# `OutOfOrderProxy` one, if a table is discontinuous and multiple sections of a table are separated by a non-related
# table, but we were too strict in our type check assertions.
# So adding `tool.black` here ensure that we have discontinuous tables, so that we don't re-introduce the issue caused
# by the type check assertion that ended up being reverted.
[tool.black]
preview = true

[tool.poetry.group.dev.dependencies]
pytest = "~3.4"


[tool.poetry.scripts]
my-script = "my_package:main"


[tool.poetry.plugins."blogtool.parsers"]
".rst" = "some_module::SomeClass"
Empty file.
Empty file.
Empty file.
20 changes: 20 additions & 0 deletions tests/masonry/builders/fixtures/complete_dynamic/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Copyright (c) 2018 Sébastien Eustace

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
2 changes: 2 additions & 0 deletions tests/masonry/builders/fixtures/complete_dynamic/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
My Package
==========
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/env bash

echo "Hello World!"
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__version__ = "1.2.3"
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
62 changes: 62 additions & 0 deletions tests/masonry/builders/fixtures/complete_dynamic/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
[project]
name = "my-package"
version = "1.2.3"
description = "Some description."
requires-python = ">=3.6,<4.0"
license = { "text" = "MIT" }
authors = [
{ "name" = "Sébastien Eustace", "email" = "[email protected]" }
]
maintainers = [
{ name = "People Everywhere", email = "[email protected]" }
]
keywords = ["packaging", "dependency", "poetry"]
dynamic = [ "version", "classifiers", "readme", "dependencies" ]

[project.optional-dependencies]
time = [ "pendulum>=1.4,<2.0 ; python_version ~= '2.7' and sys_platform == 'win32' or python_version in '3.4 3.5'" ]

[project.urls]
homepage = "https://python-poetry.org/"
repository = "https://github.com/python-poetry/poetry"
documentation = "https://python-poetry.org/docs"
"Issue Tracker" = "https://github.com/python-poetry/poetry/issues"

[project.scripts]
my-script = "my_package:main"
my-2nd-script = "my_package:main2"
extra-script = "my_package.extra:main"

[project.entry-points."poetry.application.plugin"]
my-command = "my_package.plugins:MyApplicationPlugin"


[tool.poetry]
version = "1.2.3"
readme = "README.rst"

classifiers = [
"Topic :: Software Development :: Build Tools",
"Topic :: Software Development :: Libraries :: Python Modules"
]

exclude = [
"does-not-exist",
"**/*.xml"
]

# Requirements
[tool.poetry.dependencies]
cleo = "^0.6"
cachy = { version = "^0.2.0", extras = ["msgpack"] }

[tool.poetry.dependencies.pendulum]
version = "^1.4"
markers = 'python_version ~= "2.7" and sys_platform == "win32" or python_version in "3.4 3.5"'
optional = true

[tool.poetry.group.dev.dependencies]
pytest = "~3.4"

[tool.poetry.scripts]
file-script = { reference = "bin/script.sh", type = "file" }
2 changes: 1 addition & 1 deletion tests/masonry/builders/test_complete.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ def test_wheel_c_extension(project: str, exptected_c_dir: str) -> None:
assert len(set(record_files)) == len(record_files)


@pytest.mark.parametrize("project", ["complete", "complete_new"])
@pytest.mark.parametrize("project", ["complete", "complete_new", "complete_dynamic"])
@pytest.mark.parametrize("no_vcs", [False, True])
def test_complete(project: str, no_vcs: bool) -> None:
module_path = fixtures_dir / project
Expand Down
28 changes: 21 additions & 7 deletions tests/masonry/builders/test_sdist.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,9 @@ def test_convert_dependencies() -> None:
assert result == (main, extras)


@pytest.mark.parametrize("project_name", ["complete", "complete_new"])
@pytest.mark.parametrize(
"project_name", ["complete", "complete_new", "complete_dynamic"]
)
def test_make_setup(project_name: str) -> None:
poetry = Factory().create_poetry(project(project_name))

Expand Down Expand Up @@ -156,7 +158,9 @@ def test_make_setup(project_name: str) -> None:
}


@pytest.mark.parametrize("project_name", ["complete", "complete_new"])
@pytest.mark.parametrize(
"project_name", ["complete", "complete_new", "complete_dynamic"]
)
def test_make_pkg_info(project_name: str, mocker: MockerFixture) -> None:
get_metadata_content = mocker.patch(
"poetry.core.masonry.builders.builder.Builder.get_metadata_content"
Expand All @@ -180,7 +184,9 @@ def test_make_pkg_info_any_python() -> None:
assert "Requires-Python" not in parsed


@pytest.mark.parametrize("project_name", ["complete", "complete_new"])
@pytest.mark.parametrize(
"project_name", ["complete", "complete_new", "complete_dynamic"]
)
def test_find_files_to_add(project_name: str) -> None:
poetry = Factory().create_poetry(project(project_name))

Expand Down Expand Up @@ -239,7 +245,9 @@ def test_make_pkg_info_multi_constraints_dependency() -> None:
]


@pytest.mark.parametrize("project_name", ["complete", "complete_new"])
@pytest.mark.parametrize(
"project_name", ["complete", "complete_new", "complete_dynamic"]
)
def test_find_packages(project_name: str) -> None:
poetry = Factory().create_poetry(project(project_name))

Expand Down Expand Up @@ -277,7 +285,9 @@ def test_find_packages(project_name: str) -> None:
assert pkg_data == {"": ["*"]}


@pytest.mark.parametrize("project_name", ["complete", "complete_new"])
@pytest.mark.parametrize(
"project_name", ["complete", "complete_new", "complete_dynamic"]
)
def test_package(project_name: str) -> None:
poetry = Factory().create_poetry(project(project_name))

Expand Down Expand Up @@ -309,7 +319,9 @@ def test_package_target_dir(tmp_path: Path, target_dir: str | None) -> None:
assert "my_package-1.2.3/LICENSE" in tar.getnames()


@pytest.mark.parametrize("project_name", ["complete", "complete_new"])
@pytest.mark.parametrize(
"project_name", ["complete", "complete_new", "complete_dynamic"]
)
def test_sdist_reproducibility(project_name: str) -> None:
poetry = Factory().create_poetry(project(project_name))

Expand All @@ -328,7 +340,9 @@ def test_sdist_reproducibility(project_name: str) -> None:
assert len(hashes) == 1


@pytest.mark.parametrize("project_name", ["complete", "complete_new"])
@pytest.mark.parametrize(
"project_name", ["complete", "complete_new", "complete_dynamic"]
)
def test_setup_py_context(project_name: str) -> None:
poetry = Factory().create_poetry(project(project_name))

Expand Down
4 changes: 2 additions & 2 deletions tests/masonry/builders/test_wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def test_wheel_module() -> None:
assert "module1.py" in z.namelist()


@pytest.mark.parametrize("project", ["complete", "complete_new"])
@pytest.mark.parametrize("project", ["complete", "complete_new", "complete_dynamic"])
def test_wheel_package(project: str) -> None:
module_path = fixtures_dir / project
WheelBuilder.make(Factory().create_poetry(module_path))
Expand Down Expand Up @@ -231,7 +231,7 @@ def test_wheel_build_script_creates_package() -> None:
shutil.rmtree(module_path / "my_package")


@pytest.mark.parametrize("project", ["complete", "complete_new"])
@pytest.mark.parametrize("project", ["complete", "complete_new", "complete_dynamic"])
def test_dist_info_file_permissions(project: str) -> None:
module_path = fixtures_dir / project
WheelBuilder.make(Factory().create_poetry(module_path))
Expand Down
Loading
Loading