Repository: wemake-services/wemake-python-styleguide

Issue:
WPS474 False Positive
### What's wrong

This rule is falsely flagging a second function level import of the same name as a collision. Appears to have been introduced in version 0.19.0, does not happen in 0.18.0.

Example which reproduces the error:
```python
def a():
    from datetime import timedelta

    return timedelta(days=1)


def b():
    from datetime import timedelta  # <-- WPS474 Found import object collision: timedelta

    return timedelta(days=7)
```

### How it should be

It should not be an error

### Flake8 version and plugins

```json
{
  "platform": {
    "python_implementation": "CPython",
    "python_version": "3.11.8",
    "system": "Darwin"
  },
  "plugins": [
    {
      "plugin": "darglint",
      "version": "1.8.1"
    },
    {
      "plugin": "flake8-bandit",
      "version": "4.1.1"
    },
    {
      "plugin": "flake8-broken-line",
      "version": "1.0.0"
    },
    {
      "plugin": "flake8-bugbear",
      "version": "24.4.26"
    },
    {
      "plugin": "flake8-commas",
      "version": "2.1.0"
    },
    {
      "plugin": "flake8-comprehensions",
      "version": "3.14.0"
    },
    {
      "plugin": "flake8-debugger",
      "version": "4.1.2"
    },
    {
      "plugin": "flake8-docstrings",
      "version": "1.7.0"
    },
    {
      "plugin": "flake8-eradicate",
      "version": "1.5.0"
    },
    {
      "plugin": "flake8-isort",
      "version": "6.1.1"
    },
    {
      "plugin": "flake8-quotes",
      "version": "3.4.0"
    },
    {
      "plugin": "flake8-rst-docstrings",
      "version": "0.3.0"
    },
    {
      "plugin": "flake8-string-format",
      "version": "0.3.0"
    },
    {
      "plugin": "mccabe",
      "version": "0.7.0"
    },
    {
      "plugin": "pep8-naming",
      "version": "0.13.3"
    },
    {
      "plugin": "pycodestyle",
      "version": "2.11.1"
    },
    {
      "plugin": "pyflakes",
      "version": "3.2.0"
    },
    {
      "plugin": "wemake-python-styleguide",
      "version": "0.19.2"
    }
  ],
  "version": "7.0.0"
}
```

### pip information

```
(.venv) nparker@Neils-Work-MBP bug_test % pip --version            
pip 24.0 from /Users/nparker/Projects/bug_test/.venv/lib/python3.11/site-packages/pip (python 3.11)
(.venv) nparker@Neils-Work-MBP bug_test % pip freeze               
astor==0.8.1
attrs==23.2.0
bandit==1.7.8
darglint==1.8.1
docutils==0.21.2
eradicate==2.3.0
flake8==7.0.0
flake8-bandit==4.1.1
flake8-broken-line==1.0.0
flake8-bugbear==24.4.26
flake8-commas==2.1.0
flake8-comprehensions==3.14.0
flake8-debugger==4.1.2
flake8-docstrings==1.7.0
flake8-eradicate==1.5.0
flake8-isort==6.1.1
flake8-quotes==3.4.0
flake8-rst-docstrings==0.3.0
flake8-string-format==0.3.0
isort==5.13.2
markdown-it-py==3.0.0
mccabe==0.7.0
mdurl==0.1.2
pbr==6.0.0
pep8-naming==0.13.3
pycodestyle==2.11.1
pydocstyle==6.3.0
pyflakes==3.2.0
Pygments==2.18.0
PyYAML==6.0.1
restructuredtext_lint==1.4.0
rich==13.7.1
snowballstemmer==2.2.0
stevedore==5.2.0
typing_extensions==4.12.0
wemake-python-styleguide==0.19.2
```

### OS information

MacOS 14.5

Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0c65e6e3..390be3f4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -99,6 +99,7 @@ Semantic versioning in our case means:
 - Fixes unnormalized pathes in formatter output
 - Fixes `WPS434` to allow reassignment in a form of `x = [y, *x]`
 - Fixes `WPS221` to ignore PEP695's `TypeAlias` from line complexity checks
+- Fixes `WPS474` to only count import collisions in the same context, #2962
 
 ### Misc
 
