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
22 changes: 22 additions & 0 deletions docs/pyproject.md
Original file line number Diff line number Diff line change
Expand Up @@ -924,6 +924,28 @@ my-plugin = ">=1.0,<2.0"

See [Project plugins]({{< relref "plugins#project-plugins" >}}) for more information.

### build-constraints

In this section, you can specify additional constraints to apply when creating the build
environment for a dependency. This is useful if a package does not provide wheels
(or shall be built from source for other reasons)
and specifies too loose build requirements (without an upper bound)
and is not compatible with current versions of one of its build requirements.

For example, if your project depends on `some-package`, which only provides an sdist
and defines its build requirements as `build-requires = ["setuptools"]`,
but is incompatible with `setuptools >= 78`, building the package will probably fail
because per default the latest setuptools will be chosen. In this case, you can
work around this issue of `some-package` as follows:

```toml
[tool.poetry.build-constraints]
some-package = { setuptools = "<78" }
```

The syntax for specifying constraints is the same as for specifying dependencies
in the `tool.poetry` section.

## Poetry and PEP-517

[PEP-517](https://www.python.org/dev/peps/pep-0517/) introduces a standard way
Expand Down
1 change: 1 addition & 0 deletions src/poetry/console/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -616,6 +616,7 @@ def configure_installer_for_command(command: InstallerCommand, io: IO) -> None:
poetry.pool,
poetry.config,
disable_cache=poetry.disable_cache,
build_constraints=poetry.build_constraints,
)
command.set_installer(installer)

Expand Down
23 changes: 22 additions & 1 deletion src/poetry/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from typing import cast

from cleo.io.null_io import NullIO
from packaging.utils import NormalizedName
from packaging.utils import canonicalize_name
from poetry.core.constraints.version import Version
from poetry.core.constraints.version import parse_constraint
Expand All @@ -24,13 +25,15 @@
from poetry.plugins.plugin_manager import PluginManager
from poetry.poetry import Poetry
from poetry.toml.file import TOMLFile
from poetry.utils.isolated_build import CONSTRAINTS_GROUP_NAME


if TYPE_CHECKING:
from collections.abc import Iterable
from pathlib import Path

from cleo.io.io import IO
from poetry.core.packages.dependency import Dependency
from poetry.core.packages.package import Package
from tomlkit.toml_document import TOMLDocument

Expand Down Expand Up @@ -68,6 +71,23 @@ def create_poetry(
f" but you are using Poetry {version}"
)

build_constraints: dict[NormalizedName, list[Dependency]] = {}
for name, constraints in base_poetry.local_config.get(
"build-constraints", {}
).items():
name = canonicalize_name(name)
build_constraints[name] = []
for dep_name, constraint in constraints.items():
_constraints = (
constraint if isinstance(constraint, list) else [constraint]
)
for _constraint in _constraints:
build_constraints[name].append(
Factory.create_dependency(
dep_name, _constraint, groups=[CONSTRAINTS_GROUP_NAME]
)
)

poetry_file = base_poetry.pyproject_path
locker = Locker(poetry_file.parent / "poetry.lock", base_poetry.pyproject.data)

Expand Down Expand Up @@ -99,7 +119,8 @@ def create_poetry(
base_poetry.package,
locker,
config,
disable_cache,
disable_cache=disable_cache,
build_constraints=build_constraints,
)

