def make_msg(style=None): if replaced_by is not None: try: doc_url = ' (see: {})'.format(get_doc_url(replaced_by)) except Exception: doc_url = '' replacement_msg = ', use {} instead{}'.format( getname(replaced_by, style=style), doc_url, ) else: replacement_msg = '' if removed_in: removal_msg = ' and will be removed in version {}'.format( format_version(removed_in)) else: removal_msg = '' return '{name} is deprecated{remove}{replace}{msg}'.format( name=getname(obj, style=style, abbrev=True), replace=replacement_msg, remove=removal_msg, msg=': ' + msg if msg else '', )
def make_msg(deprecated_obj, parameter=None, style=None, show_doc_url=True): if replaced_by is not None: doc_url = '' if show_doc_url: with contextlib.suppress(Exception): doc_url = ' (see: {})'.format(get_doc_url(replaced_by)) replacement_msg = ', use {} instead{}'.format( getname(replaced_by, style=style), doc_url, ) else: replacement_msg = '' if removed_in: removal_msg = ' and will be removed in version {}'.format( format_version(removed_in)) else: removal_msg = '' name = getname(deprecated_obj, style=style, abbrev=True) if parameter: if style == 'rst': parameter = '``{}``'.format(parameter) name = '{} parameter of {}'.format(parameter, name) return '{name} is deprecated{remove}{replace}{msg}'.format( name=name, replace=replacement_msg, remove=removal_msg, msg=': ' + msg if msg else '', )
def make_entry(entry): msg = entry.get('msg') or '' removed_in = entry.get('removed_in') if removed_in is None: removed_in = '' else: removed_in = '*Removed in: {}*\n\n'.format(format_version(removed_in)) name = get_sphinx_name(entry['obj'], style='rst') replaced_by = entry.get('replaced_by') if replaced_by is None: replaced_by = '' else: replaced_by = '*Replaced by:* {}\n\n'.format(get_sphinx_name(replaced_by, style='rst')) return "* - {name}{msg}{replaced_by}{removed_in}".format( name=indent(name + '\n\n'), msg=indent(msg + '\n\n' if msg else ''), replaced_by=indent(replaced_by), removed_in=indent(removed_in), )
def make_table(entries, removed_in): entries = '\n'.join( make_entry(entry) for entry in sorted(entries, key=itemgetter('name')) ) if removed_in: if removed_in > lisa.version.version_tuple: remove = 'to be removed' else: remove = 'removed' removed_in = ' {} in {}'.format(remove, format_version(removed_in)) else: removed_in = '' table = ".. list-table:: Deprecated names{removed_in}\n :align: left{entries}".format( entries=indent('\n\n' + entries), removed_in=removed_in, ) header = 'Deprecated names{}'.format(removed_in) header += '\n' + '+' * len(header) return header + '\n\n' + table
def decorator(obj): obj_name = getname(obj) if removed_in and current_version >= removed_in: raise DeprecationWarning( '{name} was marked as being removed in version {removed_in} but is still present in current version {version}' .format( name=obj_name, removed_in=format_version(removed_in), version=format_version(current_version), )) def make_msg(style=None): if replaced_by is not None: try: doc_url = ' (see: {})'.format(get_doc_url(replaced_by)) except Exception: doc_url = '' replacement_msg = ', use {} instead{}'.format( getname(replaced_by, style=style), doc_url, ) else: replacement_msg = '' if removed_in: removal_msg = ' and will be removed in version {}'.format( format_version(removed_in)) else: removal_msg = '' return '{name} is deprecated{remove}{replace}{msg}'.format( name=getname(obj, style=style, abbrev=True), replace=replacement_msg, remove=removal_msg, msg=': ' + msg if msg else '', ) # stacklevel != 1 breaks the filtering for warnings emitted by APIs # called from external modules, like __init_subclass__ that is called # from other modules like abc.py def wrap_func(func, stacklevel=1): @functools.wraps(func) def wrapper(*args, **kwargs): warnings.warn(make_msg(), DeprecationWarning, stacklevel=stacklevel) return func(*args, **kwargs) return wrapper # Make sure we don't accidentally override an existing entry assert obj_name not in DEPRECATED_MAP DEPRECATED_MAP[obj_name] = { 'obj': obj, 'replaced_by': replaced_by, 'msg': msg, 'removed_in': removed_in, 'deprecated_in': deprecated_in, } # For classes, wrap __new__ and update docstring if isinstance(obj, type): # Warn on instance creation obj.__new__ = wrap_func(obj.__new__) # Will show the warning when the class is subclassed # in Python >= 3.6 obj.__init_subclass__ = wrap_func(obj.__init_subclass__) return_obj = obj update_doc_of = obj elif isinstance(obj, property): # Since we cannot update the property itself, replace it with a new # one that uses a wrapped getter. This should be safe as properties # seems to be immutable, so there is no risk of somebody # monkey-patching the object and us throwing away the extra # attributes. # Note that this will only wrap accessors that are visible at the # time the decorator is applied. obj = property( fget=wrap_func(obj.fget, stacklevel=2), fset=wrap_func(obj.fset, stacklevel=2), fdel=wrap_func(obj.fdel, stacklevel=2), ) return_obj = obj update_doc_of = obj elif isinstance(obj, (staticmethod, classmethod)): func = obj.__func__ stacklevel = get_meth_stacklevel(func.__name__) func = wrap_func(func, stacklevel=stacklevel) # Build a new staticmethod/classmethod with the updated function return_obj = obj.__class__(func) # Updating the __doc__ of the staticmethod/classmethod itself will # have no effect, so update the doc of the underlying function update_doc_of = func # For other callables, emit the warning when called else: stacklevel = get_meth_stacklevel(obj.__name__) return_obj = wrap_func(obj, stacklevel=stacklevel) update_doc_of = return_obj doc = inspect.getdoc(update_doc_of) or '' update_doc_of.__doc__ = doc + '\n\n' + textwrap.dedent(""" .. attention:: .. deprecated:: {deprecated_in} {msg} """.format( deprecated_in=deprecated_in if deprecated_in else '<unknown>', msg=make_msg(style='rst'), )).strip() return return_obj
def decorator(obj): obj_name = getname(obj) if removed_in and current_version >= removed_in: raise DeprecationWarning( '{name} was marked as being removed in version {removed_in} but is still present in current version {version}' .format( name=obj_name, removed_in=format_version(removed_in), version=format_version(current_version), )) # stacklevel != 1 breaks the filtering for warnings emitted by APIs # called from external modules, like __init_subclass__ that is called # from other modules like abc.py if parameter: def wrap_func(func, stacklevel=1): sig = inspect.signature(func) @functools.wraps(func) def wrapper(*args, **kwargs): kwargs = sig.bind(*args, **kwargs).arguments if parameter in kwargs: warnings.warn(make_msg(obj, parameter), DeprecationWarning, stacklevel=stacklevel) return func(**kwargs) return wrapper else: def wrap_func(func, stacklevel=1): @functools.wraps(func) def wrapper(*args, **kwargs): warnings.warn(make_msg(obj), DeprecationWarning, stacklevel=stacklevel) return func(*args, **kwargs) return wrapper # Make sure we don't accidentally override an existing entry assert obj_name not in DEPRECATED_MAP DEPRECATED_MAP[obj_name] = { 'obj': obj, 'replaced_by': replaced_by, 'msg': msg, 'removed_in': removed_in, 'deprecated_in': deprecated_in, } # For classes, wrap __new__ and update docstring if isinstance(obj, type): # Warn on instance creation obj.__init__ = wrap_func(obj.__init__) # Will show the warning when the class is subclassed # in Python >= 3.6 . Earlier versions of Python don't have # object.__init_subclass__ if hasattr(obj, '__init_subclass__'): obj.__init_subclass__ = wrap_func(obj.__init_subclass__) return_obj = obj update_doc_of = obj elif isinstance(obj, property): # Since we cannot update the property itself, replace it with a new # one that uses a wrapped getter. This should be safe as properties # seems to be immutable, so there is no risk of somebody # monkey-patching the object and us throwing away the extra # attributes. # Note that this will only wrap accessors that are visible at the # time the decorator is applied. obj = property( fget=wrap_func(obj.fget, stacklevel=2), fset=wrap_func(obj.fset, stacklevel=2), fdel=wrap_func(obj.fdel, stacklevel=2), ) return_obj = obj update_doc_of = obj elif isinstance(obj, (staticmethod, classmethod)): func = obj.__func__ stacklevel = get_meth_stacklevel(func.__name__) func = wrap_func(func, stacklevel=stacklevel) # Build a new staticmethod/classmethod with the updated function return_obj = obj.__class__(func) # Updating the __doc__ of the staticmethod/classmethod itself will # have no effect, so update the doc of the underlying function update_doc_of = func # For other callables, emit the warning when called else: stacklevel = get_meth_stacklevel(obj.__name__) return_obj = wrap_func(obj, stacklevel=stacklevel) update_doc_of = return_obj extra_doc = textwrap.dedent(""" .. attention:: .. deprecated:: {deprecated_in} {msg} """.format( deprecated_in=deprecated_in if deprecated_in else '<unknown>', # The documentation already creates references to the replacement, # so we can avoid downloading the inventory for nothing. msg=make_msg(obj, parameter, style='rst', show_doc_url=False), )).strip() doc = inspect.getdoc(update_doc_of) or '' # Update the description of the parameter in the right spot in the docstring if parameter: # Split into chunks of restructured text at boundaries such as # ":param foo: ..." or ":type foo: ..." blocks = [] curr_block = [] for line in doc.splitlines(keepends=True): if re.match(r'\s*:', line): curr_block = [] blocks.append(curr_block) curr_block.append(line) # Add the extra bits in the right block and join lines of the block def update_block(block): if re.match(':param\s+{}'.format(re.escape(parameter)), block[0]): if len(block) > 1: indentation = re.match(r'^(\s*)', block[-1]).group(0) else: indentation = ' ' * 4 block.append('\n' + textwrap.indent(extra_doc, indentation) + '\n') return ''.join(block) doc = ''.join(map(update_block, blocks)) # Otherwise just append the extra bits at the end of the docstring else: doc += '\n\n' + extra_doc update_doc_of.__doc__ = doc return return_obj