Skip to content

Commit

Permalink
Merge branch 'main' into brain-typing-cast
Browse files Browse the repository at this point in the history
  • Loading branch information
Pierre-Sassoulas authored Jul 12, 2021
2 parents 1d4a2a0 + 820a755 commit e791ed1
Show file tree
Hide file tree
Showing 16 changed files with 253 additions and 46 deletions.
11 changes: 11 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,22 @@ What's New in astroid 2.6.3?
============================
Release date: TBA


* Fix a bad inferenece type for yield values inside of a derived class.

Closes PyCQA/astroid#1090

* Fix a crash when the node is a 'Module' in the brain builtin inference

Closes PyCQA/pylint#4671

* Added support to infer return type of typing.cast()

* Fix issues when inferring match variables

Closes PyCQA/pylint#4685


What's New in astroid 2.6.2?
============================
Release date: 2021-06-30
Expand All @@ -31,6 +41,7 @@ Release date: 2021-06-30
Closes PyCQA/pylint#4631
Closes #1080


What's New in astroid 2.6.1?
============================
Release date: 2021-06-29
Expand Down
2 changes: 1 addition & 1 deletion astroid/as_string.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ def visit_joinedstr(self, node):
# Try to find surrounding quotes that don't appear at all in the string.
# Because the formatted values inside {} can't contain backslash (\)
# using a triple quote is sometimes necessary
for quote in ["'", '"', '"""', "'''"]:
for quote in ("'", '"', '"""', "'''"):
if quote not in string:
break

Expand Down
9 changes: 7 additions & 2 deletions astroid/bases.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
import collections

