Repository URL to install this package:
|
Version:
1.26.0.dev0+gite506aa5f ▾
|
# Copyright 2015 Pants project contributors (see CONTRIBUTORS.md).
# Licensed under the Apache License, Version 2.0 (see LICENSE).
import inspect
import sys
import warnings
from functools import wraps
from typing import TYPE_CHECKING, Callable, Optional
from packaging.version import InvalidVersion, Version
from pants.util.memo import memoized_method
from pants.version import PANTS_SEMVER
if TYPE_CHECKING:
from pants.option.option_value_container import OptionValueContainer
class DeprecationApplicationError(Exception):
"""The base exception type thrown for any form of @deprecation application error."""
class MissingSemanticVersionError(DeprecationApplicationError):
"""Indicates the required removal_version was not supplied."""
class BadSemanticVersionError(DeprecationApplicationError):
"""Indicates the supplied removal_version was not a valid semver string."""
class NonDevSemanticVersionError(DeprecationApplicationError):
"""Indicates the supplied removal_version was not a pre-release version."""
class InvalidSemanticVersionOrderingError(DeprecationApplicationError):
"""Indicates that multiple semantic version strings were provided in an inconsistent
ordering."""
class CodeRemovedError(Exception):
"""Indicates that the removal_version is not in the future.
I.e., that the option/function/module with that removal_version has already been removed.
Note, the code in question may not actually have been excised from the codebase yet, but
it may be at any time, and no control paths access it.
"""
class BadDecoratorNestingError(DeprecationApplicationError):
"""Indicates the @deprecated decorator was innermost in a sequence of layered decorators."""
def get_deprecated_tense(
removal_version: str, future_tense: str = "will be", past_tense: str = "was"
) -> str:
"""Provides the grammatical tense for a given deprecated version vs the current version."""
return future_tense if (Version(removal_version) >= PANTS_SEMVER) else past_tense
@memoized_method
def validate_deprecation_semver(version_string: str, version_description: str) -> Version:
"""Validates that version_string is a valid semver.
If so, returns that semver. Raises an error otherwise.
:param version_string: A pantsbuild.pants version which affects some deprecated entity.
:param version_description: A string used in exception messages to describe what the
`version_string` represents.
:raises DeprecationApplicationError: if the version_string parameter is invalid.
"""
if version_string is None:
raise MissingSemanticVersionError("The {} must be provided.".format(version_description))
if not isinstance(version_string, str):
raise BadSemanticVersionError(
"The {} must be a version string.".format(version_description)
)
try:
# NB: packaging will see versions like 1.a.0 as 1a0, and are "valid"
# We explicitly want our versions to be of the form x.y.z.
v = Version(version_string)
if len(v.base_version.split(".")) != 3:
raise BadSemanticVersionError(
"The given {} is not a valid version: "
"{}".format(version_description, version_string)
)
if not v.is_prerelease:
raise NonDevSemanticVersionError(
"The given {} is not a dev version: {}\n"
"Features should generally be removed in the first `dev` release "
"of a release cycle.".format(version_description, version_string)
)
return v
except InvalidVersion as e:
raise BadSemanticVersionError(
"The given {} {} is not a valid version: "
"{}".format(version_description, version_string, e)
)
def _get_frame_info(stacklevel: int) -> inspect.FrameInfo:
"""Get a Traceback for the given `stacklevel`.
For example:
`stacklevel=0` means this function's frame (_get_frame_info()).
`stacklevel=1` means the calling function's frame.
See https://docs.python.org/3/library/inspect.html#inspect.getouterframes for more info.
NB: If `stacklevel` is greater than the number of actual frames, the outermost frame is used
instead.
"""
frame_list = inspect.getouterframes(inspect.currentframe())
frame_stack_index = stacklevel if stacklevel < len(frame_list) else len(frame_list) - 1
return frame_list[frame_stack_index]
# TODO: propagate `deprecation_start_version` to other methods in this file!
def warn_or_error(
removal_version: str,
deprecated_entity_description: str,
hint: Optional[str] = None,
deprecation_start_version: Optional[str] = None,
stacklevel: int = 3,
frame_info: Optional[inspect.FrameInfo] = None,
ensure_stderr: bool = False,
print_warning: bool = True,
) -> None:
"""Check the removal_version against the current pants version.
Issues a warning if the removal version is > current pants version, or an error otherwise.
:param removal_version: The pantsbuild.pants version at which the deprecated entity will
be/was removed.
:param deprecated_entity_description: A short description of the deprecated entity, that
we can embed in warning/error messages.
:param hint: A message describing how to migrate from the removed entity.
:param deprecation_start_version: The pantsbuild.pants version at which the entity will
begin to display a deprecation warning. This must be less
than the `removal_version`. If not provided, the
deprecation warning is always displayed.
:param stacklevel: The stacklevel to pass to warnings.warn, which determines the file name and
line number of the error message.
:param frame_info: If provided, use this frame info instead of getting one from `stacklevel`.
:param ensure_stderr: Whether use warnings.warn, or use warnings.showwarning to print
directly to stderr.
:param print_warning: Whether to print a warning for deprecations *before* their removal.
If this flag is off, an exception will still be raised for options
past their deprecation date.
:raises DeprecationApplicationError: if the removal_version parameter is invalid.
:raises CodeRemovedError: if the current version is later than the version marked for removal.
"""
removal_semver = validate_deprecation_semver(removal_version, "removal version")
if deprecation_start_version:
deprecation_start_semver = validate_deprecation_semver(
deprecation_start_version, "deprecation start version"
)
if deprecation_start_semver >= removal_semver:
raise InvalidSemanticVersionOrderingError(
"The deprecation start version {} must be less than the end version {}.".format(
deprecation_start_version, removal_version
)
)
elif PANTS_SEMVER < deprecation_start_semver:
return
msg = "DEPRECATED: {} {} removed in version {}.".format(
deprecated_entity_description, get_deprecated_tense(removal_version), removal_version
)
if hint:
msg += "\n {}".format(hint)
# We need to have filename and line_number for warnings.formatwarning, which appears to be the only
# way to get a warning message to display to stderr. We get that from frame_info.
if frame_info is None:
frame_info = _get_frame_info(stacklevel)
_, filename, line_number, _, _, _ = frame_info
if removal_semver > PANTS_SEMVER:
if ensure_stderr:
# No warning filters can stop us from printing this message directly to stderr.
warning_msg = warnings.formatwarning(msg, DeprecationWarning, filename, line_number,)
print(warning_msg, file=sys.stderr)
elif print_warning:
# This output is filtered by warning filters.
warnings.warn_explicit(
message=msg, category=DeprecationWarning, filename=filename, lineno=line_number,
)
return
else:
raise CodeRemovedError(msg)
def deprecated_conditional(
predicate: Callable[[], bool],
removal_version: str,
entity_description: str,
hint_message: Optional[str] = None,
deprecation_start_version: Optional[str] = None,
stacklevel: int = 4,
) -> None:
"""Marks a certain configuration as deprecated.
The predicate is used to determine if that configuration is deprecated. It is a function that
will be called, if true, then the deprecation warning will issue.
:param predicate: A function that returns True if the deprecation warning should be on.
:param removal_version: The pants version which will remove the deprecated functionality.
:param entity_description: A description of the deprecated entity.
:param hint_message: An optional hint pointing to alternatives to the deprecation.
:param stacklevel: How far up in the stack do we go to find the calling fn to report
:raises DeprecationApplicationError if the deprecation is applied improperly.
"""
validate_deprecation_semver(removal_version, "removal version")
if predicate():
warn_or_error(
removal_version,
entity_description,
hint_message,
deprecation_start_version=deprecation_start_version,
stacklevel=stacklevel,
)
def deprecated(
removal_version: str,
hint_message: Optional[str] = None,
subject: Optional[str] = None,
ensure_stderr: bool = False,
):
"""Marks a function or method as deprecated.
A removal version must be supplied and it must be greater than the current 'pantsbuild.pants'
version.
When choosing a removal version there is a natural tension between the code-base, which benefits
from short deprecation cycles, and the user-base which may prefer to deal with deprecations less
frequently. As a rule of thumb, if the hint message can fully convey corrective action
succinctly and you judge the impact to be on the small side (effects custom tasks as opposed to
effecting BUILD files), lean towards the next release version as the removal version; otherwise,
consider initiating a discussion to win consensus on a reasonable removal version.
:param removal_version: The pantsbuild.pants version which will remove the deprecated
function.
:param hint_message: An optional hint pointing to alternatives to the deprecation.
:param subject: The name of the subject that has been deprecated for logging clarity. Defaults
to the name of the decorated function/method.
:param ensure_stderr: Forwarded to `ensure_stderr` in warn_or_error().
:raises DeprecationApplicationError if the @deprecation is applied improperly.
"""
validate_deprecation_semver(removal_version, "removal version")
def decorator(func):
if not inspect.isfunction(func):
raise BadDecoratorNestingError(
"The @deprecated decorator must be applied innermost of all " "decorators."
)
func_full_name = "{}.{}".format(func.__module__, func.__name__)
@wraps(func)
def wrapper(*args, **kwargs):
warn_or_error(
removal_version,
subject or func_full_name,
hint_message,
ensure_stderr=ensure_stderr,
)
return func(*args, **kwargs)
return wrapper
return decorator
def deprecated_module(
removal_version: str, hint_message: Optional[str] = None, stacklevel: int = 3
) -> None:
"""Marks an entire module as deprecated.
Add a call to this at the top of the deprecated module, and it will print a warning message
when the module is imported.
:param removal_version: The pantsbuild.pants version which will remove the deprecated
function.
:param hint_message: An optional hint pointing to alternatives to the deprecation.
:param stacklevel: The stacklevel to pass to warnings.warn.
"""
warn_or_error(removal_version, "module", hint_message, stacklevel=stacklevel)
def resolve_conflicting_options(
*,
old_option: str,
new_option: str,
old_scope: str,
new_scope: str,
old_container: "OptionValueContainer",
new_container: "OptionValueContainer",
):
"""Utility for resolving an option that's been migrated to a new location.
This ensures that the user does not try to specify both options.
"""
old_configured = not old_container.is_default(old_option)
new_configured = not new_container.is_default(new_option)
if old_configured and new_configured:
def format_option(*, scope: str, option: str) -> str:
return f"`--{scope}-{option}`".replace("_", "-")
old_display = format_option(scope=old_scope, option=old_option)
new_display = format_option(scope=new_scope, option=new_option)
raise ValueError(
f"Conflicting options used. You used the new, preferred {new_display}, but also "
f"used the deprecated {old_display}.\n\nPlease use only one of these "
f"(preferably {new_display})."
)
if old_configured:
return old_container.get(old_option)
return new_container.get(new_option)