Skip to content

Fix Invalid no-name-in-module when shadowing a base module with an alias then calling a method named format on that alias. #10409

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 12 commits into from
3 changes: 3 additions & 0 deletions doc/whatsnew/fragments/10193.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix incorrect `no-name-in-module` when calling methods on shadowed imports (e.g. `import pkg.sub as pkg`).

Closes #10193
25 changes: 24 additions & 1 deletion pylint/checkers/variables.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,20 @@ class C: ...
def _infer_name_module(node: nodes.Import, name: str) -> Generator[InferenceResult]:
context = astroid.context.InferenceContext()
context.lookupname = name
return node.infer(context, asname=False) # type: ignore[no-any-return]

if (
len(node.names) == 1
and node.names[0][1]
and "." in node.names[0][0]
and node.names[0][0].startswith(node.names[0][1] + ".")
):

module = next(node.infer(context, asname=False), None)
if isinstance(module, nodes.Module):
yield module
return

yield from node.infer(context, asname=False)


def _fix_dot_imports(
Expand Down Expand Up @@ -3148,6 +3161,16 @@ def _check_module_attrs(
"""Check that module_names (list of string) are accessible through the
given module, if the latest access name corresponds to a module, return it.
"""
if isinstance(node, nodes.Import) and module_names:
for alias, asname in node.names:
if (
asname
and "." in alias
and alias.startswith(asname + ".")
and module_names[0] == alias.split(".")[-1]
):
module_names.pop(0)
break
while module_names:
name = module_names.pop(0)
if name == "__dict__":
Expand Down
97 changes: 97 additions & 0 deletions tests/checkers/unittest_import_module_shadowing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE
# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt

# pylint: disable=missing-docstring,consider-using-with,trailing-whitespace,unused-import

Check notice on line 5 in tests/checkers/unittest_import_module_shadowing.py

View workflow job for this annotation

GitHub Actions / pylint

I0021

Useless suppression of 'unused-import'

Check notice on line 5 in tests/checkers/unittest_import_module_shadowing.py

View workflow job for this annotation

GitHub Actions / pylint

I0021

Useless suppression of 'trailing-whitespace'

import os
import tempfile
import unittest

from pylint import lint
from pylint.testutils import GenericTestReporter


class ModuleShadowingTest(unittest.TestCase):

def setUp(self) -> None:
self.tempdir = tempfile.TemporaryDirectory()
self.addCleanup(self.tempdir.cleanup)
self.pkg_dir = os.path.join(self.tempdir.name, "my_module")
os.makedirs(self.pkg_dir)

with open(
os.path.join(self.pkg_dir, "__init__.py"), "w", encoding="utf-8"
) as f:
f.write("")

self.utils_file = os.path.join(self.pkg_dir, "utils.py")
with open(self.utils_file, "w", encoding="utf-8") as f:
f.write("def format():\n pass\n\ndef other_method():\n pass\n")

self.test_file = os.path.join(self.tempdir.name, "main.py")

def _run_pylint(self, code: str) -> list[Message]:

Check failure on line 34 in tests/checkers/unittest_import_module_shadowing.py

View workflow job for this annotation

GitHub Actions / pylint

E0602

Undefined variable 'Message'
with open(self.test_file, "w", encoding="utf-8") as f:
f.write(code)

reporter = GenericTestReporter()
lint.Run(
[
"--disable=all",
"--enable=no-name-in-module",
"--persistent=no",
"--rcfile=",
self.test_file,
],
reporter=reporter,
exit=False,
)
return reporter.messages

def test_shadowed_format_call(self) -> None:
code = "import my_module.utils as my_module\nmy_module.format()\n"
messages = self._run_pylint(code)
errors = [msg for msg in messages if msg.msg_id == "E0611"]
self.assertEqual(len(errors), 0)

def test_early_module_yield(self) -> None:
code = """
import my_module.utils as my_module
x = my_module.other_method()
y = [my_module.format() for _ in range(1)]
"""
messages = self._run_pylint(code)
errors = [msg for msg in messages if msg.msg_id == "E0611"]
self.assertEqual(len(errors), 0)

def test_nested_shadowed_imports(self) -> None:
code = """
import my_module.submodule.utils as my_module
my_module.format()
"""
messages = self._run_pylint(code)
errors = [msg for msg in messages if msg.msg_id == "E0611"]
self.assertEqual(len(errors), 0)

def test_access_module_dict(self) -> None:
code = "import my_module.utils as utils\nprint(utils.__dict__)\n"
messages = self._run_pylint(code)
errors = [msg for msg in messages if msg.msg_id == "E0611"]
self.assertEqual(len(errors), 0)

def test_non_shadowed_import(self) -> None:
code = "import my_module.utils as utils\nutils.format()\n"
messages = self._run_pylint(code)
errors = [msg for msg in messages if msg.msg_id == "E0611"]
self.assertEqual(len(errors), 0)

def test_shadowed_import_without_call(self) -> None:
code = "import my_module.utils as my_module\n"
messages = self._run_pylint(code)
errors = [msg for msg in messages if msg.msg_id == "E0611"]
self.assertEqual(len(errors), 0)


if __name__ == "__main__":
unittest.main()
Loading