Skip to content
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

Sync with conda.deprecations #5270

Merged
merged 3 commits into from
Apr 5, 2024
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
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
Loading
Loading