Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-pylint for openSUSE:Factory 
checked in at 2026-01-26 12:34:33
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-pylint (Old)
 and      /work/SRC/openSUSE:Factory/.python-pylint.new.1928 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-pylint"

Mon Jan 26 12:34:33 2026 rev:52 rq:1329152 version:4.0.4

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-pylint/python-pylint.changes      
2025-11-18 15:35:43.725039478 +0100
+++ /work/SRC/openSUSE:Factory/.python-pylint.new.1928/python-pylint.changes    
2026-01-26 12:34:39.530019815 +0100
@@ -1,0 +2,12 @@
+Mon Jan 26 08:08:45 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 4.0.4:
+  * Fixed false positive for ``invalid-name`` where module-level
+    constants were incorrectly classified as variables when a
+    class-level attribute with the same name exists.
+  * Closes #10719
+  * Fix a false positive for ``invalid-name`` on an UPPER_CASED
+    name inside an ``if`` branch that assigns an object.
+  * Closes #10745
+
+-------------------------------------------------------------------

Old:
----
  pylint-4.0.3-gh.tar.gz

New:
----
  pylint-4.0.4-gh.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-pylint.spec ++++++
--- /var/tmp/diff_new_pack.KF4g5q/_old  2026-01-26 12:34:40.382055568 +0100
+++ /var/tmp/diff_new_pack.KF4g5q/_new  2026-01-26 12:34:40.382055568 +0100
@@ -1,7 +1,7 @@
 #
 # spec file for package python-pylint
 #
-# Copyright (c) 2025 SUSE LLC and contributors
+# Copyright (c) 2026 SUSE LLC and contributors
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -24,7 +24,7 @@
 %endif
 %{?sle15_python_module_pythons}
 Name:           python-pylint
-Version:        4.0.3
+Version:        4.0.4
 Release:        0
 Summary:        Syntax and style checker for Python code
 License:        GPL-2.0-or-later

++++++ pylint-4.0.3-gh.tar.gz -> pylint-4.0.4-gh.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pylint-4.0.3/custom_dict.txt 
new/pylint-4.0.4/custom_dict.txt
--- old/pylint-4.0.3/custom_dict.txt    2025-11-13 15:56:42.000000000 +0100
+++ new/pylint-4.0.4/custom_dict.txt    2025-11-30 14:24:33.000000000 +0100
@@ -24,6 +24,7 @@
 attr
 attrib
 attrname
+attrs
 backport
 BaseChecker
 basename
@@ -144,6 +145,7 @@
 hashable
 hmac
 html
+iattrs
 idgeneratormixin
 ifexpr
 igetattr
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pylint-4.0.3/doc/whatsnew/4/4.0/index.rst 
new/pylint-4.0.4/doc/whatsnew/4/4.0/index.rst
--- old/pylint-4.0.3/doc/whatsnew/4/4.0/index.rst       2025-11-13 
15:56:42.000000000 +0100
+++ new/pylint-4.0.4/doc/whatsnew/4/4.0/index.rst       2025-11-30 
14:24:33.000000000 +0100
@@ -74,6 +74,24 @@
 
 .. towncrier release notes start
 
+What's new in Pylint 4.0.4?
+--------------------------------
+Release date: 2025-11-30
+
+
+False Positives Fixed
+---------------------
+
+- Fixed false positive for ``invalid-name`` where module-level constants were 
incorrectly classified as variables when a class-level attribute with the same 
name exists.
+
+  Closes #10719 (`#10719 <https://github.com/pylint-dev/pylint/issues/10719>`_)
+
+- Fix a false positive for ``invalid-name`` on an UPPER_CASED name inside an 
``if`` branch that assigns an object.
+
+  Closes #10745 (`#10745 <https://github.com/pylint-dev/pylint/issues/10745>`_)
+
+
+
 What's new in Pylint 4.0.3?
 ---------------------------
 Release date: 2025-11-13
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pylint-4.0.3/pylint/__pkginfo__.py 
new/pylint-4.0.4/pylint/__pkginfo__.py
--- old/pylint-4.0.3/pylint/__pkginfo__.py      2025-11-13 15:56:42.000000000 
+0100
+++ new/pylint-4.0.4/pylint/__pkginfo__.py      2025-11-30 14:24:33.000000000 
+0100
@@ -9,7 +9,7 @@
 
 from __future__ import annotations
 
