Skip to content

Commit 781eedb

Browse files
committed
Add PyExc_ImportCycleError and raise it when a cycle is detected
1 parent 00e7800 commit 781eedb

File tree

9 files changed

+57
-15
lines changed

9 files changed

+57
-15
lines changed

Include/internal/pycore_import.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ _PyImport_LazyImportModuleLevelObject(PyThreadState *tstate, PyObject *name, PyO
5757
#define IMPORTS_INIT \
5858
{ \
5959
DLOPENFLAGS_INIT \
60-
.lazy_import_resolution_depth = 0, \
6160
.find_and_load = { \
6261
.header = 1, \
6362
}, \

Include/internal/pycore_interp_structs.h

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -316,8 +316,7 @@ struct _import_state {
316316
PyObject *lazy_import_func;
317317
int lazy_imports_mode;
318318
PyObject *lazy_imports_filter;
319-
/* Counter to prevent recursive lazy import creation */
320-
int lazy_import_resolution_depth;
319+
PyObject *lazy_importing_modules;
321320
/* The global import lock. */
322321
_PyRecursiveMutex lock;
323322
/* diagnostic info in PyImport_ImportModuleLevelObject() */

Include/pyerrors.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,9 @@ PyAPI_DATA(PyObject *) PyExc_EOFError;
9191
PyAPI_DATA(PyObject *) PyExc_FloatingPointError;
9292
PyAPI_DATA(PyObject *) PyExc_OSError;
9393
PyAPI_DATA(PyObject *) PyExc_ImportError;
94+
#if !defined(Py_LIMITED_API)
95+
PyAPI_DATA(PyObject *) PyExc_ImportCycleError;
96+
#endif
9497
#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x03060000
9598
PyAPI_DATA(PyObject *) PyExc_ModuleNotFoundError;
9699
#endif

Lib/_compat_pickle.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@
240240
REVERSE_NAME_MAPPING[('builtins', excname)] = ('exceptions', 'OSError')
241241

242242
PYTHON3_IMPORTERROR_EXCEPTIONS = (
243+
'ImportCycleError',
243244
'ModuleNotFoundError',
244245
)
245246

Lib/test/exception_hierarchy.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ BaseException
1414
├── EOFError
1515
├── ExceptionGroup [BaseExceptionGroup]
1616
├── ImportError
17+
│ └── ImportCycleError
1718
│ └── ModuleNotFoundError
1819
├── LookupError
1920
│ ├── IndexError
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
def f():
2+
import test.test_import.data.lazy_imports.basic2 as basic2
3+
return basic2

Objects/exceptions.c

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1958,6 +1958,12 @@ static PyTypeObject _PyExc_ImportError = {
19581958
};
19591959
PyObject *PyExc_ImportError = (PyObject *)&_PyExc_ImportError;
19601960

1961+
/*
1962+
* ImportCycleError extends ImportError
1963+
*/
1964+
1965+
MiddlingExtendsException(PyExc_ImportError, ImportCycleError, ImportError,
1966+
"Import produces a cycle.");
19611967
/*
19621968
* ModuleNotFoundError extends ImportError
19631969
*/
@@ -4391,6 +4397,7 @@ static struct static_exception static_exceptions[] = {
43914397
{&_PyExc_IncompleteInputError, "_IncompleteInputError"}, // base: SyntaxError(Exception)
43924398
ITEM(IndexError), // base: LookupError(Exception)
43934399
ITEM(KeyError), // base: LookupError(Exception)
4400+
ITEM(ImportCycleError), // base: ImportError(Exception)
43944401
ITEM(ModuleNotFoundError), // base: ImportError(Exception)
43954402
ITEM(NotImplementedError), // base: RuntimeError(Exception)
43964403
ITEM(PythonFinalizationError), // base: RuntimeError(Exception)
@@ -4586,4 +4593,3 @@ _PyException_AddNote(PyObject *exc, PyObject *note)
45864593
Py_XDECREF(r);
45874594
return res;
45884595
}
4589-

Objects/moduleobject.c

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1046,6 +1046,13 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress)
10461046
if (PyLazyImport_CheckExact(attr)) {
10471047
PyObject *new_value = _PyImport_LoadLazyImportTstate(PyThreadState_GET(), attr);
10481048
if (new_value == NULL) {
1049+
if (suppress && PyErr_ExceptionMatches(PyExc_ImportCycleError)) {
1050+
// ImportCycleError is raised when a lazy object tries to import itself.
1051+
// In this case, the error should not propagate to the caller and
1052+
// instead treated as if the attribute doesn't exist.
1053+
PyErr_Clear();
1054+
}
1055+
10491056
return NULL;
10501057
} else if (PyDict_SetItem(m->md_dict, name, new_value) < 0) {
10511058
Py_DECREF(new_value);

Python/import.c

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3710,6 +3710,33 @@ _PyImport_LoadLazyImportTstate(PyThreadState *tstate, PyObject *lazy_import)
37103710

37113711
PyLazyImportObject *lz = (PyLazyImportObject *)lazy_import;
37123712

3713+
// Check if we are already importing this module, if so, then we want to return an error
3714+
// that indicates we've hit a cycle which will indicate the value isn't yet available.
3715+
PyInterpreterState *interp = tstate->interp;
3716+
PyObject *importing = interp->imports.lazy_importing_modules;
3717+
if (importing == NULL) {
3718+
importing = interp->imports.lazy_importing_modules = PySet_New(NULL);
3719+
if (importing == NULL) {
3720+
return NULL;
3721+
}
3722+
}
3723+
3724+
int is_loading = PySet_Contains(importing, lazy_import);
3725+
if (is_loading < 0 ) {
3726+
return NULL;
3727+
} else if (is_loading == 1) {
3728+
PyObject *name = _PyLazyImport_GetName(lazy_import);
3729+
PyObject *errmsg = PyUnicode_FromFormat("cannot import name %R "
3730+
"(most likely due to a circular import)",
3731+
name);
3732+
PyErr_SetImportErrorSubclass(PyExc_ImportCycleError, errmsg, lz->lz_from, NULL);
3733+
Py_XDECREF(errmsg);
3734+
Py_XDECREF(name);
3735+
return NULL;
3736+
} else if (PySet_Add(importing, lazy_import) < 0) {
3737+
goto error;
3738+
}
3739+
37133740
Py_ssize_t dot = -1;
37143741
int full = 0;
37153742
if (lz->lz_attr != NULL) {
@@ -3738,10 +3765,6 @@ _PyImport_LoadLazyImportTstate(PyThreadState *tstate, PyObject *lazy_import)
37383765

37393766
PyObject *globals = PyEval_GetGlobals();
37403767

3741-
// Increment counter to prevent recursive lazy import creation
3742-
PyInterpreterState *interp = tstate->interp;
3743-
interp->imports.lazy_import_resolution_depth++;
3744-
37453768
if (full) {
37463769
obj = _PyEval_ImportNameWithImport(tstate,
37473770
lz->lz_import_func,
@@ -3753,7 +3776,6 @@ _PyImport_LoadLazyImportTstate(PyThreadState *tstate, PyObject *lazy_import)
37533776
} else {
37543777
PyObject *name = PyUnicode_Substring(lz->lz_from, 0, dot);
37553778
if (name == NULL) {
3756-
interp->imports.lazy_import_resolution_depth--;
37573779
goto error;
37583780
}
37593781
obj = _PyEval_ImportNameWithImport(tstate,
@@ -3766,9 +3788,6 @@ _PyImport_LoadLazyImportTstate(PyThreadState *tstate, PyObject *lazy_import)
37663788
Py_DECREF(name);
37673789
}
37683790

3769-
// Decrement counter
3770-
interp->imports.lazy_import_resolution_depth--;
3771-
37723791
if (obj == NULL) {
37733792
goto error;
37743793
}
@@ -3860,6 +3879,11 @@ _PyImport_LoadLazyImportTstate(PyThreadState *tstate, PyObject *lazy_import)
38603879
}
38613880

38623881
ok:
3882+
if (PySet_Discard(importing, lazy_import) < 0) {
3883+
Py_DECREF(obj);
3884+
obj = NULL;
3885+
}
3886+
38633887
Py_XDECREF(fromlist);
38643888
return obj;
38653889
}
@@ -3950,8 +3974,7 @@ PyImport_ImportModuleLevelObject(PyObject *name, PyObject *globals,
39503974
goto error;
39513975
}
39523976

3953-
// Only check __lazy_modules__ if we're not already resolving a lazy import
3954-
if (interp->imports.lazy_import_resolution_depth == 0 && globals != NULL &&
3977+
if (globals != NULL &&
39553978
PyMapping_GetOptionalItem(globals, &_Py_ID(__lazy_modules__), &lazy_modules) < 0) {
39563979
goto error;
39573980
}
@@ -3974,7 +3997,7 @@ PyImport_ImportModuleLevelObject(PyObject *name, PyObject *globals,
39743997
goto error;
39753998
}
39763999

3977-
if (interp->imports.lazy_import_resolution_depth == 0 && lazy_modules != NULL) {
4000+
if (lazy_modules != NULL) {
39784001
// Check and see if the module is opting in w/o syntax for backwards compatibility
39794002
// with older Python versions.
39804003
int contains = PySequence_Contains(lazy_modules, name);

0 commit comments

Comments
 (0)