# # Licensed to the Apache Software Foundation (ASF) under one or more # contributor license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright ownership. # The ASF licenses this file to You under the Apache License, Version 2.0 # (the "License"); you may not use this file except in compliance with # the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # """Deprecated annotations. Deprecated: Signifies that users are discouraged from using a public API typically because a better alternative exists, and the current form might be removed in a future version. Usage: For internal use only; no backwards-compatibility guarantees. Annotations come in two flavors: deprecated and experimental The 'deprecated' annotation requires a 'since" parameter to specify what version deprecated it. Both 'deprecated' and 'experimental' annotations can specify the current recommended version to use by means of a 'current' parameter. The following example illustrates how to annotate coexisting versions of the same function 'multiply'.:: def multiply(arg1, arg2): print(arg1, '*', arg2, '=', end=' ') return arg1*arg2 # This annotation marks 'old_multiply' as deprecated since 'v.1' and suggests # using 'multiply' instead.:: @deprecated(since='v.1', current='multiply') def old_multiply(arg1, arg2): result = 0 for i in xrange(arg1): result += arg2 print(arg1, '*', arg2, '(the old way)=', end=' ') return result # Set a warning filter to control how often warnings are produced.:: warnings.simplefilter("always") print(multiply(5, 6)) print(old_multiply(5,6)) """ # pytype: skip-file import inspect import warnings from functools import partial from functools import wraps def _add_deprecation_notice_to_docstring(docstring, message): """Adds a deprecation notice to a docstring. Args: docstring: The original docstring (can be None or empty). message: The deprecation message to add. Returns: The modified docstring. """ if docstring: return f"{docstring}\n\n.. deprecated:: {message}" else: return f".. deprecated:: {message}" class BeamDeprecationWarning(DeprecationWarning): """Beam-specific deprecation warnings.""" # Don't ignore BeamDeprecationWarnings. warnings.simplefilter('once', BeamDeprecationWarning) class _WarningMessage: """Utility class for assembling the warning message.""" def __init__(self, label, since, current, extra_message, custom_message): """Initialize message, leave only name as placeholder.""" if custom_message is None: message = '%name% is ' + label if label == 'deprecated': message += ' since %s' % since message += '. Use %s instead.' % current if current else '.' if extra_message: message += ' ' + extra_message else: if label == 'deprecated' and '%since%' not in custom_message: raise TypeError( "Replacement string %since% not found on \ custom message") emptyArg = lambda x: '' if x is None else x message = custom_message\ .replace('%since%', emptyArg(since))\ .replace('%current%', emptyArg(current))\ .replace('%extra%', emptyArg(extra_message)) self.label = label self.message = message def emit_warning(self, fnc_name): if self.label == 'deprecated': warning_type = BeamDeprecationWarning else: warning_type = FutureWarning warnings.warn( self.message.replace('%name%', fnc_name), warning_type, stacklevel=3) def annotate(label, since, current, extra_message, custom_message=None): """Decorates an API with a deprecated or experimental annotation. Args: label: the kind of annotation ('deprecated' or 'experimental'). since: the version that causes the annotation. current: the suggested replacement function. extra_message: an optional additional message. custom_message: if the default message does not suffice, the message can be changed using this argument. A string whit replacement tokens. A replecement string is were the previus args will be located on the custom message. The following replacement strings can be used: %name% -> API.__name__ %since% -> since (Mandatory for the decapreted annotation) %current% -> current %extra% -> extra_message Returns: The decorator for the API. """ warning_message = _WarningMessage( label=label, since=since, current=current, extra_message=extra_message, custom_message=custom_message) def _annotate(fnc): if inspect.isclass(fnc): # Wrapping class into function causes documentation rendering issue. # Patch class's __new__ method instead. old_new = fnc.__new__ def wrapped_new(cls, *args, **kwargs): # Emit a warning when class instance is created. warning_message.emit_warning(fnc.__name__) if old_new is object.__new__: # object.__new__ takes no extra argument for python>=3 return old_new(cls) return old_new(cls, *args, **kwargs) fnc.__new__ = staticmethod(wrapped_new) if label == 'deprecated': fnc.__doc__ = _add_deprecation_notice_to_docstring( fnc.__doc__, warning_message.message.replace('%name%', fnc.__name__)) return fnc else: @wraps(fnc) def inner(*args, **kwargs): # Emit a warning when the function is called. warning_message.emit_warning(fnc.__name__) return fnc(*args, **kwargs) if label == 'deprecated': inner.__doc__ = _add_deprecation_notice_to_docstring( fnc.__doc__, warning_message.message.replace('%name%', fnc.__name__)) return inner return _annotate # Use partial application to customize each annotation. # 'current' will be optional in both deprecated and experimental # while 'since' will be mandatory for deprecated. deprecated = partial( annotate, label='deprecated', current=None, extra_message=None)