• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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