Skip to content

Commit 00db9e8

Browse files
gh-140824: Build the math module as an extension package
Build math as an extension package (math/__init__ and math/integer) so that math.integer is a genuine extension submodule instead of the _math_integer extension module patched at runtime to look like one. This drops the sys.modules['_math_integer'] leak and gives math.integer's functions a correct __module__. Modules/makesetup now accepts dotted module names, mapping each to its registration name, on-disk path and init symbol; a package initializer is written with an explicit __init__ leaf (math.__init__). Makefile.pre.in keeps sub-directories when staging and installing shared modules, and check_extension_modules.py and generate_stdlib_module_names.py handle package and dotted submodule names. The package works both for shared builds and for builds where math is a builtin (Windows, WebAssembly): math's exec slot sets __path__ when the import machinery does not, and the static inittab uses a symbol derived from the full dotted name (PyInit_math_integer) to stay unique, selected with Py_BUILD_CORE_BUILTIN. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 5a549e8 commit 00db9e8

13 files changed

Lines changed: 173 additions & 52 deletions

Makefile.pre.in

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1525,7 +1525,10 @@ sharedmods: $(SHAREDMODS) pybuilddir.txt
15251525
$(MKDIR_P) $$target; \
15261526
for mod in X $(SHAREDMODS); do \
15271527
if test $$mod != X; then \
1528-
$(LN) -sf ../../$$mod $$target/`basename $$mod`; \
1528+
rel=`echo $$mod | sed 's,^Modules/,,'`; \
1529+
dest=$$target/$$rel; \
1530+
$(MKDIR_P) `dirname $$dest`; \
1531+
$(LN) -sf `pwd`/$$mod $$dest; \
15291532
fi; \
15301533
done
15311534

@@ -2394,11 +2397,13 @@ sharedinstall: all
23942397
done
23952398
@for i in X $(SHAREDMODS); do \
23962399
if test $$i != X; then \
2397-
echo $(INSTALL_SHARED) $$i $(DESTSHARED)/`basename $$i`; \
2398-
$(INSTALL_SHARED) $$i $(DESTDIR)$(DESTSHARED)/`basename $$i`; \
2400+
rel=`echo $$i | sed 's,^Modules/,,'`; \
2401+
$(INSTALL) -d -m $(DIRMODE) $(DESTDIR)$(DESTSHARED)/`dirname $$rel`; \
2402+
echo $(INSTALL_SHARED) $$i $(DESTSHARED)/$$rel; \
2403+
$(INSTALL_SHARED) $$i $(DESTDIR)$(DESTSHARED)/$$rel; \
23992404
if test -d "$$i.dSYM"; then \
2400-
echo $(DSYMUTIL_PATH) $(DESTDIR)$(DESTSHARED)/`basename $$i`; \
2401-
$(DSYMUTIL_PATH) $(DESTDIR)$(DESTSHARED)/`basename $$i`; \
2405+
echo $(DSYMUTIL_PATH) $(DESTDIR)$(DESTSHARED)/$$rel; \
2406+
$(DSYMUTIL_PATH) $(DESTDIR)$(DESTSHARED)/$$rel; \
24022407
fi; \
24032408
fi; \
24042409
done
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
The :mod:`math` module is now built as an extension *package*:
2+
``math/__init__`` and ``math/integer``. :mod:`math.integer` is therefore a
3+
real extension submodule instead of the ``_math_integer`` extension module
4+
disguised as one. :file:`Modules/makesetup` now accepts dotted module names
5+
(a package initializer is written with an explicit ``__init__`` leaf, e.g.
6+
``math.__init__``) and maps each to a sub-directory path and the matching
7+
``PyInit_`` symbol. This works both for the usual shared build and for
8+
builds where ``math`` is compiled into the interpreter as a builtin module
9+
(Windows and WebAssembly).

Modules/Setup

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,8 +155,8 @@ PYTHONPATH=$(COREPYTHONPATH)
155155
#array arraymodule.c
156156
#binascii binascii.c
157157
#cmath cmathmodule.c
158-
#math mathmodule.c
159-
#_math_integer mathintegermodule.c
158+
#math.__init__ mathmodule.c
159+
#math.integer mathintegermodule.c
160160
#mmap mmapmodule.c
161161
#select selectmodule.c
162162
#_sysconfig _sysconfig.c

Modules/Setup.stdlib.in

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
@MODULE__HEAPQ_TRUE@_heapq _heapqmodule.c
3838
@MODULE__JSON_TRUE@_json _json.c
3939
@MODULE__LSPROF_TRUE@_lsprof _lsprof.c rotatingtree.c
40-
@MODULE__MATH_INTEGER_TRUE@_math_integer mathintegermodule.c
40+
@MODULE_MATH_INTEGER_TRUE@math.integer mathintegermodule.c
4141
@MODULE__PICKLE_TRUE@_pickle _pickle.c
4242
@MODULE__QUEUE_TRUE@_queue _queuemodule.c
4343
@MODULE__RANDOM_TRUE@_random _randommodule.c
@@ -52,7 +52,7 @@
5252
@MODULE__ZONEINFO_TRUE@_zoneinfo _zoneinfo.c
5353

