From 225b6fc61a03cf55b5039ccd072c4929e4e24a80 Mon Sep 17 00:00:00 2001 From: Chasar Date: Sat, 12 Apr 2025 21:29:37 +0200 Subject: [PATCH 1/8] Use correct file extension --- astroid/modutils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/astroid/modutils.py b/astroid/modutils.py index 29d09f860..0eebe1477 100644 --- a/astroid/modutils.py +++ b/astroid/modutils.py @@ -489,7 +489,7 @@ def get_source_file( """ filename = os.path.abspath(_path_from_filename(filename)) base, orig_ext = os.path.splitext(filename) - if orig_ext == ".pyi" and os.path.exists(f"{base}{orig_ext}"): + if orig_ext != ".pyi" and os.path.exists(f"{base}{orig_ext}"): return f"{base}{orig_ext}" for ext in PY_SOURCE_EXTS_STUBS_FIRST if prefer_stubs else PY_SOURCE_EXTS: source_path = f"{base}.{ext}" From dd049b7076f3d60d710e19087ef61dad999867c9 Mon Sep 17 00:00:00 2001 From: Chasar Date: Sat, 12 Apr 2025 22:48:51 +0200 Subject: [PATCH 2/8] Fix bug in bug fix --- astroid/modutils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/astroid/modutils.py b/astroid/modutils.py index 0eebe1477..56a0e7c57 100644 --- a/astroid/modutils.py +++ b/astroid/modutils.py @@ -489,7 +489,7 @@ def get_source_file( """ filename = os.path.abspath(_path_from_filename(filename)) base, orig_ext = os.path.splitext(filename) - if orig_ext != ".pyi" and os.path.exists(f"{base}{orig_ext}"): + if orig_ext not in PY_SOURCE_EXTS and os.path.exists(f"{base}{orig_ext}"): return f"{base}{orig_ext}" for ext in PY_SOURCE_EXTS_STUBS_FIRST if prefer_stubs else PY_SOURCE_EXTS: source_path = f"{base}.{ext}" From f259d930196486a7c91abbd271a55d57283ae8d0 Mon Sep 17 00:00:00 2001 From: Chasar Date: Sat, 12 Apr 2025 23:59:30 +0200 Subject: [PATCH 3/8] Omitt leading dot in file extension --- astroid/modutils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/astroid/modutils.py b/astroid/modutils.py index 56a0e7c57..db5932c88 100644 --- a/astroid/modutils.py +++ b/astroid/modutils.py @@ -489,6 +489,7 @@ def get_source_file( """ filename = os.path.abspath(_path_from_filename(filename)) base, orig_ext = os.path.splitext(filename) + orig_ext = orig_ext.lstrip(".") if orig_ext not in PY_SOURCE_EXTS and os.path.exists(f"{base}{orig_ext}"): return f"{base}{orig_ext}" for ext in PY_SOURCE_EXTS_STUBS_FIRST if prefer_stubs else PY_SOURCE_EXTS: From 6b07697d119f4a950419c6356148a4559969ab0e Mon Sep 17 00:00:00 2001 From: Chasar Date: Fri, 18 Apr 2025 10:56:51 +0200 Subject: [PATCH 4/8] Add automated tests --- astroid/modutils.py | 4 +- tests/test_modutils.py | 103 ++++++------------ .../pyi_data/find_test/__init__.weird_ext | 0 .../find_test/standalone_file.weird_ext | 0 4 files changed, 38 insertions(+), 69 deletions(-) create mode 100644 tests/testdata/python3/pyi_data/find_test/__init__.weird_ext create mode 100644 tests/testdata/python3/pyi_data/find_test/standalone_file.weird_ext diff --git a/astroid/modutils.py b/astroid/modutils.py index db5932c88..6029e33c1 100644 --- a/astroid/modutils.py +++ b/astroid/modutils.py @@ -490,8 +490,8 @@ def get_source_file( filename = os.path.abspath(_path_from_filename(filename)) base, orig_ext = os.path.splitext(filename) orig_ext = orig_ext.lstrip(".") - if orig_ext not in PY_SOURCE_EXTS and os.path.exists(f"{base}{orig_ext}"): - return f"{base}{orig_ext}" + if orig_ext not in PY_SOURCE_EXTS and os.path.exists(f"{base}.{orig_ext}"): + return f"{base}.{orig_ext}" for ext in PY_SOURCE_EXTS_STUBS_FIRST if prefer_stubs else PY_SOURCE_EXTS: source_path = f"{base}.{ext}" if os.path.exists(source_path): diff --git a/tests/test_modutils.py b/tests/test_modutils.py index e1b4be384..05352f931 100644 --- a/tests/test_modutils.py +++ b/tests/test_modutils.py @@ -3,6 +3,7 @@ # Copyright (c) https://github.com/pylint-dev/astroid/blob/main/CONTRIBUTORS.txt """Unit tests for module modutils (module manipulation utilities).""" + import email import logging import os @@ -47,9 +48,7 @@ def tearDown(self) -> None: del sys.path_importer_cache[k] def test_find_zipped_module(self) -> None: - found_spec = spec.find_spec( - [self.package], [resources.find("data/MyPyPa-0.1.0-py2.5.zip")] - ) + found_spec = spec.find_spec([self.package], [resources.find("data/MyPyPa-0.1.0-py2.5.zip")]) self.assertEqual(found_spec.type, spec.ModuleType.PY_ZIPMODULE) self.assertEqual( found_spec.location.split(os.sep)[-3:], @@ -57,9 +56,7 @@ def test_find_zipped_module(self) -> None: ) def test_find_egg_module(self) -> None: - found_spec = spec.find_spec( - [self.package], [resources.find("data/MyPyPa-0.1.0-py2.5.egg")] - ) + found_spec = spec.find_spec([self.package], [resources.find("data/MyPyPa-0.1.0-py2.5.egg")]) self.assertEqual(found_spec.type, spec.ModuleType.PY_ZIPMODULE) self.assertEqual( found_spec.location.split(os.sep)[-3:], @@ -77,9 +74,7 @@ def test_known_values_load_module_from_name_2(self) -> None: self.assertEqual(modutils.load_module_from_name("os.path"), os.path) def test_raise_load_module_from_name_1(self) -> None: - self.assertRaises( - ImportError, modutils.load_module_from_name, "_this_module_does_not_exist_" - ) + self.assertRaises(ImportError, modutils.load_module_from_name, "_this_module_does_not_exist_") def test_import_dotted_library( @@ -118,9 +113,7 @@ class GetModulePartTest(unittest.TestCase): """Given a dotted name return the module part of the name.""" def test_known_values_get_module_part_1(self) -> None: - self.assertEqual( - modutils.get_module_part("astroid.modutils"), "astroid.modutils" - ) + self.assertEqual(modutils.get_module_part("astroid.modutils"), "astroid.modutils") def test_known_values_get_module_part_2(self) -> None: self.assertEqual( @@ -144,9 +137,7 @@ def test_known_values_get_builtin_module_part(self) -> None: self.assertEqual(modutils.get_module_part("sys.path", "__file__"), "sys") def test_get_module_part_exception(self) -> None: - self.assertRaises( - ImportError, modutils.get_module_part, "unknown.module", modutils.__file__ - ) + self.assertRaises(ImportError, modutils.get_module_part, "unknown.module", modutils.__file__) def test_get_module_part_only_dot(self) -> None: self.assertEqual(modutils.get_module_part(".", modutils.__file__), ".") @@ -169,9 +160,7 @@ def test_import_symlink_with_source_outside_of_path(self) -> None: linked_file_name = "symlinked_file.py" try: os.symlink(tmpfile.name, linked_file_name) - self.assertEqual( - modutils.modpath_from_file(linked_file_name), ["symlinked_file"] - ) + self.assertEqual(modutils.modpath_from_file(linked_file_name), ["symlinked_file"]) finally: os.remove(linked_file_name) @@ -195,9 +184,7 @@ def test_modpath_from_file_path_order(self) -> None: pass # Without additional directory, return relative to tmp_dir - self.assertEqual( - modutils.modpath_from_file(module_file), [sub_dirname, mod_name] - ) + self.assertEqual(modutils.modpath_from_file(module_file), [sub_dirname, mod_name]) # With sub directory as additional directory, return relative to # sub directory @@ -211,9 +198,7 @@ def test_import_symlink_both_outside_of_path(self) -> None: linked_file_name = os.path.join(tempfile.gettempdir(), "symlinked_file.py") try: os.symlink(tmpfile.name, linked_file_name) - self.assertRaises( - ImportError, modutils.modpath_from_file, linked_file_name - ) + self.assertRaises(ImportError, modutils.modpath_from_file, linked_file_name) finally: os.remove(linked_file_name) @@ -315,9 +300,7 @@ def test_unicode_in_package_init(self) -> None: class GetSourceFileTest(unittest.TestCase): def test(self) -> None: filename = _get_file_from_object(os.path) - self.assertEqual( - modutils.get_source_file(os.path.__file__), os.path.normpath(filename) - ) + self.assertEqual(modutils.get_source_file(os.path.__file__), os.path.normpath(filename)) def test_raise(self) -> None: self.assertRaises(modutils.NoSourceFile, modutils.get_source_file, "whatever") @@ -335,6 +318,22 @@ def test_pyi_preferred(self) -> None: os.path.normpath(module) + "i", ) + def test_nonstandard_extension(self) -> None: + package = resources.find("pyi_data/find_test") + modules = [ + os.path.join(package, "__init__.weird_ext"), + os.path.join(package, "standalone_file.weird_ext"), + ] + for module in modules: + self.assertEqual( + modutils.get_source_file(module, prefer_stubs=True), + module, + ) + self.assertEqual( + modutils.get_source_file(module), + module, + ) + class IsStandardModuleTest(resources.SysPathSetup, unittest.TestCase): """ @@ -390,9 +389,7 @@ def test_custom_path(self) -> None: with pytest.warns(DeprecationWarning): assert modutils.is_standard_module("data.module", (datadir,)) with pytest.warns(DeprecationWarning): - assert modutils.is_standard_module( - "data.module", (os.path.abspath(datadir),) - ) + assert modutils.is_standard_module("data.module", (os.path.abspath(datadir),)) # "" will evaluate to cwd with pytest.warns(DeprecationWarning): assert modutils.is_standard_module("data.module", ("",)) @@ -506,16 +503,10 @@ def test_known_values_is_relative_3(self) -> None: self.assertFalse(modutils.is_relative("astroid", astroid.__path__[0])) def test_known_values_is_relative_4(self) -> None: - self.assertTrue( - modutils.is_relative("util", astroid.interpreter._import.spec.__file__) - ) + self.assertTrue(modutils.is_relative("util", astroid.interpreter._import.spec.__file__)) def test_known_values_is_relative_5(self) -> None: - self.assertFalse( - modutils.is_relative( - "objectmodel", astroid.interpreter._import.spec.__file__ - ) - ) + self.assertFalse(modutils.is_relative("objectmodel", astroid.interpreter._import.spec.__file__)) def test_deep_relative(self) -> None: self.assertTrue(modutils.is_relative("ElementTree", xml.etree.__path__[0])) @@ -530,9 +521,7 @@ def test_deep_relative4(self) -> None: self.assertTrue(modutils.is_relative("etree.gibberish", xml.__path__[0])) def test_is_relative_bad_path(self) -> None: - self.assertFalse( - modutils.is_relative("ElementTree", os.path.join(xml.__path__[0], "ftree")) - ) + self.assertFalse(modutils.is_relative("ElementTree", os.path.join(xml.__path__[0], "ftree"))) class GetModuleFilesTest(unittest.TestCase): @@ -578,38 +567,18 @@ def test_load_module_set_attribute(self) -> None: class ExtensionPackageWhitelistTest(unittest.TestCase): def test_is_module_name_part_of_extension_package_whitelist_true(self) -> None: + self.assertTrue(modutils.is_module_name_part_of_extension_package_whitelist("numpy", {"numpy"})) + self.assertTrue(modutils.is_module_name_part_of_extension_package_whitelist("numpy.core", {"numpy"})) self.assertTrue( - modutils.is_module_name_part_of_extension_package_whitelist( - "numpy", {"numpy"} - ) - ) - self.assertTrue( - modutils.is_module_name_part_of_extension_package_whitelist( - "numpy.core", {"numpy"} - ) - ) - self.assertTrue( - modutils.is_module_name_part_of_extension_package_whitelist( - "numpy.core.umath", {"numpy"} - ) + modutils.is_module_name_part_of_extension_package_whitelist("numpy.core.umath", {"numpy"}) ) def test_is_module_name_part_of_extension_package_whitelist_success(self) -> None: + self.assertFalse(modutils.is_module_name_part_of_extension_package_whitelist("numpy", {"numpy.core"})) self.assertFalse( - modutils.is_module_name_part_of_extension_package_whitelist( - "numpy", {"numpy.core"} - ) - ) - self.assertFalse( - modutils.is_module_name_part_of_extension_package_whitelist( - "numpy.core", {"numpy.core.umath"} - ) - ) - self.assertFalse( - modutils.is_module_name_part_of_extension_package_whitelist( - "core.umath", {"numpy"} - ) + modutils.is_module_name_part_of_extension_package_whitelist("numpy.core", {"numpy.core.umath"}) ) + self.assertFalse(modutils.is_module_name_part_of_extension_package_whitelist("core.umath", {"numpy"})) @pytest.mark.skipif(not HAS_URLLIB3_V1, reason="This test requires urllib3 < 2.") diff --git a/tests/testdata/python3/pyi_data/find_test/__init__.weird_ext b/tests/testdata/python3/pyi_data/find_test/__init__.weird_ext new file mode 100644 index 000000000..e69de29bb diff --git a/tests/testdata/python3/pyi_data/find_test/standalone_file.weird_ext b/tests/testdata/python3/pyi_data/find_test/standalone_file.weird_ext new file mode 100644 index 000000000..e69de29bb From 66afe8d3b77edc754d644c1d4d7dcfb50c1e4105 Mon Sep 17 00:00:00 2001 From: Chasar Date: Sat, 19 Apr 2025 06:21:23 +0200 Subject: [PATCH 5/8] Add myself to contributors --- script/.contributors_aliases.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/script/.contributors_aliases.json b/script/.contributors_aliases.json index 7bf909b14..1289a9a9b 100644 --- a/script/.contributors_aliases.json +++ b/script/.contributors_aliases.json @@ -1,4 +1,8 @@ { + "c.ringstrom@gmail.com": { + "mails": ["c.ringstrom@gmail.com"], + "name": "Charlie Ringström" + }, "134317971+correctmost@users.noreply.github.com": { "mails": ["134317971+correctmost@users.noreply.github.com"], "name": "correctmost" From d08e1b302da327d15ca2837b595904ff573d1e11 Mon Sep 17 00:00:00 2001 From: Chasar Date: Sat, 19 Apr 2025 06:29:35 +0200 Subject: [PATCH 6/8] Add bug fix to changelog --- ChangeLog | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ChangeLog b/ChangeLog index d730e8e82..bacf9baa6 100644 --- a/ChangeLog +++ b/ChangeLog @@ -41,6 +41,10 @@ Release date: TBA Closes #2684 +* Fix bug where ``pylint code.custom_extension`` would analyze ``code.py`` or ``code.pyi`` instead if they existed. + + Closes pylint-dev/pylint#3631 + What's New in astroid 3.3.9? ============================ From dd5391c0c4ff2a73100fa836d5a37c4b09f49b54 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 19 Apr 2025 04:30:58 +0000 Subject: [PATCH 7/8] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_modutils.py | 86 ++++++++++++++++++++++++++++++++---------- 1 file changed, 67 insertions(+), 19 deletions(-) diff --git a/tests/test_modutils.py b/tests/test_modutils.py index 05352f931..0f18f8512 100644 --- a/tests/test_modutils.py +++ b/tests/test_modutils.py @@ -48,7 +48,9 @@ def tearDown(self) -> None: del sys.path_importer_cache[k] def test_find_zipped_module(self) -> None: - found_spec = spec.find_spec([self.package], [resources.find("data/MyPyPa-0.1.0-py2.5.zip")]) + found_spec = spec.find_spec( + [self.package], [resources.find("data/MyPyPa-0.1.0-py2.5.zip")] + ) self.assertEqual(found_spec.type, spec.ModuleType.PY_ZIPMODULE) self.assertEqual( found_spec.location.split(os.sep)[-3:], @@ -56,7 +58,9 @@ def test_find_zipped_module(self) -> None: ) def test_find_egg_module(self) -> None: - found_spec = spec.find_spec([self.package], [resources.find("data/MyPyPa-0.1.0-py2.5.egg")]) + found_spec = spec.find_spec( + [self.package], [resources.find("data/MyPyPa-0.1.0-py2.5.egg")] + ) self.assertEqual(found_spec.type, spec.ModuleType.PY_ZIPMODULE) self.assertEqual( found_spec.location.split(os.sep)[-3:], @@ -74,7 +78,9 @@ def test_known_values_load_module_from_name_2(self) -> None: self.assertEqual(modutils.load_module_from_name("os.path"), os.path) def test_raise_load_module_from_name_1(self) -> None: - self.assertRaises(ImportError, modutils.load_module_from_name, "_this_module_does_not_exist_") + self.assertRaises( + ImportError, modutils.load_module_from_name, "_this_module_does_not_exist_" + ) def test_import_dotted_library( @@ -113,7 +119,9 @@ class GetModulePartTest(unittest.TestCase): """Given a dotted name return the module part of the name.""" def test_known_values_get_module_part_1(self) -> None: - self.assertEqual(modutils.get_module_part("astroid.modutils"), "astroid.modutils") + self.assertEqual( + modutils.get_module_part("astroid.modutils"), "astroid.modutils" + ) def test_known_values_get_module_part_2(self) -> None: self.assertEqual( @@ -137,7 +145,9 @@ def test_known_values_get_builtin_module_part(self) -> None: self.assertEqual(modutils.get_module_part("sys.path", "__file__"), "sys") def test_get_module_part_exception(self) -> None: - self.assertRaises(ImportError, modutils.get_module_part, "unknown.module", modutils.__file__) + self.assertRaises( + ImportError, modutils.get_module_part, "unknown.module", modutils.__file__ + ) def test_get_module_part_only_dot(self) -> None: self.assertEqual(modutils.get_module_part(".", modutils.__file__), ".") @@ -160,7 +170,9 @@ def test_import_symlink_with_source_outside_of_path(self) -> None: linked_file_name = "symlinked_file.py" try: os.symlink(tmpfile.name, linked_file_name) - self.assertEqual(modutils.modpath_from_file(linked_file_name), ["symlinked_file"]) + self.assertEqual( + modutils.modpath_from_file(linked_file_name), ["symlinked_file"] + ) finally: os.remove(linked_file_name) @@ -184,7 +196,9 @@ def test_modpath_from_file_path_order(self) -> None: pass # Without additional directory, return relative to tmp_dir - self.assertEqual(modutils.modpath_from_file(module_file), [sub_dirname, mod_name]) + self.assertEqual( + modutils.modpath_from_file(module_file), [sub_dirname, mod_name] + ) # With sub directory as additional directory, return relative to # sub directory @@ -198,7 +212,9 @@ def test_import_symlink_both_outside_of_path(self) -> None: linked_file_name = os.path.join(tempfile.gettempdir(), "symlinked_file.py") try: os.symlink(tmpfile.name, linked_file_name) - self.assertRaises(ImportError, modutils.modpath_from_file, linked_file_name) + self.assertRaises( + ImportError, modutils.modpath_from_file, linked_file_name + ) finally: os.remove(linked_file_name) @@ -300,7 +316,9 @@ def test_unicode_in_package_init(self) -> None: class GetSourceFileTest(unittest.TestCase): def test(self) -> None: filename = _get_file_from_object(os.path) - self.assertEqual(modutils.get_source_file(os.path.__file__), os.path.normpath(filename)) + self.assertEqual( + modutils.get_source_file(os.path.__file__), os.path.normpath(filename) + ) def test_raise(self) -> None: self.assertRaises(modutils.NoSourceFile, modutils.get_source_file, "whatever") @@ -389,7 +407,9 @@ def test_custom_path(self) -> None: with pytest.warns(DeprecationWarning): assert modutils.is_standard_module("data.module", (datadir,)) with pytest.warns(DeprecationWarning): - assert modutils.is_standard_module("data.module", (os.path.abspath(datadir),)) + assert modutils.is_standard_module( + "data.module", (os.path.abspath(datadir),) + ) # "" will evaluate to cwd with pytest.warns(DeprecationWarning): assert modutils.is_standard_module("data.module", ("",)) @@ -503,10 +523,16 @@ def test_known_values_is_relative_3(self) -> None: self.assertFalse(modutils.is_relative("astroid", astroid.__path__[0])) def test_known_values_is_relative_4(self) -> None: - self.assertTrue(modutils.is_relative("util", astroid.interpreter._import.spec.__file__)) + self.assertTrue( + modutils.is_relative("util", astroid.interpreter._import.spec.__file__) + ) def test_known_values_is_relative_5(self) -> None: - self.assertFalse(modutils.is_relative("objectmodel", astroid.interpreter._import.spec.__file__)) + self.assertFalse( + modutils.is_relative( + "objectmodel", astroid.interpreter._import.spec.__file__ + ) + ) def test_deep_relative(self) -> None: self.assertTrue(modutils.is_relative("ElementTree", xml.etree.__path__[0])) @@ -521,7 +547,9 @@ def test_deep_relative4(self) -> None: self.assertTrue(modutils.is_relative("etree.gibberish", xml.__path__[0])) def test_is_relative_bad_path(self) -> None: - self.assertFalse(modutils.is_relative("ElementTree", os.path.join(xml.__path__[0], "ftree"))) + self.assertFalse( + modutils.is_relative("ElementTree", os.path.join(xml.__path__[0], "ftree")) + ) class GetModuleFilesTest(unittest.TestCase): @@ -567,18 +595,38 @@ def test_load_module_set_attribute(self) -> None: class ExtensionPackageWhitelistTest(unittest.TestCase): def test_is_module_name_part_of_extension_package_whitelist_true(self) -> None: - self.assertTrue(modutils.is_module_name_part_of_extension_package_whitelist("numpy", {"numpy"})) - self.assertTrue(modutils.is_module_name_part_of_extension_package_whitelist("numpy.core", {"numpy"})) self.assertTrue( - modutils.is_module_name_part_of_extension_package_whitelist("numpy.core.umath", {"numpy"}) + modutils.is_module_name_part_of_extension_package_whitelist( + "numpy", {"numpy"} + ) + ) + self.assertTrue( + modutils.is_module_name_part_of_extension_package_whitelist( + "numpy.core", {"numpy"} + ) + ) + self.assertTrue( + modutils.is_module_name_part_of_extension_package_whitelist( + "numpy.core.umath", {"numpy"} + ) ) def test_is_module_name_part_of_extension_package_whitelist_success(self) -> None: - self.assertFalse(modutils.is_module_name_part_of_extension_package_whitelist("numpy", {"numpy.core"})) self.assertFalse( - modutils.is_module_name_part_of_extension_package_whitelist("numpy.core", {"numpy.core.umath"}) + modutils.is_module_name_part_of_extension_package_whitelist( + "numpy", {"numpy.core"} + ) + ) + self.assertFalse( + modutils.is_module_name_part_of_extension_package_whitelist( + "numpy.core", {"numpy.core.umath"} + ) + ) + self.assertFalse( + modutils.is_module_name_part_of_extension_package_whitelist( + "core.umath", {"numpy"} + ) ) - self.assertFalse(modutils.is_module_name_part_of_extension_package_whitelist("core.umath", {"numpy"})) @pytest.mark.skipif(not HAS_URLLIB3_V1, reason="This test requires urllib3 < 2.") From e278288e5f1f425b3a0639ec7304d21fc5d206d0 Mon Sep 17 00:00:00 2001 From: Chasar Date: Sat, 19 Apr 2025 06:32:33 +0200 Subject: [PATCH 8/8] Remove accidental newline --- tests/test_modutils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_modutils.py b/tests/test_modutils.py index 0f18f8512..cdd677edd 100644 --- a/tests/test_modutils.py +++ b/tests/test_modutils.py @@ -3,7 +3,6 @@ # Copyright (c) https://github.com/pylint-dev/astroid/blob/main/CONTRIBUTORS.txt """Unit tests for module modutils (module manipulation utilities).""" - import email import logging import os