poetry.set_pool(
Expand Down
12 changes: 11 additions & 1 deletion src/poetry/installation/chef.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from collections.abc import Sequence

from build import DistributionType
from poetry.core.packages.dependency import Dependency

from poetry.repositories import RepositoryPool
from poetry.utils.cache import ArtifactCache
Expand All @@ -40,6 +41,7 @@ def prepare(
*,
editable: bool = False,
config_settings: Mapping[str, str | Sequence[str]] | None = None,
build_constraints: list[Dependency] | None = None,
) -> Path:
if not self._should_prepare(archive):
return archive
Expand All @@ -51,10 +53,14 @@ def prepare(
destination=destination,
editable=editable,
config_settings=config_settings,
build_constraints=build_constraints,
)

return self._prepare_sdist(
archive, destination=output_dir, config_settings=config_settings
archive,
destination=output_dir,
config_settings=config_settings,
build_constraints=build_constraints,
)

def _prepare(
Expand All @@ -64,13 +70,15 @@ def _prepare(
*,
editable: bool = False,
config_settings: Mapping[str, str | Sequence[str]] | None = None,
build_constraints: list[Dependency] | None = None,
) -> Path:
distribution: DistributionType = "editable" if editable else "wheel"
with isolated_builder(
source=directory,
distribution=distribution,
python_executable=self._env.python,
pool=self._pool,
build_constraints=build_constraints,
) as builder:
return Path(
builder.build(
Expand All @@ -85,6 +93,7 @@ def _prepare_sdist(
archive: Path,
destination: Path | None = None,
config_settings: Mapping[str, str | Sequence[str]] | None = None,
build_constraints: list[Dependency] | None = None,
) -> Path:
from poetry.core.packages.utils.link import Link

Expand Down Expand Up @@ -115,6 +124,7 @@ def _prepare_sdist(
sdist_dir,
destination,
config_settings=config_settings,
build_constraints=build_constraints,
)

def _should_prepare(self, archive: Path) -> bool:
Expand Down
12 changes: 10 additions & 2 deletions src/poetry/installation/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
from cleo.io.io import IO
from cleo.io.outputs.section_output import SectionOutput
from packaging.utils import NormalizedName
from poetry.core.packages.dependency import Dependency
from poetry.core.packages.package import Package

from poetry.config.config import Config
Expand All @@ -68,13 +69,16 @@ def __init__(
io: IO,
parallel: bool | None = None,
disable_cache: bool = False,
*,
build_constraints: Mapping[NormalizedName, list[Dependency]] | None = None,
) -> None:
self._env = env
self._io = io
self._dry_run = False
self._enabled = True
self._verbose = False
self._wheel_installer = WheelInstaller(self._env)
self._build_constraints = build_constraints or {}

if parallel is None:
parallel = config.get("installer.parallel", True)
Expand Down Expand Up @@ -647,11 +651,13 @@ def _prepare_archive(

self._populate_hashes_dict(archive, package)

name = operation.package.name
return self._chef.prepare(
archive,
editable=package.develop,
output_dir=output_dir,
config_settings=self._build_config_settings.get(operation.package.name),
config_settings=self._build_config_settings.get(name),
build_constraints=self._build_constraints.get(name),
)

def _prepare_git_archive(self, operation: Install | Update) -> Path:
Expand Down Expand Up @@ -761,10 +767,12 @@ def _download_link(self, operation: Install | Update, link: Link) -> Path:
)
self._write(operation, message)

name = operation.package.name
archive = self._chef.prepare(
archive,
output_dir=original_archive.parent,
config_settings=self._build_config_settings.get(operation.package.name),
config_settings=self._build_config_settings.get(name),
build_constraints=self._build_constraints.get(name),
)

# Use the original archive to provide the correct hash.
Expand Down
11 changes: 10 additions & 1 deletion src/poetry/installation/installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@

if TYPE_CHECKING:
from collections.abc import Iterable
from collections.abc import Mapping

from cleo.io.io import IO
from packaging.utils import NormalizedName
from poetry.core.packages.dependency import Dependency
from poetry.core.packages.package import Package
from poetry.core.packages.path_dependency import PathDependency
from poetry.core.packages.project_package import ProjectPackage
Expand All @@ -42,6 +44,8 @@ def __init__(
installed: InstalledRepository | None = None,
executor: Executor | None = None,
disable_cache: bool = False,
*,
build_constraints: Mapping[NormalizedName, list[Dependency]] | None = None,
) -> None:
self._io = io
self._env = env
Expand All @@ -64,7 +68,12 @@ def __init__(

if executor is None:
executor = Executor(
self._env, self._pool, config, self._io, disable_cache=disable_cache
self._env,
self._pool,
config,
self._io,
disable_cache=disable_cache,
build_constraints=build_constraints,
)

self._executor = executor
Expand Down
9 changes: 9 additions & 0 deletions src/poetry/json/schemas/poetry.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@
"items": {
"$ref": "#/definitions/repository"
}
},
"build-constraints": {
"type": "object",
"description": "This is a dict of package name (keys) and version constraints (values) to restrict build requirements for a package.",
"patternProperties": {
"^[a-zA-Z-_.0-9]+$": {
"$ref": "#/definitions/dependencies"
}
}
}
},
"definitions": {
Expand Down
10 changes: 10 additions & 0 deletions src/poetry/poetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@


if TYPE_CHECKING:
from collections.abc import Mapping
from pathlib import Path

from packaging.utils import NormalizedName
from poetry.core.packages.dependency import Dependency
from poetry.core.packages.project_package import ProjectPackage

from poetry.config.config import Config
Expand All @@ -34,6 +37,8 @@ def __init__(
locker: Locker,
config: Config,
disable_cache: bool = False,
*,
build_constraints: Mapping[NormalizedName, list[Dependency]] | None = None,
) -> None:
from poetry.repositories.repository_pool import RepositoryPool

Expand All @@ -44,6 +49,7 @@ def __init__(
self._pool = RepositoryPool(config=config)
self._plugin_manager: PluginManager | None = None
self._disable_cache = disable_cache
self._build_constraints = build_constraints or {}

@property
def pyproject(self) -> PyProjectTOML:
Expand All @@ -70,6 +76,10 @@ def config(self) -> Config:
def disable_cache(self) -> bool:
return self._disable_cache

@property
def build_constraints(self) -> Mapping[NormalizedName, list[Dependency]]:
return self._build_constraints

def set_locker(self, locker: Locker) -> Poetry:
self._locker = locker

Expand Down
28 changes: 25 additions & 3 deletions src/poetry/utils/isolated_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from build import BuildBackendException
from build.env import IsolatedEnv as BaseIsolatedEnv
from poetry.core.packages.dependency_group import DependencyGroup

from poetry.utils._compat import decode
from poetry.utils.env import Env
Expand All @@ -24,10 +25,14 @@

from build import DistributionType
from build import ProjectBuilder
from poetry.core.packages.dependency import Dependency

from poetry.repositories import RepositoryPool


CONSTRAINTS_GROUP_NAME = "constraints"


class IsolatedBuildBaseError(Exception): ...


Expand Down Expand Up @@ -111,7 +116,12 @@ def make_extra_environ(self) -> dict[str, str]:
)
}

def install(self, requirements: Collection[str]) -> None:
def install(
self,
requirements: Collection[str],
*,
constraints: list[Dependency] | None = None,
) -> None:
from cleo.io.buffered_io import BufferedIO
from poetry.core.packages.dependency import Dependency
from poetry.core.packages.project_package import ProjectPackage
Expand All @@ -136,6 +146,13 @@ def install(self, requirements: Collection[str]) -> None:
# safe as this environment is ephemeral
package.add_dependency(dependency)

if constraints:
constraints_group = DependencyGroup(CONSTRAINTS_GROUP_NAME, optional=True)
for constraint in constraints:
if constraint.marker.validate(env_markers):
constraints_group.add_dependency(constraint)
package.add_dependency_group(constraints_group)

io = BufferedIO()

installer = Installer(
Expand All @@ -161,6 +178,8 @@ def isolated_builder(
distribution: DistributionType = "wheel",
python_executable: Path | None = None,
pool: RepositoryPool | None = None,
*,
build_constraints: list[Dependency] | None = None,
) -> Iterator[ProjectBuilder]:
from build import ProjectBuilder
from pyproject_hooks import quiet_subprocess_runner
Expand Down Expand Up @@ -196,12 +215,15 @@ def isolated_builder(
)

with redirect_stdout(stdout):
env.install(builder.build_system_requires)
env.install(
builder.build_system_requires, constraints=build_constraints
)

# we repeat the build system requirements to avoid poetry installer from removing them
env.install(
builder.build_system_requires
| builder.get_requires_for_build(distribution)
| builder.get_requires_for_build(distribution),
constraints=build_constraints,
)

yield builder
Expand Down
14 changes: 14 additions & 0 deletions tests/fixtures/build_constraints/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[project]
name = "build-constraints"
version = "0.1.0"

[tool.poetry.build-constraints]
Legacy-Lib = { setuptools = "<75" }
no-constraints = {}

[tool.poetry.build-constraints.c-ext-lib]
Cython = { version = "<3.1", source = "pypi" }
setuptools = [
{ version = ">=60,<75", python = "<3.9" },
{ version = ">=75", python = ">=3.8" }
]
Loading