From 25dad3d78ac09164573f4098a478dd250c23ae10 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 27 Feb 2022 02:05:57 +0100 Subject: [PATCH] Limit expensive decorator function --- ChangeLog | 5 ++ astroid/decorators.py | 131 ++++++++++++++++++++++++------------------ astroid/util.py | 13 +++++ 3 files changed, 94 insertions(+), 55 deletions(-) diff --git a/ChangeLog b/ChangeLog index 8174067d62..ace1f32dbe 100644 --- a/ChangeLog +++ b/ChangeLog @@ -61,6 +61,11 @@ Release date: TBA * Fix ``ClassDef.fromlineno``. For Python < 3.8 the ``lineno`` attribute includes decorators. ``fromlineno`` should return the line of the ``class`` statement itself. +* Performance improvements. Only run expensive decorator functions when + non-default Deprecation warnings are enabled, eg. during a Pytest run. + + Closes #1383 + What's New in astroid 2.9.4? ============================ Release date: TBA diff --git a/astroid/decorators.py b/astroid/decorators.py index 96d3bba444..ef79851915 100644 --- a/astroid/decorators.py +++ b/astroid/decorators.py @@ -152,58 +152,79 @@ def raise_if_nothing_inferred(func, instance, args, kwargs): yield from generator -def deprecate_default_argument_values( - astroid_version: str = "3.0", **arguments: str -) -> Callable[[Callable[P, R]], Callable[P, R]]: - """Decorator which emitts a DeprecationWarning if any arguments specified - are None or not passed at all. - - Arguments should be a key-value mapping, with the key being the argument to check - and the value being a type annotation as string for the value of the argument. - """ - # Helpful links - # Decorator for DeprecationWarning: https://stackoverflow.com/a/49802489 - # Typing of stacked decorators: https://stackoverflow.com/a/68290080 - - def deco(func: Callable[P, R]) -> Callable[P, R]: - """Decorator function.""" - - @functools.wraps(func) - def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: - """Emit DeprecationWarnings if conditions are met.""" - - keys = list(inspect.signature(func).parameters.keys()) - for arg, type_annotation in arguments.items(): - try: - index = keys.index(arg) - except ValueError: - raise Exception( - f"Can't find argument '{arg}' for '{args[0].__class__.__qualname__}'" - ) from None - if ( - # Check kwargs - # - if found, check it's not None - (arg in kwargs and kwargs[arg] is None) - # Check args - # - make sure not in kwargs - # - len(args) needs to be long enough, if too short - # arg can't be in args either - # - args[index] should not be None - or arg not in kwargs - and ( - index == -1 - or len(args) <= index - or (len(args) > index and args[index] is None) - ) - ): - warnings.warn( - f"'{arg}' will be a required argument for " - f"'{args[0].__class__.__qualname__}.{func.__name__}' in astroid {astroid_version} " - f"('{arg}' should be of type: '{type_annotation}')", - DeprecationWarning, - ) - return func(*args, **kwargs) - - return wrapper - - return deco +# Expensive decorators only used to emit Deprecation warnings. +# If no other than the default DeprecationWarning are enabled, +# fall back to passthrough implementations. +if util.check_warnings_filter(): + + def deprecate_default_argument_values( + astroid_version: str = "3.0", **arguments: str + ) -> Callable[[Callable[P, R]], Callable[P, R]]: + """Decorator which emits a DeprecationWarning if any arguments specified + are None or not passed at all. + + Arguments should be a key-value mapping, with the key being the argument to check + and the value being a type annotation as string for the value of the argument. + + To improve performance, only used when DeprecationWarnings other than + the default one are enabled. + """ + # Helpful links + # Decorator for DeprecationWarning: https://stackoverflow.com/a/49802489 + # Typing of stacked decorators: https://stackoverflow.com/a/68290080 + + def deco(func: Callable[P, R]) -> Callable[P, R]: + """Decorator function.""" + + @functools.wraps(func) + def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: + """Emit DeprecationWarnings if conditions are met.""" + + keys = list(inspect.signature(func).parameters.keys()) + for arg, type_annotation in arguments.items(): + try: + index = keys.index(arg) + except ValueError: + raise Exception( + f"Can't find argument '{arg}' for '{args[0].__class__.__qualname__}'" + ) from None + if ( + # Check kwargs + # - if found, check it's not None + (arg in kwargs and kwargs[arg] is None) + # Check args + # - make sure not in kwargs + # - len(args) needs to be long enough, if too short + # arg can't be in args either + # - args[index] should not be None + or arg not in kwargs + and ( + index == -1 + or len(args) <= index + or (len(args) > index and args[index] is None) + ) + ): + warnings.warn( + f"'{arg}' will be a required argument for " + f"'{args[0].__class__.__qualname__}.{func.__name__}' in astroid {astroid_version} " + f"('{arg}' should be of type: '{type_annotation}')", + DeprecationWarning, + ) + return func(*args, **kwargs) + + return wrapper + + return deco + +else: + + def deprecate_default_argument_values( + astroid_version: str = "3.0", **arguments: str + ) -> Callable[[Callable[P, R]], Callable[P, R]]: + """Passthrough decorator to improve performance if DeprecationWarnings are disabled.""" + + def deco(func: Callable[P, R]) -> Callable[P, R]: + """Decorator function.""" + return func + + return deco diff --git a/astroid/util.py b/astroid/util.py index b54b2ec492..fb4285eef4 100644 --- a/astroid/util.py +++ b/astroid/util.py @@ -140,3 +140,16 @@ def proxy_alias(alias_name, node_type): }, ) return proxy(lambda: node_type) + + +def check_warnings_filter() -> bool: + """Return True if any other than the default DeprecationWarning filter is enabled. + + https://docs.python.org/3/library/warnings.html#default-warning-filter + """ + return any( + issubclass(DeprecationWarning, filter[2]) + and filter[0] != "ignore" + and filter[3] != "__main__" + for filter in warnings.filters + )