|
8 | 8 | import os |
9 | 9 | import warnings |
10 | 10 | from collections.abc import Iterable |
| 11 | +from dataclasses import dataclass, field, fields |
| 12 | +from typing import TYPE_CHECKING |
11 | 13 |
|
12 | 14 | # This class is really only used by the "build_ext" command, so it might |
13 | 15 | # make sense to put it in distutils.command.build_ext. However, that |
|
20 | 22 | # order to do anything. |
21 | 23 |
|
22 | 24 |
|
23 | | -class Extension: |
| 25 | +@dataclass |
| 26 | +class _Extension: |
24 | 27 | """Just a collection of attributes that describes an extension |
25 | 28 | module and everything needed to build it (hopefully in a portable |
26 | 29 | 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 | + """ |
27 | 115 |
|
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) |
88 | 117 | """ |
| 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)}") |
89 | 173 |
|
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) |
154 | 178 |
|
155 | 179 |
|
156 | 180 | def read_setup_file(filename): # noqa: C901 |
|
0 commit comments