Skip to content

Limit expensive decorator function #1407

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Feb 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
131 changes: 76 additions & 55 deletions astroid/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
13 changes: 13 additions & 0 deletions astroid/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
)