Skip to content

Commit 9b6dd82

Browse files
committed
Verify nodes' str() and repr() don't raise errors/warnings
Calling str() or repr() on certain nodes fails either with errors or warnings. A unittest was added to verify this behaviour and find the offending nodes. Code has been corrected, mainly by accesing node's attributes safely and using placeholders if necessary. Closes #1881
1 parent b08166b commit 9b6dd82

File tree

4 files changed

+52
-13
lines changed

4 files changed

+52
-13
lines changed

astroid/nodes/node_classes.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -859,33 +859,33 @@ def find_argname(self, argname, rec=False):
859859
return None, None
860860

861861
def get_children(self):
862-
yield from self.posonlyargs or ()
862+
yield from getattr(self, "posonlyargs", ())
863863

864-
for elt in self.posonlyargs_annotations:
864+
for elt in getattr(self, "posonlyargs_annotations", False) or ():
865865
if elt is not None:
866866
yield elt
867867

868-
yield from self.args or ()
868+
yield from getattr(self, "args", ())
869869

870-
if self.defaults is not None:
870+
if getattr(self, "defaults", None) is not None:
871871
yield from self.defaults
872-
yield from self.kwonlyargs
872+
yield from getattr(self, "kwonlyargs", ())
873873

874-
for elt in self.kw_defaults or ():
874+
for elt in getattr(self, "kw_defaults", False) or ():
875875
if elt is not None:
876876
yield elt
877877

878-
for elt in self.annotations:
878+
for elt in getattr(self, "annotations", False) or ():
879879
if elt is not None:
880880
yield elt
881881

882-
if self.varargannotation is not None:
882+
if getattr(self, "varargannotation", None) is not None:
883883
yield self.varargannotation
884884

885-
if self.kwargannotation is not None:
885+
if getattr(self, "kwargannotation", None) is not None:
886886
yield self.kwargannotation
887887

888-
for elt in self.kwonlyargs_annotations:
888+
for elt in getattr(self, "kwonlyargs_annotations", False) or ():
889889
if elt is not None:
890890
yield elt
891891

astroid/nodes/node_ng.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ def __str__(self) -> str:
210210
alignment = len(cname) + 1
211211
result = []
212212
for field in self._other_fields + self._astroid_fields:
213-
value = getattr(self, field)
213+
value = getattr(self, field, "Unknown")
214214
width = 80 - len(field) - alignment
215215
lines = pprint.pformat(value, indent=2, width=width).splitlines(True)
216216

@@ -227,14 +227,18 @@ def __str__(self) -> str:
227227

228228
def __repr__(self) -> str:
229229
rname = self.repr_name()
230+
try:
231+
lineno = self.fromlineno
232+
except AttributeError:
233+
lineno = 0
230234
if rname:
231235
string = "<%(cname)s.%(rname)s l.%(lineno)s at 0x%(id)x>"
232236
else:
233237
string = "<%(cname)s l.%(lineno)s at 0x%(id)x>"
234238
return string % {
235239
"cname": type(self).__name__,
236240
"rname": rname,
237-
"lineno": self.fromlineno,
241+
"lineno": lineno,
238242
"id": id(self),
239243
}
240244

astroid/nodes/scoped_nodes/scoped_nodes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1350,7 +1350,7 @@ def fromlineno(self) -> int:
13501350
# lineno is the line number of the first decorator, we want the def
13511351
# statement lineno. Similar to 'ClassDef.fromlineno'
13521352
lineno = self.lineno or 0
1353-
if self.decorators is not None:
1353+
if getattr(self, "decorators", None) is not None:
13541354
lineno += sum(
13551355
node.tolineno - (node.lineno or 0) + 1 for node in self.decorators.nodes
13561356
)

tests/test_nodes.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
from __future__ import annotations
88

99
import copy
10+
import inspect
1011
import os
12+
import random
1113
import sys
1214
import textwrap
1315
import unittest
@@ -1880,3 +1882,36 @@ def return_from_match(x):
18801882
inferred = node.inferred()
18811883
assert len(inferred) == 2
18821884
assert [inf.value for inf in inferred] == [10, -1]
1885+
1886+
1887+
@pytest.mark.parametrize(
1888+
"node",
1889+
list(
1890+
filter(
1891+
lambda node: node.__name__
1892+
not in ["_BaseContainer", "BaseContainer", "NodeNG", "const_factory"],
1893+
astroid.nodes.ALL_NODE_CLASSES,
1894+
)
1895+
),
1896+
)
1897+
@pytest.mark.filterwarnings("error")
1898+
def test_str_repr_no_warnings(node):
1899+
parameters = inspect.signature(node.__init__).parameters
1900+
1901+
args = {}
1902+
for name, param_type in parameters.items():
1903+
if name == "self":
1904+
continue
1905+
1906+
if "int" in param_type.annotation:
1907+
args[name] = random.randint(0, 50)
1908+
elif "NodeNG" in param_type.annotation:
1909+
args[name] = nodes.Unknown()
1910+
elif "str" in param_type.annotation:
1911+
args[name] = ""
1912+
else:
1913+
args[name] = None
1914+
1915+
test_node = node(**args)
1916+
str(test_node)
1917+
repr(test_node)

0 commit comments

Comments
 (0)