Skip to content

Commit 9fab878

Browse files
committed
Use dataclass for typing Extension
1 parent 603b94e commit 9fab878

File tree

3 files changed

+151
-145
lines changed

3 files changed

+151
-145
lines changed

distutils/core.py

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
DistutilsSetupError,
2626
)
2727
from .extension import Extension
28+
from .extension import _safe as extension_keywords # noqa # backwards compatibility
2829

2930
__all__ = ['Distribution', 'Command', 'Extension', 'setup']
3031

@@ -74,25 +75,6 @@ def gen_usage(script_name):
7475
'obsoletes',
7576
)
7677

77-
# Legal keyword arguments for the Extension constructor
78-
extension_keywords = (
79-
'name',
80-
'sources',
81-
'include_dirs',
82-
'define_macros',
83-
'undef_macros',
84-
'library_dirs',
85-
'libraries',
86-
'runtime_library_dirs',
87-
'extra_objects',
88-
'extra_compile_args',
89-
'extra_link_args',
90-
'swig_opts',
91-
'export_symbols',
92-
'depends',
93-
'language',
94-
)
95-
9678

9779
def setup(**attrs): # noqa: C901
9880
"""The gateway to the Distutils: do everything your setup script needs

distutils/extension.py

Lines changed: 149 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import os
99
import warnings
1010
from collections.abc import Iterable
11+
from dataclasses import dataclass, field, fields
12+
from typing import TYPE_CHECKING
1113

1214
# This class is really only used by the "build_ext" command, so it might
1315
# make sense to put it in distutils.command.build_ext. However, that
@@ -20,137 +22,159 @@
2022
# order to do anything.
2123

2224

23-
class Extension:
25+
@dataclass
26+
class _Extension:
2427
"""Just a collection of attributes that describes an extension
2528
module and everything needed to build it (hopefully in a portable
2629
way, but there are hooks that let you be as unportable as you need).
30+
"""
31+
32+
# The use of a parent class as a "trick":
33+
# - We need to modify __init__ so to achieve backwards compatibility
34+
# - But we don't want to throw away the dataclass-generated __init__
35+
# - We also want to fool the typechecker to consider the same type
36+
# signature as the dataclass-generated __init__
37+
38+
name: str
39+
"""
40+
the full name of the extension, including any packages -- ie.
41+
*not* a filename or pathname, but Python dotted name
42+
"""
43+
44+
sources: Iterable[str | os.PathLike[str]]
45+
"""
46+
iterable of source filenames (except strings, which could be misinterpreted
47+
as a single filename), relative to the distribution root (where the setup
48+
script lives), in Unix form (slash-separated) for portability. Can be any
49+
non-string iterable (list, tuple, set, etc.) containing strings or
50+
PathLike objects. Source files may be C, C++, SWIG (.i), platform-specific
51+
resource files, or whatever else is recognized by the "build_ext" command
52+
as source for a Python extension.
53+
"""
54+
55+
include_dirs: list[str] = field(default_factory=list)
56+
"""
57+
list of directories to search for C/C++ header files (in Unix
58+
form for portability)
59+
"""
60+
61+
define_macros: list[tuple[str, str | None]] = field(default_factory=list)
62+
"""
63+
list of macros to define; each macro is defined using a 2-tuple,
64+
where 'value' is either the string to define it to or None to
65+
define it without a particular value (equivalent of "#define
66+
FOO" in source or -DFOO on Unix C compiler command line)
67+
"""
68+
69+
undef_macros: list[str] = field(default_factory=list)
70+
"""list of macros to undefine explicitly"""
71+
72+
library_dirs: list[str] = field(default_factory=list)
73+
"""list of directories to search for C/C++ libraries at link time"""
74+
75+
libraries: list[str] = field(default_factory=list)
76+
"""list of library names (not filenames or paths) to link against"""
77+
78+
runtime_library_dirs: list[str] = field(default_factory=list)
79+
"""
80+
list of directories to search for C/C++ libraries at run time
81+
(for shared extensions, this is when the extension is loaded)
82+
"""
83+
84+
extra_objects: list[str] = field(default_factory=list)
85+
"""
86+
list of extra files to link with (eg. object files not implied
87+
by 'sources', static library that must be explicitly specified,
88+
binary resource files, etc.)
89+
"""
90+
91+
extra_compile_args: list[str] = field(default_factory=list)
92+
"""
93+
any extra platform- and compiler-specific information to use
94+
when compiling the source files in 'sources'. For platforms and
95+
compilers where "command line" makes sense, this is typically a
96+
list of command-line arguments, but for other platforms it could
97+
be anything.
98+
"""
99+
100+
extra_link_args: list[str] = field(default_factory=list)
101+
"""
102+
any extra platform- and compiler-specific information to use
103+
when linking object files together to create the extension (or
104+
to create a new static Python interpreter). Similar
105+
interpretation as for 'extra_compile_args'.
106+
"""
107+
108+
export_symbols: list[str] = field(default_factory=list)
109+
"""
110+
list of symbols to be exported from a shared extension. Not
111+
used on all platforms, and not generally necessary for Python
112+
extensions, which typically export exactly one symbol: "init" +
113+
extension_name.
114+
"""
27115

28-
Instance attributes:
29-
name : string
30-
the full name of the extension, including any packages -- ie.
31-
*not* a filename or pathname, but Python dotted name
32-
sources : Iterable[string | os.PathLike]
33-
iterable of source filenames (except strings, which could be misinterpreted
34-
as a single filename), relative to the distribution root (where the setup
35-
script lives), in Unix form (slash-separated) for portability. Can be any
36-
non-string iterable (list, tuple, set, etc.) containing strings or
37-
PathLike objects. Source files may be C, C++, SWIG (.i), platform-specific
38-
resource files, or whatever else is recognized by the "build_ext" command
39-
as source for a Python extension.
40-
include_dirs : [string]
41-
list of directories to search for C/C++ header files (in Unix
42-
form for portability)
43-
define_macros : [(name : string, value : string|None)]
44-
list of macros to define; each macro is defined using a 2-tuple,
45-
where 'value' is either the string to define it to or None to
46-
define it without a particular value (equivalent of "#define
47-
FOO" in source or -DFOO on Unix C compiler command line)
48-
undef_macros : [string]
49-
list of macros to undefine explicitly
50-
library_dirs : [string]
51-
list of directories to search for C/C++ libraries at link time
52-
libraries : [string]
53-
list of library names (not filenames or paths) to link against
54-
runtime_library_dirs : [string]
55-
list of directories to search for C/C++ libraries at run time
56-
(for shared extensions, this is when the extension is loaded)
57-
extra_objects : [string]
58-
list of extra files to link with (eg. object files not implied
59-
by 'sources', static library that must be explicitly specified,
60-
binary resource files, etc.)
61-
extra_compile_args : [string]
62-
any extra platform- and compiler-specific information to use
63-
when compiling the source files in 'sources'. For platforms and
64-
compilers where "command line" makes sense, this is typically a
65-
list of command-line arguments, but for other platforms it could
66-
be anything.
67-
extra_link_args : [string]
68-
any extra platform- and compiler-specific information to use
69-
when linking object files together to create the extension (or
70-
to create a new static Python interpreter). Similar
71-
interpretation as for 'extra_compile_args'.
72-
export_symbols : [string]
73-
list of symbols to be exported from a shared extension. Not
74-
used on all platforms, and not generally necessary for Python
75-
extensions, which typically export exactly one symbol: "init" +
76-
extension_name.
77-
swig_opts : [string]
78-
any extra options to pass to SWIG if a source file has the .i
79-
extension.
80-
depends : [string]
81-
list of files that the extension depends on
82-
language : string
83-
extension language (i.e. "c", "c++", "objc"). Will be detected
84-
from the source extensions if not provided.
85-
optional : boolean
86-
specifies that a build failure in the extension should not abort the
87-
build process, but simply not install the failing extension.
116+
swig_opts: list[str] = field(default_factory=list)
88117
"""
118+
any extra options to pass to SWIG if a source file has the .i
119+
extension.
120+
"""
121+
122+
depends: list[str] = field(default_factory=list)
123+
"""list of files that the extension depends on"""
124+
125+
language: str | None = None
126+
"""
127+
extension language (i.e. "c", "c++", "objc"). Will be detected
128+
from the source extensions if not provided.
129+
"""
130+
131+
optional: bool = False
132+
"""
133+
specifies that a build failure in the extension should not abort the
134+
build process, but simply not install the failing extension.
135+
"""
136+
137+
138+
# Legal keyword arguments for the Extension constructor
139+
_safe = tuple(f.name for f in fields(_Extension))
140+
141+
142+
if TYPE_CHECKING:
143+
144+
@dataclass
145+
class Extension(_Extension):
146+
pass
147+
148+
else:
149+
150+
@dataclass(init=False)
151+
class Extension(_Extension):
152+
def __init__(self, name, sources, *args, **kwargs):
153+
if not isinstance(name, str):
154+
raise TypeError("'name' must be a string")
155+
156+
# handle the string case first; since strings are iterable, disallow them
157+
if isinstance(sources, str):
158+
raise TypeError(
159+
"'sources' must be an iterable of strings or PathLike objects, not a string"
160+
)
161+
162+
# now we check if it's iterable and contains valid types
163+
try:
164+
sources = list(map(os.fspath, sources))
165+
except TypeError:
166+
raise TypeError(
167+
"'sources' must be an iterable of strings or PathLike objects"
168+
)
169+
170+
extra = {repr(k): kwargs.pop(k) for k in tuple(kwargs) if k not in _safe}
171+
if extra:
172+
warnings.warn(f"Unknown Extension options: {','.join(extra)}")
89173

90-
# When adding arguments to this constructor, be sure to update
91-
# setup_keywords in core.py.
92-
def __init__(
93-
self,
94-
name: str,
95-
sources: Iterable[str | os.PathLike[str]],
96-
include_dirs: list[str] | None = None,
97-
define_macros: list[tuple[str, str | None]] | None = None,
98-
undef_macros: list[str] | None = None,
99-
library_dirs: list[str] | None = None,
100-
libraries: list[str] | None = None,
101-
runtime_library_dirs: list[str] | None = None,
102-
extra_objects: list[str] | None = None,
103-
extra_compile_args: list[str] | None = None,
104-
extra_link_args: list[str] | None = None,
105-
export_symbols: list[str] | None = None,
106-
swig_opts: list[str] | None = None,
107-
depends: list[str] | None = None,
108-
language: str | None = None,
109-
optional: bool | None = None,
110-
**kw, # To catch unknown keywords
111-
):
112-
if not isinstance(name, str):
113-
raise TypeError("'name' must be a string")
114-
115-
# handle the string case first; since strings are iterable, disallow them
116-
if isinstance(sources, str):
117-
raise TypeError(
118-
"'sources' must be an iterable of strings or PathLike objects, not a string"
119-
)
120-
121-
# now we check if it's iterable and contains valid types
122-
try:
123-
self.sources = list(map(os.fspath, sources))
124-
except TypeError:
125-
raise TypeError(
126-
"'sources' must be an iterable of strings or PathLike objects"
127-
)
128-
129-
self.name = name
130-
self.include_dirs = include_dirs or []
131-
self.define_macros = define_macros or []
132-
self.undef_macros = undef_macros or []
133-
self.library_dirs = library_dirs or []
134-
self.libraries = libraries or []
135-
self.runtime_library_dirs = runtime_library_dirs or []
136-
self.extra_objects = extra_objects or []
137-
self.extra_compile_args = extra_compile_args or []
138-
self.extra_link_args = extra_link_args or []
139-
self.export_symbols = export_symbols or []
140-
self.swig_opts = swig_opts or []
141-
self.depends = depends or []
142-
self.language = language
143-
self.optional = optional
144-
145-
# If there are unknown keyword options, warn about them
146-
if len(kw) > 0:
147-
options = [repr(option) for option in kw]
148-
options = ', '.join(sorted(options))
149-
msg = f"Unknown Extension options: {options}"
150-
warnings.warn(msg)
151-
152-
def __repr__(self):
153-
return f'<{self.__class__.__module__}.{self.__class__.__qualname__}({self.name!r}) at {id(self):#x}>'
174+
# Ensure default values (e.g. []) are used instead of None:
175+
positional = {k: v for k, v in zip(_safe[2:], args) if v is not None}
176+
keywords = {k: v for k, v in kwargs.items() if v is not None}
177+
super().__init__(name, sources, **positional, **keywords)
154178

155179

156180
def read_setup_file(filename): # noqa: C901

distutils/tests/test_extension.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ def test_extension_init(self):
106106
assert getattr(ext, attr) == []
107107

108108
assert ext.language is None
109-
assert ext.optional is None
109+
assert ext.optional is False
110110

111111
# if there are unknown keyword options, warn about them
112112
with check_warnings() as w:

0 commit comments

Comments
 (0)