5454
# needs libm
55-
@MODULE_MATH_TRUE@math mathmodule.c
55+
@MODULE_MATH_TRUE@math.__init__ mathmodule.c
5656
@MODULE_CMATH_TRUE@cmath cmathmodule.c
5757
@MODULE__STATISTICS_TRUE@_statistics _statisticsmodule.c
5858

Modules/makesetup

Lines changed: 59 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,32 @@ esac
8787
NL='\
8888
'
8989

90+
# Map a module name as written in Setup to its three distinct attributes.
91+
# Submodules use dotted names (math.integer); a package's initializer is
92+
# written with an explicit __init__ leaf (math.__init__).
93+
# mod_regname: import/registration name math.integer -> math.integer
94+
# math.__init__ -> math
95+
# mod_path: path under srcdir (no ext) math.integer -> math/integer
96+
# math.__init__ -> math/__init__
97+
# mod_sym: static PyInit_<sym> symbol math.integer -> math_integer
98+
# math.__init__ -> math
99+
# mod_sym (the static inittab symbol) uses the full dotted name so it stays
100+
# unique across packages; shared submodules instead export
101+
# PyInit_<last component> (see Py_BUILD_CORE_BUILTIN).
102+
mod_regname() {
103+
case $1 in
104+
*.__init__) echo "${1%.__init__}";;
105+
*) echo "$1";;
106+
esac
107+
}
108+
mod_path() {
109+
echo "$1" | tr '.' '/'
110+
}
111+
mod_sym() {
112+
reg=`mod_regname "$1"`
113+
echo "$reg" | tr '.' '_'
114+
}
115+
90116
# Main loop
91117
for i in ${*-Setup}
92118
do
@@ -183,21 +209,35 @@ sed -e 's/[ ]*#.*//' -e '/^[ ]*$/d' |
183209
\$\(*_RPATH\)) libs="$libs $arg";;
184210
\$*) libs="$libs $arg"
185211
cpps="$cpps $arg";;
186-
*.*) echo 1>&2 "bad word $arg in $line"
187-
exit 1;;
188212
-u) skip=libs; libs="$libs -u";;
189213
[a-zA-Z_]*)
190-
mods="$mods $arg"
191-
mods_upper=$(echo $mods | tr '[a-z]' '[A-Z]');;
214+
# Module name, possibly dotted (math.integer) or a
215+
# package initializer (math.__init__). Only identifier
216+
# characters and dots are allowed.
217+
case $arg in
218+
*[!a-zA-Z0-9_.]*)
219+
echo 1>&2 "bad word $arg in $line"
220+
exit 1;;
221+
esac
222+
mods="$mods $arg";;
192223
*) echo 1>&2 "bad word $arg in $line"
193224
exit 1;;
194225
esac
195226
done
227+
regmods=
228+
for mod in $mods
229+
do
230+
regmods="$regmods `mod_regname $mod`"
231+
done
232+
# MODULE_<NAME>_* variables are keyed by the registration name with
233+
# dots replaced by underscores (math.integer -> MODULE_MATH_INTEGER,
234+
# math.__init__ -> MODULE_MATH).
235+
mods_upper=`echo $regmods | tr 'a-z' 'A-Z' | tr '.' '_'`
196236
if test -z "$cpps" -a -z "$libs"; then
197237
cpps="\$(MODULE_${mods_upper}_CFLAGS)"
198238
libs="\$(MODULE_${mods_upper}_LDFLAGS)"
199239
fi
200-
for mod in $mods
240+
for mod in $regmods
201241
do
202242
case $CONFIGURED in
203243
*,${mod},*)
@@ -211,14 +251,14 @@ sed -e 's/[ ]*#.*//' -e '/^[ ]*$/d' |
211251
case $doconfig in
212252
yes)
213253
LIBS="$LIBS $libs"
214-
MODS="$MODS $mods"
215-
BUILT="$BUILT $mods"
254+
MODS="$MODS $regmods"
255+
BUILT="$BUILT $regmods"
216256
;;
217257
no)
218-
BUILT="$BUILT $mods"
258+
BUILT="$BUILT $regmods"
219259
;;
220260
disabled)
221-
DISABLED="$DISABLED $mods"
261+
DISABLED="$DISABLED $regmods"
222262
continue
223263
;;
224264
esac
@@ -269,15 +309,17 @@ sed -e 's/[ ]*#.*//' -e '/^[ ]*$/d' |
269309
esac
270310
for mod in $mods
271311
do
272-
file="$srcdir/$mod\$(EXT_SUFFIX)"
312+
file="$srcdir/`mod_path $mod`\$(EXT_SUFFIX)"
273313
case $doconfig in
274314
no)
275315
SHAREDMODS="$SHAREDMODS $file"
276-
BUILT_SHARED="$BUILT_SHARED $mod"
316+
BUILT_SHARED="$BUILT_SHARED `mod_regname $mod`"
277317
;;
278318
esac
279319
rule="$file: $objs \$(MODULE_${mods_upper}_LDEPS)"
280-
rule="$rule; \$(BLDSHARED) $objs $libs \$(LIBPYTHON) -o $file"
320+
# Submodules/packages land in a sub-directory (math/integer,
321+
# math/__init__); create it before linking.
322+
rule="$rule; \$(MKDIR_P) \$(@D); \$(BLDSHARED) $objs $libs \$(LIBPYTHON) -o $file"
281323
echo "$rule" >>$rulesf
282324
done
283325
done
@@ -299,8 +341,11 @@ sed -e 's/[ ]*#.*//' -e '/^[ ]*$/d' |
299341
INITBITS=
300342
for mod in $MODS
301343
do
302-
EXTDECLS="${EXTDECLS}extern PyObject* PyInit_$mod(void);$NL"
303-
INITBITS="${INITBITS} {\"$mod\", PyInit_$mod},$NL"
344+
# $mod is the registration name (e.g. math.integer); the C init
345+
# symbol is named after its last component only (PyInit_integer).
346+
modsym=`mod_sym $mod`
347+
EXTDECLS="${EXTDECLS}extern PyObject* PyInit_$modsym(void);$NL"
348+
INITBITS="${INITBITS} {\"$mod\", PyInit_$modsym},$NL"
304349
done
305350

