diff --git a/Lib/importlib/_bootstrap_external.py b/Lib/importlib/_bootstrap_external.py index a1cb729efb7fef..7470c499ecd345 100644 --- a/Lib/importlib/_bootstrap_external.py +++ b/Lib/importlib/_bootstrap_external.py @@ -1037,8 +1037,10 @@ def exec_module(self, module): def is_package(self, fullname): """Return True if the extension module is a package.""" file_name = _path_split(self.path)[1] - return any(file_name == '__init__' + suffix - for suffix in EXTENSION_SUFFIXES) + tail_name = fullname.rpartition('.')[2] + return tail_name != '__init__' and any( + file_name == '__init__' + suffix + for suffix in EXTENSION_SUFFIXES) def get_code(self, fullname): """Return None as an extension module cannot create a code object.""" diff --git a/Makefile.pre.in b/Makefile.pre.in index b9914369ad1bed..6f30f2d79bed08 100644 --- a/Makefile.pre.in +++ b/Makefile.pre.in @@ -1525,7 +1525,10 @@ sharedmods: $(SHAREDMODS) pybuilddir.txt $(MKDIR_P) $$target; \ for mod in X $(SHAREDMODS); do \ if test $$mod != X; then \ - $(LN) -sf ../../$$mod $$target/`basename $$mod`; \ + rel=`echo $$mod | sed 's,^Modules/,,'`; \ + dest=$$target/$$rel; \ + $(MKDIR_P) `dirname $$dest`; \ + $(LN) -sf `pwd`/$$mod $$dest; \ fi; \ done @@ -2394,11 +2397,13 @@ sharedinstall: all done @for i in X $(SHAREDMODS); do \ if test $$i != X; then \ - echo $(INSTALL_SHARED) $$i $(DESTSHARED)/`basename $$i`; \ - $(INSTALL_SHARED) $$i $(DESTDIR)$(DESTSHARED)/`basename $$i`; \ + rel=`echo $$i | sed 's,^Modules/,,'`; \ + $(INSTALL) -d -m $(DIRMODE) $(DESTDIR)$(DESTSHARED)/`dirname $$rel`; \ + echo $(INSTALL_SHARED) $$i $(DESTSHARED)/$$rel; \ + $(INSTALL_SHARED) $$i $(DESTDIR)$(DESTSHARED)/$$rel; \ if test -d "$$i.dSYM"; then \ - echo $(DSYMUTIL_PATH) $(DESTDIR)$(DESTSHARED)/`basename $$i`; \ - $(DSYMUTIL_PATH) $(DESTDIR)$(DESTSHARED)/`basename $$i`; \ + echo $(DSYMUTIL_PATH) $(DESTDIR)$(DESTSHARED)/$$rel; \ + $(DSYMUTIL_PATH) $(DESTDIR)$(DESTSHARED)/$$rel; \ fi; \ fi; \ done diff --git a/Misc/NEWS.d/next/Build/2026-06-27-01-56-53.gh-issue-140824.gUq8JV.rst b/Misc/NEWS.d/next/Build/2026-06-27-01-56-53.gh-issue-140824.gUq8JV.rst new file mode 100644 index 00000000000000..32b217e1f19e59 --- /dev/null +++ b/Misc/NEWS.d/next/Build/2026-06-27-01-56-53.gh-issue-140824.gUq8JV.rst @@ -0,0 +1,9 @@ +The :mod:`math` module is now built as an extension *package*: +``math/__init__`` and ``math/integer``. :mod:`math.integer` is therefore a +real extension submodule instead of the ``_math_integer`` extension module +disguised as one. :file:`Modules/makesetup` now accepts dotted module names +(a package initializer is written with an explicit ``__init__`` leaf, e.g. +``math.__init__``) and maps each to a sub-directory path and the matching +``PyInit_`` symbol. This works both for the usual shared build and for +builds where ``math`` is compiled into the interpreter as a builtin module +(Windows and WebAssembly). diff --git a/Modules/Setup b/Modules/Setup index e97a78e628693d..6a811653fd8175 100644 --- a/Modules/Setup +++ b/Modules/Setup @@ -155,8 +155,8 @@ PYTHONPATH=$(COREPYTHONPATH) #array arraymodule.c #binascii binascii.c #cmath cmathmodule.c -#math mathmodule.c -#_math_integer mathintegermodule.c +#math.__init__ mathmodule.c +#math.integer mathintegermodule.c #mmap mmapmodule.c #select selectmodule.c #_sysconfig _sysconfig.c diff --git a/Modules/Setup.stdlib.in b/Modules/Setup.stdlib.in index 8efea27824f0e8..ce4e5806dc3fa2 100644 --- a/Modules/Setup.stdlib.in +++ b/Modules/Setup.stdlib.in @@ -37,7 +37,7 @@ @MODULE__HEAPQ_TRUE@_heapq _heapqmodule.c @MODULE__JSON_TRUE@_json _json.c @MODULE__LSPROF_TRUE@_lsprof _lsprof.c rotatingtree.c -@MODULE__MATH_INTEGER_TRUE@_math_integer mathintegermodule.c +@MODULE_MATH_INTEGER_TRUE@math.integer mathintegermodule.c @MODULE__PICKLE_TRUE@_pickle _pickle.c @MODULE__QUEUE_TRUE@_queue _queuemodule.c @MODULE__RANDOM_TRUE@_random _randommodule.c @@ -52,7 +52,7 @@ @MODULE__ZONEINFO_TRUE@_zoneinfo _zoneinfo.c # needs libm -@MODULE_MATH_TRUE@math mathmodule.c +@MODULE_MATH_TRUE@math.__init__ mathmodule.c @MODULE_CMATH_TRUE@cmath cmathmodule.c @MODULE__STATISTICS_TRUE@_statistics _statisticsmodule.c diff --git a/Modules/makesetup b/Modules/makesetup index 104c824b846540..0f0eabab232052 100755 --- a/Modules/makesetup +++ b/Modules/makesetup @@ -87,6 +87,32 @@ esac NL='\ ' +# Map a module name as written in Setup to its three distinct attributes. +# Submodules use dotted names (math.integer); a package's initializer is +# written with an explicit __init__ leaf (math.__init__). +# mod_regname: import/registration name math.integer -> math.integer +# math.__init__ -> math +# mod_path: path under srcdir (no ext) math.integer -> math/integer +# math.__init__ -> math/__init__ +# mod_sym: static PyInit_ symbol math.integer -> math_integer +# math.__init__ -> math +# mod_sym (the static inittab symbol) uses the full dotted name so it stays +# unique across packages; shared submodules instead export +# PyInit_ (see Py_BUILD_CORE_BUILTIN). +mod_regname() { + case $1 in + *.__init__) echo "${1%.__init__}";; + *) echo "$1";; + esac +} +mod_path() { + echo "$1" | tr '.' '/' +} +mod_sym() { + reg=`mod_regname "$1"` + echo "$reg" | tr '.' '_' +} + # Main loop for i in ${*-Setup} do @@ -183,21 +209,35 @@ sed -e 's/[ ]*#.*//' -e '/^[ ]*$/d' | \$\(*_RPATH\)) libs="$libs $arg";; \$*) libs="$libs $arg" cpps="$cpps $arg";; - *.*) echo 1>&2 "bad word $arg in $line" - exit 1;; -u) skip=libs; libs="$libs -u";; [a-zA-Z_]*) - mods="$mods $arg" - mods_upper=$(echo $mods | tr '[a-z]' '[A-Z]');; + # Module name, possibly dotted (math.integer) or a + # package initializer (math.__init__). Only identifier + # characters and dots are allowed. + case $arg in + *[!a-zA-Z0-9_.]*) + echo 1>&2 "bad word $arg in $line" + exit 1;; + esac + mods="$mods $arg";; *) echo 1>&2 "bad word $arg in $line" exit 1;; esac done + regmods= + for mod in $mods + do + regmods="$regmods `mod_regname $mod`" + done + # MODULE__* variables are keyed by the registration name with + # dots replaced by underscores (math.integer -> MODULE_MATH_INTEGER, + # math.__init__ -> MODULE_MATH). + mods_upper=`echo $regmods | tr 'a-z' 'A-Z' | tr '.' '_'` if test -z "$cpps" -a -z "$libs"; then cpps="\$(MODULE_${mods_upper}_CFLAGS)" libs="\$(MODULE_${mods_upper}_LDFLAGS)" fi - for mod in $mods + for mod in $regmods do case $CONFIGURED in *,${mod},*) @@ -211,14 +251,14 @@ sed -e 's/[ ]*#.*//' -e '/^[ ]*$/d' | case $doconfig in yes) LIBS="$LIBS $libs" - MODS="$MODS $mods" - BUILT="$BUILT $mods" + MODS="$MODS $regmods" + BUILT="$BUILT $regmods" ;; no) - BUILT="$BUILT $mods" + BUILT="$BUILT $regmods" ;; disabled) - DISABLED="$DISABLED $mods" + DISABLED="$DISABLED $regmods" continue ;; esac @@ -269,15 +309,17 @@ sed -e 's/[ ]*#.*//' -e '/^[ ]*$/d' | esac for mod in $mods do - file="$srcdir/$mod\$(EXT_SUFFIX)" + file="$srcdir/`mod_path $mod`\$(EXT_SUFFIX)" case $doconfig in no) SHAREDMODS="$SHAREDMODS $file" - BUILT_SHARED="$BUILT_SHARED $mod" + BUILT_SHARED="$BUILT_SHARED `mod_regname $mod`" ;; esac rule="$file: $objs \$(MODULE_${mods_upper}_LDEPS)" - rule="$rule; \$(BLDSHARED) $objs $libs \$(LIBPYTHON) -o $file" + # Submodules/packages land in a sub-directory (math/integer, + # math/__init__); create it before linking. + rule="$rule; \$(MKDIR_P) \$(@D); \$(BLDSHARED) $objs $libs \$(LIBPYTHON) -o $file" echo "$rule" >>$rulesf done done @@ -299,8 +341,11 @@ sed -e 's/[ ]*#.*//' -e '/^[ ]*$/d' | INITBITS= for mod in $MODS do - EXTDECLS="${EXTDECLS}extern PyObject* PyInit_$mod(void);$NL" - INITBITS="${INITBITS} {\"$mod\", PyInit_$mod},$NL" + # $mod is the registration name (e.g. math.integer); the C init + # symbol is named after its last component only (PyInit_integer). + modsym=`mod_sym $mod` + EXTDECLS="${EXTDECLS}extern PyObject* PyInit_$modsym(void);$NL" + INITBITS="${INITBITS} {\"$mod\", PyInit_$modsym},$NL" done diff --git a/Modules/mathintegermodule.c b/Modules/mathintegermodule.c index cfad4154b2d361..7adef0f7281da6 100644 --- a/Modules/mathintegermodule.c +++ b/Modules/mathintegermodule.c @@ -1287,8 +1287,17 @@ static struct PyModuleDef math_integer_module = { .m_slots = math_integer_slots, }; +/* Shared, the dynamic loader looks up PyInit_integer (the last component of + * the name). Built into the interpreter (Py_BUILD_CORE_BUILTIN), the inittab + * uses PyInit_math_integer instead, so the static symbol stays unique. */ +#ifdef Py_BUILD_CORE_BUILTIN +# define MATH_INTEGER_PYINIT PyInit_math_integer +#else +# define MATH_INTEGER_PYINIT PyInit_integer +#endif + PyMODINIT_FUNC -PyInit__math_integer(void) +MATH_INTEGER_PYINIT(void) { return PyModuleDef_Init(&math_integer_module); } diff --git a/Modules/mathmodule.c b/Modules/mathmodule.c index 64e5372d73d2f2..1a43007c3b5f10 100644 --- a/Modules/mathmodule.c +++ b/Modules/mathmodule.c @@ -3188,7 +3188,26 @@ math_exec(PyObject *module) return -1; } - PyObject *intmath = PyImport_ImportModule("_math_integer"); + /* math is a package. A shared build gets __path__ from the math/ + * directory; a builtin build (e.g. Windows, WebAssembly) does not, so set + * an empty one to mark math as a package and find the builtin submodule. */ + PyObject *path; + if (PyObject_GetOptionalAttrString(module, "__path__", &path) < 0) { + return -1; + } + if (path == NULL) { + path = PyList_New(0); + if (path == NULL || PyModule_Add(module, "__path__", path) < 0) { + return -1; + } + } + else { + Py_DECREF(path); + } + + /* Importing the math.integer submodule sets sys.modules['math.integer'] + * and the parent's 'integer' attribute automatically. */ + PyObject *intmath = PyImport_ImportModule("math.integer"); if (!intmath) { return -1; } @@ -3206,13 +3225,9 @@ math_exec(PyObject *module) IMPORT_FROM_INTMATH(isqrt); IMPORT_FROM_INTMATH(lcm); IMPORT_FROM_INTMATH(perm); - if (_PyImport_SetModuleString("math.integer", intmath) < 0) { - Py_DECREF(intmath); - return -1; - } - if (PyModule_Add(module, "integer", intmath) < 0) { - return -1; - } + /* sys.modules['math.integer'] and the parent 'integer' attribute were + * set by the import machinery above. */ + Py_DECREF(intmath); return 0; } diff --git a/PC/config.c b/PC/config.c index 51b46c64d99b81..b8b7dde86bb877 100644 --- a/PC/config.c +++ b/PC/config.c @@ -13,7 +13,7 @@ extern PyObject* PyInit_errno(void); extern PyObject* PyInit_faulthandler(void); extern PyObject* PyInit__tracemalloc(void); extern PyObject* PyInit_gc(void); -extern PyObject* PyInit__math_integer(void); +extern PyObject* PyInit_math_integer(void); extern PyObject* PyInit_math(void); extern PyObject* PyInit_nt(void); extern PyObject* PyInit__operator(void); @@ -101,8 +101,8 @@ struct _inittab _PyImport_Inittab[] = { {"errno", PyInit_errno}, {"faulthandler", PyInit_faulthandler}, {"gc", PyInit_gc}, - {"_math_integer", PyInit__math_integer}, {"math", PyInit_math}, + {"math.integer", PyInit_math_integer}, {"nt", PyInit_nt}, /* Use the NT os functions, not posix */ {"_operator", PyInit__operator}, {"_signal", PyInit__signal}, diff --git a/Python/stdlib_module_names.h b/Python/stdlib_module_names.h index 8937e666bbbdd5..47c88839bc87b2 100644 --- a/Python/stdlib_module_names.h +++ b/Python/stdlib_module_names.h @@ -51,7 +51,6 @@ static const char* _Py_stdlib_module_names[] = { "_lsprof", "_lzma", "_markupbase", -"_math_integer", "_md5", "_multibytecodec", "_multiprocessing", diff --git a/Tools/build/check_extension_modules.py b/Tools/build/check_extension_modules.py index c619a9a0c1c5a1..ac614725ee079b 100644 --- a/Tools/build/check_extension_modules.py +++ b/Tools/build/check_extension_modules.py @@ -147,6 +147,18 @@ def __bool__(self) -> bool: return self.value in {"builtin", "shared"} +def registration_name(setup_token: str) -> str: + """Map a Setup module token to its import/registration name. + + Submodules use dotted names (math.integer); an extension package's + initializer is written with an explicit __init__ leaf (math.__init__), + whose registration name is the package itself (math). + """ + if setup_token.endswith(".__init__"): + return setup_token[: -len(".__init__")] + return setup_token + + class ModuleInfo(NamedTuple): name: str state: ModuleState @@ -380,6 +392,14 @@ def get_sysconfig_modules(self) -> Iterable[ModuleInfo]: else: modbuiltin = set(sys.builtin_module_names) + # MODULE__STATE keys flatten dotted registration names to + # underscores (math.integer -> MODULE_MATH_INTEGER_STATE). Recover + # the real, possibly dotted, name from the MOD*_NAMES lists. + realnames = {} + for var in ("MODBUILT_NAMES", "MODSHARED_NAMES", "MODDISABLED_NAMES"): + for name in (sysconfig.get_config_var(var) or "").split(): + realnames[name.replace(".", "_")] = name + for key, value in sysconfig.get_config_vars().items(): if not key.startswith("MODULE_") or not key.endswith("_STATE"): continue @@ -387,6 +407,7 @@ def get_sysconfig_modules(self) -> Iterable[ModuleInfo]: raise ValueError(f"Unsupported value '{value}' for {key}") modname = key[7:-6].lower() + modname = realnames.get(modname, modname) if modname in moddisabled: # Setup "*disabled*" rule state = ModuleState.DISABLED_SETUP @@ -427,12 +448,12 @@ def parse_setup_file(self, setup_file: pathlib.Path) -> Iterable[ModuleInfo]: if state == ModuleState.DISABLED: # *disabled* can disable multiple modules per line for item in items: - modinfo = ModuleInfo(item, state) + modinfo = ModuleInfo(registration_name(item), state) logger.debug("Found %s in %s", modinfo, setup_file) yield modinfo elif state in {ModuleState.SHARED, ModuleState.BUILTIN}: # *shared* and *static*, first item is the name of the module. - modinfo = ModuleInfo(items[0], state) + modinfo = ModuleInfo(registration_name(items[0]), state) logger.debug("Found %s in %s", modinfo, setup_file) yield modinfo @@ -456,7 +477,16 @@ def get_spec(self, modinfo: ModuleInfo) -> ModuleSpec: def get_location(self, modinfo: ModuleInfo) -> pathlib.Path | None: """Get shared library location in build directory""" if modinfo.state == ModuleState.SHARED: - return self.builddir / f"{modinfo.name}{self.ext_suffix}" + # Dotted submodule names map onto sub-directories + # (math.integer -> math/integer.so). An extension *package* is + # registered under its bare name but lives in /__init__.so. + stem = modinfo.name.replace(".", "/") + location = self.builddir / f"{stem}{self.ext_suffix}" + if not location.exists(): + pkg_init = self.builddir / stem / f"__init__{self.ext_suffix}" + if pkg_init.exists(): + return pkg_init + return location else: return None diff --git a/Tools/build/generate_stdlib_module_names.py b/Tools/build/generate_stdlib_module_names.py index f8828a56b4c7da..e649ae08e02f47 100644 --- a/Tools/build/generate_stdlib_module_names.py +++ b/Tools/build/generate_stdlib_module_names.py @@ -121,6 +121,14 @@ def list_modules() -> set[str]: if package_name in IGNORE: names.discard(name) + # Extension submodules (e.g. math.integer) are not listed individually, + # but their parent package must be present. + for name in list(names): + if "." in name: + if name.partition(".")[0] not in names: + raise Exception(f"submodule without a known parent: {name}") + names.discard(name) + # Sanity checks for name in names: if "." in name: diff --git a/configure b/configure index e96b87989793a8..8c16021f26299f 100755 --- a/configure +++ b/configure @@ -819,8 +819,8 @@ MODULE__BISECT_FALSE MODULE__BISECT_TRUE MODULE__ASYNCIO_FALSE MODULE__ASYNCIO_TRUE -MODULE__MATH_INTEGER_FALSE -MODULE__MATH_INTEGER_TRUE +MODULE_MATH_INTEGER_FALSE +MODULE_MATH_INTEGER_TRUE MODULE_ARRAY_FALSE MODULE_ARRAY_TRUE MODULE_TIME_FALSE @@ -32502,20 +32502,20 @@ then : fi - if test "$py_cv_module__math_integer" != "n/a" + if test "$py_cv_module_math_integer" != "n/a" then : - py_cv_module__math_integer=yes + py_cv_module_math_integer=yes fi - if test "$py_cv_module__math_integer" = yes; then - MODULE__MATH_INTEGER_TRUE= - MODULE__MATH_INTEGER_FALSE='#' + if test "$py_cv_module_math_integer" = yes; then + MODULE_MATH_INTEGER_TRUE= + MODULE_MATH_INTEGER_FALSE='#' else - MODULE__MATH_INTEGER_TRUE='#' - MODULE__MATH_INTEGER_FALSE= + MODULE_MATH_INTEGER_TRUE='#' + MODULE_MATH_INTEGER_FALSE= fi - as_fn_append MODULE_BLOCK "MODULE__MATH_INTEGER_STATE=$py_cv_module__math_integer$as_nl" - if test "x$py_cv_module__math_integer" = xyes + as_fn_append MODULE_BLOCK "MODULE_MATH_INTEGER_STATE=$py_cv_module_math_integer$as_nl" + if test "x$py_cv_module_math_integer" = xyes then : @@ -35696,8 +35696,8 @@ if test -z "${MODULE_ARRAY_TRUE}" && test -z "${MODULE_ARRAY_FALSE}"; then as_fn_error $? "conditional \"MODULE_ARRAY\" was never defined. Usually this means the macro was only invoked conditionally." "$LINENO" 5 fi -if test -z "${MODULE__MATH_INTEGER_TRUE}" && test -z "${MODULE__MATH_INTEGER_FALSE}"; then - as_fn_error $? "conditional \"MODULE__MATH_INTEGER\" was never defined. +if test -z "${MODULE_MATH_INTEGER_TRUE}" && test -z "${MODULE_MATH_INTEGER_FALSE}"; then + as_fn_error $? "conditional \"MODULE_MATH_INTEGER\" was never defined. Usually this means the macro was only invoked conditionally." "$LINENO" 5 fi if test -z "${MODULE__ASYNCIO_TRUE}" && test -z "${MODULE__ASYNCIO_FALSE}"; then diff --git a/configure.ac b/configure.ac index cd1883f0195c47..63ff1cd72a3d22 100644 --- a/configure.ac +++ b/configure.ac @@ -8150,7 +8150,8 @@ PY_STDLIB_MOD_SIMPLE([time], [], [$TIMEMODULE_LIB]) dnl always enabled extension modules PY_STDLIB_MOD_SIMPLE([array]) -PY_STDLIB_MOD_SIMPLE([_math_integer]) +dnl math.integer, a submodule of the math extension package +PY_STDLIB_MOD_SIMPLE([math_integer]) PY_STDLIB_MOD_SIMPLE([_asyncio]) PY_STDLIB_MOD_SIMPLE([_bisect]) PY_STDLIB_MOD_SIMPLE([_csv])