Skip to content

Commit d45cc80

Browse files
gh-98627: Add the _testsinglephase Module (gh-99039)
This makes it more clear that a given test is definitely testing against a single-phase init (legacy) extension module. The new module is a companion to _testmultiphase. #98627
1 parent 4d5fcca commit d45cc80

13 files changed

+356
-21
lines changed

Lib/test/test_importlib/extension/test_case_sensitivity.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
machinery = util.import_importlib('importlib.machinery')
99

1010

11-
@unittest.skipIf(util.EXTENSIONS.filename is None, '_testcapi not available')
11+
@unittest.skipIf(util.EXTENSIONS.filename is None, f'{util.EXTENSIONS.name} not available')
1212
@util.case_insensitive_tests
1313
class ExtensionModuleCaseSensitivityTest(util.CASEOKTestBase):
1414

Lib/test/test_importlib/extension/test_loader.py

+100-17
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@
1313
from test.support.script_helper import assert_python_failure
1414

1515

16-
class LoaderTests(abc.LoaderTests):
16+
class LoaderTests:
1717

18-
"""Test load_module() for extension modules."""
18+
"""Test ExtensionFileLoader."""
1919

2020
def setUp(self):
2121
if not self.machinery.EXTENSION_SUFFIXES:
@@ -32,15 +32,6 @@ def load_module(self, fullname):
3232
warnings.simplefilter("ignore", DeprecationWarning)
3333
return self.loader.load_module(fullname)
3434

35-
def test_load_module_API(self):
36-
# Test the default argument for load_module().
37-
with warnings.catch_warnings():
38-
warnings.simplefilter("ignore", DeprecationWarning)
39-
self.loader.load_module()
40-
self.loader.load_module(None)
41-
with self.assertRaises(ImportError):
42-
self.load_module('XXX')
43-
4435
def test_equality(self):
4536
other = self.machinery.ExtensionFileLoader(util.EXTENSIONS.name,
4637
util.EXTENSIONS.file_path)
@@ -51,6 +42,15 @@ def test_inequality(self):
5142
util.EXTENSIONS.file_path)
5243
self.assertNotEqual(self.loader, other)
5344

45+
def test_load_module_API(self):
46+
# Test the default argument for load_module().
47+
with warnings.catch_warnings():
48+
warnings.simplefilter("ignore", DeprecationWarning)
49+
self.loader.load_module()
50+
self.loader.load_module(None)
51+
with self.assertRaises(ImportError):
52+
self.load_module('XXX')
53+
5454
def test_module(self):
5555
with util.uncache(util.EXTENSIONS.name):
5656
module = self.load_module(util.EXTENSIONS.name)
@@ -68,12 +68,6 @@ def test_module(self):
6868
# No extension module in a package available for testing.
6969
test_lacking_parent = None
7070

71-
def test_module_reuse(self):
72-
with util.uncache(util.EXTENSIONS.name):
73-
module1 = self.load_module(util.EXTENSIONS.name)
74-
module2 = self.load_module(util.EXTENSIONS.name)
75-
self.assertIs(module1, module2)
76-
7771
# No easy way to trigger a failure after a successful import.
7872
test_state_after_failure = None
7973

@@ -83,17 +77,106 @@ def test_unloadable(self):
8377
self.load_module(name)
8478
self.assertEqual(cm.exception.name, name)
8579

80+
def test_module_reuse(self):
81+
with util.uncache(util.EXTENSIONS.name):
82+
module1 = self.load_module(util.EXTENSIONS.name)
83+
module2 = self.load_module(util.EXTENSIONS.name)
84+
self.assertIs(module1, module2)
85+
8686
def test_is_package(self):
8787
self.assertFalse(self.loader.is_package(util.EXTENSIONS.name))
8888
for suffix in self.machinery.EXTENSION_SUFFIXES:
8989
path = os.path.join('some', 'path', 'pkg', '__init__' + suffix)
9090
loader = self.machinery.ExtensionFileLoader('pkg', path)
9191
self.assertTrue(loader.is_package('pkg'))
9292

93+
9394
(Frozen_LoaderTests,
9495
Source_LoaderTests
9596
) = util.test_both(LoaderTests, machinery=machinery)
9697

