Skip to content

Commit

Permalink
Add the __annotations__ attribute to the ClassDef object mode…
Browse files Browse the repository at this point in the history
…l. (#2431)

* Add the ``__annotations__`` attribute to the ``ClassDef`` object model.

Pylint now does not emit a ``no-member`` error when accessing
``__annotations`` in the following cases:

```
class Test:
    print(__annotations__)
```

```
from typing import TypedDict

OtherTypedDict = TypedDict('OtherTypedDict', {'a': int, 'b': str})
print(OtherTypedDict.__annotations__)
```

Closes pylint-dev/pylint#7126
  • Loading branch information
mbyrnepr2 authored Jul 28, 2024
1 parent e68c016 commit 6f1eb89
Show file tree
Hide file tree
Showing 5 changed files with 55 additions and 2 deletions.
4 changes: 4 additions & 0 deletions astroid/interpreter/objectmodel.py
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,10 @@ def __init__(self):

super().__init__()

@property
def attr___annotations__(self) -> node_classes.Unkown:
return node_classes.Unknown()

@property
def attr___module__(self):
return node_classes.Const(self._instance.root().qname())
Expand Down
5 changes: 4 additions & 1 deletion astroid/nodes/scoped_nodes/scoped_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1946,7 +1946,10 @@ def implicit_locals(self):
"""
locals_ = (("__module__", self.special_attributes.attr___module__),)
# __qualname__ is defined in PEP3155
locals_ += (("__qualname__", self.special_attributes.attr___qualname__),)
locals_ += (
("__qualname__", self.special_attributes.attr___qualname__),
("__annotations__", self.special_attributes.attr___annotations__),
)
return locals_

# pylint: disable=redefined-outer-name
Expand Down
3 changes: 2 additions & 1 deletion tests/test_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -841,12 +841,13 @@ def test_class_locals(self) -> None:
klass1 = module["YO"]
locals1 = klass1.locals
keys = sorted(locals1.keys())
assert_keys = ["__init__", "__module__", "__qualname__", "a"]
assert_keys = ["__annotations__", "__init__", "__module__", "__qualname__", "a"]
self.assertEqual(keys, assert_keys)
klass2 = module["YOUPI"]
locals2 = klass2.locals
keys = locals2.keys()
assert_keys = [
"__annotations__",
"__init__",
"__module__",
"__qualname__",
Expand Down
44 changes: 44 additions & 0 deletions tests/test_object_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -855,3 +855,47 @@ def foo():
assert wrapped.name == "foo"
cache_info = next(ast_nodes[2].infer())
assert isinstance(cache_info, astroid.Instance)


def test_class_annotations() -> None:
"""Test that the `__annotations__` attribute is avaiable in the class scope"""
annotations, klass_attribute = builder.extract_node(
"""
class Test:
__annotations__ #@
Test.__annotations__ #@
"""
)
# Test that `__annotations__` attribute is available in the class scope:
assert isinstance(annotations, nodes.Name)
# The `__annotations__` attribute is `Uninferable`:
assert next(annotations.infer()) is astroid.Uninferable

# Test that we can access the class annotations:
assert isinstance(klass_attribute, nodes.Attribute)


def test_class_annotations_typed_dict() -> None:
"""Test that we can access class annotations on various TypedDicts"""
apple, pear = builder.extract_node(
"""
from typing import TypedDict
class Apple(TypedDict):
a: int
b: str
Pear = TypedDict('OtherTypedDict', {'a': int, 'b': str})
Apple.__annotations__ #@
Pear.__annotations__ #@
"""
)

assert isinstance(apple, nodes.Attribute)
assert next(apple.infer()) is astroid.Uninferable
assert isinstance(pear, nodes.Attribute)
assert next(pear.infer()) is astroid.Uninferable
1 change: 1 addition & 0 deletions tests/test_scoped_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1209,6 +1209,7 @@ def registered(cls, application):
astroid = builder.parse(data, __name__)
cls = astroid["WebAppObject"]
assert_keys = [
"__annotations__",
"__module__",
"__qualname__",
"appli",
Expand Down

0 comments on commit 6f1eb89

Please sign in to comment.