Skip to content
Open
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
39 changes: 39 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,19 @@ If a source does not provide upload times for a release,
that release is not filtered out by this setting.
{{% /note %}}

This setting can also be configured in your `pyproject.toml`:

```toml
[tool.poetry.config]
solver.min-release-age = 7
```

Values set in `[tool.poetry.config]` take precedence over
[project-local configuration]({{< relref "#project-local-configuration" >}})
and global configuration.
They can still be overridden by the `POETRY_SOLVER_MIN_RELEASE_AGE`
environment variable.

### `solver.min-release-age-exclude`

**Type**: `string`
Expand All @@ -456,6 +469,19 @@ regardless of their upload age.
poetry config solver.min-release-age-exclude "my-package,other-package"
```

This setting can also be configured in your `pyproject.toml`:

```toml
[tool.poetry.config]
solver.min-release-age-exclude = ["my-package", "other-package"]
```

Values set in `[tool.poetry.config]` take precedence over
[project-local configuration]({{< relref "#project-local-configuration" >}})
and global configuration.
They can still be overridden by the `POETRY_SOLVER_MIN_RELEASE_AGE_EXCLUDE`
environment variable.

### `solver.min-release-age-exclude-source`

**Type**: `string`
Expand All @@ -476,6 +502,19 @@ Sources can be referenced by the name defined in `pyproject.toml` or by URL.
poetry config solver.min-release-age-exclude-source "private-repo,https://example.com/simple/"
```

This setting can also be configured in your `pyproject.toml`:

```toml
[tool.poetry.config]
solver.min-release-age-exclude-source = ["private-repo", "https://example.com/simple/"]
```

Values set in `[tool.poetry.config]` take precedence over
[project-local configuration]({{< relref "#project-local-configuration" >}})
and global configuration.
They can still be overridden by the `POETRY_SOLVER_MIN_RELEASE_AGE_EXCLUDE_SOURCE`
environment variable.

### `system-git-client`

**Type**: `boolean`
Expand Down
20 changes: 20 additions & 0 deletions docs/pyproject.md
Original file line number Diff line number Diff line change
Expand Up @@ -1012,6 +1012,26 @@ some-package = { setuptools = "<78" }
The syntax for specifying constraints is the same as for specifying dependencies
in the `tool.poetry` section.

### config

The `solver.min-release-age`, `solver.min-release-age-exclude`,
and `solver.min-release-age-exclude-source` options can be configured
directly in your `pyproject.toml`:

```toml
[tool.poetry.config]
solver.min-release-age = 7
solver.min-release-age-exclude = ["my-package", "other-package"]
solver.min-release-age-exclude-source = ["private-repo"]
```

These values override the equivalent settings in
[project-local]({{< relref "configuration#project-local-configuration" >}})
and [global configuration]({{< relref "configuration#global-configuration" >}}),
but can still be overridden by environment variables.

For details, see the [configuration documentation]({{< relref "configuration" >}}).

## Poetry and PEP-517

[PEP-517](https://www.python.org/dev/peps/pep-0517/) introduces a standard way
Expand Down
31 changes: 31 additions & 0 deletions src/poetry/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
from cleo.io.io import IO
from poetry.core.packages.dependency import Dependency
from poetry.core.packages.package import Package
from poetry.core.poetry import Poetry as CorePoetry
from tomlkit.toml_document import TOMLDocument

from poetry.repositories import RepositoryPool
Expand Down Expand Up @@ -63,6 +64,33 @@ def _ensure_valid_poetry_version(self, cwd: Path | None) -> None:
f" but you are using Poetry {version}"
)

@staticmethod
def _load_project_config(
base_poetry: CorePoetry,
config: Config,
) -> None:
"""Load project config from [tool.poetry.config]."""
project_config = base_poetry.local_config.get("config", {})
if not project_config:
return

validated: dict[str, Any] = {}
solver = project_config.get("solver")
if solver:
validated_solver: dict[str, Any] = {}
for key in (
"min-release-age",
"min-release-age-exclude",
"min-release-age-exclude-source",
):
if key in solver:
validated_solver[key] = solver[key]
if validated_solver:
validated["solver"] = validated_solver

if validated:
config.merge(validated)

