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

Standardize and centralize StrPath TypeAlias #4241

Merged
merged 1 commit into from Mar 3, 2024
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions newsfragments/4241.misc.rst
@@ -0,0 +1 @@
Improvements to `Path`-related type annotations when it could be ``str | PathLike`` -- by :user:`Avasam`
Copy link
Contributor Author

@Avasam Avasam Feb 27, 2024

Choose a reason for hiding this comment

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

I marked this as misc rather than feature because for now type-checking users will still use types-setuptools

4 changes: 0 additions & 4 deletions setuptools/_normalization.py
Expand Up @@ -4,13 +4,9 @@
"""

import re
from pathlib import Path
from typing import Union

from .extern import packaging

_Path = Union[str, Path]

# https://packaging.python.org/en/latest/specifications/core-metadata/#name
_VALID_NAME = re.compile(r"^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$", re.I)
_UNSAFE_NAME_CHARS = re.compile(r"[^A-Z0-9._-]+", re.I)
Expand Down
9 changes: 6 additions & 3 deletions setuptools/_path.py
Expand Up @@ -2,7 +2,10 @@
import sys
from typing import Union

_Path = Union[str, os.PathLike]
if sys.version_info >= (3, 9):
StrPath = Union[str, os.PathLike[str]] # Same as _typeshed.StrPath
else:
StrPath = Union[str, os.PathLike]


def ensure_directory(path):
Expand All @@ -11,7 +14,7 @@ def ensure_directory(path):
os.makedirs(dirname, exist_ok=True)


def same_path(p1: _Path, p2: _Path) -> bool:
def same_path(p1: StrPath, p2: StrPath) -> bool:
"""Differs from os.path.samefile because it does not require paths to exist.
Purely string based (no comparison between i-nodes).
>>> same_path("a/b", "./a/b")
Expand All @@ -30,7 +33,7 @@ def same_path(p1: _Path, p2: _Path) -> bool:
return normpath(p1) == normpath(p2)


def normpath(filename: _Path) -> str:
def normpath(filename: StrPath) -> str:
"""Normalize a file/dir name for comparison purposes."""
# See pkg_resources.normalize_path for notes about cygwin
file = os.path.abspath(filename) if sys.platform == 'cygwin' else filename
Expand Down
25 changes: 14 additions & 11 deletions setuptools/command/editable_wheel.py
Expand Up @@ -33,7 +33,6 @@
Protocol,
Tuple,
TypeVar,
Union,
)

