From efac8053ba88e725d96d89fd58b4b07223c71735 Mon Sep 17 00:00:00 2001 From: Eddy Xu Date: Fri, 1 May 2026 22:18:20 -0400 Subject: [PATCH 1/3] Have the import order issue done --- Lib/test/test_py3kwarn.py | 81 +++++++++++++++++++++++++++++++++++++ Python/import.c | 85 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 166 insertions(+) diff --git a/Lib/test/test_py3kwarn.py b/Lib/test/test_py3kwarn.py index afd3c1b420bce3..d77aa92b811e12 100644 --- a/Lib/test/test_py3kwarn.py +++ b/Lib/test/test_py3kwarn.py @@ -1,9 +1,11 @@ import unittest import sys +import os from test.test_support import check_py3k_warnings, CleanImport, run_unittest import warnings import base64 from test import test_support +from test import script_helper if not sys.py3kwarning: raise unittest.SkipTest('%s must be run with the -3 flag' % __name__) @@ -41,6 +43,43 @@ def assertWarning(self, _, warning, expected_message): def assertNoWarning(self, _, recorder): self.assertEqual(len(recorder.warnings), 0) + def _write_file(self, path, contents): + with open(path, 'w') as f: + f.write(contents) + + def _check_import_order_warning(self, importer_source, expected_message=None, + imported_name='foo', + top_source="WHO = 'TOP_LEVEL_FOO'\n", + sibling_source="WHO = 'PKG_FOO'\n", + package=True): + with test_support.temp_dir() as tmp: + if top_source is not None: + self._write_file(os.path.join(tmp, imported_name + '.py'), + top_source) + + if package: + pkg_dir = os.path.join(tmp, 'pkg') + os.mkdir(pkg_dir) + self._write_file(os.path.join(pkg_dir, '__init__.py'), '') + if sibling_source is not None: + self._write_file(os.path.join(pkg_dir, imported_name + '.py'), + sibling_source) + import_target = 'pkg.importer' + importer_path = os.path.join(pkg_dir, 'importer.py') + else: + import_target = 'importer' + importer_path = os.path.join(tmp, 'importer.py') + + self._write_file(importer_path, importer_source) + rc, out, err = script_helper.assert_python_ok( + '-S', '-3', '-c', 'import %s' % import_target, PYTHONPATH=tmp) + self.assertEqual(rc, 0) + self.assertEqual(out, '') + if expected_message is None: + self.assertNotIn('implicit relative import', err) + else: + self.assertEqual(err.count(expected_message), 1) + def test_backquote(self): expected = 'backquote not supported in 3.x; use repr()' with check_py3k_warnings((expected, SyntaxWarning)): @@ -429,6 +468,48 @@ def test_b16encode_warns(self): expected = "base64.b16encode returns str in Python 2 (bytes in 3.x)" base64.b16encode(b'test') check_py3k_warnings(expected, UserWarning) + + def test_import_order_implicit_import_local_sibling(self): + expected = ("implicit relative import of 'foo' resolved to package " + "sibling 'pkg.foo'; in 3.x imports are absolute by " + "default and this may resolve differently or fail: " + "use 'from . import foo' if the package sibling is " + "intended") + self._check_import_order_warning("import foo\n", expected) + + def test_import_order_implicit_from_import_local_sibling(self): + expected = ("implicit relative import from 'foo' resolved to package " + "sibling 'pkg.foo'; in 3.x imports are absolute by " + "default and this may resolve differently or fail: " + "use 'from .foo import ...' if the package sibling is " + "intended") + self._check_import_order_warning("from foo import WHO\n", expected) + + def test_import_order_implicit_import_stdlib_name_conflict(self): + expected = ("implicit relative import of 'string' resolved to package " + "sibling 'pkg.string'; in 3.x imports are absolute by " + "default and this may resolve differently or fail: " + "use 'from . import string' if the package sibling is " + "intended") + self._check_import_order_warning("import string\n", + expected_message=expected, + imported_name='string', + top_source=None, + sibling_source="WHO = 'PKG_STRING'\n") + + def test_import_order_future_absolute_import_is_not_warned(self): + self._check_import_order_warning( + "from __future__ import absolute_import\nimport foo\n") + + def test_import_order_explicit_relative_import_is_not_warned(self): + self._check_import_order_warning( + "from __future__ import absolute_import\nfrom . import foo\n") + + def test_import_order_top_level_script_is_not_warned(self): + self._check_import_order_warning("import foo\n", package=False) + + def test_import_order_absolute_import_without_sibling_is_not_warned(self): + self._check_import_order_warning("import foo\n", sibling_source=None) class TestStdlibRemovals(unittest.TestCase): diff --git a/Python/import.c b/Python/import.c index b79354b37a4064..a58a6e65edc95e 100644 --- a/Python/import.c +++ b/Python/import.c @@ -2236,6 +2236,10 @@ static int mark_miss(char *name); static int ensure_fromlist(PyObject *mod, PyObject *fromlist, char *buf, Py_ssize_t buflen, int recursive); static PyObject * import_submodule(PyObject *mod, char *name, char *fullname); +static int warn_implicit_relative_sibling(PyObject *module, + const char *imported_name, + const char *package_name, + int from_import); /* The Magnum Opus of dotted-name import :-) */ @@ -2246,6 +2250,10 @@ import_module_level(char *name, PyObject *globals, PyObject *locals, char *buf; Py_ssize_t buflen = 0; PyObject *parent, *head, *next, *tail; + int from_import = 0; + int warn_if_relative_sibling = 0; + char package_name[MAXPATHLEN + 1]; + char imported_name[MAXPATHLEN + 1]; if (strchr(name, '/') != NULL #ifdef MS_WINDOWS @@ -2265,6 +2273,13 @@ import_module_level(char *name, PyObject *globals, PyObject *locals, if (parent == NULL) goto error_exit; + if (level < 0 && parent != Py_None && strchr(name, '.') == NULL && + buflen < sizeof(package_name) && strlen(name) < sizeof(imported_name)) { + strcpy(package_name, buf); + strcpy(imported_name, name); + warn_if_relative_sibling = 1; + } + Py_INCREF(parent); head = load_next(parent, level < 0 ? Py_None : parent, &name, buf, &buflen); @@ -2303,6 +2318,17 @@ import_module_level(char *name, PyObject *globals, PyObject *locals, } if (!b) fromlist = NULL; + else + from_import = 1; + } + + if (warn_if_relative_sibling) { + if (warn_implicit_relative_sibling(head, imported_name, + package_name, from_import) < 0) { + Py_DECREF(tail); + Py_DECREF(head); + goto error_exit; + } } if (fromlist == NULL) { @@ -2325,6 +2351,65 @@ import_module_level(char *name, PyObject *globals, PyObject *locals, return NULL; } +static int +warn_implicit_relative_sibling(PyObject *module, const char *imported_name, + const char *package_name, int from_import) +{ + PyObject *msg = NULL; + PyObject *fix = NULL; + const char *resolved_name; + size_t package_name_len; + int result; + + if (!Py_Py3kWarningFlag || module == NULL || !PyModule_Check(module)) + return 0; + if (imported_name == NULL || package_name == NULL) + return 0; + + resolved_name = PyModule_GetName(module); + if (resolved_name == NULL) { + PyErr_Clear(); + return 0; + } + + package_name_len = strlen(package_name); + if (strncmp(resolved_name, package_name, package_name_len) != 0 || + resolved_name[package_name_len] != '.' || + strcmp(resolved_name + package_name_len + 1, imported_name) != 0) + return 0; + + if (from_import) { + msg = PyString_FromFormat( + "implicit relative import from '%.200s' resolved to package sibling '%.200s'; " + "in 3.x imports are absolute by default and this may resolve differently or fail", + imported_name, resolved_name); + fix = PyString_FromFormat( + "use 'from .%.200s import ...' if the package sibling is intended", + imported_name); + } + else { + msg = PyString_FromFormat( + "implicit relative import of '%.200s' resolved to package sibling '%.200s'; " + "in 3.x imports are absolute by default and this may resolve differently or fail", + imported_name, resolved_name); + fix = PyString_FromFormat( + "use 'from . import %.200s' if the package sibling is intended", + imported_name); + } + if (msg == NULL || fix == NULL) { + Py_XDECREF(msg); + Py_XDECREF(fix); + return -1; + } + + result = PyErr_WarnEx_WithFix(PyExc_DeprecationWarning, + PyString_AsString(msg), + PyString_AsString(fix), 1); + Py_DECREF(msg); + Py_DECREF(fix); + return result; +} + PyObject * PyImport_ImportModuleLevel(char *name, PyObject *globals, PyObject *locals, PyObject *fromlist, int level) From 37eb8f36f7d11453986fe1050dd9b62ae762cf91 Mon Sep 17 00:00:00 2001 From: Eddy Xu Date: Fri, 1 May 2026 22:22:51 -0400 Subject: [PATCH 2/3] fix format --- Lib/test/test_py3kwarn.py | 6 +++--- Python/import.c | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_py3kwarn.py b/Lib/test/test_py3kwarn.py index d77aa92b811e12..a296bb0e9295e9 100644 --- a/Lib/test/test_py3kwarn.py +++ b/Lib/test/test_py3kwarn.py @@ -472,7 +472,7 @@ def test_b16encode_warns(self): def test_import_order_implicit_import_local_sibling(self): expected = ("implicit relative import of 'foo' resolved to package " "sibling 'pkg.foo'; in 3.x imports are absolute by " - "default and this may resolve differently or fail: " + "default and this will resolve differently or fail: " "use 'from . import foo' if the package sibling is " "intended") self._check_import_order_warning("import foo\n", expected) @@ -480,7 +480,7 @@ def test_import_order_implicit_import_local_sibling(self): def test_import_order_implicit_from_import_local_sibling(self): expected = ("implicit relative import from 'foo' resolved to package " "sibling 'pkg.foo'; in 3.x imports are absolute by " - "default and this may resolve differently or fail: " + "default and this will resolve differently or fail: " "use 'from .foo import ...' if the package sibling is " "intended") self._check_import_order_warning("from foo import WHO\n", expected) @@ -488,7 +488,7 @@ def test_import_order_implicit_from_import_local_sibling(self): def test_import_order_implicit_import_stdlib_name_conflict(self): expected = ("implicit relative import of 'string' resolved to package " "sibling 'pkg.string'; in 3.x imports are absolute by " - "default and this may resolve differently or fail: " + "default and this will resolve differently or fail: " "use 'from . import string' if the package sibling is " "intended") self._check_import_order_warning("import string\n", diff --git a/Python/import.c b/Python/import.c index a58a6e65edc95e..97ecdc6a62b8d4 100644 --- a/Python/import.c +++ b/Python/import.c @@ -2381,7 +2381,7 @@ warn_implicit_relative_sibling(PyObject *module, const char *imported_name, if (from_import) { msg = PyString_FromFormat( "implicit relative import from '%.200s' resolved to package sibling '%.200s'; " - "in 3.x imports are absolute by default and this may resolve differently or fail", + "in 3.x imports are absolute by default and this will resolve differently or fail", imported_name, resolved_name); fix = PyString_FromFormat( "use 'from .%.200s import ...' if the package sibling is intended", @@ -2390,7 +2390,7 @@ warn_implicit_relative_sibling(PyObject *module, const char *imported_name, else { msg = PyString_FromFormat( "implicit relative import of '%.200s' resolved to package sibling '%.200s'; " - "in 3.x imports are absolute by default and this may resolve differently or fail", + "in 3.x imports are absolute by default and this will resolve differently or fail", imported_name, resolved_name); fix = PyString_FromFormat( "use 'from . import %.200s' if the package sibling is intended", From 848763f4f30fc9253be6de49f893c719b1ff1738 Mon Sep 17 00:00:00 2001 From: Eddy Xu Date: Fri, 8 May 2026 19:31:10 -0400 Subject: [PATCH 3/3] fix format and style issues. --- Lib/test/test_py3kwarn.py | 19 +++++++------------ Python/import.c | 30 +++++++++++++++--------------- 2 files changed, 22 insertions(+), 27 deletions(-) diff --git a/Lib/test/test_py3kwarn.py b/Lib/test/test_py3kwarn.py index a296bb0e9295e9..49bf2dcb5f1630 100644 --- a/Lib/test/test_py3kwarn.py +++ b/Lib/test/test_py3kwarn.py @@ -43,10 +43,6 @@ def assertWarning(self, _, warning, expected_message): def assertNoWarning(self, _, recorder): self.assertEqual(len(recorder.warnings), 0) - def _write_file(self, path, contents): - with open(path, 'w') as f: - f.write(contents) - def _check_import_order_warning(self, importer_source, expected_message=None, imported_name='foo', top_source="WHO = 'TOP_LEVEL_FOO'\n", @@ -54,23 +50,22 @@ def _check_import_order_warning(self, importer_source, expected_message=None, package=True): with test_support.temp_dir() as tmp: if top_source is not None: - self._write_file(os.path.join(tmp, imported_name + '.py'), - top_source) + script_helper.make_script(tmp, imported_name, top_source) if package: pkg_dir = os.path.join(tmp, 'pkg') os.mkdir(pkg_dir) - self._write_file(os.path.join(pkg_dir, '__init__.py'), '') + script_helper.make_script(pkg_dir, '__init__', '') if sibling_source is not None: - self._write_file(os.path.join(pkg_dir, imported_name + '.py'), - sibling_source) + script_helper.make_script(pkg_dir, imported_name, + sibling_source) import_target = 'pkg.importer' - importer_path = os.path.join(pkg_dir, 'importer.py') + script_helper.make_script(pkg_dir, 'importer', + importer_source) else: import_target = 'importer' - importer_path = os.path.join(tmp, 'importer.py') + script_helper.make_script(tmp, 'importer', importer_source) - self._write_file(importer_path, importer_source) rc, out, err = script_helper.assert_python_ok( '-S', '-3', '-c', 'import %s' % import_target, PYTHONPATH=tmp) self.assertEqual(rc, 0) diff --git a/Python/import.c b/Python/import.c index 97ecdc6a62b8d4..50b4a0092a54cf 100644 --- a/Python/import.c +++ b/Python/import.c @@ -2274,7 +2274,7 @@ import_module_level(char *name, PyObject *globals, PyObject *locals, goto error_exit; if (level < 0 && parent != Py_None && strchr(name, '.') == NULL && - buflen < sizeof(package_name) && strlen(name) < sizeof(imported_name)) { + strlen(name) < sizeof(imported_name)) { strcpy(package_name, buf); strcpy(imported_name, name); warn_if_relative_sibling = 1; @@ -2322,13 +2322,12 @@ import_module_level(char *name, PyObject *globals, PyObject *locals, from_import = 1; } - if (warn_if_relative_sibling) { - if (warn_implicit_relative_sibling(head, imported_name, - package_name, from_import) < 0) { - Py_DECREF(tail); - Py_DECREF(head); - goto error_exit; - } + if (warn_if_relative_sibling && + warn_implicit_relative_sibling(head, imported_name, + package_name, from_import) < 0) { + Py_DECREF(tail); + Py_DECREF(head); + goto error_exit; } if (fromlist == NULL) { @@ -2386,8 +2385,7 @@ warn_implicit_relative_sibling(PyObject *module, const char *imported_name, fix = PyString_FromFormat( "use 'from .%.200s import ...' if the package sibling is intended", imported_name); - } - else { + } else { msg = PyString_FromFormat( "implicit relative import of '%.200s' resolved to package sibling '%.200s'; " "in 3.x imports are absolute by default and this will resolve differently or fail", @@ -2396,11 +2394,8 @@ warn_implicit_relative_sibling(PyObject *module, const char *imported_name, "use 'from . import %.200s' if the package sibling is intended", imported_name); } - if (msg == NULL || fix == NULL) { - Py_XDECREF(msg); - Py_XDECREF(fix); - return -1; - } + if (msg == NULL || fix == NULL) + goto error; result = PyErr_WarnEx_WithFix(PyExc_DeprecationWarning, PyString_AsString(msg), @@ -2408,6 +2403,11 @@ warn_implicit_relative_sibling(PyObject *module, const char *imported_name, Py_DECREF(msg); Py_DECREF(fix); return result; + +error: + Py_XDECREF(msg); + Py_XDECREF(fix); + return -1; } PyObject *