98+
99+
class SinglePhaseExtensionModuleTests(abc.LoaderTests):
100+
# Test loading extension modules without multi-phase initialization.
101+
102+
def setUp(self):
103+
if not self.machinery.EXTENSION_SUFFIXES:
104+
raise unittest.SkipTest("Requires dynamic loading support.")
105+
self.name = '_testsinglephase'
106+
if self.name in sys.builtin_module_names:
107+
raise unittest.SkipTest(
108+
f"{self.name} is a builtin module"
109+
)
110+
finder = self.machinery.FileFinder(None)
111+
self.spec = importlib.util.find_spec(self.name)
112+
assert self.spec
113+
self.loader = self.machinery.ExtensionFileLoader(
114+
self.name, self.spec.origin)
115+
116+
def load_module(self):
117+
with warnings.catch_warnings():
118+
warnings.simplefilter("ignore", DeprecationWarning)
119+
return self.loader.load_module(self.name)
120+
121+
def load_module_by_name(self, fullname):
122+
# Load a module from the test extension by name.
123+
origin = self.spec.origin
124+
loader = self.machinery.ExtensionFileLoader(fullname, origin)
125+
spec = importlib.util.spec_from_loader(fullname, loader)
126+
module = importlib.util.module_from_spec(spec)
127+
loader.exec_module(module)
128+
return module
129+
130+
def test_module(self):
131+
# Test loading an extension module.
132+
with util.uncache(self.name):
133+
module = self.load_module()
134+
for attr, value in [('__name__', self.name),
135+
('__file__', self.spec.origin),
136+
('__package__', '')]:
137+
self.assertEqual(getattr(module, attr), value)
138+
with self.assertRaises(AttributeError):
139+
module.__path__
140+
self.assertIs(module, sys.modules[self.name])
141+
self.assertIsInstance(module.__loader__,
142+
self.machinery.ExtensionFileLoader)
143+
144+
# No extension module as __init__ available for testing.
145+
test_package = None
146+
147+
# No extension module in a package available for testing.
148+
test_lacking_parent = None
149+
150+
# No easy way to trigger a failure after a successful import.
151+
test_state_after_failure = None
152+
153+
def test_unloadable(self):
154+
name = 'asdfjkl;'
155+
with self.assertRaises(ImportError) as cm:
156+
self.load_module_by_name(name)
157+
self.assertEqual(cm.exception.name, name)
158+
159+
def test_unloadable_nonascii(self):
160+
# Test behavior with nonexistent module with non-ASCII name.
161+
name = 'fo\xf3'
162+
with self.assertRaises(ImportError) as cm:
163+
self.load_module_by_name(name)
164+
self.assertEqual(cm.exception.name, name)
165+
166+
# It may make sense to add the equivalent to
167+
# the following MultiPhaseExtensionModuleTests tests:
168+
#
169+
# * test_nonmodule
170+
# * test_nonmodule_with_methods
171+
# * test_bad_modules
172+
# * test_nonascii
173+
174+
175+
(Frozen_SinglePhaseExtensionModuleTests,
176+
Source_SinglePhaseExtensionModuleTests
177+
) = util.test_both(SinglePhaseExtensionModuleTests, machinery=machinery)
178+
179+
97180
class MultiPhaseExtensionModuleTests(abc.LoaderTests):
98181
# Test loading extension modules with multi-phase initialization (PEP 489).
99182

Lib/test/test_importlib/util.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
EXTENSIONS.ext = None
2828
EXTENSIONS.filename = None
2929
EXTENSIONS.file_path = None
30-
EXTENSIONS.name = '_testcapi'
30+
EXTENSIONS.name = '_testsinglephase'
3131

3232
def _extension_details():
3333
global EXTENSIONS

Modules/Setup

+1
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,7 @@ PYTHONPATH=$(COREPYTHONPATH)
291291
#_testcapi _testcapimodule.c
292292
#_testimportmultiple _testimportmultiple.c
293293
#_testmultiphase _testmultiphase.c
294+
#_testsinglephase _testsinglephase.c
294295

295296
# ---
296297
# Uncommenting the following line tells makesetup that all following modules

Modules/Setup.stdlib.in

