From 5eae215faba239c6bc9eca2e08b3855411595fd4 Mon Sep 17 00:00:00 2001 From: Hashem Date: Tue, 27 Aug 2024 00:28:35 -0400 Subject: [PATCH 1/3] brain_attrs: Support annotation-only members (#2515) Similar to dataclasses, the following class which uses instance variable annotations is valid: ```py @attrs.define class AttrsCls: x: int AttrsCls(1).x ``` However, before this commit astroid failed to transform the class attribute into an instance attribute and this led to `no-member` errors in pylint. Only the new `attrs` API supports this form out-of-the-box, so just address the common case. Closes #2514 --- ChangeLog | 4 ++++ astroid/brain/brain_attrs.py | 14 ++++++++++---- script/.contributors_aliases.json | 4 ++++ tests/brain/test_attr.py | 30 +++++++++++++++++++++++++++++- 4 files changed, 47 insertions(+), 5 deletions(-) diff --git a/ChangeLog b/ChangeLog index d4b2ca2450..fdd60ec09e 100644 --- a/ChangeLog +++ b/ChangeLog @@ -13,6 +13,10 @@ What's New in astroid 3.3.3? ============================ Release date: TBA +* Add annotation-only instance attributes to attrs classes to fix `no-member` false positives. + + Closes #2514 + What's New in astroid 3.3.2? diff --git a/astroid/brain/brain_attrs.py b/astroid/brain/brain_attrs.py index b7a7eafe1b..23ec9f66a4 100644 --- a/astroid/brain/brain_attrs.py +++ b/astroid/brain/brain_attrs.py @@ -24,6 +24,13 @@ "field", ) ) +NEW_ATTRS_NAMES = frozenset( + ( + "attrs.define", + "attrs.mutable", + "attrs.frozen", + ) +) ATTRS_NAMES = frozenset( ( "attr.s", @@ -33,9 +40,7 @@ "attr.define", "attr.mutable", "attr.frozen", - "attrs.define", - "attrs.mutable", - "attrs.frozen", + *NEW_ATTRS_NAMES, ) ) @@ -64,13 +69,14 @@ def attr_attributes_transform(node: ClassDef) -> None: # Prevents https://github.com/pylint-dev/pylint/issues/1884 node.locals["__attrs_attrs__"] = [Unknown(parent=node)] + use_bare_annotations = is_decorated_with_attrs(node, NEW_ATTRS_NAMES) for cdef_body_node in node.body: if not isinstance(cdef_body_node, (Assign, AnnAssign)): continue if isinstance(cdef_body_node.value, Call): if cdef_body_node.value.func.as_string() not in ATTRIB_NAMES: continue - else: + elif not use_bare_annotations: continue targets = ( cdef_body_node.targets diff --git a/script/.contributors_aliases.json b/script/.contributors_aliases.json index 73e9e0db14..53d53f13bb 100644 --- a/script/.contributors_aliases.json +++ b/script/.contributors_aliases.json @@ -85,6 +85,10 @@ "name": "Hippo91", "team": "Maintainers" }, + "Hnasar@users.noreply.github.com": { + "mails": ["Hnasar@users.noreply.github.com", "hashem@hudson-trading.com"], + "name": "Hashem Nasarat" + }, "hugovk@users.noreply.github.com": { "mails": ["hugovk@users.noreply.github.com"], "name": "Hugo van Kemenade" diff --git a/tests/brain/test_attr.py b/tests/brain/test_attr.py index e428b0c8d2..ef4887378f 100644 --- a/tests/brain/test_attr.py +++ b/tests/brain/test_attr.py @@ -7,7 +7,7 @@ import unittest import astroid -from astroid import nodes +from astroid import exceptions, nodes try: import attr # type: ignore[import] # pylint: disable=unused-import @@ -201,3 +201,31 @@ class Foo: """ should_be_unknown = next(astroid.extract_node(code).infer()).getattr("bar")[0] self.assertIsInstance(should_be_unknown, astroid.Unknown) + + def test_attr_with_only_annotation_fails(self) -> None: + code = """ + import attr + + @attr.s + class Foo: + bar: int + Foo() + """ + with self.assertRaises(exceptions.AttributeInferenceError): + next(astroid.extract_node(code).infer()).getattr("bar") + + def test_attrs_with_only_annotation_works(self) -> None: + code = """ + import attrs + + @attrs.define + class Foo: + bar: int + baz: str = "hello" + Foo(1) + """ + for attr_name in ("bar", "baz"): + should_be_unknown = next(astroid.extract_node(code).infer()).getattr( + attr_name + )[0] + self.assertIsInstance(should_be_unknown, astroid.Unknown) From 3840ff6ee550873aac770c7804eaec01e90da54b Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Thu, 19 Sep 2024 18:57:31 -0300 Subject: [PATCH 2/3] Fix inference regression with property setters (#2567) (#2568) Closes pylint-dev/pylint#9811 (cherry picked from commit 5a93a9f9e7e8d1cbcbfc93783e86572b6c678152) --- ChangeLog | 5 ++++- astroid/nodes/scoped_nodes/scoped_nodes.py | 12 +++++++++++- tests/test_inference.py | 17 +++++++++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/ChangeLog b/ChangeLog index fdd60ec09e..fcced3bc2c 100644 --- a/ChangeLog +++ b/ChangeLog @@ -13,12 +13,15 @@ What's New in astroid 3.3.3? ============================ Release date: TBA +* Fix inference regression with property setters. + + Closes pylint-dev/pylint#9811 + * Add annotation-only instance attributes to attrs classes to fix `no-member` false positives. Closes #2514 - What's New in astroid 3.3.2? ============================ Release date: 2024-08-11 diff --git a/astroid/nodes/scoped_nodes/scoped_nodes.py b/astroid/nodes/scoped_nodes/scoped_nodes.py index af3b9d39a8..d733a6ae21 100644 --- a/astroid/nodes/scoped_nodes/scoped_nodes.py +++ b/astroid/nodes/scoped_nodes/scoped_nodes.py @@ -2506,6 +2506,16 @@ def igetattr( if attr.parent and attr.parent.scope() == first_scope ] functions = [attr for attr in attributes if isinstance(attr, FunctionDef)] + setter = None + for function in functions: + dec_names = function.decoratornames(context=context) + for dec_name in dec_names: + if dec_name is util.Uninferable: + continue + if dec_name.split(".")[-1] == "setter": + setter = function + if setter: + break if functions: # Prefer only the last function, unless a property is involved. last_function = functions[-1] @@ -2529,7 +2539,7 @@ def igetattr( elif isinstance(inferred, objects.Property): function = inferred.function if not class_context: - if not context.callcontext: + if not context.callcontext and not setter: context.callcontext = CallContext( args=function.args.arguments, callee=function ) diff --git a/tests/test_inference.py b/tests/test_inference.py index a8b11b1614..185ee0f650 100644 --- a/tests/test_inference.py +++ b/tests/test_inference.py @@ -4416,6 +4416,23 @@ def func(): inferred = list(node.inferred()) assert [const.value for const in inferred] == [42, False] + def test_infer_property_setter(self) -> None: + node = extract_node( + """ + class PropertyWithSetter: + @property + def host(self): + return self._host + + @host.setter + def host(self, value: str): + self._host = value + + PropertyWithSetter().host #@ + """ + ) + assert not isinstance(next(node.infer()), Instance) + def test_delayed_attributes_without_slots(self) -> None: ast_node = extract_node( """ From a01a9c9fb8f7748f915562cc992c68772d98d2dd Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Fri, 20 Sep 2024 06:30:48 -0400 Subject: [PATCH 3/3] Bump astroid to 3.3.3, update changelog --- CONTRIBUTORS.txt | 1 + ChangeLog | 8 +++++++- astroid/__pkginfo__.py | 2 +- script/.contributors_aliases.json | 8 ++++---- tbump.toml | 2 +- 5 files changed, 14 insertions(+), 7 deletions(-) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 04345d7cb5..57270668c0 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -209,3 +209,4 @@ under this name, or we did not manage to find their commits in the history. - correctmost <134317971+correctmost@users.noreply.github.com> - Oleh Prypin - Eric Vergnaud +- Hashem Nasarat diff --git a/ChangeLog b/ChangeLog index fcced3bc2c..1ad303a154 100644 --- a/ChangeLog +++ b/ChangeLog @@ -9,10 +9,16 @@ Release date: TBA -What's New in astroid 3.3.3? +What's New in astroid 3.3.4? ============================ Release date: TBA + + +What's New in astroid 3.3.3? +============================ +Release date: 2024-09-20 + * Fix inference regression with property setters. Closes pylint-dev/pylint#9811 diff --git a/astroid/__pkginfo__.py b/astroid/__pkginfo__.py index 99c1a03511..5308ae2a47 100644 --- a/astroid/__pkginfo__.py +++ b/astroid/__pkginfo__.py @@ -2,5 +2,5 @@ # For details: https://github.com/pylint-dev/astroid/blob/main/LICENSE # Copyright (c) https://github.com/pylint-dev/astroid/blob/main/CONTRIBUTORS.txt -__version__ = "3.3.2" +__version__ = "3.3.3" version = __version__ diff --git a/script/.contributors_aliases.json b/script/.contributors_aliases.json index 53d53f13bb..1dc38a4870 100644 --- a/script/.contributors_aliases.json +++ b/script/.contributors_aliases.json @@ -22,6 +22,10 @@ "mails": ["55152140+jayaddison@users.noreply.github.com", "jay@jp-hosting.net"], "name": "James Addison" }, + "Hnasar@users.noreply.github.com": { + "mails": ["Hnasar@users.noreply.github.com", "hashem@hudson-trading.com"], + "name": "Hashem Nasarat" + }, "adam.grant.hendry@gmail.com": { "mails": ["adam.grant.hendry@gmail.com"], "name": "Adam Hendry" @@ -85,10 +89,6 @@ "name": "Hippo91", "team": "Maintainers" }, - "Hnasar@users.noreply.github.com": { - "mails": ["Hnasar@users.noreply.github.com", "hashem@hudson-trading.com"], - "name": "Hashem Nasarat" - }, "hugovk@users.noreply.github.com": { "mails": ["hugovk@users.noreply.github.com"], "name": "Hugo van Kemenade" diff --git a/tbump.toml b/tbump.toml index 83d0b32940..4b3f2f246a 100644 --- a/tbump.toml +++ b/tbump.toml @@ -1,7 +1,7 @@ github_url = "https://github.com/pylint-dev/astroid" [version] -current = "3.3.2" +current = "3.3.3" regex = ''' ^(?P0|[1-9]\d*) \.