• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2017 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"""Decorator that provides a warning if the wrapped object is never used."""
16from __future__ import absolute_import
17from __future__ import division
18from __future__ import print_function
19
20import copy
21import sys
22import textwrap
23import traceback
24import types
25
26from tensorflow.python.eager import context
27from tensorflow.python.framework import ops
28from tensorflow.python.platform import tf_logging
29from tensorflow.python.util import tf_decorator
30
31
32class _TFShouldUseHelper(object):
33  """Object stored in TFShouldUse-wrapped objects.
34
35  When it is deleted it will emit a warning or error if its `sate` method
36  has not been called by time of deletion, and Tensorflow is not executing
37  eagerly or inside a tf.function (which use autodeps and resolve the
38  main issues this wrapper warns about).
39  """
40
41  def __init__(self, type_, repr_, stack_frame, error_in_function,
42               warn_in_eager):
43    self._type = type_
44    self._repr = repr_
45    self._stack_frame = stack_frame
46    self._error_in_function = error_in_function
47    if context.executing_eagerly():
48      # If warn_in_eager, sated == False.  Otherwise true.
49      self._sated = not warn_in_eager
50    elif ops.inside_function():
51      if error_in_function:
52        self._sated = False
53        ops.add_exit_callback_to_default_func_graph(
54            lambda: self._check_sated(raise_error=True))
55      else:
56        self._sated = True
57    else:
58      # TF1 graph building mode
59      self._sated = False
60
61  def sate(self):
62    self._sated = True
63    self._type = None
64    self._repr = None
65    self._stack_frame = None
66    self._logging_module = None
67
68  def _check_sated(self, raise_error):
69    """Check if the object has been sated."""
70    if self._sated:
71      return
72    creation_stack = ''.join(
73        [line.rstrip()
74         for line in traceback.format_stack(self._stack_frame, limit=5)])
75    if raise_error:
76      try:
77        raise RuntimeError(
78            'Object was never used (type {}): {}.  If you want to mark it as '
79            'used call its "mark_used()" method.  It was originally created '
80            'here:\n{}'.format(self._type, self._repr, creation_stack))
81      finally:
82        self.sate()
83    else:
84      tf_logging.error(
85          '==================================\n'
86          'Object was never used (type {}):\n{}\nIf you want to mark it as '
87          'used call its "mark_used()" method.\nIt was originally created '
88          'here:\n{}\n'
89          '=================================='
90          .format(self._type, self._repr, creation_stack))
91
92  def __del__(self):
93    self._check_sated(raise_error=False)
94
95
96def _new__init__(self, wrapped_value, tf_should_use_helper):
97  # pylint: disable=protected-access
98  self._tf_should_use_helper = tf_should_use_helper
99  self._tf_should_use_wrapped_value = wrapped_value
100
101
102def _new__setattr__(self, key, value):
103  if key in ('_tf_should_use_helper', '_tf_should_use_wrapped_value'):
104    return object.__setattr__(self, key, value)
105  return setattr(
106      object.__getattribute__(self, '_tf_should_use_wrapped_value'),
107      key, value)
108
109
110def _new__getattribute__(self, key):
111  if key not in ('_tf_should_use_helper', '_tf_should_use_wrapped_value'):
112    object.__getattribute__(self, '_tf_should_use_helper').sate()
113  if key in ('_tf_should_use_helper', 'mark_used', '__setatt__'):
114    return object.__getattribute__(self, key)
115  return getattr(
116      object.__getattribute__(self, '_tf_should_use_wrapped_value'), key)
117
118
119def _new_mark_used(self, *args, **kwargs):
120  object.__getattribute__(self, '_tf_should_use_helper').sate()
121  try:
122    mu = object.__getattribute__(
123        object.__getattribute__(self, '_tf_should_use_wrapped_value'),
124        'mark_used')
125    return mu(*args, **kwargs)
126  except AttributeError:
127    pass
128
129
130_WRAPPERS = {}
131
132
133def _get_wrapper(x, tf_should_use_helper):
134  """Create a wrapper for object x, whose class subclasses type(x).
135
136  The wrapper will emit a warning if it is deleted without any of its
137  properties being accessed or methods being called.
138
139  Args:
140    x: The instance to wrap.
141    tf_should_use_helper: The object that tracks usage.
142
143  Returns:
144    An object wrapping `x`, of type `type(x)`.
145  """
146  type_x = type(x)
147  memoized = _WRAPPERS.get(type_x, None)
148  if memoized:
149    return memoized(x, tf_should_use_helper)
150
151  tx = copy.deepcopy(type_x)
152  # Prefer using __orig_bases__, which preserve generic type arguments.
153  bases = getattr(tx, '__orig_bases__', tx.__bases__)
154
155  # Use types.new_class when available, which is preferred over plain type in
156  # some distributions.
157  if sys.version_info >= (3, 5):
158    def set_body(ns):
159      ns.update(tx.__dict__)
160      return ns
161
162    copy_tx = types.new_class(tx.__name__, bases, exec_body=set_body)
163  else:
164    copy_tx = type(tx.__name__, bases, dict(tx.__dict__))
165
166  copy_tx.__init__ = _new__init__
167  copy_tx.__getattribute__ = _new__getattribute__
168  copy_tx.mark_used = _new_mark_used
169  copy_tx.__setattr__ = _new__setattr__
170  _WRAPPERS[type_x] = copy_tx
171
172  return copy_tx(x, tf_should_use_helper)
173
174
175def _add_should_use_warning(x, error_in_function=False, warn_in_eager=False):
176  """Wraps object x so that if it is never used, a warning is logged.
177
178  Args:
179    x: Python object.
180    error_in_function: Python bool.  If `True`, a `RuntimeError` is raised
181      if the returned value is never used when created during `tf.function`
182      tracing.
183    warn_in_eager: Python bool. If `True` raise warning if in Eager mode as well
184      as graph mode.
185
186  Returns:
187    An instance of `TFShouldUseWarningWrapper` which subclasses `type(x)`
188    and is a very shallow wrapper for `x` which logs access into `x`.
189  """
190  if x is None or (isinstance(x, list) and not x):
191    return x
192
193  if context.executing_eagerly() and not warn_in_eager:
194    return x
195
196  if ops.inside_function() and not error_in_function:
197    # We don't currently log warnings in tf.function calls, so just skip it.
198    return x
199
200  # Extract the current frame for later use by traceback printing.
201  try:
202    raise ValueError()
203  except ValueError:
204    stack_frame = sys.exc_info()[2].tb_frame.f_back
205
206  tf_should_use_helper = _TFShouldUseHelper(
207      type_=type(x),
208      repr_=repr(x),
209      stack_frame=stack_frame,
210      error_in_function=error_in_function,
211      warn_in_eager=warn_in_eager)
212
213  return _get_wrapper(x, tf_should_use_helper)
214
215
216def should_use_result(fn=None, warn_in_eager=False, error_in_function=False):
217  """Function wrapper that ensures the function's output is used.
218
219  If the output is not used, a `logging.error` is logged.  If
220  `error_in_function` is set, then a `RuntimeError` will be raised at the
221  end of function tracing if the output is not used by that point.
222
223  An output is marked as used if any of its attributes are read, modified, or
224  updated.  Examples when the output is a `Tensor` include:
225
226  - Using it in any capacity (e.g. `y = t + 0`, `sess.run(t)`)
227  - Accessing a property (e.g. getting `t.name` or `t.op`).
228  - Calling `t.mark_used()`.
229
230  Note, certain behaviors cannot be tracked - for these the object may not
231  be marked as used.  Examples include:
232
233  - `t != 0`.  In this case, comparison is done on types / ids.
234  - `isinstance(t, tf.Tensor)`.  Similar to above.
235
236  Args:
237    fn: The function to wrap.
238    warn_in_eager: Whether to create warnings in Eager as well.
239    error_in_function: Whether to raise an error when creating a tf.function.
240
241  Returns:
242    The wrapped function.
243  """
244  def decorated(fn):
245    """Decorates the input function."""
246    def wrapped(*args, **kwargs):
247      return _add_should_use_warning(fn(*args, **kwargs),
248                                     warn_in_eager=warn_in_eager,
249                                     error_in_function=error_in_function)
250    fn_doc = fn.__doc__ or ''
251    split_doc = fn_doc.split('\n', 1)
252    if len(split_doc) == 1:
253      updated_doc = fn_doc
254    else:
255      brief, rest = split_doc
256      updated_doc = '\n'.join([brief, textwrap.dedent(rest)])
257
258    note = ('\n\nNote: The output of this function should be used. If it is '
259            'not, a warning will be logged or an error may be raised. '
260            'To mark the output as used, call its .mark_used() method.')
261    return tf_decorator.make_decorator(
262        target=fn,
263        decorator_func=wrapped,
264        decorator_name='should_use_result',
265        decorator_doc=updated_doc + note)
266
267  if fn is not None:
268    return decorated(fn)
269  else:
270    return decorated
271