from .. import (
Expand All @@ -43,6 +42,7 @@
errors,
namespaces,
)
from .._path import StrPath
from ..discovery import find_package_path
from ..dist import Distribution
from ..warnings import (
Expand All @@ -55,8 +55,7 @@
if TYPE_CHECKING:
from wheel.wheelfile import WheelFile # noqa

_Path = Union[str, Path]
_P = TypeVar("_P", bound=_Path)
_P = TypeVar("_P", bound=StrPath)
_logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -181,7 +180,7 @@ def _find_egg_info_dir(self) -> Optional[str]:
return next(candidates, None)

def _configure_build(
self, name: str, unpacked_wheel: _Path, build_lib: _Path, tmp_dir: _Path
self, name: str, unpacked_wheel: StrPath, build_lib: StrPath, tmp_dir: StrPath
):
"""Configure commands to behave in the following ways:

Expand Down Expand Up @@ -256,7 +255,11 @@ def _collect_build_outputs(self) -> Tuple[List[str], Dict[str, str]]:
return files, mapping

def _run_build_commands(
self, dist_name: str, unpacked_wheel: _Path, build_lib: _Path, tmp_dir: _Path
self,
dist_name: str,
unpacked_wheel: StrPath,
build_lib: StrPath,
tmp_dir: StrPath,
) -> Tuple[List[str], Dict[str, str]]:
self._configure_build(dist_name, unpacked_wheel, build_lib, tmp_dir)
self._run_build_subcommands()
Expand Down Expand Up @@ -354,7 +357,7 @@ def _select_strategy(
self,
name: str,
tag: str,
build_lib: _Path,
build_lib: StrPath,
) -> "EditableStrategy":
"""Decides which strategy to use to implement an editable installation."""
build_name = f"__editable__.{name}-{tag}"
Expand Down Expand Up @@ -424,8 +427,8 @@ def __init__(
self,
dist: Distribution,
name: str,
auxiliary_dir: _Path,
build_lib: _Path,
auxiliary_dir: StrPath,
build_lib: StrPath,
):
self.auxiliary_dir = Path(auxiliary_dir)
self.build_lib = Path(build_lib).resolve()
Expand Down Expand Up @@ -567,7 +570,7 @@ def _can_symlink_files(base_dir: Path) -> bool:


def _simple_layout(
packages: Iterable[str], package_dir: Dict[str, str], project_dir: Path
packages: Iterable[str], package_dir: Dict[str, str], project_dir: StrPath
) -> bool:
"""Return ``True`` if:
- all packages are contained by the same parent directory, **and**
Expand Down Expand Up @@ -649,7 +652,7 @@ def _find_top_level_modules(dist: Distribution) -> Iterator[str]:
def _find_package_roots(
packages: Iterable[str],
package_dir: Mapping[str, str],
src_root: _Path,
src_root: StrPath,
) -> Dict[str, str]:
pkg_roots: Dict[str, str] = {
pkg: _absolute_root(find_package_path(pkg, package_dir, src_root))
Expand All @@ -659,7 +662,7 @@ def _find_package_roots(
return _remove_nested(pkg_roots)


def _absolute_root(path: _Path) -> str:
def _absolute_root(path: StrPath) -> str:
"""Works for packages and top-level modules"""
path_ = Path(path)
parent = path_.parent
Expand Down
19 changes: 9 additions & 10 deletions setuptools/config/_apply_pyprojecttoml.py
Expand Up @@ -29,7 +29,7 @@
Union,
cast,
)

from .._path import StrPath
from ..errors import RemovedConfigError
from ..warnings import SetuptoolsWarning

Expand All @@ -38,15 +38,14 @@
from setuptools.dist import Distribution # noqa

EMPTY: Mapping = MappingProxyType({}) # Immutable dict-like
_Path = Union[os.PathLike, str]
_DictOrStr = Union[dict, str]
_CorrespFn = Callable[["Distribution", Any, _Path], None]
_CorrespFn = Callable[["Distribution", Any, StrPath], None]
_Correspondence = Union[str, _CorrespFn]

_logger = logging.getLogger(__name__)


def apply(dist: "Distribution", config: dict, filename: _Path) -> "Distribution":
def apply(dist: "Distribution", config: dict, filename: StrPath) -> "Distribution":
"""Apply configuration dict read with :func:`read_configuration`"""

if not config:
Expand All @@ -68,7 +67,7 @@ def apply(dist: "Distribution", config: dict, filename: _Path) -> "Distribution"
return dist


def _apply_project_table(dist: "Distribution", config: dict, root_dir: _Path):
def _apply_project_table(dist: "Distribution", config: dict, root_dir: StrPath):
project_table = config.get("project", {}).copy()
if not project_table:
return # short-circuit
Expand All @@ -85,7 +84,7 @@ def _apply_project_table(dist: "Distribution", config: dict, root_dir: _Path):
_set_config(dist, corresp, value)


def _apply_tool_table(dist: "Distribution", config: dict, filename: _Path):
def _apply_tool_table(dist: "Distribution", config: dict, filename: StrPath):
tool_table = config.get("tool", {}).get("setuptools", {})
if not tool_table:
return # short-circuit
Expand Down Expand Up @@ -153,7 +152,7 @@ def _guess_content_type(file: str) -> Optional[str]:
raise ValueError(f"Undefined content type for {file}, {msg}")


def _long_description(dist: "Distribution", val: _DictOrStr, root_dir: _Path):
def _long_description(dist: "Distribution", val: _DictOrStr, root_dir: StrPath):
from setuptools.config import expand

if isinstance(val, str):
Expand All @@ -174,7 +173,7 @@ def _long_description(dist: "Distribution", val: _DictOrStr, root_dir: _Path):
dist._referenced_files.add(cast(str, file))


def _license(dist: "Distribution", val: dict, root_dir: _Path):
def _license(dist: "Distribution", val: dict, root_dir: StrPath):
from setuptools.config import expand

if "file" in val:
Expand All @@ -184,7 +183,7 @@ def _license(dist: "Distribution", val: dict, root_dir: _Path):
_set_config(dist, "license", val["text"])


def _people(dist: "Distribution", val: List[dict], _root_dir: _Path, kind: str):
def _people(dist: "Distribution", val: List[dict], _root_dir: StrPath, kind: str):
field = []
email_field = []
for person in val:
Expand Down Expand Up @@ -244,7 +243,7 @@ def _unify_entry_points(project_table: dict):
# intentional (for resetting configurations that are missing `dynamic`).


def _copy_command_options(pyproject: dict, dist: "Distribution", filename: _Path):
def _copy_command_options(pyproject: dict, dist: "Distribution", filename: StrPath):
tool_table = pyproject.get("tool", {})
cmdclass = tool_table.get("setuptools", {}).get("cmdclass", {})
valid_options = _valid_command_options(cmdclass)
Expand Down
31 changes: 15 additions & 16 deletions setuptools/config/expand.py
Expand Up @@ -46,7 +46,7 @@

from distutils.errors import DistutilsOptionError

from .._path import same_path as _same_path
from .._path import same_path as _same_path, StrPath
from ..warnings import SetuptoolsWarning

if TYPE_CHECKING:
Expand All @@ -55,7 +55,6 @@
from distutils.dist import DistributionMetadata # noqa

chain_iter = chain.from_iterable
_Path = Union[str, os.PathLike]
_K = TypeVar("_K")
_V = TypeVar("_V", covariant=True)

Expand Down Expand Up @@ -88,7 +87,7 @@ def __getattr__(self, attr):


def glob_relative(
patterns: Iterable[str], root_dir: Optional[_Path] = None
patterns: Iterable[str], root_dir: Optional[StrPath] = None
) -> List[str]:
"""Expand the list of glob patterns, but preserving relative paths.

Expand Down Expand Up @@ -120,7 +119,7 @@ def glob_relative(
return expanded_values


def read_files(filepaths: Union[str, bytes, Iterable[_Path]], root_dir=None) -> str:
def read_files(filepaths: Union[str, bytes, Iterable[StrPath]], root_dir=None) -> str:
"""Return the content of the files concatenated using ``\n`` as str

This function is sandboxed and won't reach anything outside ``root_dir``
Expand All @@ -138,20 +137,20 @@ def read_files(filepaths: Union[str, bytes, Iterable[_Path]], root_dir=None) ->
)


def _filter_existing_files(filepaths: Iterable[_Path]) -> Iterator[_Path]:
def _filter_existing_files(filepaths: Iterable[StrPath]) -> Iterator[StrPath]:
for path in filepaths:
if os.path.isfile(path):
yield path
else:
SetuptoolsWarning.emit(f"File {path!r} cannot be found")


def _read_file(filepath: Union[bytes, _Path]) -> str:
def _read_file(filepath: Union[bytes, StrPath]) -> str:
with open(filepath, encoding='utf-8') as f:
return f.read()


def _assert_local(filepath: _Path, root_dir: str):
def _assert_local(filepath: StrPath, root_dir: str):
if Path(os.path.abspath(root_dir)) not in Path(os.path.abspath(filepath)).parents:
msg = f"Cannot access {filepath!r} (or anything outside {root_dir!r})"
raise DistutilsOptionError(msg)
Expand All @@ -162,7 +161,7 @@ def _assert_local(filepath: _Path, root_dir: str):
def read_attr(
attr_desc: str,
package_dir: Optional[Mapping[str, str]] = None,
root_dir: Optional[_Path] = None,
root_dir: Optional[StrPath] = None,
):
"""Reads the value of an attribute from a module.

Expand Down Expand Up @@ -197,7 +196,7 @@ def read_attr(
return getattr(module, attr_name)


def _find_spec(module_name: str, module_path: Optional[_Path]) -> ModuleSpec:
def _find_spec(module_name: str, module_path: Optional[StrPath]) -> ModuleSpec:
spec = importlib.util.spec_from_file_location(module_name, module_path)
spec = spec or importlib.util.find_spec(module_name)

Expand All @@ -218,8 +217,8 @@ def _load_spec(spec: ModuleSpec, module_name: str) -> ModuleType:


def _find_module(
module_name: str, package_dir: Optional[Mapping[str, str]], root_dir: _Path
) -> Tuple[_Path, Optional[str], str]:
module_name: str, package_dir: Optional[Mapping[str, str]], root_dir: StrPath
) -> Tuple[StrPath, Optional[str], str]:
"""Given a module (that could normally be imported by ``module_name``
after the build is complete), find the path to the parent directory where
it is contained and the canonical name that could be used to import it
Expand Down Expand Up @@ -254,7 +253,7 @@ def _find_module(
def resolve_class(
qualified_class_name: str,
package_dir: Optional[Mapping[str, str]] = None,
root_dir: Optional[_Path] = None,
root_dir: Optional[StrPath] = None,
) -> Callable:
"""Given a qualified class name, return the associated class object"""
root_dir = root_dir or os.getcwd()
Expand All @@ -270,7 +269,7 @@ def resolve_class(
def cmdclass(
values: Dict[str, str],
package_dir: Optional[Mapping[str, str]] = None,
root_dir: Optional[_Path] = None,
root_dir: Optional[StrPath] = None,
) -> Dict[str, Callable]:
"""Given a dictionary mapping command names to strings for qualified class
names, apply :func:`resolve_class` to the dict values.
Expand All @@ -282,7 +281,7 @@ def find_packages(
*,
namespaces=True,
fill_package_dir: Optional[Dict[str, str]] = None,
root_dir: Optional[_Path] = None,
root_dir: Optional[StrPath] = None,
**kwargs,
) -> List[str]:
"""Works similarly to :func:`setuptools.find_packages`, but with all
Expand Down Expand Up @@ -331,7 +330,7 @@ def find_packages(
return packages


def _nest_path(parent: _Path, path: _Path) -> str:
def _nest_path(parent: StrPath, path: StrPath) -> str:
path = parent if path in {".", ""} else os.path.join(parent, path)
return os.path.normpath(path)

Expand Down Expand Up @@ -361,7 +360,7 @@ def canonic_package_data(package_data: dict) -> dict:


def canonic_data_files(
data_files: Union[list, dict], root_dir: Optional[_Path] = None
data_files: Union[list, dict], root_dir: Optional[StrPath] = None
) -> List[Tuple[str, List[str]]]:
"""For compatibility with ``setup.py``, ``data_files`` should be a list
of pairs instead of a dict.
Expand Down