1# Copyright 2016 The TensorFlow Authors. All Rights Reserved. 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14# ============================================================================== 15 16"""Utility functions for writing decorators (which modify docstrings).""" 17import sys 18 19 20def get_qualified_name(function): 21 # Python 3 22 if hasattr(function, '__qualname__'): 23 return function.__qualname__ 24 25 # Python 2 26 if hasattr(function, 'im_class'): 27 return function.im_class.__name__ + '.' + function.__name__ 28 return function.__name__ 29 30 31def _normalize_docstring(docstring): 32 """Normalizes the docstring. 33 34 Replaces tabs with spaces, removes leading and trailing blanks lines, and 35 removes any indentation. 36 37 Copied from PEP-257: 38 https://www.python.org/dev/peps/pep-0257/#handling-docstring-indentation 39 40 Args: 41 docstring: the docstring to normalize 42 43 Returns: 44 The normalized docstring 45 """ 46 if not docstring: 47 return '' 48 # Convert tabs to spaces (following the normal Python rules) 49 # and split into a list of lines: 50 lines = docstring.expandtabs().splitlines() 51 # Determine minimum indentation (first line doesn't count): 52 # (we use sys.maxsize because sys.maxint doesn't exist in Python 3) 53 indent = sys.maxsize 54 for line in lines[1:]: 55 stripped = line.lstrip() 56 if stripped: 57 indent = min(indent, len(line) - len(stripped)) 58 # Remove indentation (first line is special): 59 trimmed = [lines[0].strip()] 60 if indent < sys.maxsize: 61 for line in lines[1:]: 62 trimmed.append(line[indent:].rstrip()) 63 # Strip off trailing and leading blank lines: 64 while trimmed and not trimmed[-1]: 65 trimmed.pop() 66 while trimmed and not trimmed[0]: 67 trimmed.pop(0) 68 # Return a single string: 69 return '\n'.join(trimmed) 70 71 72def add_notice_to_docstring(doc, 73 instructions, 74 no_doc_str, 75 suffix_str, 76 notice, 77 notice_type='Warning'): 78 """Adds a deprecation notice to a docstring. 79 80 Args: 81 doc: The original docstring. 82 instructions: A string, describing how to fix the problem. 83 no_doc_str: The default value to use for `doc` if `doc` is empty. 84 suffix_str: Is added to the end of the first line. 85 notice: A list of strings. The main notice warning body. 86 notice_type: The type of notice to use. Should be one of `[Caution, 87 Deprecated, Important, Note, Warning]` 88 89 Returns: 90 A new docstring, with the notice attached. 91 92 Raises: 93 ValueError: If `notice` is empty. 94 """ 95 allowed_notice_types = ['Deprecated', 'Warning', 'Caution', 'Important', 96 'Note'] 97 if notice_type not in allowed_notice_types: 98 raise ValueError( 99 f'Unrecognized notice type. Should be one of: {allowed_notice_types}') 100 101 if not doc: 102 lines = [no_doc_str] 103 else: 104 lines = _normalize_docstring(doc).splitlines() 105 lines[0] += ' ' + suffix_str 106 107 if not notice: 108 raise ValueError('The `notice` arg must not be empty.') 109 110 notice[0] = f'{notice_type}: {notice[0]}' 111 notice = [''] + notice + ([instructions] if instructions else []) 112 113 if len(lines) > 1: 114 # Make sure that we keep our distance from the main body 115 if lines[1].strip(): 116 notice.append('') 117 118 lines[1:1] = notice 119 else: 120 lines += notice 121 122 return '\n'.join(lines) 123 124 125def validate_callable(func, decorator_name): 126 if not hasattr(func, '__call__'): 127 raise ValueError( 128 '%s is not a function. If this is a property, make sure' 129 ' @property appears before @%s in your source code:' 130 '\n\n@property\n@%s\ndef method(...)' % ( 131 func, decorator_name, decorator_name)) 132 133 134class classproperty(object): # pylint: disable=invalid-name 135 """Class property decorator. 136 137 Example usage: 138 139 class MyClass(object): 140 141 @classproperty 142 def value(cls): 143 return '123' 144 145 > print MyClass.value 146 123 147 """ 148 149 def __init__(self, func): 150 self._func = func 151 152 def __get__(self, owner_self, owner_cls): 153 return self._func(owner_cls) 154 155 156class _CachedClassProperty(object): 157 """Cached class property decorator. 158 159 Transforms a class method into a property whose value is computed once 160 and then cached as a normal attribute for the life of the class. Example 161 usage: 162 163 >>> class MyClass(object): 164 ... @cached_classproperty 165 ... def value(cls): 166 ... print("Computing value") 167 ... return '<property of %s>' % cls.__name__ 168 >>> class MySubclass(MyClass): 169 ... pass 170 >>> MyClass.value 171 Computing value 172 '<property of MyClass>' 173 >>> MyClass.value # uses cached value 174 '<property of MyClass>' 175 >>> MySubclass.value 176 Computing value 177 '<property of MySubclass>' 178 179 This decorator is similar to `functools.cached_property`, but it adds a 180 property to the class, not to individual instances. 181 """ 182 183 def __init__(self, func): 184 self._func = func 185 self._cache = {} 186 187 def __get__(self, obj, objtype): 188 if objtype not in self._cache: 189 self._cache[objtype] = self._func(objtype) 190 return self._cache[objtype] 191 192 def __set__(self, obj, value): 193 raise AttributeError('property %s is read-only' % self._func.__name__) 194 195 def __delete__(self, obj): 196 raise AttributeError('property %s is read-only' % self._func.__name__) 197 198 199def cached_classproperty(func): 200 return _CachedClassProperty(func) 201 202 203cached_classproperty.__doc__ = _CachedClassProperty.__doc__ 204