Skip to content

Commit d9e413e

Browse files
committed
Generate distutils-stubs on install
1 parent 0cffd61 commit d9e413e

15 files changed

+44
-34
lines changed

Diff for: mypy.ini

+1-9
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
strict = False
66

77
# Early opt-in even when strict = False
8-
# warn_unused_ignores = True # Disabled until we have distutils stubs for Python 3.12+
8+
# warn_unused_ignores = True # Disabled as long as there's inconsistent typing issues between pypa and stdlib's distutils
99
warn_redundant_casts = True
1010
enable_error_code = ignore-without-code
1111

@@ -48,14 +48,6 @@ disable_error_code =
4848
[mypy-pkg_resources.tests.*]
4949
disable_error_code = import-not-found
5050

51-
# - distutils doesn't exist on Python 3.12, unfortunately, this means typing
52-
# will be missing for subclasses of distutils on Python 3.12 until either:
53-
# - support for `SETUPTOOLS_USE_DISTUTILS=stdlib` is dropped (#3625)
54-
# for setuptools to import `_distutils` directly
55-
# - or non-stdlib distutils typings are exposed
56-
[mypy-distutils.*]
57-
ignore_missing_imports = True
58-
5951
# - wheel: does not intend on exposing a programmatic API https://github.com/pypa/wheel/pull/610#issuecomment-2081687671
6052
[mypy-wheel.*]
6153
follow_untyped_imports = True

Diff for: newsfragments/4861.feature.rst

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
``setuptools`` now provide its own ``distutils-stubs`` instead of relying on typeshed -- by :user:`Avasam`

Diff for: pyrightconfig.json

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
],
1313
// Our testing setup doesn't allow passing CLI arguments, so local devs have to set this manually.
1414
// "pythonVersion": "3.9",
15+
// Allow using distutils-stubs on Python 3.12+
16+
"reportMissingModuleSource": false,
1517
// For now we don't mind if mypy's `type: ignore` comments accidentally suppresses pyright issues
1618
"enableTypeIgnoreComments": true,
1719
"typeCheckingMode": "basic",

Diff for: setup.py

+21
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import os
44
import sys
55
import textwrap
6+
from pathlib import Path
67

78
import setuptools
89
from setuptools.command.install import install
@@ -37,6 +38,22 @@ def pypi_link(pkg_filename):
3738
return '/'.join(parts)
3839

3940

