Skip to content

Commit ac80f2d

Browse files
committed
Fix some crashes
``` import types types.LazyImportType({}, "3", 0) ``` ``` def f(): exec("lazy import json") f() ```
1 parent a05b50d commit ac80f2d

File tree

3 files changed

+73
-3
lines changed

3 files changed

+73
-3
lines changed

Lib/test/test_import/test_lazy_imports.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,38 @@ def test_lazy_import_type_exposed(self):
190190
self.assertTrue(hasattr(types, 'LazyImportType'))
191191
self.assertEqual(types.LazyImportType.__name__, 'lazy_import')
192192

193+
def test_lazy_import_type_invalid_fromlist_type(self):
194+
"""LazyImportType should reject invalid fromlist types."""
195+
# fromlist must be None, a string, or a tuple - not an int
196+
with self.assertRaises(TypeError) as cm:
197+
types.LazyImportType({}, "module", 0)
198+
self.assertIn("fromlist must be None, a string, or a tuple", str(cm.exception))
199+
200+
# Also test with other invalid types
201+
with self.assertRaises(TypeError):
202+
types.LazyImportType({}, "module", []) # list not allowed
203+
204+
with self.assertRaises(TypeError):
205+
types.LazyImportType({}, "module", {"x": 1}) # dict not allowed
206+
207+
def test_lazy_import_type_valid_fromlist(self):
208+
"""LazyImportType should accept valid fromlist types."""
209+
# None is valid (implicit)
210+
lazy1 = types.LazyImportType({}, "module")
211+
self.assertIsNotNone(lazy1)
212+
213+
# Explicit None is valid
214+
lazy2 = types.LazyImportType({}, "module", None)
215+
self.assertIsNotNone(lazy2)
216+
217+
# String is valid
218+
lazy3 = types.LazyImportType({}, "module", "attr")
219+
self.assertIsNotNone(lazy3)
220+
221+
# Tuple is valid
222+
lazy4 = types.LazyImportType({}, "module", ("attr1", "attr2"))
223+
self.assertIsNotNone(lazy4)
224+
193225

194226
class SyntaxRestrictionTests(unittest.TestCase):
195227
"""Tests for syntax restrictions on lazy imports."""
@@ -230,6 +262,35 @@ def test_lazy_import_func(self):
230262
with self.assertRaises(SyntaxError):
231263
import test.test_import.data.lazy_imports.lazy_import_func
232264

265+
def test_lazy_import_exec_in_function(self):
266+
"""lazy import via exec() inside a function should raise SyntaxError."""
267+
# exec() inside a function creates a non-module-level context
268+
# where lazy imports are not allowed
269+
def f():
270+
exec("lazy import json")
271+
272+
with self.assertRaises(SyntaxError) as cm:
273+
f()
274+
self.assertIn("only allowed at module level", str(cm.exception))
275+
276+
def test_lazy_import_exec_at_module_level(self):
277+
"""lazy import via exec() at module level should work."""
278+
# exec() at module level (globals == locals) should allow lazy imports
279+
code = textwrap.dedent("""
280+
import sys
281+
exec("lazy import json")
282+
# Should be lazy - not loaded yet
283+
assert 'json' not in sys.modules
284+
print("OK")
285+
""")
286+
result = subprocess.run(
287+
[sys.executable, "-c", code],
288+
capture_output=True,
289+
text=True
290+
)
291+
self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}")
292+
self.assertIn("OK", result.stdout)
293+
233294

234295
class EagerImportInLazyModeTests(unittest.TestCase):
235296
"""Tests for imports that should remain eager even in lazy mode."""

Objects/lazyimportobject.c

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,14 @@ _PyLazyImport_New(PyObject *builtins, PyObject *from, PyObject *attr)
1818
PyErr_BadArgument();
1919
return NULL;
2020
}
21-
if (attr == Py_None) {
21+
if (attr == Py_None || attr == NULL) {
2222
attr = NULL;
2323
}
24-
assert(!attr || PyObject_IsTrue(attr));
24+
else if (!PyUnicode_Check(attr) && !PyTuple_Check(attr)) {
25+
PyErr_SetString(PyExc_TypeError,
26+
"lazy_import: fromlist must be None, a string, or a tuple");
27+
return NULL;
28+
}
2529
m = PyObject_GC_New(PyLazyImportObject, &PyLazyImport_Type);
2630
if (m == NULL) {
2731
return NULL;

Python/import.c

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4358,7 +4358,12 @@ _PyImport_LazyImportModuleLevelObject(PyThreadState *tstate,
43584358

43594359
PyInterpreterState *interp = tstate->interp;
43604360
_PyInterpreterFrame *frame = _PyEval_GetFrame();
4361-
assert(frame != NULL && frame->f_globals == frame->f_locals); // should only be called in global scope
4361+
if (frame == NULL || frame->f_globals != frame->f_locals) {
4362+
Py_DECREF(abs_name);
4363+
PyErr_SetString(PyExc_SyntaxError,
4364+
"'lazy import' is only allowed at module level");
4365+
return NULL;
4366+
}
43624367

43634368
// Check if the filter disables the lazy import.
43644369
// We must hold a reference to the filter while calling it to prevent

0 commit comments

Comments
 (0)