from astroid import context as contextmod
from astroid import util
from astroid import decorators, util
from astroid.const import BUILTINS, PY310_PLUS
from astroid.exceptions import (
AstroidTypeError,
Expand Down Expand Up @@ -543,9 +543,14 @@ class Generator(BaseInstance):

special_attributes = util.lazy_descriptor(objectmodel.GeneratorModel)

def __init__(self, parent=None):
def __init__(self, parent=None, generator_initial_context=None):
super().__init__()
self.parent = parent
self._call_context = contextmod.copy_context(generator_initial_context)

@decorators.cached
def infer_yield_types(self):
yield from self.parent.infer_yield_result(self._call_context)

def callable(self):
return False
Expand Down
2 changes: 1 addition & 1 deletion astroid/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ def _find_statement_by_line(node, line):
can be found.
:rtype: astroid.bases.NodeNG or None
"""
if isinstance(node, (nodes.ClassDef, nodes.FunctionDef)):
if isinstance(node, (nodes.ClassDef, nodes.FunctionDef, nodes.MatchCase)):
# This is an inaccuracy in the AST: the nodes that can be
# decorated do not carry explicit information on which line
# the actual definition (class/def), but .fromline seems to
Expand Down
6 changes: 3 additions & 3 deletions astroid/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ class Context(enum.Enum):


# TODO Remove in 3.0 in favor of Context
Load = Context.Load # pylint: disable=invalid-name
Store = Context.Store # pylint: disable=invalid-name
Del = Context.Del # pylint: disable=invalid-name
Load = Context.Load
Store = Context.Store
Del = Context.Del

BUILTINS = builtins.__name__ # Could be just 'builtins' ?
2 changes: 1 addition & 1 deletion astroid/interpreter/_import/spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ def find_module(self, modname, module_parts, processed, submodule_path):

for entry in submodule_path:
package_directory = os.path.join(entry, modname)
for suffix in [".py", importlib.machinery.BYTECODE_SUFFIXES[0]]:
for suffix in (".py", importlib.machinery.BYTECODE_SUFFIXES[0]):
package_file_name = "__init__" + suffix
file_path = os.path.join(package_directory, package_file_name)
if os.path.isfile(file_path):
Expand Down
40 changes: 35 additions & 5 deletions astroid/node_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,11 @@
import abc
import itertools
import pprint
import sys
import typing
from functools import lru_cache
from functools import singledispatch as _singledispatch
from typing import ClassVar, Optional
from typing import Callable, ClassVar, Generator, Optional

from astroid import as_string, bases
from astroid import context as contextmod
Expand All @@ -55,10 +56,10 @@
)
from astroid.manager import AstroidManager

try:
if sys.version_info >= (3, 8):
# pylint: disable=no-name-in-module
from typing import Literal
except ImportError:
# typing.Literal was added in Python 3.8
else:
from typing_extensions import Literal


Expand Down Expand Up @@ -1385,7 +1386,6 @@ class Arguments(mixins.AssignTypeMixin, NodeNG):
# - we expose 'annotation', a list with annotations for
# for each normal argument. If an argument doesn't have an
# annotation, its value will be None.
# pylint: disable=too-many-instance-attributes
_astroid_fields = (
"args",
"defaults",
Expand Down Expand Up @@ -5026,6 +5026,16 @@ def postinit(
self.patterns = patterns
self.rest = rest

assigned_stmts: Callable[
[
"MatchMapping",
AssignName,
Optional[contextmod.InferenceContext],
Literal[None],
],
Generator[NodeNG, None, None],
]


class MatchClass(Pattern):
"""Class representing a :class:`ast.MatchClass` node.
Expand Down Expand Up @@ -5098,6 +5108,16 @@ def __init__(
def postinit(self, *, name: Optional[AssignName]) -> None:
self.name = name

assigned_stmts: Callable[
[
"MatchStar",
AssignName,
Optional[contextmod.InferenceContext],
Literal[None],
],
Generator[NodeNG, None, None],
]


class MatchAs(mixins.AssignTypeMixin, Pattern):
"""Class representing a :class:`ast.MatchAs` node.
Expand Down Expand Up @@ -5144,6 +5164,16 @@ def postinit(
self.pattern = pattern
self.name = name

assigned_stmts: Callable[
[
"MatchAs",
AssignName,
Optional[contextmod.InferenceContext],
Literal[None],
],
Generator[NodeNG, None, None],
]


class MatchOr(Pattern):
"""Class representing a :class:`ast.MatchOr` node.
Expand Down
79 changes: 64 additions & 15 deletions astroid/protocols.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
import collections
import itertools
import operator as operator_mod
import sys
from typing import Generator, Optional

from astroid import arguments, bases
from astroid import context as contextmod
Expand All @@ -41,6 +43,12 @@
NoDefault,
)

if sys.version_info >= (3, 8):
# pylint: disable=no-name-in-module
from typing import Literal
else:
from typing_extensions import Literal

raw_building = util.lazy_import("raw_building")
objects = util.lazy_import("objects")

Expand Down Expand Up @@ -481,22 +489,8 @@ def _infer_context_manager(self, mgr, context):
# It doesn't interest us.
raise InferenceError(node=func)

# Get the first yield point. If it has multiple yields,
# then a RuntimeError will be raised.
yield next(inferred.infer_yield_types())

possible_yield_points = func.nodes_of_class(nodes.Yield)
# Ignore yields in nested functions
yield_point = next(
(node for node in possible_yield_points if node.scope() == func), None
)
if yield_point:
if not yield_point.value:
const = nodes.Const(None)
const.parent = yield_point
const.lineno = yield_point.lineno
yield const
else:
yield from yield_point.value.infer(context=context)
elif isinstance(inferred, bases.Instance):
try:
enter = next(inferred.igetattr("__enter__", context=context))
Expand Down Expand Up @@ -785,3 +779,58 @@ def _determine_starred_iteration_lookups(starred, target, lookups):


nodes.Starred.assigned_stmts = starred_assigned_stmts


@decorators.yes_if_nothing_inferred
def match_mapping_assigned_stmts(
self: nodes.MatchMapping,
node: nodes.AssignName,
context: Optional[contextmod.InferenceContext] = None,
assign_path: Literal[None] = None,
) -> Generator[nodes.NodeNG, None, None]:
"""Return empty generator (return -> raises StopIteration) so inferred value
is Uninferable.
"""
return
yield # pylint: disable=unreachable


nodes.MatchMapping.assigned_stmts = match_mapping_assigned_stmts


@decorators.yes_if_nothing_inferred
def match_star_assigned_stmts(
self: nodes.MatchStar,
node: nodes.AssignName,
context: Optional[contextmod.InferenceContext] = None,
assign_path: Literal[None] = None,
) -> Generator[nodes.NodeNG, None, None]:
"""Return empty generator (return -> raises StopIteration) so inferred value
is Uninferable.
"""
return
yield # pylint: disable=unreachable


nodes.MatchStar.assigned_stmts = match_star_assigned_stmts


@decorators.yes_if_nothing_inferred
def match_as_assigned_stmts(
self: nodes.MatchAs,
node: nodes.AssignName,
context: Optional[contextmod.InferenceContext] = None,
assign_path: Literal[None] = None,
) -> Generator[nodes.NodeNG, None, None]:
"""Infer MatchAs as the Match subject if it's the only MatchCase pattern
else raise StopIteration to yield Uninferable.
"""
if (
isinstance(self.parent, nodes.MatchCase)
and isinstance(self.parent.parent, nodes.Match)
and self.pattern is None
):
yield self.parent.parent.subject


nodes.MatchAs.assigned_stmts = match_as_assigned_stmts
13 changes: 6 additions & 7 deletions astroid/rebuilder.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,18 +45,18 @@
overload,
)