41+
vendored_distutils_path = Path(here) / "setuptools" / "_distutils"
42+
43+
44+
def generate_distutils_stubs(destination: Path) -> None:
45+
for path in vendored_distutils_path.rglob("*.py"):
46+
relative_path = path.relative_to(vendored_distutils_path)
47+
if relative_path.parts[0] == "tests":
48+
continue
49+
stub_path = (destination / relative_path).with_suffix(".pyi")
50+
stub_path.parent.mkdir(parents=True, exist_ok=True)
51+
module = "setuptools._distutils." + str(relative_path.with_suffix("")).replace(
52+
os.sep, "."
53+
).removesuffix(".__init__")
54+
stub_path.write_text(f"from {module} import *\n")
55+
56+
4057
class install_with_pth(install):
4158
"""
4259
Custom install command to install a .pth file for distutils patching.
@@ -68,6 +85,10 @@ def initialize_options(self):
6885
install.initialize_options(self)
6986
self.extra_path = self._pth_name, self._pth_contents
7087

88+
def run(self):
89+
install.run(self)
90+
generate_distutils_stubs(Path(self.install_lib) / 'distutils-stubs')
91+
7192
def finalize_options(self):
7293
install.finalize_options(self)
7394
self._restore_install_lib()

Diff for: setuptools/__init__.py

+2-7
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,4 @@
11
"""Extensions to the 'distutils' for large or complex distributions"""
2-
# mypy: disable_error_code=override
3-
# Command.reinitialize_command has an extra **kw param that distutils doesn't have
4-
# Can't disable on the exact line because distutils doesn't exists on Python 3.12
5-
# and mypy isn't aware of distutils_hack, causing distutils.core.Command to be Any,
6-
# and a [unused-ignore] to be raised on 3.12+
72

83
from __future__ import annotations
94

@@ -211,7 +206,7 @@ def ensure_string_list(self, option: str) -> None:
211206
f"'{option}' must be a list of strings (got {val!r})"
212207
)
213208

214-
@overload
209+
@overload # type: ignore[override] # extra **kw param that distutils doesn't have
215210
def reinitialize_command(
216211
self, command: str, reinit_subcommands: bool = False, **kw
217212
) -> _Command: ...
@@ -224,7 +219,7 @@ def reinitialize_command(
224219
) -> _Command:
225220
cmd = _Command.reinitialize_command(self, command, reinit_subcommands)
226221
vars(cmd).update(kw)
227-
return cmd # pyright: ignore[reportReturnType] # pypa/distutils#307
222+
return cmd
228223

229224
@abstractmethod
230225
def initialize_options(self) -> None:

Diff for: setuptools/build_meta.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -91,11 +91,11 @@ def patch(cls):
9191
for the duration of this context.
9292
"""
9393
orig = distutils.core.Distribution
94-
distutils.core.Distribution = cls # type: ignore[misc] # monkeypatching
94+
distutils.core.Distribution = cls
9595
try:
9696
yield
9797
finally:
98-
distutils.core.Distribution = orig # type: ignore[misc] # monkeypatching
98+
distutils.core.Distribution = orig
9999

100100

101101
@contextlib.contextmanager

Diff for: setuptools/command/build_ext.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ def setup_shlib_compiler(self):
248248
compiler.set_link_objects(self.link_objects)
249249

250250
# hack so distutils' build_extension() builds a library instead
251-
compiler.link_shared_object = link_shared_object.__get__(compiler) # type: ignore[method-assign]
251+
compiler.link_shared_object = link_shared_object.__get__(compiler)
252252

253253
def get_export_symbols(self, ext):
254254
if isinstance(ext, Library):

Diff for: setuptools/command/sdist.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ class sdist(orig.sdist):
5050
]
5151

5252
distribution: Distribution # override distutils.dist.Distribution with setuptools.dist.Distribution
53-
negative_opt: ClassVar[dict[str, str]] = {}
53+
negative_opt: ClassVar[dict[str, str]] = {} # type: ignore[misc] # TODO: Fix upstream
5454

5555
README_EXTENSIONS = ['', '.rst', '.txt', '.md']
5656
READMES = tuple(f'README{ext}' for ext in README_EXTENSIONS)