diff --git a/scripts/action-config.cfg b/scripts/action-config.cfg
index a9c731ab..eb07d94e 100644
--- a/scripts/action-config.cfg
+++ b/scripts/action-config.cfg
@@ -6,5 +6,5 @@
 # Base flake8 configuration:
 # https://flake8.pycqa.org/en/latest/user/configuration.html
 format = default
-show-source = False
-statistics = False
+show-source = false
+statistics = false
diff --git a/wemake_python_styleguide/visitors/ast/imports.py b/wemake_python_styleguide/visitors/ast/imports.py
index 31c5b796..7a97788b 100644
--- a/wemake_python_styleguide/visitors/ast/imports.py
+++ b/wemake_python_styleguide/visitors/ast/imports.py
@@ -2,7 +2,7 @@ import ast
 from collections import defaultdict
 from collections.abc import Iterable
 from itertools import chain, product
-from typing import Final
+from typing import Final, TypeAlias
 
 from typing_extensions import final
 
@@ -31,6 +31,8 @@ from wemake_python_styleguide.visitors.base import BaseNodeVisitor
 #: We use `.` to separate module names.
 _MODULE_MEMBERS_SEPARATOR: Final = '.'
 
+_NameAndContext: TypeAlias = tuple[str, ast.AST | None]
+
 
 class _BaseImportValidator:
     """Base utility class to separate logic from the visitor."""
@@ -154,7 +156,9 @@ class _ImportCollisionValidator:
         self._imported_names: list[imports.ImportedObjectInfo] = []
         # This helps us to detect cases like:
         # `from x import y, y as z`
-        self._imported_objects: defaultdict[str, set[str]] = defaultdict(set)
+        self._imported_objects: defaultdict[_NameAndContext, set[str]] = (
+            defaultdict(set)
+        )
 
     def validate(self) -> None:
         """Validates that there are no intersecting imported modules."""
@@ -188,11 +192,12 @@ class _ImportCollisionValidator:
         """Extract info needed for validation from ``ast.ImportFrom``."""
         for alias in node.names:
             identifier = imports.get_module_name(node)
-            if alias.name in self._imported_objects[identifier]:
+            context = nodes.get_context(node)
+            if alias.name in self._imported_objects[identifier, context]:
                 self._error_callback(
                     ImportObjectCollisionViolation(node, alias.name),
                 )
-            self._imported_objects[identifier].add(alias.name)
+            self._imported_objects[identifier, context].add(alias.name)
 
             if not alias.asname:
                 self._imported_names.append(


Test patch:
diff --git a/tests/test_visitors/test_ast/test_imports/test_import_object_collision.py b/tests/test_visitors/test_ast/test_imports/test_import_object_collision.py
index 706b8c3c..bd192635 100644
--- a/tests/test_visitors/test_ast/test_imports/test_import_object_collision.py
+++ b/tests/test_visitors/test_ast/test_imports/test_import_object_collision.py
@@ -2,6 +2,7 @@ import pytest
 
 from wemake_python_styleguide.violations.best_practices import (
     ImportObjectCollisionViolation,
+    NestedImportViolation,
 )
 from wemake_python_styleguide.violations.consistency import (
     LocalFolderImportViolation,
@@ -41,6 +42,14 @@ from ..sub import name as alias1
 from ...sub import name as alias2
 """
 
+regression2962 = """
+def first():
+    from package import sub
+
+def second():
+    from package import sub
+"""
+
 # Wrong:
 
 colliding_object_import1 = """
@@ -77,6 +86,7 @@ from ...package import sub as alias
         correct_same_module_imports,
         correct_dot_imports,
         correct_relative_imports,
+        regression2962,
     ],
 )
 def test_correct_imports(
@@ -91,7 +101,14 @@ def test_correct_imports(
     visitor = WrongImportVisitor(default_options, tree=tree)
     visitor.run()
 
-    assert_errors(visitor, [], ignored_types=(LocalFolderImportViolation,))
+    assert_errors(
+        visitor,
+        [],
+        ignored_types=(
+            LocalFolderImportViolation,
+            NestedImportViolation,
+        ),
+    )
 
 
 @pytest.mark.parametrize(
@@ -117,5 +134,5 @@ def test_imports_collision(
     assert_errors(
         visitor,
         [ImportObjectCollisionViolation],
-        ignored_types=(LocalFolderImportViolation,),
+        ignored_types=LocalFolderImportViolation,
     )