mirror of
https://github.com/RYDE-WORK/ballistica.git
synced 2026-01-21 14:23:37 +08:00
226 lines
8.1 KiB
Python
226 lines
8.1 KiB
Python
# Copyright (c) 2011-2020 Eric Froemling
|
|
#
|
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
# of this software and associated documentation files (the "Software"), to deal
|
|
# in the Software without restriction, including without limitation the rights
|
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
# copies of the Software, and to permit persons to whom the Software is
|
|
# furnished to do so, subject to the following conditions:
|
|
#
|
|
# The above copyright notice and this permission notice shall be included in
|
|
# all copies or substantial portions of the Software.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
# SOFTWARE.
|
|
# -----------------------------------------------------------------------------
|
|
"""Plugins for pylint"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
import astroid
|
|
|
|
if TYPE_CHECKING:
|
|
from astroid import node_classes as nc
|
|
from typing import Set, Dict, Any
|
|
|
|
VERBOSE = False
|
|
|
|
|
|
def register(linter: Any) -> None:
|
|
"""Unused here; we're modifying the ast; not linters."""
|
|
del linter # Unused.
|
|
|
|
|
|
failed_imports: Set[str] = set()
|
|
|
|
|
|
def failed_import_hook(modname: str) -> None:
|
|
"""Custom failed import callback."""
|
|
|
|
# We don't actually do anything here except note in our log that
|
|
# something couldn't be imported. (may help sanity-check our filtering)
|
|
if VERBOSE:
|
|
if modname not in failed_imports:
|
|
failed_imports.add(modname)
|
|
print('GOT FAILED IMPORT OF', modname)
|
|
raise astroid.AstroidBuildingError(modname=modname)
|
|
|
|
|
|
def ignore_type_check_filter(node: nc.NodeNG) -> nc.NodeNG:
|
|
"""Ignore stuff under 'if TYPE_CHECKING:' block at module level."""
|
|
|
|
# Look for a non-nested 'if TYPE_CHECKING:'
|
|
if (isinstance(node.test, astroid.Name)
|
|
and node.test.name == 'TYPE_CHECKING'
|
|
and isinstance(node.parent, astroid.Module)):
|
|
|
|
# Find the module node.
|
|
mnode = node
|
|
while mnode.parent is not None:
|
|
mnode = mnode.parent
|
|
|
|
# First off, remove any names that are getting defined
|
|
# in this block from the module locals.
|
|
for cnode in node.body:
|
|
_strip_import(cnode, mnode)
|
|
|
|
# Now replace the body with a simple 'pass'. This will
|
|
# keep pylint from complaining about grouped imports/etc.
|
|
passnode = astroid.Pass(parent=node,
|
|
lineno=node.lineno + 1,
|
|
col_offset=node.col_offset + 1)
|
|
node.body = [passnode]
|
|
return node
|
|
|
|
|
|
def ignore_reveal_type_call(node: nc.NodeNG) -> nc.NodeNG:
|
|
"""Make 'reveal_type()' not trigger an error.
|
|
|
|
The 'reveal_type()' fake call is used for type debugging types with
|
|
mypy and it is annoying having pylint errors pop up alongside the mypy
|
|
info.
|
|
"""
|
|
|
|
# Let's just replace any reveal_type(x) call with print(x)..
|
|
if (isinstance(node.func, astroid.Name)
|
|
and node.func.name == 'reveal_type'):
|
|
node.func.name = 'print'
|
|
return node
|
|
return node
|
|
|
|
|
|
def _strip_import(cnode: nc.NodeNG, mnode: nc.NodeNG) -> None:
|
|
if isinstance(cnode, (astroid.Import, astroid.ImportFrom)):
|
|
for name, val in list(mnode.locals.items()):
|
|
if cnode in val:
|
|
|
|
# Pull us out of the list.
|
|
valnew = [v for v in val if v is not cnode]
|
|
if valnew:
|
|
mnode.locals[name] = valnew
|
|
else:
|
|
del mnode.locals[name]
|
|
|
|
|
|
def using_future_annotations(node: nc.NodeNG) -> nc.NodeNG:
|
|
"""Return whether postponed annotation evaluation is enabled (PEP 563)."""
|
|
|
|
# Find the module.
|
|
mnode = node
|
|
while mnode.parent is not None:
|
|
mnode = mnode.parent
|
|
|
|
# Look for 'from __future__ import annotations' to decide
|
|
# if we should assume all annotations are defer-eval'ed.
|
|
# NOTE: this will become default at some point within a few years..
|
|
annotations_set = mnode.locals.get('annotations')
|
|
if (annotations_set and isinstance(annotations_set[0], astroid.ImportFrom)
|
|
and annotations_set[0].modname == '__future__'):
|
|
return True
|
|
return False
|
|
|
|
|
|
def func_annotations_filter(node: nc.NodeNG) -> nc.NodeNG:
|
|
"""Filter annotated function args/retvals.
|
|
|
|
This accounts for deferred evaluation available in in Python 3.7+
|
|
via 'from __future__ import annotations'. In this case we don't
|
|
want Pylint to complain about missing symbols in annotations when
|
|
they aren't actually needed at runtime.
|
|
"""
|
|
# Only do this if deferred annotations are on.
|
|
if not using_future_annotations(node):
|
|
return node
|
|
|
|
# Wipe out argument annotations.
|
|
|
|
# Special-case: functools.singledispatch and ba.dispatchmethod *do*
|
|
# evaluate annotations at runtime so we want to leave theirs intact.
|
|
# Lets just look for a @XXX.register decorator used by both I guess.
|
|
if node.decorators is not None:
|
|
for dnode in node.decorators.nodes:
|
|
if (isinstance(dnode, astroid.nodes.Name)
|
|
and dnode.name in ('dispatchmethod', 'singledispatch')):
|
|
return node # Leave annotations intact.
|
|
|
|
if (isinstance(dnode, astroid.nodes.Attribute)
|
|
and dnode.attrname == 'register'):
|
|
return node # Leave annotations intact.
|
|
|
|
node.args.annotations = [None for _ in node.args.args]
|
|
node.args.varargannotation = None
|
|
node.args.kwargannotation = None
|
|
node.args.kwonlyargs_annotations = [None for _ in node.args.kwonlyargs]
|
|
|
|
# Wipe out return-value annotation.
|
|
if node.returns is not None:
|
|
node.returns = None
|
|
|
|
return node
|
|
|
|
|
|
def var_annotations_filter(node: nc.NodeNG) -> nc.NodeNG:
|
|
"""Filter annotated function variable assigns.
|
|
|
|
This accounts for deferred evaluation.
|
|
"""
|
|
if using_future_annotations(node):
|
|
# Future behavior:
|
|
# Annotations are never evaluated.
|
|
willeval = False
|
|
else:
|
|
# Legacy behavior:
|
|
# Annotated assigns under functions are not evaluated,
|
|
# but class or module vars are.
|
|
fnode = node
|
|
willeval = True
|
|
while fnode is not None:
|
|
if isinstance(fnode,
|
|
(astroid.FunctionDef, astroid.AsyncFunctionDef)):
|
|
willeval = False
|
|
break
|
|
if isinstance(fnode, astroid.ClassDef):
|
|
willeval = True
|
|
break
|
|
fnode = fnode.parent
|
|
|
|
# If this annotation won't be eval'ed, replace it with a dummy string.
|
|
if not willeval:
|
|
dummyval = astroid.Const(parent=node, value='dummyval')
|
|
node.annotation = dummyval
|
|
|
|
return node
|
|
|
|
|
|
def register_plugins(manager: astroid.Manager) -> None:
|
|
"""Apply our transforms to a given astroid manager object."""
|
|
|
|
if VERBOSE:
|
|
manager.register_failed_import_hook(failed_import_hook)
|
|
|
|
# Completely ignore everything under an 'if TYPE_CHECKING' conditional.
|
|
# That stuff only gets run for mypy, and in general we want to
|
|
# check code as if it doesn't exist at all.
|
|
manager.register_transform(astroid.If, ignore_type_check_filter)
|
|
|
|
manager.register_transform(astroid.Call, ignore_reveal_type_call)
|
|
|
|
# Annotations on variables within a function are defer-eval'ed
|
|
# in some cases, so lets replace them with simple strings in those
|
|
# cases to avoid type complaints.
|
|
# (mypy will still properly alert us to type errors for them)
|
|
manager.register_transform(astroid.AnnAssign, var_annotations_filter)
|
|
manager.register_transform(astroid.FunctionDef, func_annotations_filter)
|
|
manager.register_transform(astroid.AsyncFunctionDef,
|
|
func_annotations_filter)
|
|
|
|
|
|
register_plugins(astroid.MANAGER)
|