306351

Modules/mathintegermodule.c

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1287,8 +1287,17 @@ static struct PyModuleDef math_integer_module = {
12871287
.m_slots = math_integer_slots,
12881288
};
12891289

1290+
/* Shared, the dynamic loader looks up PyInit_integer (the last component of
1291+
* the name). Built into the interpreter (Py_BUILD_CORE_BUILTIN), the inittab
1292+
* uses PyInit_math_integer instead, so the static symbol stays unique. */
1293+
#ifdef Py_BUILD_CORE_BUILTIN
1294+
# define MATH_INTEGER_PYINIT PyInit_math_integer
1295+
#else
1296+
# define MATH_INTEGER_PYINIT PyInit_integer
1297+
#endif
1298+
12901299
PyMODINIT_FUNC
1291-
PyInit__math_integer(void)
1300+
MATH_INTEGER_PYINIT(void)
12921301
{
12931302
return PyModuleDef_Init(&math_integer_module);
12941303
}

Modules/mathmodule.c

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3188,7 +3188,26 @@ math_exec(PyObject *module)
31883188
return -1;
31893189
}
31903190

3191-
PyObject *intmath = PyImport_ImportModule("_math_integer");
3191+
/* math is a package. A shared build gets __path__ from the math/
3192+
* directory; a builtin build (e.g. Windows, WebAssembly) does not, so set
3193+
* an empty one to mark math as a package and find the builtin submodule. */
3194+
PyObject *path;
3195+
if (PyObject_GetOptionalAttrString(module, "__path__", &path) < 0) {
3196+
return -1;
3197+
}
3198+
if (path == NULL) {
3199+
path = PyList_New(0);
3200+
if (path == NULL || PyModule_Add(module, "__path__", path) < 0) {
3201+
return -1;
3202+
}
3203+
}
3204+
else {
3205+
Py_DECREF(path);
3206+
}
3207+
3208+
/* Importing the math.integer submodule sets sys.modules['math.integer']
3209+
* and the parent's 'integer' attribute automatically. */
3210+
PyObject *intmath = PyImport_ImportModule("math.integer");
31923211
if (!intmath) {
31933212
return -1;
31943213
}
@@ -3206,13 +3225,9 @@ math_exec(PyObject *module)
32063225
IMPORT_FROM_INTMATH(isqrt);
32073226
IMPORT_FROM_INTMATH(lcm);
32083227
IMPORT_FROM_INTMATH(perm);
3209-
if (_PyImport_SetModuleString("math.integer", intmath) < 0) {
3210-
Py_DECREF(intmath);
3211-
return -1;
3212-
}
3213-
if (PyModule_Add(module, "integer", intmath) < 0) {
3214-
return -1;
3215-
}
3228+
/* sys.modules['math.integer'] and the parent 'integer' attribute were
3229+
* set by the import machinery above. */
3230+
Py_DECREF(intmath);
32163231
return 0;
32173232
}
32183233