def create_poetry(
self,
cwd: Path | None = None,
Expand Down Expand Up @@ -120,6 +148,9 @@ def create_poetry(

config.merge({"repositories": repositories})

# Load project config from [tool.poetry.config]
self._load_project_config(base_poetry, config)

poetry = Poetry(
poetry_file,
base_poetry.local_config,
Expand Down
33 changes: 33 additions & 0 deletions src/poetry/json/schemas/poetry.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,39 @@
"$ref": "#/definitions/dependencies"
}
}
},
"config": {
"type": "object",
"description": "Poetry configuration values that override global and local config.",
"properties": {
"solver": {
"type": "object",
"description": "Solver-specific configuration.",
"properties": {
"min-release-age": {
"type": "integer",
"minimum": 0,
"description": "Minimum age in days for a package release to be considered."
},
"min-release-age-exclude": {
"type": "array",
"items": {
"type": "string"
},
"description": "Package names excluded from the min-release-age filter."
},
"min-release-age-exclude-source": {
"type": "array",
"items": {
"type": "string"
},
"description": "Source names or URLs excluded from the min-release-age filter."
}
},
"additionalProperties": false
}
},
"additionalProperties": false
}
},
"definitions": {
Expand Down
146 changes: 146 additions & 0 deletions tests/test_factory.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations

import re

from pathlib import Path
from typing import TYPE_CHECKING
from typing import Any
Expand Down Expand Up @@ -36,6 +38,42 @@
from tests.types import FixtureDirGetter


def _make_project(
tmp_path: Path,
extra_config: dict[str, Any] | None = None,
poetry_toml: dict[str, Any] | None = None,
) -> Path:
"""Create a minimal pyproject.toml (and optionally poetry.toml) project dir."""
import tomlkit

project_dir = tmp_path / "project"
project_dir.mkdir()

pyproject: dict[str, Any] = {
"tool": {
"poetry": {
"name": "test",
"version": "1.0",
"description": "",
"authors": [],
"dependencies": {"python": "^3.10"},
}
}
}

if extra_config:
pyproject["tool"]["poetry"]["config"] = extra_config

with (project_dir / "pyproject.toml").open("w", encoding="utf-8") as f:
tomlkit.dump(pyproject, f)

if poetry_toml:
with (project_dir / "poetry.toml").open("w", encoding="utf-8") as f:
tomlkit.dump(poetry_toml, f)

return project_dir


class MyPlugin(Plugin):
def activate(self, poetry: Poetry, io: IO) -> None:
io.write_line("Setting readmes")
Expand Down Expand Up @@ -510,3 +548,111 @@ def test_create_package_source_invalid(
Factory().create_poetry(fixture_dir("with_source_pypi_url"))

assert str(e.value) == expected


def test_project_config_min_release_age(tmp_path: Path) -> None:
project_dir = _make_project(tmp_path, {"solver": {"min-release-age": 7}})
poetry = Factory().create_poetry(project_dir)
assert poetry._config.get("solver.min-release-age") == 7


def test_project_config_exclude_options(tmp_path: Path) -> None:
project_dir = _make_project(
tmp_path,
{
"solver": {
"min-release-age": 7,
"min-release-age-exclude": ["foo", "bar"],
"min-release-age-exclude-source": ["private-repo"],
}
},
)
poetry = Factory().create_poetry(project_dir)
assert poetry._config.get("solver.min-release-age") == 7
assert poetry._config.get("solver.min-release-age-exclude") == ["foo", "bar"]
assert poetry._config.get("solver.min-release-age-exclude-source") == [
"private-repo"
]


def test_project_config_invalid_age_type_raises(tmp_path: Path) -> None:
project_dir = _make_project(tmp_path, {"solver": {"min-release-age": "not-an-int"}})
with pytest.raises(
RuntimeError,
match=re.escape("tool.poetry.config.solver.min-release-age must be integer"),
):
Factory().create_poetry(project_dir)


def test_project_config_negative_age_raises(tmp_path: Path) -> None:
project_dir = _make_project(tmp_path, {"solver": {"min-release-age": -1}})
with pytest.raises(
RuntimeError,
match=re.escape(
"tool.poetry.config.solver.min-release-age must be bigger than or equal to 0"
),
):
Factory().create_poetry(project_dir)


def test_project_config_bad_exclude_type_raises(tmp_path: Path) -> None:
project_dir = _make_project(
tmp_path, {"solver": {"min-release-age-exclude": "foo,bar"}}
)
with pytest.raises(
RuntimeError,
match=re.escape(
"tool.poetry.config.solver.min-release-age-exclude must be array"
),
):
Factory().create_poetry(project_dir)


def test_project_config_unknown_solver_key_raises(tmp_path: Path) -> None:
project_dir = _make_project(
tmp_path,
{"solver": {"min-release-age": 7, "foo": 1}},
)
with pytest.raises(
RuntimeError,
match=re.escape("tool.poetry.config.solver must not contain"),
):
Factory().create_poetry(project_dir)


def test_project_config_unknown_config_key_raises(tmp_path: Path) -> None:
project_dir = _make_project(
tmp_path,
{"unknown-config-key": 1},
)
with pytest.raises(
RuntimeError,
match=re.escape("tool.poetry.config must not contain"),
):
Factory().create_poetry(project_dir)


def test_project_config_overrides_poetry_toml(tmp_path: Path) -> None:
project_dir = _make_project(
tmp_path,
{"solver": {"min-release-age": 14}},
poetry_toml={"solver": {"min-release-age": 7}},
)
poetry = Factory().create_poetry(project_dir)
assert poetry._config.get("solver.min-release-age") == 14


Comment thread
finswimmer marked this conversation as resolved.
def test_project_config_overrides_poetry_toml_array_option(tmp_path: Path) -> None:
project_dir = _make_project(
tmp_path,
{"solver": {"min-release-age-exclude": ["foo", "bar"]}},
poetry_toml={"solver": {"min-release-age-exclude": ["baz"]}},
)
poetry = Factory().create_poetry(project_dir)
assert poetry._config.get("solver.min-release-age-exclude") == ["foo", "bar"]


def test_project_config_empty_keeps_default(tmp_path: Path) -> None:
project_dir = _make_project(tmp_path)
poetry = Factory().create_poetry(project_dir)
assert poetry._config.get("solver.min-release-age") == 0
Loading