diff --git a/src/pdm/cli/commands/fix/fixers.py b/src/pdm/cli/commands/fix/fixers.py index 6d2e92407e..16f1bba07b 100644 --- a/src/pdm/cli/commands/fix/fixers.py +++ b/src/pdm/cli/commands/fix/fixers.py @@ -76,7 +76,7 @@ def get_message(self) -> str: dist = str(package_type == "library").lower() return ( rf'[success]package-type = "{package_type}"[/] has been renamed to ' - rf"[info]distribution = {dist}[/] under \[tool.pdm\] table" + rf"[info]distribution = {dist}[/] under \[tool.pdm] table" ) def check(self) -> bool: diff --git a/src/pdm/environments/base.py b/src/pdm/environments/base.py index 021b3173fe..2bd1a1109c 100644 --- a/src/pdm/environments/base.py +++ b/src/pdm/environments/base.py @@ -1,6 +1,7 @@ from __future__ import annotations import abc +import functools import os import re import shutil @@ -10,13 +11,13 @@ from contextlib import contextmanager from functools import cached_property, partial from pathlib import Path -from typing import TYPE_CHECKING, Generator +from typing import TYPE_CHECKING, Generator, no_type_check from pdm.exceptions import BuildError, PdmUsageError from pdm.models.in_process import get_pep508_environment, get_python_abis, get_uname, sysconfig_get_platform from pdm.models.python import PythonInfo from pdm.models.working_set import WorkingSet -from pdm.utils import get_trusted_hosts, is_pip_compatible_with_python +from pdm.utils import deprecation_warning, get_trusted_hosts, is_pip_compatible_with_python if TYPE_CHECKING: import unearth @@ -26,11 +27,31 @@ from pdm.project import Project +@no_type_check +def get_paths_wrapper(get_paths): # pragma: no cover + @functools.wraps(get_paths) + def wrapped(self: BaseEnvironment, dist_name: str | None = None) -> dict[str, str]: + result = get_paths(self) + if dist_name and "headers" in result: + result["headers"] = os.path.join(result["headers"], dist_name) + return result + + return wrapped + + class BaseEnvironment(abc.ABC): """Environment dependent stuff related to the selected Python interpreter.""" is_local = False + def __init_subclass__(cls) -> None: + import inspect + + get_paths_params = inspect.signature(cls.get_paths).parameters + if "dist_name" not in get_paths_params: # pragma: no cover + deprecation_warning("get_paths() should accept a `dist_name` argument", stacklevel=3) + cls.get_paths = get_paths_wrapper(cls.get_paths) # type: ignore[method-assign] + def __init__(self, project: Project, *, python: str | None = None) -> None: """ :param project: the project instance @@ -55,8 +76,11 @@ def interpreter(self) -> PythonInfo: return self._interpreter @abc.abstractmethod - def get_paths(self) -> dict[str, str]: - """Get paths like ``sysconfig.get_paths()`` for installation.""" + def get_paths(self, dist_name: str | None = None) -> dict[str, str]: + """Get paths like ``sysconfig.get_paths()`` for installation. + + :param dist_name: The package name to be installed, if any. + """ ... @property @@ -252,7 +276,7 @@ class BareEnvironment(BaseEnvironment): def __init__(self, project: Project) -> None: super().__init__(project, python=sys.executable) - def get_paths(self) -> dict[str, str]: + def get_paths(self, dist_name: str | None = None) -> dict[str, str]: return {} def get_working_set(self) -> WorkingSet: diff --git a/src/pdm/environments/local.py b/src/pdm/environments/local.py index ff93d2670b..f7f17eb90b 100644 --- a/src/pdm/environments/local.py +++ b/src/pdm/environments/local.py @@ -106,8 +106,10 @@ def packages_path(self) -> Path: pypackages.joinpath(subdir).mkdir(exist_ok=True, parents=True) return pypackages - def get_paths(self) -> dict[str, str]: - return pdm_scheme(self.packages_path.as_posix()) + def get_paths(self, dist_name: str | None = None) -> dict[str, str]: + scheme = pdm_scheme(self.packages_path.as_posix()) + scheme["headers"] = os.path.join(scheme["headers"], dist_name or "UNKNOWN") + return scheme def update_shebangs(self, new_path: str) -> None: """Update the shebang lines""" diff --git a/src/pdm/environments/python.py b/src/pdm/environments/python.py index 32d02c5d58..2c3bdc9494 100644 --- a/src/pdm/environments/python.py +++ b/src/pdm/environments/python.py @@ -18,7 +18,7 @@ def __init__(self, project: Project, *, python: str | None = None, prefix: str | super().__init__(project, python=python) self.prefix = prefix - def get_paths(self) -> dict[str, str]: + def get_paths(self, dist_name: str | None = None) -> dict[str, str]: is_venv = self.interpreter.get_venv() is not None if self.prefix is not None: replace_vars = {"base": self.prefix, "platbase": self.prefix} @@ -27,9 +27,13 @@ def get_paths(self) -> dict[str, str]: replace_vars = None kind = "user" if not is_venv and self.project.global_config["global_project.user_site"] else "default" paths = get_sys_config_paths(str(self.interpreter.executable), replace_vars, kind=kind) + if not dist_name and not is_venv: + dist_name = "UNKNOWN" if is_venv and self.prefix is None: python_xy = f"python{self.interpreter.identifier}" paths["include"] = os.path.join(paths["data"], "include", "site", python_xy) + if dist_name: + paths["include"] = os.path.join(paths["include"], dist_name) paths["prefix"] = paths["data"] paths["headers"] = paths["include"] return paths diff --git a/src/pdm/installers/installers.py b/src/pdm/installers/installers.py index 122e23f35e..e700cdc6f7 100644 --- a/src/pdm/installers/installers.py +++ b/src/pdm/installers/installers.py @@ -48,6 +48,12 @@ def _is_python_package(root: str | Path) -> bool: return False +def _get_dist_name(wheel_path: str) -> str: + from packaging.utils import parse_wheel_filename + + return parse_wheel_filename(os.path.basename(wheel_path))[0] + + _namespace_package_lines = frozenset( [ # pkg_resources style @@ -192,7 +198,7 @@ def install_wheel(wheel: str, environment: BaseEnvironment, direct_url: dict[str if direct_url is not None: additional_metadata = {"direct_url.json": json.dumps(direct_url, indent=2).encode()} destination = InstallDestination( - scheme_dict=environment.get_paths(), + scheme_dict=environment.get_paths(_get_dist_name(wheel)), interpreter=str(environment.interpreter.executable), script_kind=_get_kind(environment), ) @@ -263,7 +269,7 @@ def skip_files(source: WheelFile, element: WheelContentElement) -> bool: additional_contents.append(((filename, "", str(len(stream.getvalue()))), stream, False)) destination = InstallDestination( - scheme_dict=environment.get_paths(), + scheme_dict=environment.get_paths(_get_dist_name(wheel)), interpreter=interpreter, script_kind=script_kind, link_to=lib_path,