-__version__ = "4.0.3"
+__version__ = "4.0.4"
 
 
 def get_numversion_from_version(v: str) -> tuple[int, int, int]:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/pylint-4.0.3/pylint/checkers/base/name_checker/checker.py 
new/pylint-4.0.4/pylint/checkers/base/name_checker/checker.py
--- old/pylint-4.0.3/pylint/checkers/base/name_checker/checker.py       
2025-11-13 15:56:42.000000000 +0100
+++ new/pylint-4.0.4/pylint/checkers/base/name_checker/checker.py       
2025-11-30 14:24:33.000000000 +0100
@@ -518,13 +518,12 @@
                         and self._name_regexps["const"].match(node.name) is 
not None
                     ):
                         return
-                    if (
-                        util.Uninferable not in iattrs
-                        and len(iattrs) > 1
-                        and all(
-                            astroid.are_exclusive(*combo)
-                            for combo in itertools.combinations(iattrs, 2)
-                        )
+                    # Do the exclusive assignment analysis on attrs, not 
iattrs.
+                    # iattrs locations could be anywhere (inference result).
+                    attrs = tuple(node.frame().getattr(node.name))
+                    if len(attrs) > 1 and all(
+                        astroid.are_exclusive(*combo)
+                        for combo in itertools.combinations(attrs, 2)
                     ):
                         node_type = "const"
                     if not self._meets_exception_for_non_consts(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pylint-4.0.3/pylint/checkers/utils.py 
new/pylint-4.0.4/pylint/checkers/utils.py
--- old/pylint-4.0.3/pylint/checkers/utils.py   2025-11-13 15:56:42.000000000 
+0100
+++ new/pylint-4.0.4/pylint/checkers/utils.py   2025-11-30 14:24:33.000000000 
+0100
@@ -1843,28 +1843,50 @@
     return False
 
 
+def _is_node_in_same_scope(
+    candidate: nodes.NodeNG, node_scope: nodes.LocalsDictNodeNG
+) -> bool:
+    if isinstance(candidate, (nodes.ClassDef, nodes.FunctionDef)):
+        return candidate.parent is not None and candidate.parent.scope() is 
node_scope
+    return candidate.scope() is node_scope
+
+
+def _is_reassigned_relative_to_current(
+    node: nodes.NodeNG, varname: str, before: bool
+) -> bool:
+    """Check if the given variable name is reassigned in the same scope 
relative to
+    the current node.
+    """
+    node_scope = node.scope()
+    node_lineno = node.lineno
+    if node_lineno is None:
+        return False
+    for a in node_scope.nodes_of_class(
+        (nodes.AssignName, nodes.ClassDef, nodes.FunctionDef)
+    ):
+        if a.name == varname and a.lineno is not None:
+            if before:
+                if a.lineno < node_lineno:
+                    if _is_node_in_same_scope(a, node_scope):
+                        return True
+            elif a.lineno > node_lineno:
+                if _is_node_in_same_scope(a, node_scope):
+                    return True
+    return False
+
+
 def is_reassigned_before_current(node: nodes.NodeNG, varname: str) -> bool:
     """Check if the given variable name is reassigned in the same scope before 
the
     current node.
     """
-    return any(
-        a.name == varname and a.lineno < node.lineno
-        for a in node.scope().nodes_of_class(
-            (nodes.AssignName, nodes.ClassDef, nodes.FunctionDef)
-        )
-    )
+    return _is_reassigned_relative_to_current(node, varname, before=True)
 
 
 def is_reassigned_after_current(node: nodes.NodeNG, varname: str) -> bool:
     """Check if the given variable name is reassigned in the same scope after 
the
     current node.
     """
-    return any(
-        a.name == varname and a.lineno > node.lineno
-        for a in node.scope().nodes_of_class(
-            (nodes.AssignName, nodes.ClassDef, nodes.FunctionDef)
-        )
-    )
+    return _is_reassigned_relative_to_current(node, varname, before=False)
 
 
 def is_deleted_after_current(node: nodes.NodeNG, varname: str) -> bool:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pylint-4.0.3/tbump.toml new/pylint-4.0.4/tbump.toml
--- old/pylint-4.0.3/tbump.toml 2025-11-13 15:56:42.000000000 +0100
+++ new/pylint-4.0.4/tbump.toml 2025-11-30 14:24:33.000000000 +0100
@@ -1,7 +1,7 @@
 github_url = "https://github.com/pylint-dev/pylint";
 
 [version]
-current = "4.0.3"
+current = "4.0.4"
 regex = '''
 ^(?P<major>0|[1-9]\d*)
 \.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pylint-4.0.3/tests/checkers/unittest_utils.py 
new/pylint-4.0.4/tests/checkers/unittest_utils.py
--- old/pylint-4.0.3/tests/checkers/unittest_utils.py   2025-11-13 
15:56:42.000000000 +0100
+++ new/pylint-4.0.4/tests/checkers/unittest_utils.py   2025-11-30 
14:24:33.000000000 +0100
@@ -520,3 +520,105 @@
     )
     assert not utils.is_typing_member(code[0], ("Literal",))
     assert not utils.is_typing_member(code[1], ("Literal",))
+
+
+def test_is_reassigned_after_current_requires_isinstance_check() -> None:
+    tree = astroid.parse(
+        """
+    CONSTANT = 1
+
+    def global_function_assign():
+        global CONSTANT
+        def CONSTANT():
+            pass
+        CONSTANT()
+    """
+    )
+    func = tree.body[1]
+    global_stmt = func.body[0]
+    nested_func = func.body[1]
+
+    assert isinstance(global_stmt, nodes.Global)
+    assert isinstance(nested_func, nodes.FunctionDef)
+
+    node_scope = global_stmt.scope()
+
+    assert nested_func.scope() == nested_func
+    assert nested_func.scope() != node_scope
+
+    assert nested_func.parent.scope() == node_scope
+
+    assert utils.is_reassigned_after_current(global_stmt, "CONSTANT") is True
+
+
+def test_is_reassigned_before_current() -> None:
+    tree = astroid.parse(
+        """
+    x = 1
+    x = 2
+    x = 3
+    """
+    )
+    first_assign = tree.body[0]
+    second_assign = tree.body[1]
+    third_assign = tree.body[2]
+
+    assert isinstance(first_assign, nodes.Assign)
+    assert isinstance(second_assign, nodes.Assign)
+    assert isinstance(third_assign, nodes.Assign)
+
+    third_assign_name = third_assign.targets[0]
+    first_assign_name = first_assign.targets[0]
+
+    assert isinstance(third_assign_name, nodes.AssignName)
+    assert isinstance(first_assign_name, nodes.AssignName)
+
+    assert utils.is_reassigned_before_current(third_assign_name, "x") is True
+    assert utils.is_reassigned_before_current(first_assign_name, "x") is False
+
+
+def test_is_reassigned_after_current_with_assignname() -> None:
+    tree = astroid.parse(
+        """
+    x = 1
+    x = 2
+    x = 3
+    """
+    )
+    first_assign = tree.body[0]
+    second_assign = tree.body[1]
+    third_assign = tree.body[2]
+
+    assert isinstance(first_assign, nodes.Assign)
+    assert isinstance(second_assign, nodes.Assign)
+    assert isinstance(third_assign, nodes.Assign)
+
+    first_assign_name = first_assign.targets[0]
+    third_assign_name = third_assign.targets[0]
+
+    assert isinstance(first_assign_name, nodes.AssignName)
+    assert isinstance(third_assign_name, nodes.AssignName)
+
+    assert utils.is_reassigned_after_current(first_assign_name, "x") is True
+    assert utils.is_reassigned_after_current(third_assign_name, "x") is False
+
+
+def test_is_reassigned_with_node_no_lineno() -> None:
+    tree = astroid.parse(
+        """
+    x = 1
+    x = 2
+    """
+    )
+    first_assign = tree.body[0]
+    first_assign_name = first_assign.targets[0]
+
+    assert isinstance(first_assign_name, nodes.AssignName)
+    original_lineno = first_assign_name.lineno
+    first_assign_name.lineno = None
+
+    try:
+        assert utils.is_reassigned_after_current(first_assign_name, "x") is 
False
+        assert utils.is_reassigned_before_current(first_assign_name, "x") is 
False
+    finally:
+        first_assign_name.lineno = original_lineno
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/pylint-4.0.3/tests/functional/i/invalid/invalid_name/invalid_name_module_level.py
 
new/pylint-4.0.4/tests/functional/i/invalid/invalid_name/invalid_name_module_level.py
--- 
old/pylint-4.0.3/tests/functional/i/invalid/invalid_name/invalid_name_module_level.py
       2025-11-13 15:56:42.000000000 +0100
+++ 
new/pylint-4.0.4/tests/functional/i/invalid/invalid_name/invalid_name_module_level.py
       2025-11-30 14:24:33.000000000 +0100
@@ -44,6 +44,12 @@
     other_const = [3]
 
 
+if CONST:
+    ANOTHER_CONST = A()
+else:
+    ANOTHER_CONST = 5
+
+
 from importlib.metadata import PackageNotFoundError
 from importlib.metadata import version
 
@@ -55,3 +61,15 @@
 
 from typing import Annotated
 IntWithAnnotation = Annotated[int, "anything"]
+
+
+# Regression test for #10719: module-level constants should not be incorrectly
+# classified as variables when a class-level attribute with the same name 
exists.
+class Theme:
+    INPUT = ">>> "
+
+
+INPUT = Theme()
+input = Theme()  # pylint: disable=redefined-builtin
+OUTPUT = Theme()
+output = Theme()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/pylint-4.0.3/tests/functional/n/no/no_dummy_redefined.py 
new/pylint-4.0.4/tests/functional/n/no/no_dummy_redefined.py
--- old/pylint-4.0.3/tests/functional/n/no/no_dummy_redefined.py        
2025-11-13 15:56:42.000000000 +0100
+++ new/pylint-4.0.4/tests/functional/n/no/no_dummy_redefined.py        
2025-11-30 14:24:33.000000000 +0100
@@ -1,4 +1,5 @@
 """Make sure warnings about redefinitions do not trigger for dummy 
variables."""
+# pylint: disable=invalid-name
 
 
 _, INTERESTING = 'a=b'.split('=')
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/pylint-4.0.3/tests/functional/n/no/no_dummy_redefined.txt 
new/pylint-4.0.4/tests/functional/n/no/no_dummy_redefined.txt
--- old/pylint-4.0.3/tests/functional/n/no/no_dummy_redefined.txt       
2025-11-13 15:56:42.000000000 +0100
+++ new/pylint-4.0.4/tests/functional/n/no/no_dummy_redefined.txt       
2025-11-30 14:24:33.000000000 +0100
@@ -1 +1 @@
-redefined-outer-name:11:4:11:9:clobbering:Redefining name 'value' from outer 
scope (line 6):UNDEFINED
+redefined-outer-name:12:4:12:9:clobbering:Redefining name 'value' from outer 
scope (line 7):UNDEFINED

Reply via email to