Diff for: setuptools/command/setopt.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def edit_config(filename, settings, dry_run=False):
3737
"""
3838
log.debug("Reading configuration from %s", filename)
3939
opts = configparser.RawConfigParser()
40-
opts.optionxform = lambda optionstr: optionstr # type: ignore[method-assign] # overriding method
40+
opts.optionxform = lambda optionstr: optionstr
4141
_cfg_read_utf8_with_fallback(opts, filename)
4242

4343
for section, options in settings.items():

Diff for: setuptools/config/setupcfg.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from collections import defaultdict
1818
from collections.abc import Iterable, Iterator
1919
from functools import partial, wraps
20-
from typing import TYPE_CHECKING, Any, Callable, ClassVar, Generic, TypeVar, cast
20+
from typing import TYPE_CHECKING, Any, Callable, ClassVar, Generic, TypeVar
2121

2222
from packaging.markers import default_environment as marker_env
2323
from packaging.requirements import InvalidRequirement, Requirement
@@ -101,8 +101,7 @@ def _apply(
101101
filenames = [*other_files, filepath]
102102

103103
try:
104-
# TODO: Temporary cast until mypy 1.12 is released with upstream fixes from typeshed
105-
_Distribution.parse_config_files(dist, filenames=cast(list[str], filenames))
104+
_Distribution.parse_config_files(dist, filenames=filenames) # type: ignore[arg-type] # Vendored version of distutils supports PathLike
106105
handlers = parse_configuration(
107106
dist, dist.command_options, ignore_option_errors=ignore_option_errors
108107
)

Diff for: setuptools/errors.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,15 @@
3030
BaseError = _distutils_errors.DistutilsError
3131

3232

33-
class InvalidConfigError(OptionError): # type: ignore[valid-type, misc] # distutils imports are `Any` on python 3.12+
33+
class InvalidConfigError(OptionError):
3434
"""Error used for invalid configurations."""
3535

3636

37-
class RemovedConfigError(OptionError): # type: ignore[valid-type, misc] # distutils imports are `Any` on python 3.12+
37+
class RemovedConfigError(OptionError):
3838
"""Error used for configurations that were deprecated and removed."""
3939

4040

41-
class RemovedCommandError(BaseError, RuntimeError): # type: ignore[valid-type, misc] # distutils imports are `Any` on python 3.12+
41+
class RemovedCommandError(BaseError, RuntimeError):
4242
"""Error used for commands that have been removed in setuptools.
4343
4444
Since ``setuptools`` is built on ``distutils``, simply removing a command
@@ -48,7 +48,7 @@ class RemovedCommandError(BaseError, RuntimeError): # type: ignore[valid-type,
4848
"""
4949

5050

51-
class PackageDiscoveryError(BaseError, RuntimeError): # type: ignore[valid-type, misc] # distutils imports are `Any` on python 3.12+
51+
class PackageDiscoveryError(BaseError, RuntimeError):
5252
"""Impossible to perform automatic discovery of packages and/or modules.
5353
5454
The current project layout or given discovery options can lead to problems when

Diff for: setuptools/extension.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ def __init__(
153153
self.py_limited_api = py_limited_api
154154
super().__init__(
155155
name,
156-
sources, # type: ignore[arg-type] # Vendored version of setuptools supports PathLike
156+
sources, # type: ignore[arg-type] # Vendored version of distutils supports PathLike
157157
*args,
158158
**kw,
159159
)

Diff for: setuptools/logging.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def configure() -> None:
3232
# and then loaded again when patched,
3333
# implying: id(distutils.log) != id(distutils.dist.log).
3434
# Make sure the same module object is used everywhere:
35-
distutils.dist.log = distutils.log
35+
distutils.dist.log = distutils.log # type: ignore[assignment]
3636

3737

3838
def set_threshold(level: int) -> int:

Diff for: setuptools/modified.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
try:
22
# Ensure a DistutilsError raised by these methods is the same as distutils.errors.DistutilsError
3-
from distutils._modified import (
3+
from distutils._modified import ( # type: ignore[import-not-found]
44
newer,
55
newer_group,
66
newer_pairwise,

Diff for: setuptools/monkey.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ def patch_all():
7373
import setuptools
7474

7575
# we can't patch distutils.cmd, alas
76-
distutils.core.Command = setuptools.Command # type: ignore[misc,assignment] # monkeypatching
76+
distutils.core.Command = setuptools.Command
7777

7878
_patch_distribution_metadata()
7979

@@ -82,8 +82,8 @@ def patch_all():
8282
module.Distribution = setuptools.dist.Distribution
8383

8484
# Install the patched Extension
85-
distutils.core.Extension = setuptools.extension.Extension # type: ignore[misc,assignment] # monkeypatching
86-
distutils.extension.Extension = setuptools.extension.Extension # type: ignore[misc,assignment] # monkeypatching
85+
distutils.core.Extension = setuptools.extension.Extension
86+
distutils.extension.Extension = setuptools.extension.Extension
8787
if 'distutils.command.build_ext' in sys.modules:
8888
sys.modules[
8989
'distutils.command.build_ext'

0 commit comments

Comments
 (0)