From 82d1fadebb6eafca23cf979f9fe7bf6e60ad9b7f Mon Sep 17 00:00:00 2001 From: MWR27 <64335495+MWR27@users.noreply.github.com> Date: Wed, 3 Jun 2026 12:32:11 -0400 Subject: [PATCH 1/5] Add warning for implicit relative imports --- Python/import.c | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/Python/import.c b/Python/import.c index b79354b37a40645..1262a1c2b6c9f46 100644 --- a/Python/import.c +++ b/Python/import.c @@ -2247,6 +2247,11 @@ import_module_level(char *name, PyObject *globals, PyObject *locals, Py_ssize_t buflen = 0; PyObject *parent, *head, *next, *tail; + char parentstr[MAXPATHLEN + 1]; /* maybe rename to pkgstr or pkgname? */ + char *orig_name = name; + int is_from_form; + int might_warn_implicit; + if (strchr(name, '/') != NULL #ifdef MS_WINDOWS || strchr(name, '\\') != NULL @@ -2265,6 +2270,13 @@ import_module_level(char *name, PyObject *globals, PyObject *locals, if (parent == NULL) goto error_exit; + might_warn_implicit = level < 0 && + parent != Py_None && + buflen + strlen(name) + 1 <= MAXPATHLEN; + if (might_warn_implicit) { + memcpy(parentstr, buf, buflen + 1); + } + Py_INCREF(parent); head = load_next(parent, level < 0 ? Py_None : parent, &name, buf, &buflen); @@ -2303,6 +2315,35 @@ import_module_level(char *name, PyObject *globals, PyObject *locals, } if (!b) fromlist = NULL; + is_from_form = b; + } + + if (might_warn_implicit) { + char potential_resolved[MAXPATHLEN + 2]; + sprintf(potential_resolved, "%s.%s", parentstr, orig_name); + + if (strcmp(potential_resolved, buf) == 0) { + char msg[524]; + char fix[260]; + if (is_from_form) { + sprintf(msg, + "implicit relative import from '%.200s' resolved to '%.200s'; " + "in 3.x imports are absolute by default, so this may resolve differently", + orig_name, buf); + sprintf(fix, + "use 'from %.200s import ...' if the parent package is intended", buf); + } else { + sprintf(msg, + "implicit relative import '%.200s' resolved to '%.200s'; " + "in 3.x imports are absolute by default, so this may resolve differently", + orig_name, buf); + sprintf(fix, + "use 'import %.200s' if the parent package is intended", buf); + } + if (PyErr_WarnPy3k_WithFix(msg, fix, 1) < 0) { + goto error_exit; + } + } } if (fromlist == NULL) { From 411eee548d121bb82476decd8377c33ad1f16fff Mon Sep 17 00:00:00 2001 From: MWR27 <64335495+MWR27@users.noreply.github.com> Date: Wed, 3 Jun 2026 13:09:33 -0400 Subject: [PATCH 2/5] Fix not recognizing submodules in multiple levels of subpackages --- Python/import.c | 1 + 1 file changed, 1 insertion(+) diff --git a/Python/import.c b/Python/import.c index 1262a1c2b6c9f46..9b8759164d00cb4 100644 --- a/Python/import.c +++ b/Python/import.c @@ -2272,6 +2272,7 @@ import_module_level(char *name, PyObject *globals, PyObject *locals, might_warn_implicit = level < 0 && parent != Py_None && + strchr(name, '.') == NULL && buflen + strlen(name) + 1 <= MAXPATHLEN; if (might_warn_implicit) { memcpy(parentstr, buf, buflen + 1); From 1bbc51aa12c78e697f84f2e1b3dc7b08639342c1 Mon Sep 17 00:00:00 2001 From: MWR27 <64335495+MWR27@users.noreply.github.com> Date: Tue, 9 Jun 2026 18:20:35 -0400 Subject: [PATCH 3/5] Add tests --- Lib/test/test_py3kwarn.py | 54 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_py3kwarn.py b/Lib/test/test_py3kwarn.py index 0aa981089606775..c2963f1317a7bf8 100644 --- a/Lib/test/test_py3kwarn.py +++ b/Lib/test/test_py3kwarn.py @@ -1,11 +1,11 @@ import unittest import sys +import os import contextlib from test.test_support import check_py3k_warnings, CleanImport, run_unittest -from test.script_helper import assert_python_ok import warnings import base64 -from test import test_support +from test import support, test_support, script_helper if not sys.py3kwarning: raise unittest.SkipTest('%s must be run with the -3 flag' % __name__) @@ -61,6 +61,56 @@ def assertWarning(self, _, warning, expected_message): def assertNoWarning(self, _, recorder): self.assertEqual(len(recorder.warnings), 0) + def assertWarningWithFix(self, _, warning, expected_msg, expected_fix): + self.assertTrue(hasattr(warning, 'fix')) + self.assertEqual(str(warning.message) + ': ' + str(warning.fix), expected_msg + ': ' + expected_fix) + + def test_implicit_relative_import(self): + expected_msg = ("implicit relative import 'importee' resolved to 'testpkg.subpkg.importee'; " + "in 3.x imports are absolute by default, so this may resolve differently") + expected_fix = "use 'import testpkg.subpkg.importee' if the parent package is intended" + with self.check_py3k_warnings_with_fix() as w, test_support.temp_dir() as test_dir: + try: + sys.path.append(test_dir) + + pkg_dir = os.path.join(test_dir, 'testpkg') + subpkg_dir = os.path.join(pkg_dir, 'subpkg') + script_helper.make_pkg(pkg_dir) + script_helper.make_pkg(subpkg_dir) + script_helper.make_script(subpkg_dir, 'importee', '') + script_helper.make_script(subpkg_dir, 'importer', 'import importee') + + import testpkg.subpkg.importer + self.assertWarningWithFix(None, w, expected_msg, expected_fix) + finally: + sys.path.remove(test_dir) + + def test_implicit_relative_import_from(self): + expected_msg = ("implicit relative import from 'importee' resolved to 'testpkg2.subpkg.importee'; " + "in 3.x imports are absolute by default, so this may resolve differently") + expected_fix = "use 'from testpkg2.subpkg.importee import ...' if the parent package is intended" + with self.check_py3k_warnings_with_fix() as w, test_support.temp_dir() as test_dir: + try: + sys.path.append(test_dir) + + pkg_dir = os.path.join(test_dir, 'testpkg2') + subpkg_dir = os.path.join(pkg_dir, 'subpkg') + script_helper.make_pkg(pkg_dir) + script_helper.make_pkg(subpkg_dir) + script_helper.make_script(subpkg_dir, 'importee', 'foo = 0') + script_helper.make_script(subpkg_dir, 'importer', 'from importee import foo') + + import testpkg2.subpkg.importer + self.assertWarningWithFix(None, w, expected_msg, expected_fix) + finally: + sys.path.remove(test_dir) + + def test_absolute_import_no_warning(self): + pass + + def test_absolute_import_from_no_warning(self): + pass + def test_backquote(self): expected = 'backquote not supported in 3.x; use repr()' with check_py3k_warnings((expected, SyntaxWarning)): From 09ddb475bb327c780777552b26e84c2a050bf675 Mon Sep 17 00:00:00 2001 From: MWR27 <64335495+MWR27@users.noreply.github.com> Date: Wed, 10 Jun 2026 11:23:19 -0400 Subject: [PATCH 4/5] Implement suggested changes --- Lib/test/test_py3kwarn.py | 2 +- Python/import.c | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_py3kwarn.py b/Lib/test/test_py3kwarn.py index c2963f1317a7bf8..7b2d624b0b523f7 100644 --- a/Lib/test/test_py3kwarn.py +++ b/Lib/test/test_py3kwarn.py @@ -63,7 +63,7 @@ def assertNoWarning(self, _, recorder): def assertWarningWithFix(self, _, warning, expected_msg, expected_fix): self.assertTrue(hasattr(warning, 'fix')) - self.assertEqual(str(warning.message) + ': ' + str(warning.fix), expected_msg + ': ' + expected_fix) + self.assertEqual('{}: {}'.format(warning.message, warning.fix), '{}: {}'.format(warning.message, warning.fix)) def test_implicit_relative_import(self): expected_msg = ("implicit relative import 'importee' resolved to 'testpkg.subpkg.importee'; " diff --git a/Python/import.c b/Python/import.c index 9b8759164d00cb4..7f6731952e6760c 100644 --- a/Python/import.c +++ b/Python/import.c @@ -2247,9 +2247,9 @@ import_module_level(char *name, PyObject *globals, PyObject *locals, Py_ssize_t buflen = 0; PyObject *parent, *head, *next, *tail; - char parentstr[MAXPATHLEN + 1]; /* maybe rename to pkgstr or pkgname? */ + char parentstr[MAXPATHLEN + 1]; char *orig_name = name; - int is_from_form; + int is_from_form = 0; int might_warn_implicit; if (strchr(name, '/') != NULL From d8022d9347b27c0c636745e8857896d2bffc77fe Mon Sep 17 00:00:00 2001 From: MWR27 <64335495+MWR27@users.noreply.github.com> Date: Wed, 10 Jun 2026 11:26:36 -0400 Subject: [PATCH 5/5] assertWarningWithFix hotfix --- Lib/test/test_py3kwarn.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_py3kwarn.py b/Lib/test/test_py3kwarn.py index 7b2d624b0b523f7..a7b5a6ee8ca78ec 100644 --- a/Lib/test/test_py3kwarn.py +++ b/Lib/test/test_py3kwarn.py @@ -63,7 +63,7 @@ def assertNoWarning(self, _, recorder): def assertWarningWithFix(self, _, warning, expected_msg, expected_fix): self.assertTrue(hasattr(warning, 'fix')) - self.assertEqual('{}: {}'.format(warning.message, warning.fix), '{}: {}'.format(warning.message, warning.fix)) + self.assertEqual('{}: {}'.format(warning.message, warning.fix), '{}: {}'.format(expected_msg, expected_fix)) def test_implicit_relative_import(self): expected_msg = ("implicit relative import 'importee' resolved to 'testpkg.subpkg.importee'; "