Skip to content

Commit

Permalink
Sync with conda.deprecations (#5270)
Browse files Browse the repository at this point in the history
* Sync with conda.deprecations
* Avoid RuntimeError in module detection
* Update test_deprecations.py
  • Loading branch information
kenodegard committed Apr 5, 2024
1 parent 6d118c2 commit 6982cbd
Show file tree
Hide file tree
Showing 2 changed files with 247 additions and 289 deletions.
140 changes: 83 additions & 57 deletions conda_build/deprecations.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,22 @@

import sys
import warnings
from argparse import Action
from functools import wraps
from types import ModuleType
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from argparse import Action
from typing import Any, Callable
from argparse import ArgumentParser, Namespace
from typing import Any, Callable, ParamSpec, Self, TypeVar

from packaging.version import Version

T = TypeVar("T")
P = ParamSpec("P")

ActionType = TypeVar("ActionType", bound=type[Action])

from . import __version__


Expand All @@ -30,7 +36,7 @@ class DeprecationHandler:
_version_tuple: tuple[int, ...] | None
_version_object: Version | None

def __init__(self, version: str):
def __init__(self: Self, version: str) -> None:
"""Factory to create a deprecation handle for the specified version.
:param version: The version to compare against when checking deprecation statuses.
Expand All @@ -52,14 +58,13 @@ def _get_version_tuple(version: str) -> tuple[int, ...] | None:
except (AttributeError, ValueError):
return None

def _version_less_than(self, version: str) -> bool:
def _version_less_than(self: Self, version: str) -> bool:
"""Test whether own version is less than the given version.
:param version: Version string to compare against.
"""
if self._version_tuple:
if version_tuple := self._get_version_tuple(version):
return self._version_tuple < version_tuple
if self._version_tuple and (version_tuple := self._get_version_tuple(version)):
return self._version_tuple < version_tuple

# If self._version or version could not be represented by a simple
# tuple[int, ...], do a more elaborate version parsing and comparison.
Expand All @@ -68,19 +73,20 @@ def _version_less_than(self, version: str) -> bool:

if self._version_object is None:
try:
self._version_object = parse(self._version)
self._version_object = parse(self._version) # type: ignore[arg-type]
except TypeError:
# TypeError: self._version could not be parsed
self._version_object = parse("0.0.0.dev0+placeholder")
return self._version_object < parse(version)

def __call__(
self,
self: Self,
deprecate_in: str,
remove_in: str,
*,
addendum: str | None = None,
stack: int = 0,
) -> Callable[[Callable], Callable]:
) -> Callable[[Callable[P, T]], Callable[P, T]]:
"""Deprecation decorator for functions, methods, & classes.
:param deprecate_in: Version in which code will be marked as deprecated.
Expand All @@ -89,12 +95,12 @@ def __call__(
:param stack: Optional stacklevel increment.
"""

def deprecated_decorator(func: Callable) -> Callable:
def deprecated_decorator(func: Callable[P, T]) -> Callable[P, T]:
# detect function name and generate message
category, message = self._generate_message(
deprecate_in,
remove_in,
f"{func.__module__}.{func.__qualname__}",
deprecate_in=deprecate_in,
remove_in=remove_in,
prefix=f"{func.__module__}.{func.__qualname__}",
addendum=addendum,
)

Expand All @@ -104,7 +110,7 @@ def deprecated_decorator(func: Callable) -> Callable:

# alert user that it's time to remove something
@wraps(func)
def inner(*args, **kwargs):
def inner(*args: P.args, **kwargs: P.kwargs) -> T:
warnings.warn(message, category, stacklevel=2 + stack)

return func(*args, **kwargs)
Expand All @@ -114,15 +120,15 @@ def inner(*args, **kwargs):
return deprecated_decorator

def argument(
self,
self: Self,
deprecate_in: str,
remove_in: str,
argument: str,
*,
rename: str | None = None,
addendum: str | None = None,
stack: int = 0,
) -> Callable[[Callable], Callable]:
) -> Callable[[Callable[P, T]], Callable[P, T]]:
"""Deprecation decorator for keyword arguments.
:param deprecate_in: Version in which code will be marked as deprecated.
Expand All @@ -133,16 +139,16 @@ def argument(
:param stack: Optional stacklevel increment.
"""

def deprecated_decorator(func: Callable) -> Callable:
def deprecated_decorator(func: Callable[P, T]) -> Callable[P, T]:
# detect function name and generate message
category, message = self._generate_message(
deprecate_in,
remove_in,
f"{func.__module__}.{func.__qualname__}({argument})",
deprecate_in=deprecate_in,
remove_in=remove_in,
prefix=f"{func.__module__}.{func.__qualname__}({argument})",
# provide a default addendum if renaming and no addendum is provided
addendum=f"Use '{rename}' instead."
if rename and not addendum
else addendum,
addendum=(
f"Use '{rename}' instead." if rename and not addendum else addendum
),
)

# alert developer that it's time to remove something
Expand All @@ -151,7 +157,7 @@ def deprecated_decorator(func: Callable) -> Callable:

# alert user that it's time to remove something
@wraps(func)
def inner(*args, **kwargs):
def inner(*args: P.args, **kwargs: P.kwargs) -> T:
# only warn about argument deprecations if the argument is used
if argument in kwargs:
warnings.warn(message, category, stacklevel=2 + stack)
Expand All @@ -168,22 +174,27 @@ def inner(*args, **kwargs):
return deprecated_decorator

def action(
self,
self: Self,
deprecate_in: str,
remove_in: str,
action: type[Action],
action: ActionType,
*,
addendum: str | None = None,
stack: int = 0,
):
class DeprecationMixin:
def __init__(inner_self, *args, **kwargs):
) -> ActionType:
"""Wraps any argparse.Action to issue a deprecation warning."""

class DeprecationMixin(Action):
category: type[Warning]
help: str # override argparse.Action's help type annotation

def __init__(inner_self: Self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)

category, message = self._generate_message(
deprecate_in,
remove_in,
(
deprecate_in=deprecate_in,
remove_in=remove_in,
prefix=(
# option_string are ordered shortest to longest,
# use the longest as it's the most descriptive
f"`{inner_self.option_strings[-1]}`"
Expand All @@ -192,6 +203,7 @@ def __init__(inner_self, *args, **kwargs):
else f"`{inner_self.dest}`"
),
addendum=addendum,
deprecation_type=FutureWarning,
)

# alert developer that it's time to remove something
Expand All @@ -201,18 +213,26 @@ def __init__(inner_self, *args, **kwargs):
inner_self.category = category
inner_self.help = message

def __call__(inner_self, parser, namespace, values, option_string=None):
def __call__(
inner_self: Self,
parser: ArgumentParser,
namespace: Namespace,
values: Any,
option_string: str | None = None,
) -> None:
# alert user that it's time to remove something
warnings.warn(
inner_self.help, inner_self.category, stacklevel=7 + stack
inner_self.help,
inner_self.category,
stacklevel=7 + stack,
)

super().__call__(parser, namespace, values, option_string)

return type(action.__name__, (DeprecationMixin, action), {})
return type(action.__name__, (DeprecationMixin, action), {}) # type: ignore[return-value]

def module(
self,
self: Self,
deprecate_in: str,
remove_in: str,
*,
Expand All @@ -235,7 +255,7 @@ def module(
)

def constant(
self,
self: Self,
deprecate_in: str,
remove_in: str,
constant: str,
Expand All @@ -257,10 +277,10 @@ def constant(
module, fullname = self._get_module(stack)
# detect function name and generate message
category, message = self._generate_message(
deprecate_in,
remove_in,
f"{fullname}.{constant}",
addendum,
deprecate_in=deprecate_in,
remove_in=remove_in,
prefix=f"{fullname}.{constant}",
addendum=addendum,
)

# alert developer that it's time to remove something
Expand All @@ -280,10 +300,10 @@ def __getattr__(name: str) -> Any:

raise AttributeError(f"module '{fullname}' has no attribute '{name}'")

module.__getattr__ = __getattr__
module.__getattr__ = __getattr__ # type: ignore[method-assign]

def topic(
self,
self: Self,
deprecate_in: str,
remove_in: str,
*,
Expand All @@ -301,10 +321,10 @@ def topic(
"""
# detect function name and generate message
category, message = self._generate_message(
deprecate_in,
remove_in,
topic,
addendum,
deprecate_in=deprecate_in,
remove_in=remove_in,
prefix=topic,
addendum=addendum,
)

# alert developer that it's time to remove something
Expand All @@ -314,7 +334,7 @@ def topic(
# alert user that it's time to remove something
warnings.warn(message, category, stacklevel=2 + stack)

def _get_module(self, stack: int) -> tuple[ModuleType, str]:
def _get_module(self: Self, stack: int) -> tuple[ModuleType, str]:
"""Detect the module from which we are being called.
:param stack: The stacklevel increment.
Expand All @@ -333,13 +353,15 @@ def _get_module(self, stack: int) -> tuple[ModuleType, str]:
# AttributeError: frame.f_code.co_filename is undefined
pass
else:
for module in sys.modules.values():
if not isinstance(module, ModuleType):
# use a copy of sys.modules to avoid RuntimeError during iteration
# see https://github.com/conda/conda/issues/13754
for loaded in tuple(sys.modules.values()):
if not isinstance(loaded, ModuleType):
continue
if not hasattr(module, "__file__"):
if not hasattr(loaded, "__file__"):
continue
if module.__file__ == filename:
return (module, module.__name__)
if loaded.__file__ == filename:
return (loaded, loaded.__name__)

# If above failed, do an expensive import and costly getmodule call.
import inspect
Expand All @@ -351,26 +373,30 @@ def _get_module(self, stack: int) -> tuple[ModuleType, str]:
raise DeprecatedError("unable to determine the calling module")

def _generate_message(
self,
self: Self,
deprecate_in: str,
remove_in: str,
prefix: str,
addendum: str | None,
*,
deprecation_type: type[Warning] = DeprecationWarning,
) -> tuple[type[Warning] | None, str]:
"""Deprecation decorator for functions, methods, & classes.
"""Generate the standardized deprecation message and determine whether the
deprecation is pending, active, or past.
:param deprecate_in: Version in which code will be marked as deprecated.
:param remove_in: Version in which code is expected to be removed.
:param prefix: The message prefix, usually the function name.
:param addendum: Additional messaging. Useful to indicate what to do instead.
:param deprecation_type: The warning type to use for active deprecations.
:return: The warning category (if applicable) and the message.
"""
category: type[Warning] | None
if self._version_less_than(deprecate_in):
category = PendingDeprecationWarning
warning = f"is pending deprecation and will be removed in {remove_in}."
elif self._version_less_than(remove_in):
category = DeprecationWarning
category = deprecation_type
warning = f"is deprecated and will be removed in {remove_in}."
else:
category = None
Expand Down

0 comments on commit 6982cbd

Please sign in to comment.