Skip to content

Commit 98a090a

Browse files
Merge branch 'main' into namedtuple
2 parents be55cb9 + 8f477fd commit 98a090a

11 files changed

+276
-24
lines changed

ChangeLog

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,17 @@ Release date: TBA
2727

2828
Closes PyCQA/pylint#5771
2929

30+
* Fixed a crash inferring on a ``NewType`` named with an f-string.
31+
32+
Closes PyCQA/pylint#5770
33+
3034
* Add support for [attrs v21.3.0](https://github.com/python-attrs/attrs/releases/tag/21.3.0) which
3135
added a new `attrs` module alongside the existing `attr`.
3236

3337
Closes #1330
3438

39+
* Add ``is_dataclass`` attribute to ``ClassDef`` nodes.
40+
3541
* Use ``sysconfig`` instead of ``distutils`` to determine the location of
3642
python stdlib files and packages.
3743

@@ -44,6 +50,9 @@ Release date: TBA
4450

4551
Closes #1085
4652

53+
* Fix ``ClassDef.fromlineno``. For Python < 3.8 the ``lineno`` attribute includes decorators.
54+
``fromlineno`` should return the line of the ``class`` statement itself.
55+
4756
What's New in astroid 2.9.4?
4857
============================
4958
Release date: TBA

astroid/brain/brain_dataclasses.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ def is_decorated_with_dataclass(node, decorator_names=DATACLASSES_DECORATORS):
6767

6868
def dataclass_transform(node: ClassDef) -> None:
6969
"""Rewrite a dataclass to be easily understood by pylint"""
70+
node.is_dataclass = True
7071

7172
for assign_node in _get_dataclass_attributes(node):
7273
name = assign_node.target.name

astroid/brain/brain_typing.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
Attribute,
3232
Call,
3333
Const,
34+
JoinedStr,
3435
Name,
3536
NodeNG,
3637
Subscript,
@@ -128,6 +129,9 @@ def infer_typing_typevar_or_newtype(node, context_itton=None):
128129
raise UseInferenceDefault
129130
if not node.args:
130131
raise UseInferenceDefault
132+
# Cannot infer from a dynamic class name (f-string)
133+
if isinstance(node.args[0], JoinedStr):
134+
raise UseInferenceDefault
131135

132136
typename = node.args[0].as_string().strip("'")
133137
node = extract_node(TYPING_TYPE_TEMPLATE.format(typename))

astroid/const.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
else:
88
from typing_extensions import Final
99

10+
PY36 = sys.version_info[:2] == (3, 6)
1011
PY38 = sys.version_info[:2] == (3, 8)
1112
PY37_PLUS = sys.version_info >= (3, 7)
1213
PY38_PLUS = sys.version_info >= (3, 8)

astroid/interpreter/_import/util.py

Lines changed: 63 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,70 @@
33
# Copyright (c) 2021 Daniël van Noord <[email protected]>
44
# Copyright (c) 2021 Neil Girdhar <[email protected]>
55

6-
try:
7-
import pkg_resources
8-
except ImportError:
9-
pkg_resources = None # type: ignore[assignment]
106

7+
from importlib import abc, util
8+
9+
from astroid.const import PY36
10+
11+
12+
def _is_old_setuptools_namespace_package(modname: str) -> bool:
13+
"""Check for old types of setuptools namespace packages.
14+
15+
See https://setuptools.pypa.io/en/latest/pkg_resources.html and
16+
https://packaging.python.org/en/latest/guides/packaging-namespace-packages/
17+
18+
Because pkg_resources is slow to import we only do so if explicitly necessary.
19+
"""
20+
try:
21+
import pkg_resources # pylint: disable=import-outside-toplevel
22+
except ImportError:
23+
return False
1124

12-
def is_namespace(modname):
1325
return (
14-
pkg_resources is not None
15-
and hasattr(pkg_resources, "_namespace_packages")
16-
and modname in pkg_resources._namespace_packages
26+
hasattr(pkg_resources, "_namespace_packages")
27+
and modname in pkg_resources._namespace_packages # type: ignore[attr-defined]
28+
)
29+
30+
31+
def is_namespace(modname: str) -> bool:
32+
"""Determine whether we encounter a namespace package."""
33+
if PY36:
34+
# On Python 3.6 an AttributeError is raised when a package
35+
# is lacking a __path__ attribute and thus is not a
36+
# package.
37+
try:
38+
spec = util.find_spec(modname)
39+
except (AttributeError, ValueError):
40+
return _is_old_setuptools_namespace_package(modname)
41+
else:
42+
try:
43+
spec = util.find_spec(modname)
44+
except ValueError:
45+
return _is_old_setuptools_namespace_package(modname)
46+
47+
# If there is no spec or origin this is a namespace package
48+
# See: https://docs.python.org/3/library/importlib.html#importlib.machinery.ModuleSpec.origin
49+
# We assume builtin packages are never namespace
50+
if not spec or not spec.origin or spec.origin == "built-in":
51+
return False
52+
53+
# If there is no loader the package is namespace
54+
# See https://docs.python.org/3/library/importlib.html#importlib.abc.PathEntryFinder.find_loader
55+
if not spec.loader:
56+
return True
57+
# This checks for _frozen_importlib.FrozenImporter, which does not inherit from InspectLoader
58+
if hasattr(spec.loader, "_ORIGIN") and spec.loader._ORIGIN == "frozen":
59+
return False
60+
# Other loaders are namespace packages
61+
if not isinstance(spec.loader, abc.InspectLoader):
62+
return True
63+
64+
# Lastly we check if the package declares itself a namespace package
65+
try:
66+
source = spec.loader.get_source(spec.origin)
67+
# If the loader can't handle the spec, we're dealing with a namespace package
68+
except ImportError:
69+
return False
70+
return bool(
71+
source and "pkg_resources" in source and "declare_namespace(__name__)" in source
1772
)

astroid/nodes/scoped_nodes/scoped_nodes.py

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
from astroid import bases
5656
from astroid import decorators as decorators_mod
5757
from astroid import mixins, util
58-
from astroid.const import NAMEDTUPLE_BASENAMES, PY39_PLUS
58+
from astroid.const import NAMEDTUPLE_BASENAMES, PY38_PLUS, PY39_PLUS
5959
from astroid.context import (
6060
CallContext,
6161
InferenceContext,
@@ -1369,7 +1369,7 @@ def callable(self):
13691369
def argnames(self) -> List[str]:
13701370
"""Get the names of each of the arguments, including that
13711371
of the collections of variable-length arguments ("args", "kwargs",
1372-
etc.), as well as keyword-only arguments.
1372+
etc.), as well as positional-only and keyword-only arguments.
13731373
13741374
:returns: The names of the arguments.
13751375
:rtype: list(str)
@@ -1378,9 +1378,9 @@ def argnames(self) -> List[str]:
13781378
names = _rec_get_names(self.args.arguments)
13791379
else:
13801380
names = []
1381-
names += [elt.name for elt in self.args.kwonlyargs]
13821381
if self.args.vararg:
13831382
names.append(self.args.vararg)
1383+
names += [elt.name for elt in self.args.kwonlyargs]
13841384
if self.args.kwarg:
13851385
names.append(self.args.kwarg)
13861386
return names
@@ -1706,13 +1706,10 @@ def type(
17061706
return type_name
17071707

17081708
@decorators_mod.cachedproperty
1709-
def fromlineno(self):
1710-
"""The first line that this node appears on in the source code.
1711-
1712-
:type: int or None
1713-
"""
1709+
def fromlineno(self) -> Optional[int]:
1710+
"""The first line that this node appears on in the source code."""
17141711
# lineno is the line number of the first decorator, we want the def
1715-
# statement lineno
1712+
# statement lineno. Similar to 'ClassDef.fromlineno'
17161713
lineno = self.lineno
17171714
if self.decorators is not None:
17181715
lineno += sum(
@@ -2134,7 +2131,7 @@ def my_meth(self, arg):
21342131
":type: str"
21352132
),
21362133
)
2137-
_other_fields = ("name", "doc")
2134+
_other_fields = ("name", "doc", "is_dataclass")
21382135
_other_other_fields = ("locals", "_newstyle")
21392136
_newstyle = None
21402137

@@ -2212,6 +2209,9 @@ def __init__(
22122209
:type doc: str or None
22132210
"""
22142211

2212+
self.is_dataclass: bool = False
2213+
"""Whether this class is a dataclass."""
2214+
22152215
super().__init__(
22162216
lineno=lineno,
22172217
col_offset=col_offset,
@@ -2297,6 +2297,21 @@ def _newstyle_impl(self, context=None):
22972297
doc=("Whether this is a new style class or not\n\n" ":type: bool or None"),
22982298
)
22992299

2300+
@decorators_mod.cachedproperty
2301+
def fromlineno(self) -> Optional[int]:
2302+
"""The first line that this node appears on in the source code."""
2303+
if not PY38_PLUS:
2304+
# For Python < 3.8 the lineno is the line number of the first decorator.
2305+
# We want the class statement lineno. Similar to 'FunctionDef.fromlineno'
2306+
lineno = self.lineno
2307+
if self.decorators is not None:
2308+
lineno += sum(
2309+
node.tolineno - node.lineno + 1 for node in self.decorators.nodes
2310+
)
2311+
2312+
return lineno
2313+
return super().fromlineno
2314+
23002315
@decorators_mod.cachedproperty
23012316
def blockstart_tolineno(self):
23022317
"""The line on which the beginning of this block ends.

astroid/test_utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,9 @@ def check_require_version(f):
4848

4949
@functools.wraps(f)
5050
def new_f(*args, **kwargs):
51-
if minver != "0.0.0":
51+
if current <= min_version:
5252
pytest.skip(f"Needs Python > {minver}. Current version is {version}.")
53-
elif maxver != "4.0.0":
53+
elif current > max_version:
5454
pytest.skip(f"Needs Python <= {maxver}. Current version is {version}.")
5555

5656
return new_f

tests/unittest_brain.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,11 @@
5757
from astroid import MANAGER, bases, builder, nodes, objects, test_utils, util
5858
from astroid.bases import Instance
5959
from astroid.const import PY37_PLUS
60-
from astroid.exceptions import AttributeInferenceError, InferenceError
60+
from astroid.exceptions import (
61+
AttributeInferenceError,
62+
InferenceError,
63+
UseInferenceDefault,
64+
)
6165
from astroid.nodes.node_classes import Const
6266
from astroid.nodes.scoped_nodes import ClassDef
6367

@@ -1661,6 +1665,19 @@ def test_typing_types(self) -> None:
16611665
inferred = next(node.infer())
16621666
self.assertIsInstance(inferred, nodes.ClassDef, node.as_string())
16631667

1668+
def test_typing_type_without_tip(self):
1669+
"""Regression test for https://github.com/PyCQA/pylint/issues/5770"""
1670+
node = builder.extract_node(
1671+
"""
1672+
from typing import NewType
1673+
1674+
def make_new_type(t):
1675+
new_type = NewType(f'IntRange_{t}', t) #@
1676+
"""
1677+
)
1678+
with self.assertRaises(UseInferenceDefault):
1679+
astroid.brain.brain_typing.infer_typing_typevar_or_newtype(node.value)
1680+
16641681
def test_namedtuple_nested_class(self):
16651682
result = builder.extract_node(
16661683
"""

tests/unittest_brain_dataclasses.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ class A:
184184
inferred = next(class_def.infer())
185185
assert isinstance(inferred, nodes.ClassDef)
186186
assert inferred.instance_attrs == {}
187+
assert inferred.is_dataclass
187188

188189
# Both the class and instance can still access the attribute
189190
for node in (klass, instance):
@@ -216,6 +217,7 @@ class A:
216217
inferred = next(class_def.infer())
217218
assert isinstance(inferred, nodes.ClassDef)
218219
assert inferred.instance_attrs == {}
220+
assert inferred.is_dataclass
219221

220222
# Both the class and instance can still access the attribute
221223
for node in (klass, instance):
@@ -248,6 +250,7 @@ class A:
248250
inferred = next(class_def.infer())
249251
assert isinstance(inferred, nodes.ClassDef)
250252
assert inferred.instance_attrs == {}
253+
assert inferred.is_dataclass
251254

252255
# Both the class and instance can still access the attribute
253256
for node in (klass, instance):
@@ -666,6 +669,7 @@ class A:
666669
inferred = node.inferred()
667670
assert len(inferred) == 1 and isinstance(inferred[0], nodes.ClassDef)
668671
assert "attribute" in inferred[0].instance_attrs
672+
assert inferred[0].is_dataclass
669673

670674

671675
@parametrize_module
@@ -683,3 +687,30 @@ class A:
683687
inferred = code.inferred()
684688
assert len(inferred) == 1
685689
assert isinstance(inferred[0], nodes.ClassDef)
690+
assert inferred[0].is_dataclass
691+
692+
693+
def test_non_dataclass_is_not_dataclass() -> None:
694+
"""Test that something that isn't a dataclass has the correct attribute."""
695+
module = astroid.parse(
696+
"""
697+
class A:
698+
val: field()
699+
700+
def dataclass():
701+
return
702+
703+
@dataclass
704+
class B:
705+
val: field()
706+
"""
707+
)
708+
class_a = module.body[0].inferred()
709+
assert len(class_a) == 1
710+
assert isinstance(class_a[0], nodes.ClassDef)
711+
assert not class_a[0].is_dataclass
712+
713+
class_b = module.body[2].inferred()
714+
assert len(class_b) == 1
715+
assert isinstance(class_b[0], nodes.ClassDef)
716+
assert not class_b[0].is_dataclass

0 commit comments

Comments
 (0)