+1
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@
175175
*shared*
176176
@MODULE__TESTIMPORTMULTIPLE_TRUE@_testimportmultiple _testimportmultiple.c
177177
@MODULE__TESTMULTIPHASE_TRUE@_testmultiphase _testmultiphase.c
178+
@MODULE__TESTMULTIPHASE_TRUE@_testsinglephase _testsinglephase.c
178179
@MODULE__CTYPES_TEST_TRUE@_ctypes_test _ctypes/_ctypes_test.c
179180

180181
# Limited API template modules; must be built as shared modules.

Modules/_testsinglephase.c

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
2+
/* Testing module for single-phase initialization of extension modules
3+
*/
4+
#ifndef Py_BUILD_CORE_BUILTIN
5+
# define Py_BUILD_CORE_MODULE 1
6+
#endif
7+
8+
#include "Python.h"
9+
#include "pycore_namespace.h" // _PyNamespace_New()
10+
11+
12+
/* Function of two integers returning integer */
13+
14+
PyDoc_STRVAR(testexport_foo_doc,
15+
"foo(i,j)\n\
16+
\n\
17+
Return the sum of i and j.");
18+
19+
static PyObject *
20+
testexport_foo(PyObject *self, PyObject *args)
21+
{
22+
long i, j;
23+
long res;
24+
if (!PyArg_ParseTuple(args, "ll:foo", &i, &j))
25+
return NULL;
26+
res = i + j;
27+
return PyLong_FromLong(res);
28+
}
29+
30+
31+
static PyMethodDef TestMethods[] = {
32+
{"foo", testexport_foo, METH_VARARGS,
33+
testexport_foo_doc},
34+
{NULL, NULL} /* sentinel */
35+
};
36+
37+
38+
static struct PyModuleDef _testsinglephase = {
39+
PyModuleDef_HEAD_INIT,
40+
.m_name = "_testsinglephase",
41+
.m_doc = PyDoc_STR("Test module _testsinglephase (main)"),
42+
.m_size = -1, // no module state
43+
.m_methods = TestMethods,
44+
};
45+
46+
47+
PyMODINIT_FUNC
48+
PyInit__testsinglephase(void)
49+
{
50+
PyObject *module = PyModule_Create(&_testsinglephase);
51+
if (module == NULL) {
52+
return NULL;
53+
}
54+
55+
/* Add an exception type */
56+
PyObject *temp = PyErr_NewException("_testsinglephase.error", NULL, NULL);
57+
if (temp == NULL) {
58+
goto error;
59+
}
60+
if (PyModule_AddObject(module, "error", temp) != 0) {
61+
Py_DECREF(temp);
62+
goto error;
63+
}
64+
65+
if (PyModule_AddIntConstant(module, "int_const", 1969) != 0) {
66+
goto error;
67+
}
68+
69+
if (PyModule_AddStringConstant(module, "str_const", "something different") != 0) {
70+
goto error;
71+
}
72+
73+
return module;
74+
75+
error:
76+
Py_DECREF(module);
77+
return NULL;
78+
}

PCbuild/_testsinglephase.vcxproj

+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
3+
<ItemGroup Label="ProjectConfigurations">
4+
<ProjectConfiguration Include="Debug|ARM">
5+
<Configuration>Debug</Configuration>
6+
<Platform>ARM</Platform>
7+
</ProjectConfiguration>
8+
<ProjectConfiguration Include="Debug|ARM64">
9+
<Configuration>Debug</Configuration>
10+
<Platform>ARM64</Platform>
11+
</ProjectConfiguration>
12+
<ProjectConfiguration Include="Debug|Win32">
13+
<Configuration>Debug</Configuration>
14+
<Platform>Win32</Platform>
15+
</ProjectConfiguration>
16+
<ProjectConfiguration Include="Debug|x64">
17+
<Configuration>Debug</Configuration>
18+
<Platform>x64</Platform>
19+
</ProjectConfiguration>
20+
<ProjectConfiguration Include="PGInstrument|ARM">
21+
<Configuration>PGInstrument</Configuration>
22+
<Platform>ARM</Platform>
23+
</ProjectConfiguration>
24+
<ProjectConfiguration Include="PGInstrument|ARM64">
25+
<Configuration>PGInstrument</Configuration>
26+
<Platform>ARM64</Platform>
27+
</ProjectConfiguration>
28+
<ProjectConfiguration Include="PGInstrument|Win32">
29+
<Configuration>PGInstrument</Configuration>
30+
<Platform>Win32</Platform>
31+
</ProjectConfiguration>
32+
<ProjectConfiguration Include="PGInstrument|x64">
33+
<Configuration>PGInstrument</Configuration>
34+
<Platform>x64</Platform>
35+
</ProjectConfiguration>
36+
<ProjectConfiguration Include="PGUpdate|ARM">
37+
<Configuration>PGUpdate</Configuration>
38+
<Platform>ARM</Platform>
39+
</ProjectConfiguration>
40+
<ProjectConfiguration Include="PGUpdate|ARM64">
41+
<Configuration>PGUpdate</Configuration>
42+
<Platform>ARM64</Platform>
43+
</ProjectConfiguration>
44+
<ProjectConfiguration Include="PGUpdate|Win32">
45+
<Configuration>PGUpdate</Configuration>
46+
<Platform>Win32</Platform>
47+
</ProjectConfiguration>
48+
<ProjectConfiguration Include="PGUpdate|x64">
49+
<Configuration>PGUpdate</Configuration>
50+
<Platform>x64</Platform>
51+
</ProjectConfiguration>
52+
<ProjectConfiguration Include="Release|ARM">
53+
<Configuration>Release</Configuration>
54+
<Platform>ARM</Platform>
55+
</ProjectConfiguration>
56+
<ProjectConfiguration Include="Release|ARM64">
57+
<Configuration>Release</Configuration>
58+
<Platform>ARM64</Platform>
59+
</ProjectConfiguration>
60+
<ProjectConfiguration Include="Release|Win32">
61+
<Configuration>Release</Configuration>
62+
<Platform>Win32</Platform>
63+
</ProjectConfiguration>
64+
<ProjectConfiguration Include="Release|x64">
65+
<Configuration>Release</Configuration>
66+
<Platform>x64</Platform>
67+
</ProjectConfiguration>
68+
</ItemGroup>
69+
<PropertyGroup Label="Globals">
70+
<ProjectGuid>{2097F1C1-597C-4167-93E3-656A7D6339B2}</ProjectGuid>
71+
<Keyword>Win32Proj</Keyword>
72+
<RootNamespace>_testsinglephase</RootNamespace>
73+
<SupportPGO>false</SupportPGO>
74+
</PropertyGroup>
75+
<Import Project="python.props" />
76+
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
77+
<PropertyGroup Label="Configuration">
78+
<ConfigurationType>DynamicLibrary</ConfigurationType>
79+
<CharacterSet>NotSet</CharacterSet>
80+
</PropertyGroup>
81+
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
82+
<PropertyGroup>
83+
<TargetExt>.pyd</TargetExt>
84+
</PropertyGroup>
85+
<ImportGroup Label="ExtensionSettings">
86+
</ImportGroup>
87+
<ImportGroup Label="PropertySheets">
88+
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
89+
<Import Project="pyproject.props" />
90+
</ImportGroup>
91+
<PropertyGroup Label="UserMacros" />
92+
<ItemDefinitionGroup>
93+
<ClCompile>
94+
<PreprocessorDefinitions>_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
95+
</ClCompile>
96+
<Link>
97+
<SubSystem>Console</SubSystem>
98+
</Link>
99+
</ItemDefinitionGroup>
100+
<ItemGroup>
101+
<ClCompile Include="..\Modules\_testsinglephase.c" />
102+
</ItemGroup>
103+
<ItemGroup>
104+
<ResourceCompile Include="..\PC\python_nt.rc" />
105+
</ItemGroup>
106+
<ItemGroup>
107+
<ProjectReference Include="pythoncore.vcxproj">
108+
<Project>{cf7ac3d1-e2df-41d2-bea6-1e2556cdea26}</Project>
109+
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
110+
</ProjectReference>
111+
</ItemGroup>
112+
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
113+
<ImportGroup Label="ExtensionTargets">
114+
</ImportGroup>
115+
</Project>

0 commit comments

Comments
 (0)