diff --git a/tensorboard/BUILD b/tensorboard/BUILD index e4a8773f0e..239219ac28 100644 --- a/tensorboard/BUILD +++ b/tensorboard/BUILD @@ -71,6 +71,18 @@ py_library( ], ) +py_test( + name = "lib_test", + size = "small", + srcs = ["lib_test.py"], + srcs_version = "PY2AND3", + visibility = ["//tensorboard:internal"], + deps = [ + ":lib", + "@org_pythonhosted_six", + ], +) + py_library( name = "manager", srcs = ["manager.py"], diff --git a/tensorboard/__init__.py b/tensorboard/__init__.py index 6737c220d8..0594707ec0 100644 --- a/tensorboard/__init__.py +++ b/tensorboard/__init__.py @@ -22,19 +22,62 @@ from tensorboard import lazy +# Please be careful when changing the structure of this file. +# +# The lazy imports in this file must use `importlib.import_module`, not +# `import tensorboard.foo` or `from tensorboard import foo`, or it will +# be impossible to reload the TensorBoard module without breaking these +# top-level public APIs. This has to do with the gory details of +# Python's module system. Take `tensorboard.notebook` as an example: +# +# - When the `tensorboard` module (that's us!) is initialized, its +# `notebook` attribute is initialized to a new LazyModule. The +# actual `tensorboard.notebook` submodule is not loaded. +# +# - When the `tensorboard.notebook` submodule is first loaded, Python +# _reassigns_ the `notebook` attribute on the `tensorboard` module +# object to point to the underlying `tensorboard.notebook` module +# object, rather than its former LazyModule value. This occurs +# whether the module is loaded via the lazy module or directly as an +# import: +# +# - import tensorboard; tensorboard.notebook.start(...) # one way +# - from tensorboard import notebook # other way; same effect +# +# - When the `tensorboard` module is reloaded, its `notebook` +# attribute is once again bound to a (new) LazyModule, while the +# `tensorboard.notebook` module object is unaffected and still +# exists in `sys.modules`. But then... +# +# - When the new LazyModule is forced, it must resolve to the existing +# `tensorboard.notebook` module object rather than itself (which +# just creates a stack overflow). If the LazyModule load function +# uses `import tensorboard.notebook; return tensorboard.notebook`, +# then the first statement will do _nothing_ because the +# `tensorboard.notebook` module is already loaded, and the second +# statement will return the LazyModule itself. The same goes for the +# `from tensorboard import notebook` form. We need to ensure that +# the submodule is loaded and then pull the actual module object out +# of `sys.modules`... which is exactly what `importlib` handles for +# us. +# +# See for +# additional discussion. + + @lazy.lazy_load('tensorboard.notebook') def notebook(): - import tensorboard.notebook as module # pylint: disable=g-import-not-at-top - return module + import importlib # pylint: disable=g-import-not-at-top + return importlib.import_module('tensorboard.notebook') @lazy.lazy_load('tensorboard.program') def program(): - import tensorboard.program as module # pylint: disable=g-import-not-at-top - return module + import importlib # pylint: disable=g-import-not-at-top + return importlib.import_module('tensorboard.program') @lazy.lazy_load('tensorboard.summary') def summary(): - import tensorboard.summary as module # pylint: disable=g-import-not-at-top - return module + import importlib # pylint: disable=g-import-not-at-top + return importlib.import_module('tensorboard.summary') diff --git a/tensorboard/lib_test.py b/tensorboard/lib_test.py new file mode 100644 index 0000000000..aab06b023e --- /dev/null +++ b/tensorboard/lib_test.py @@ -0,0 +1,43 @@ +# Copyright 2019 The TensorFlow Authors. All Rights Reserved. +# +# Licensed 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. + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +from six import moves +import sys +import unittest + + +class ReloadTensorBoardTest(unittest.TestCase): + + def test_functional_after_reload(self): + self.assertNotIn("tensorboard", sys.modules) + import tensorboard as tensorboard # it makes the Google sync happy + submodules = ["notebook", "program", "summary"] + dirs_before = { + module_name: dir(getattr(tensorboard, module_name)) + for module_name in submodules + } + tensorboard = moves.reload_module(tensorboard) + dirs_after = { + module_name: dir(getattr(tensorboard, module_name)) + for module_name in submodules + } + self.assertEqual(dirs_before, dirs_after) + + +if __name__ == '__main__': + unittest.main()