PC/config.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ extern PyObject* PyInit_errno(void);
1313
extern PyObject* PyInit_faulthandler(void);
1414
extern PyObject* PyInit__tracemalloc(void);
1515
extern PyObject* PyInit_gc(void);
16-
extern PyObject* PyInit__math_integer(void);
16+
extern PyObject* PyInit_math_integer(void);
1717
extern PyObject* PyInit_math(void);
1818
extern PyObject* PyInit_nt(void);
1919
extern PyObject* PyInit__operator(void);
@@ -101,8 +101,8 @@ struct _inittab _PyImport_Inittab[] = {
101101
{"errno", PyInit_errno},
102102
{"faulthandler", PyInit_faulthandler},
103103
{"gc", PyInit_gc},
104-
{"_math_integer", PyInit__math_integer},
105104
{"math", PyInit_math},
105+
{"math.integer", PyInit_math_integer},
106106
{"nt", PyInit_nt}, /* Use the NT os functions, not posix */
107107
{"_operator", PyInit__operator},
108108
{"_signal", PyInit__signal},

Python/stdlib_module_names.h

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Tools/build/check_extension_modules.py

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,18 @@ def __bool__(self) -> bool:
147147
return self.value in {"builtin", "shared"}
148148

149149

150+
def registration_name(setup_token: str) -> str:
151+
"""Map a Setup module token to its import/registration name.
152+
153+
Submodules use dotted names (math.integer); an extension package's
154+
initializer is written with an explicit __init__ leaf (math.__init__),
155+
whose registration name is the package itself (math).
156+
"""
157+
if setup_token.endswith(".__init__"):
158+
return setup_token[: -len(".__init__")]
159+
return setup_token
160+
161+
150162
class ModuleInfo(NamedTuple):
151163
name: str
152164
state: ModuleState
@@ -380,13 +392,22 @@ def get_sysconfig_modules(self) -> Iterable[ModuleInfo]:
380392
else:
381393
modbuiltin = set(sys.builtin_module_names)
382394

395+
# MODULE_<NAME>_STATE keys flatten dotted registration names to
396+
# underscores (math.integer -> MODULE_MATH_INTEGER_STATE). Recover
397+
# the real, possibly dotted, name from the MOD*_NAMES lists.
398+
realnames = {}
399+
for var in ("MODBUILT_NAMES", "MODSHARED_NAMES", "MODDISABLED_NAMES"):
400+
for name in (sysconfig.get_config_var(var) or "").split():
401+
realnames[name.replace(".", "_")] = name
402+
383403
for key, value in sysconfig.get_config_vars().items():
384404
if not key.startswith("MODULE_") or not key.endswith("_STATE"):
385405
continue
386406
if value not in {"yes", "disabled", "missing", "n/a"}:
387407
raise ValueError(f"Unsupported value '{value}' for {key}")
388408

389409
modname = key[7:-6].lower()
410+
modname = realnames.get(modname, modname)
390411
if modname in moddisabled:
391412
# Setup "*disabled*" rule
392413
state = ModuleState.DISABLED_SETUP
@@ -427,12 +448,12 @@ def parse_setup_file(self, setup_file: pathlib.Path) -> Iterable[ModuleInfo]:
427448
if state == ModuleState.DISABLED:
428449
# *disabled* can disable multiple modules per line
429450
for item in items:
430-
modinfo = ModuleInfo(item, state)
451+
modinfo = ModuleInfo(registration_name(item), state)
431452
logger.debug("Found %s in %s", modinfo, setup_file)
432453
yield modinfo
433454
elif state in {ModuleState.SHARED, ModuleState.BUILTIN}:
434455
# *shared* and *static*, first item is the name of the module.
435-
modinfo = ModuleInfo(items[0], state)
456+
modinfo = ModuleInfo(registration_name(items[0]), state)
436457
logger.debug("Found %s in %s", modinfo, setup_file)
437458
yield modinfo
438459

@@ -456,7 +477,16 @@ def get_spec(self, modinfo: ModuleInfo) -> ModuleSpec:
456477
def get_location(self, modinfo: ModuleInfo) -> pathlib.Path | None:
457478
"""Get shared library location in build directory"""
458479
if modinfo.state == ModuleState.SHARED:
459-
return self.builddir / f"{modinfo.name}{self.ext_suffix}"
480+
# Dotted submodule names map onto sub-directories
481+
# (math.integer -> math/integer.so). An extension *package* is
482+
# registered under its bare name but lives in <name>/__init__.so.
483+
stem = modinfo.name.replace(".", "/")
484+
location = self.builddir / f"{stem}{self.ext_suffix}"
485+
if not location.exists():
486+
pkg_init = self.builddir / stem / f"__init__{self.ext_suffix}"
487+
if pkg_init.exists():
488+
return pkg_init
489+
return location
460490
else:
461491
return None
462492

0 commit comments

Comments
 (0)