Skip to content

Commit

Permalink
refactor: Safely parse annotations and values
Browse files Browse the repository at this point in the history
Previously, trying to unparse unsupported nodes
or badly-supported ones would raise exceptions,
crashing the program and preventing to extract
any data. We know catch such exceptions and log
errors instead.
  • Loading branch information
pawamoy committed Jun 27, 2022
1 parent 245daea commit b023e2b
Show file tree
Hide file tree
Showing 4 changed files with 62 additions and 19 deletions.
4 changes: 2 additions & 2 deletions src/griffe/agents/inspector.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@

from griffe.agents.base import BaseInspector
from griffe.agents.extensions import Extensions
from griffe.agents.nodes import ObjectKind, ObjectNode, get_annotation
from griffe.agents.nodes import ObjectKind, ObjectNode, safe_get_annotation
from griffe.collections import LinesCollection
from griffe.dataclasses import (
Alias,
Expand Down Expand Up @@ -479,4 +479,4 @@ def _convert_object_to_annotation(obj, parent):
annotation_node = compile(obj, mode="eval", filename="<>", flags=ast.PyCF_ONLY_AST, optimize=2)
except SyntaxError:
return obj
return get_annotation(annotation_node.body, parent=parent)
return safe_get_annotation(annotation_node.body, parent=parent)
47 changes: 45 additions & 2 deletions src/griffe/agents/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -778,6 +778,28 @@ def get_annotation(node: AST | None, parent: Module | Class) -> str | Name | Exp
return _get_annotation(node, parent)


def safe_get_annotation(node: AST | None, parent: Module | Class) -> str | Name | Expression | None:
"""Safely (no exception) extract a resolvable annotation.
Parameters:
node: The annotation node.
parent: The parent used to resolve the name.
Returns:
A string or resovable name or expression.
"""
try:
return get_annotation(node, parent)
except Exception as error:
message = f"Failed to parse annotation from '{node.__class__.__name__}' node"
with suppress(Exception):
message += f" at {parent.relative_filepath}:{node.lineno}" # type: ignore[union-attr]
if not isinstance(error, KeyError):
message += f": {error}"
logger.error(message)
return None


# ==========================================================
# docstrings
def get_docstring(
Expand Down Expand Up @@ -1186,10 +1208,10 @@ def _get_value(node: AST) -> str:


def get_value(node: AST | None) -> str | None:
"""Extract a complex value as a string.
"""Unparse a node to its string representation.
Parameters:
node: The node to extract the value from.
node: The node to unparse.
Returns:
The unparsed code of the node.
Expand All @@ -1199,6 +1221,27 @@ def get_value(node: AST | None) -> str | None:
return _node_value_map[type(node)](node)


def safe_get_value(node: AST | None, filepath: str | Path | None = None) -> str | None:
"""Safely (no exception) unparse a node to its string representation.
Parameters:
node: The node to unparse.
filepath: An optional filepath from where the node comes.
Returns:
The unparsed code of the node.
"""
try:
return get_value(node)
except Exception as error:
message = f"Failed to unparse node {node}"
if filepath:
message += f" at {filepath}:{node.lineno}" # type: ignore[union-attr]
message += f": {error}"
logger.error(message)
return None


# ==========================================================
# names
def _get_attribute_name(node: NodeAttribute) -> str:
Expand Down
25 changes: 13 additions & 12 deletions src/griffe/agents/visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@
get_instance_names,
get_names,
get_parameter_default,
get_value,
parse__all__,
relative_to_absolute,
safe_get_annotation,
safe_get_value,
)
from griffe.collections import LinesCollection, ModulesCollection
from griffe.dataclasses import (
Expand Down Expand Up @@ -226,7 +227,7 @@ def visit_classdef(self, node: ast.ClassDef) -> None:
for decorator_node in node.decorator_list:
decorators.append(
Decorator(
get_value(decorator_node), # type: ignore[arg-type]
safe_get_value(decorator_node, self.current.relative_filepath), # type: ignore[arg-type]
lineno=decorator_node.lineno,
endlineno=decorator_node.end_lineno, # type: ignore[attr-defined]
)
Expand All @@ -238,7 +239,7 @@ def visit_classdef(self, node: ast.ClassDef) -> None:
bases = []
if node.bases:
for base in node.bases:
bases.append(get_annotation(base, self.current))
bases.append(safe_get_annotation(base, parent=self.current))

class_ = Class(
name=node.name,
Expand Down Expand Up @@ -316,7 +317,7 @@ def handle_function(self, node: ast.AsyncFunctionDef | ast.FunctionDef, labels:
if node.decorator_list:
lineno = node.decorator_list[0].lineno
for decorator_node in node.decorator_list:
decorator_value = get_value(decorator_node)
decorator_value = safe_get_value(decorator_node, self.filepath)
overload = (
decorator_value == "typing.overload"
or decorator_value == "overload"
Expand Down Expand Up @@ -368,12 +369,12 @@ def handle_function(self, node: ast.AsyncFunctionDef | ast.FunctionDef, labels:
kind: ParameterKind
arg_default: ast.AST | None
for (arg, kind), arg_default in args_kinds_defaults:
annotation = get_annotation(arg.annotation, parent=self.current)
annotation = safe_get_annotation(arg.annotation, parent=self.current)
default = get_parameter_default(arg_default, self.filepath, self.lines_collection)
parameters.add(Parameter(arg.arg, annotation=annotation, kind=kind, default=default))

if node.args.vararg:
annotation = get_annotation(node.args.vararg.annotation, parent=self.current)
annotation = safe_get_annotation(node.args.vararg.annotation, parent=self.current)
parameters.add(
Parameter(
f"*{node.args.vararg.arg}",
Expand All @@ -396,14 +397,14 @@ def handle_function(self, node: ast.AsyncFunctionDef | ast.FunctionDef, labels:
kwarg: ast.arg
kwarg_default: ast.AST | None
for kwarg, kwarg_default in kwargs_defaults: # noqa: WPS440
annotation = get_annotation(kwarg.annotation, parent=self.current)
annotation = safe_get_annotation(kwarg.annotation, parent=self.current)
default = get_parameter_default(kwarg_default, self.filepath, self.lines_collection)
parameters.add(
Parameter(kwarg.arg, annotation=annotation, kind=ParameterKind.keyword_only, default=default)
)

if node.args.kwarg:
annotation = get_annotation(node.args.kwarg.annotation, parent=self.current)
annotation = safe_get_annotation(node.args.kwarg.annotation, parent=self.current)
parameters.add(
Parameter(
f"**{node.args.kwarg.arg}",
Expand All @@ -418,7 +419,7 @@ def handle_function(self, node: ast.AsyncFunctionDef | ast.FunctionDef, labels:
lineno=lineno,
endlineno=node.end_lineno, # type: ignore[union-attr]
parameters=parameters,
returns=get_annotation(node.returns, parent=self.current),
returns=safe_get_annotation(node.returns, parent=self.current),
decorators=decorators,
docstring=self._get_docstring(node),
runtime=not self.type_guarded,
Expand Down Expand Up @@ -550,7 +551,7 @@ def handle_attribute( # noqa: WPS231
if not names:
return

value = get_value(node.value) # type: ignore[arg-type]
value = safe_get_value(node.value, self.filepath) # type: ignore[arg-type]

try:
docstring = self._get_docstring(node.next, strict=True) # type: ignore[union-attr]
Expand Down Expand Up @@ -599,7 +600,7 @@ def visit_annassign(self, node: ast.AnnAssign) -> None:
Parameters:
node: The node to visit.
"""
self.handle_attribute(node, get_annotation(node.annotation, parent=self.current))
self.handle_attribute(node, safe_get_annotation(node.annotation, parent=self.current))

def visit_augassign(self, node: ast.AugAssign) -> None:
"""Visit an augmented assignment node.
Expand All @@ -625,7 +626,7 @@ def visit_if(self, node: ast.If) -> None:
"""
if isinstance(node.parent, (ast.Module, ast.ClassDef)): # type: ignore[attr-defined]
with suppress(KeyError): # unhandled AST nodes
condition = get_annotation(node.test, self.current)
condition = get_annotation(node.test, parent=self.current)
if str(condition) in {"typing.TYPE_CHECKING", "TYPE_CHECKING"}:
self.type_guarded = True
self.generic_visit(node)
Expand Down
5 changes: 2 additions & 3 deletions src/griffe/docstrings/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from contextlib import suppress
from typing import TYPE_CHECKING, Callable

from griffe.agents.nodes import get_annotation
from griffe.agents.nodes import safe_get_annotation
from griffe.expressions import Expression, Name
from griffe.logger import get_logger

Expand Down Expand Up @@ -55,10 +55,9 @@ def parse_annotation(annotation: str, docstring: Docstring) -> str | Name | Expr
"""
with suppress(
AttributeError, # docstring has no parent that can be used to resolve names
KeyError, # compiled annotation contains unhandled AST nodes
SyntaxError, # annotation contains syntax errors
):
code = compile(annotation, mode="eval", filename="", flags=PyCF_ONLY_AST, optimize=2)
if code.body:
return get_annotation(code.body, parent=docstring.parent) or annotation # type: ignore[arg-type]
return safe_get_annotation(code.body, parent=docstring.parent) or annotation # type: ignore[arg-type]
return annotation

0 comments on commit b023e2b

Please sign in to comment.