try:
from typing import Final
except ImportError:
# typing.Final was added in Python 3.8
from typing_extensions import Final

from astroid import node_classes, nodes
from astroid._ast import ParserModule, get_parser_module, parse_function_type_comment
from astroid.const import PY37_PLUS, PY38_PLUS, PY39_PLUS, Context
from astroid.manager import AstroidManager
from astroid.node_classes import NodeNG

if sys.version_info >= (3, 8):
# pylint: disable=no-name-in-module
from typing import Final
else:
from typing_extensions import Final

if TYPE_CHECKING:
import ast

Expand Down Expand Up @@ -85,7 +85,6 @@

# noinspection PyMethodMayBeStatic
class TreeRebuilder:
# pylint: disable=no-self-use
"""Rebuilds the _ast tree to become an Astroid tree"""

def __init__(
Expand Down
19 changes: 17 additions & 2 deletions astroid/scoped_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1708,6 +1708,21 @@ def is_generator(self):
"""
return bool(next(self._get_yield_nodes_skip_lambdas(), False))

def infer_yield_result(self, context=None):
"""Infer what the function yields when called
:returns: What the function yields
:rtype: iterable(NodeNG or Uninferable) or None
"""
for yield_ in self.nodes_of_class(node_classes.Yield):
if yield_.value is None:
const = node_classes.Const(None)
const.parent = yield_
const.lineno = yield_.lineno
yield const
elif yield_.scope() == self:
yield from yield_.value.infer(context=context)

def infer_call_result(self, caller=None, context=None):
"""Infer what the function returns when called.
Expand All @@ -1719,7 +1734,7 @@ def infer_call_result(self, caller=None, context=None):
generator_cls = bases.AsyncGenerator
else:
generator_cls = bases.Generator
result = generator_cls(self)
result = generator_cls(self, generator_initial_context=context)
yield result
return
# This is really a gigantic hack to work around metaclass generators
Expand Down Expand Up @@ -2516,7 +2531,7 @@ def _metaclass_lookup_attribute(self, name, context):
implicit_meta = self.implicit_metaclass()
context = contextmod.copy_context(context)
metaclass = self.metaclass(context=context)
for cls in {implicit_meta, metaclass}:
for cls in (implicit_meta, metaclass):
if cls and cls != self and isinstance(cls, ClassDef):
cls_attributes = self._get_attribute_from_metaclass(cls, name, context)
attrs.update(set(cls_attributes))
Expand Down
4 changes: 3 additions & 1 deletion pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ persistent=yes
# usually to register additional checkers.
load-plugins=
pylint.extensions.check_elif,
pylint.extensions.bad_builtin,
pylint.extensions.code_style,

# Use multiple processes to speed up Pylint.
jobs=1
Expand Down Expand Up @@ -48,7 +50,7 @@ output-format=text
files-output=no

# Tells whether to display a full report or only the messages
reports=yes
reports=no

# Python expression which should return a note less than 10 (10 is the highest
# note). You have access to the variables errors warning, statement which
Expand Down
2 changes: 1 addition & 1 deletion requirements_test_pre_commit.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
black==21.6b0
pylint==2.8.2
pylint==2.9.3
isort==5.8.0
flake8==3.9.2
mypy==0.910
Loading

0 comments on commit e791ed1

Please sign in to comment.