Make a better doc generator.
Change: 146177492
This commit is contained in:
parent
e2127701a5
commit
d33692e9cc
@ -39,7 +39,7 @@ def get_seed(op_seed):
|
|||||||
graph, or for only specific operations.
|
graph, or for only specific operations.
|
||||||
|
|
||||||
For details on how the graph-level seed interacts with op seeds, see
|
For details on how the graph-level seed interacts with op seeds, see
|
||||||
[`set_random_seed`](../../api_docs/python/constant_op.md#set_random_seed).
|
@{set_random_seed}.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
op_seed: integer.
|
op_seed: integer.
|
||||||
|
|||||||
@ -40,16 +40,28 @@ class PublicAPIVisitor(object):
|
|||||||
# Each entry maps a module path to a name to ignore in traversal.
|
# Each entry maps a module path to a name to ignore in traversal.
|
||||||
_do_not_descend_map = {
|
_do_not_descend_map = {
|
||||||
# TODO(drpng): This can be removed once sealed off.
|
# TODO(drpng): This can be removed once sealed off.
|
||||||
'': ['platform', 'pywrap_tensorflow'],
|
'': ['platform', 'pywrap_tensorflow', 'user_ops'],
|
||||||
|
|
||||||
# Some implementations have this internal module that we shouldn't expose.
|
# Some implementations have this internal module that we shouldn't expose.
|
||||||
'flags': ['cpp_flags'],
|
'flags': ['cpp_flags'],
|
||||||
|
|
||||||
# Everything below here is legitimate.
|
# Everything below here is legitimate.
|
||||||
'app': 'flags', # It'll stay, but it's not officially part of the API
|
'app': ['flags'], # It'll stay, but it's not officially part of the API.
|
||||||
'test': ['mock'], # Imported for compatibility between py2/3.
|
'test': ['mock'], # Imported for compatibility between py2/3.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def do_not_descend_map(self):
|
||||||
|
"""A map from parents to symbols that should not be descended into.
|
||||||
|
|
||||||
|
This map can be edited, but it should not be edited once traversal has
|
||||||
|
begun.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The map marking symbols to not explore.
|
||||||
|
"""
|
||||||
|
return self._do_not_descend_map
|
||||||
|
|
||||||
def _isprivate(self, name):
|
def _isprivate(self, name):
|
||||||
"""Return whether a name is private."""
|
"""Return whether a name is private."""
|
||||||
return name.startswith('_')
|
return name.startswith('_')
|
||||||
@ -61,6 +73,8 @@ class PublicAPIVisitor(object):
|
|||||||
|
|
||||||
def __call__(self, path, parent, children):
|
def __call__(self, path, parent, children):
|
||||||
"""Visitor interface, see `traverse` for details."""
|
"""Visitor interface, see `traverse` for details."""
|
||||||
|
|
||||||
|
# Avoid long waits in cases of pretty unambiguous failure.
|
||||||
if inspect.ismodule(parent) and len(path.split('.')) > 10:
|
if inspect.ismodule(parent) and len(path.split('.')) > 10:
|
||||||
raise RuntimeError('Modules nested too deep:\n%s\n\nThis is likely a '
|
raise RuntimeError('Modules nested too deep:\n%s\n\nThis is likely a '
|
||||||
'problem with an accidental public import.' % path)
|
'problem with an accidental public import.' % path)
|
||||||
|
|||||||
@ -16,6 +16,74 @@ py_binary(
|
|||||||
deps = ["//tensorflow:tensorflow_py"],
|
deps = ["//tensorflow:tensorflow_py"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
py_library(
|
||||||
|
name = "doc_generator_visitor",
|
||||||
|
srcs = [
|
||||||
|
"doc_generator_visitor.py",
|
||||||
|
],
|
||||||
|
srcs_version = "PY2AND3",
|
||||||
|
)
|
||||||
|
|
||||||
|
py_test(
|
||||||
|
name = "doc_generator_visitor_test",
|
||||||
|
size = "small",
|
||||||
|
srcs = [
|
||||||
|
"doc_generator_visitor_test.py",
|
||||||
|
],
|
||||||
|
srcs_version = "PY2AND3",
|
||||||
|
deps = [
|
||||||
|
":doc_generator_visitor",
|
||||||
|
"//tensorflow/python:platform_test",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
py_library(
|
||||||
|
name = "parser",
|
||||||
|
srcs = [
|
||||||
|
"parser.py",
|
||||||
|
],
|
||||||
|
srcs_version = "PY2AND3",
|
||||||
|
)
|
||||||
|
|
||||||
|
py_test(
|
||||||
|
name = "parser_test",
|
||||||
|
size = "small",
|
||||||
|
srcs = [
|
||||||
|
"parser_test.py",
|
||||||
|
],
|
||||||
|
srcs_version = "PY2AND3",
|
||||||
|
deps = [
|
||||||
|
":parser",
|
||||||
|
"//tensorflow/python:platform_test",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
py_binary(
|
||||||
|
name = "generate",
|
||||||
|
srcs = ["generate.py"],
|
||||||
|
srcs_version = "PY2AND3",
|
||||||
|
deps = [
|
||||||
|
"//tensorflow:tensorflow_py",
|
||||||
|
"//tensorflow/tools/common:public_api",
|
||||||
|
"//tensorflow/tools/common:traverse",
|
||||||
|
"//tensorflow/tools/docs:doc_generator_visitor",
|
||||||
|
"//tensorflow/tools/docs:parser",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
py_test(
|
||||||
|
name = "generate_test",
|
||||||
|
size = "small",
|
||||||
|
srcs = [
|
||||||
|
"generate_test.py",
|
||||||
|
],
|
||||||
|
srcs_version = "PY2AND3",
|
||||||
|
deps = [
|
||||||
|
":generate",
|
||||||
|
"//tensorflow/python:platform_test",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
filegroup(
|
filegroup(
|
||||||
name = "doxy_config",
|
name = "doxy_config",
|
||||||
srcs = ["tf-doxy_for_md-config"],
|
srcs = ["tf-doxy_for_md-config"],
|
||||||
|
|||||||
149
tensorflow/tools/docs/doc_generator_visitor.py
Normal file
149
tensorflow/tools/docs/doc_generator_visitor.py
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
# Copyright 2015 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.
|
||||||
|
# ==============================================================================
|
||||||
|
"""A `traverse` visitor for processing documentation."""
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from __future__ import division
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
|
||||||
|
class DocGeneratorVisitor(object):
|
||||||
|
"""A visitor that generates docs for a python object when __call__ed."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._index = {}
|
||||||
|
self._tree = {}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def index(self):
|
||||||
|
"""A map from fully qualified names to objects to be documented.
|
||||||
|
|
||||||
|
The index is filled when the visitor is passed to `traverse`.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The index filled by traversal.
|
||||||
|
"""
|
||||||
|
return self._index
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tree(self):
|
||||||
|
"""A map from fully qualified names to all its child names for traversal.
|
||||||
|
|
||||||
|
The full name to member names map is filled when the visitor is passed to
|
||||||
|
`traverse`.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The full name to member name map filled by traversal.
|
||||||
|
"""
|
||||||
|
return self._tree
|
||||||
|
|
||||||
|
def __call__(self, parent_name, parent, children):
|
||||||
|
"""Visitor interface, see `tensorflow/tools/common:traverse` for details.
|
||||||
|
|
||||||
|
This method is called for each symbol found in a traversal using
|
||||||
|
`tensorflow/tools/common:traverse`. It should not be called directly in
|
||||||
|
user code.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
parent_name: The fully qualified name of a symbol found during traversal.
|
||||||
|
parent: The Python object referenced by `parent_name`.
|
||||||
|
children: A list of `(name, py_object)` pairs enumerating, in alphabetical
|
||||||
|
order, the children (as determined by `inspect.getmembers`) of `parent`.
|
||||||
|
`name` is the local name of `py_object` in `parent`.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RuntimeError: If this visitor is called with a `parent` that is not a
|
||||||
|
class or module.
|
||||||
|
"""
|
||||||
|
self._index[parent_name] = parent
|
||||||
|
self._tree[parent_name] = []
|
||||||
|
|
||||||
|
if inspect.ismodule(parent):
|
||||||
|
print('module %s: %r' % (parent_name, parent))
|
||||||
|
elif inspect.isclass(parent):
|
||||||
|
print('class %s: %r' % (parent_name, parent))
|
||||||
|
else:
|
||||||
|
raise RuntimeError('Unexpected type in visitor -- %s: %r' %
|
||||||
|
(parent_name, parent))
|
||||||
|
|
||||||
|
for name, child in children:
|
||||||
|
full_name = '.'.join([parent_name, name]) if parent_name else name
|
||||||
|
self._index[full_name] = child
|
||||||
|
self._tree[parent_name].append(name)
|
||||||
|
|
||||||
|
def find_duplicates(self):
|
||||||
|
"""Compute data structures containing information about duplicates.
|
||||||
|
|
||||||
|
Find duplicates in `index` and decide on one to be the "master" name.
|
||||||
|
|
||||||
|
Returns a map `duplicate_of` from aliases to their master name (the master
|
||||||
|
name itself has no entry in this map), and a map `duplicates` from master
|
||||||
|
names to a lexicographically sorted list of all aliases for that name (incl.
|
||||||
|
the master name).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A tuple `(duplicate_of, duplicates)` as described above.
|
||||||
|
"""
|
||||||
|
# Maps the id of a symbol to its fully qualified name. For symbols that have
|
||||||
|
# several aliases, this map contains the first one found.
|
||||||
|
# We use id(py_object) to get a hashable value for py_object. Note all
|
||||||
|
# objects in _index are in memory at the same time so this is safe.
|
||||||
|
reverse_index = {}
|
||||||
|
|
||||||
|
# Make a preliminary duplicates map. For all sets of duplicate names, it
|
||||||
|
# maps the first name found to a list of all duplicate names.
|
||||||
|
raw_duplicates = {}
|
||||||
|
for full_name, py_object in self._index.iteritems():
|
||||||
|
# We cannot use the duplicate mechanism for constants, since e.g.,
|
||||||
|
# id(c1) == id(c2) with c1=1, c2=1. This is unproblematic since constants
|
||||||
|
# have no usable docstring and won't be documented automatically.
|
||||||
|
if (inspect.ismodule(py_object) or
|
||||||
|
inspect.isclass(py_object) or
|
||||||
|
inspect.isfunction(py_object) or
|
||||||
|
inspect.isroutine(py_object) or
|
||||||
|
inspect.ismethod(py_object) or
|
||||||
|
isinstance(py_object, property)):
|
||||||
|
object_id = id(py_object)
|
||||||
|
if object_id in reverse_index:
|
||||||
|
master_name = reverse_index[object_id]
|
||||||
|
if master_name in raw_duplicates:
|
||||||
|
raw_duplicates[master_name].append(full_name)
|
||||||
|
else:
|
||||||
|
raw_duplicates[master_name] = [master_name, full_name]
|
||||||
|
else:
|
||||||
|
reverse_index[object_id] = full_name
|
||||||
|
|
||||||
|
# Decide on master names, rewire duplicates and make a duplicate_of map
|
||||||
|
# mapping all non-master duplicates to the master name. The master symbol
|
||||||
|
# does not have an entry in this map.
|
||||||
|
duplicate_of = {}
|
||||||
|
# Duplicates maps the main symbols to the set of all duplicates of that
|
||||||
|
# symbol (incl. itself).
|
||||||
|
duplicates = {}
|
||||||
|
for names in raw_duplicates.values():
|
||||||
|
names = sorted(names)
|
||||||
|
|
||||||
|
# Choose the lexicographically first name with the minimum number of
|
||||||
|
# submodules. This will prefer highest level namespace for any symbol.
|
||||||
|
master_name = min(names, key=lambda name: name.count('.'))
|
||||||
|
|
||||||
|
duplicates[master_name] = names
|
||||||
|
for name in names:
|
||||||
|
if name != master_name:
|
||||||
|
duplicate_of[name] = master_name
|
||||||
|
|
||||||
|
return duplicate_of, duplicates
|
||||||
107
tensorflow/tools/docs/doc_generator_visitor_test.py
Normal file
107
tensorflow/tools/docs/doc_generator_visitor_test.py
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
# Copyright 2015 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.
|
||||||
|
# ==============================================================================
|
||||||
|
"""Tests for tools.docs.doc_generator_visitor."""
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from __future__ import division
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
from tensorflow.python.platform import googletest
|
||||||
|
from tensorflow.tools.docs import doc_generator_visitor
|
||||||
|
|
||||||
|
|
||||||
|
class DocGeneratorVisitorTest(googletest.TestCase):
|
||||||
|
|
||||||
|
def test_call_module(self):
|
||||||
|
visitor = doc_generator_visitor.DocGeneratorVisitor()
|
||||||
|
visitor(
|
||||||
|
'doc_generator_visitor', doc_generator_visitor,
|
||||||
|
[('DocGeneratorVisitor', doc_generator_visitor.DocGeneratorVisitor)])
|
||||||
|
|
||||||
|
self.assertEqual({'doc_generator_visitor': ['DocGeneratorVisitor']},
|
||||||
|
visitor.tree)
|
||||||
|
self.assertEqual({
|
||||||
|
'doc_generator_visitor': doc_generator_visitor,
|
||||||
|
'doc_generator_visitor.DocGeneratorVisitor':
|
||||||
|
doc_generator_visitor.DocGeneratorVisitor,
|
||||||
|
}, visitor.index)
|
||||||
|
|
||||||
|
def test_call_class(self):
|
||||||
|
visitor = doc_generator_visitor.DocGeneratorVisitor()
|
||||||
|
visitor(
|
||||||
|
'DocGeneratorVisitor', doc_generator_visitor.DocGeneratorVisitor,
|
||||||
|
[('index', doc_generator_visitor.DocGeneratorVisitor.index)])
|
||||||
|
|
||||||
|
self.assertEqual({'DocGeneratorVisitor': ['index']},
|
||||||
|
visitor.tree)
|
||||||
|
self.assertEqual({
|
||||||
|
'DocGeneratorVisitor': doc_generator_visitor.DocGeneratorVisitor,
|
||||||
|
'DocGeneratorVisitor.index':
|
||||||
|
doc_generator_visitor.DocGeneratorVisitor.index
|
||||||
|
}, visitor.index)
|
||||||
|
|
||||||
|
def test_call_raises(self):
|
||||||
|
visitor = doc_generator_visitor.DocGeneratorVisitor()
|
||||||
|
with self.assertRaises(RuntimeError):
|
||||||
|
visitor('non_class_or_module', 'non_class_or_module_object', [])
|
||||||
|
|
||||||
|
def test_duplicates(self):
|
||||||
|
visitor = doc_generator_visitor.DocGeneratorVisitor()
|
||||||
|
visitor(
|
||||||
|
'submodule.DocGeneratorVisitor',
|
||||||
|
doc_generator_visitor.DocGeneratorVisitor,
|
||||||
|
[('index', doc_generator_visitor.DocGeneratorVisitor.index),
|
||||||
|
('index2', doc_generator_visitor.DocGeneratorVisitor.index)])
|
||||||
|
visitor(
|
||||||
|
'submodule2.DocGeneratorVisitor',
|
||||||
|
doc_generator_visitor.DocGeneratorVisitor,
|
||||||
|
[('index', doc_generator_visitor.DocGeneratorVisitor.index),
|
||||||
|
('index2', doc_generator_visitor.DocGeneratorVisitor.index)])
|
||||||
|
visitor(
|
||||||
|
'DocGeneratorVisitor2',
|
||||||
|
doc_generator_visitor.DocGeneratorVisitor,
|
||||||
|
[('index', doc_generator_visitor.DocGeneratorVisitor.index),
|
||||||
|
('index2', doc_generator_visitor.DocGeneratorVisitor.index)])
|
||||||
|
|
||||||
|
duplicate_of, duplicates = visitor.find_duplicates()
|
||||||
|
|
||||||
|
# The shorter path should be master, or if equal, the lexicographically
|
||||||
|
# first will be.
|
||||||
|
self.assertEqual(
|
||||||
|
{'DocGeneratorVisitor2': sorted(['submodule.DocGeneratorVisitor',
|
||||||
|
'submodule2.DocGeneratorVisitor',
|
||||||
|
'DocGeneratorVisitor2']),
|
||||||
|
'DocGeneratorVisitor2.index': sorted([
|
||||||
|
'submodule.DocGeneratorVisitor.index',
|
||||||
|
'submodule.DocGeneratorVisitor.index2',
|
||||||
|
'submodule2.DocGeneratorVisitor.index',
|
||||||
|
'submodule2.DocGeneratorVisitor.index2',
|
||||||
|
'DocGeneratorVisitor2.index',
|
||||||
|
'DocGeneratorVisitor2.index2'
|
||||||
|
]),
|
||||||
|
}, duplicates)
|
||||||
|
self.assertEqual({
|
||||||
|
'submodule.DocGeneratorVisitor': 'DocGeneratorVisitor2',
|
||||||
|
'submodule.DocGeneratorVisitor.index': 'DocGeneratorVisitor2.index',
|
||||||
|
'submodule.DocGeneratorVisitor.index2': 'DocGeneratorVisitor2.index',
|
||||||
|
'submodule2.DocGeneratorVisitor': 'DocGeneratorVisitor2',
|
||||||
|
'submodule2.DocGeneratorVisitor.index': 'DocGeneratorVisitor2.index',
|
||||||
|
'submodule2.DocGeneratorVisitor.index2': 'DocGeneratorVisitor2.index',
|
||||||
|
'DocGeneratorVisitor2.index2': 'DocGeneratorVisitor2.index'
|
||||||
|
}, duplicate_of)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
googletest.main()
|
||||||
232
tensorflow/tools/docs/generate.py
Normal file
232
tensorflow/tools/docs/generate.py
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
# Copyright 2015 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.
|
||||||
|
# ==============================================================================
|
||||||
|
"""Generate docs for the TensorFlow Python API."""
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from __future__ import division
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import inspect
|
||||||
|
import os
|
||||||
|
|
||||||
|
import tensorflow as tf
|
||||||
|
|
||||||
|
from tensorflow.tools.common import public_api
|
||||||
|
from tensorflow.tools.common import traverse
|
||||||
|
from tensorflow.tools.docs import doc_generator_visitor
|
||||||
|
from tensorflow.tools.docs import parser
|
||||||
|
|
||||||
|
|
||||||
|
def write_docs(output_dir, base_dir, duplicate_of, duplicates, index, tree):
|
||||||
|
"""Write previously extracted docs to disk.
|
||||||
|
|
||||||
|
Write a docs page for each symbol in `index` to a tree of docs at
|
||||||
|
`output_dir`.
|
||||||
|
|
||||||
|
Symbols with multiple aliases will have only one page written about them,
|
||||||
|
which is referenced for all aliases. `duplicate_of` and `duplicates` are used
|
||||||
|
to determine which docs pages to write.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
output_dir: Directory to write documentation markdown files to. Will be
|
||||||
|
created if it doesn't exist.
|
||||||
|
base_dir: Base directory of the code being documented. This prefix is
|
||||||
|
stripped from all file paths that are part of the documentation.
|
||||||
|
duplicate_of: A `dict` mapping fully qualified names to "master" names. This
|
||||||
|
is used to resolve "@{symbol}" references to the "master" name.
|
||||||
|
duplicates: A `dict` mapping fully qualified names to a set of all
|
||||||
|
aliases of this name. This is used to automatically generate a list of all
|
||||||
|
aliases for each name.
|
||||||
|
index: A `dict` mapping fully qualified names to the corresponding Python
|
||||||
|
objects. Used to produce docs for child objects, and to check the validity
|
||||||
|
of "@{symbol}" references.
|
||||||
|
tree: A `dict` mapping a fully qualified name to the names of all its
|
||||||
|
members. Used to populate the members section of a class or module page.
|
||||||
|
"""
|
||||||
|
# Make output_dir.
|
||||||
|
try:
|
||||||
|
if not os.path.exists(output_dir):
|
||||||
|
os.makedirs(output_dir)
|
||||||
|
except OSError as e:
|
||||||
|
print('Creating output dir "%s" failed: %s' % (output_dir, e))
|
||||||
|
raise
|
||||||
|
|
||||||
|
# Parse and write Markdown pages, resolving cross-links (@{symbol}).
|
||||||
|
for full_name, py_object in index.iteritems():
|
||||||
|
|
||||||
|
if full_name in duplicate_of:
|
||||||
|
print('Not writing docs for %s, duplicate of %s.' % (
|
||||||
|
full_name, duplicate_of[full_name]))
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Methods and some routines are documented only as part of their class.
|
||||||
|
if not (inspect.ismodule(py_object) or
|
||||||
|
inspect.isclass(py_object) or
|
||||||
|
inspect.isfunction(py_object)):
|
||||||
|
print('Not writing docs for %s, not a class, module, or function.' % (
|
||||||
|
full_name))
|
||||||
|
continue
|
||||||
|
|
||||||
|
print('Writing docs for %s (%r).' % (full_name, py_object))
|
||||||
|
|
||||||
|
# Generate docs for `py_object`, resolving references.
|
||||||
|
markdown = parser.generate_markdown(full_name, py_object,
|
||||||
|
duplicate_of=duplicate_of,
|
||||||
|
duplicates=duplicates,
|
||||||
|
index=index,
|
||||||
|
tree=tree,
|
||||||
|
base_dir=base_dir)
|
||||||
|
|
||||||
|
# TODO(deannarubin): use _tree to generate sidebar information.
|
||||||
|
|
||||||
|
path = os.path.join(output_dir, parser.documentation_path(full_name))
|
||||||
|
directory = os.path.dirname(path)
|
||||||
|
try:
|
||||||
|
if not os.path.exists(directory):
|
||||||
|
os.makedirs(directory)
|
||||||
|
with open(path, 'w') as f:
|
||||||
|
f.write(markdown)
|
||||||
|
except OSError as e:
|
||||||
|
print('Cannot write documentation for %s to %s: %s' % (full_name,
|
||||||
|
directory, e))
|
||||||
|
raise
|
||||||
|
# TODO(deannarubin): write sidebar file?
|
||||||
|
|
||||||
|
# Write a global index containing all full names with links.
|
||||||
|
with open(os.path.join(output_dir, 'full_index.md'), 'w') as f:
|
||||||
|
f.write(parser.generate_global_index('TensorFlow', 'tensorflow',
|
||||||
|
index, duplicate_of))
|
||||||
|
|
||||||
|
|
||||||
|
def extract():
|
||||||
|
"""Extract docs from tf namespace and write them to disk."""
|
||||||
|
visitor = doc_generator_visitor.DocGeneratorVisitor()
|
||||||
|
api_visitor = public_api.PublicAPIVisitor(visitor)
|
||||||
|
|
||||||
|
# Access something in contrib so tf.contrib is properly loaded (it's hidden
|
||||||
|
# behind lazy loading)
|
||||||
|
# TODO(wicke): Enable contrib traversal once contrib is sealed.
|
||||||
|
# _ = tf.contrib.__name__
|
||||||
|
|
||||||
|
# Exclude some libaries in contrib from the documentation altogether.
|
||||||
|
# TODO(wicke): Shrink this list.
|
||||||
|
api_visitor.do_not_descend_map.update({
|
||||||
|
'contrib': [
|
||||||
|
'compiler',
|
||||||
|
'factorization',
|
||||||
|
'grid_rnn',
|
||||||
|
'labeled_tensor',
|
||||||
|
'ndlstm',
|
||||||
|
'quantization',
|
||||||
|
'session_bundle',
|
||||||
|
'slim',
|
||||||
|
'solvers',
|
||||||
|
'specs',
|
||||||
|
'tensor_forest',
|
||||||
|
'tensorboard',
|
||||||
|
'testing',
|
||||||
|
'tfprof',
|
||||||
|
'training',
|
||||||
|
],
|
||||||
|
'contrib.bayesflow': [
|
||||||
|
'entropy', 'monte_carlo',
|
||||||
|
'special_math', 'stochastic_gradient_estimators',
|
||||||
|
'stochastic_graph', 'stochastic_tensor',
|
||||||
|
'stochastic_variables', 'variational_inference'
|
||||||
|
],
|
||||||
|
'contrib.distributions': ['bijector'],
|
||||||
|
'contrib.graph_editor': [
|
||||||
|
'edit',
|
||||||
|
'match',
|
||||||
|
'reroute',
|
||||||
|
'subgraph',
|
||||||
|
'transform',
|
||||||
|
'select',
|
||||||
|
'util'
|
||||||
|
],
|
||||||
|
'contrib.layers': [
|
||||||
|
'feature_column',
|
||||||
|
'summaries'
|
||||||
|
],
|
||||||
|
'contrib.learn': [
|
||||||
|
'datasets',
|
||||||
|
'graph_actions',
|
||||||
|
'io',
|
||||||
|
'models',
|
||||||
|
'monitors',
|
||||||
|
'ops',
|
||||||
|
'preprocessing',
|
||||||
|
'utils',
|
||||||
|
],
|
||||||
|
'contrib.util': ['loader'],
|
||||||
|
})
|
||||||
|
|
||||||
|
traverse.traverse(tf, api_visitor)
|
||||||
|
|
||||||
|
return visitor
|
||||||
|
|
||||||
|
|
||||||
|
def write(output_dir, base_dir, visitor):
|
||||||
|
"""Write documentation for an index in a `DocGeneratorVisitor` to disk.
|
||||||
|
|
||||||
|
This function will create `output_dir` if it doesn't exist, and write
|
||||||
|
the documentation contained in `visitor`.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
output_dir: The directory to write documentation to. Must not exist.
|
||||||
|
base_dir: The base dir of the library `visitor` has traversed. This is used
|
||||||
|
to compute relative paths for file references.
|
||||||
|
visitor: A `DocGeneratorVisitor` that has traversed a library located at
|
||||||
|
`base_dir`.
|
||||||
|
"""
|
||||||
|
duplicate_of, duplicates = visitor.find_duplicates()
|
||||||
|
write_docs(output_dir, os.path.abspath(base_dir),
|
||||||
|
duplicate_of, duplicates, visitor.index, visitor.tree)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
argument_parser = argparse.ArgumentParser()
|
||||||
|
argument_parser.add_argument(
|
||||||
|
'--output_dir',
|
||||||
|
type=str,
|
||||||
|
default=None,
|
||||||
|
required=True,
|
||||||
|
help='Directory to write docs to. Must not exist.'
|
||||||
|
)
|
||||||
|
|
||||||
|
# This doc generator works on the TensorFlow codebase. Since this script lives
|
||||||
|
# at tensorflow/tools/docs, we can compute the base directory (three levels
|
||||||
|
# up), which is valid unless we're trying to apply this to a different code
|
||||||
|
# base, or are moving the script around.
|
||||||
|
script_dir = os.path.dirname(inspect.getfile(inspect.currentframe()))
|
||||||
|
default_base_dir = os.path.join(script_dir, '..', '..', '..')
|
||||||
|
|
||||||
|
argument_parser.add_argument(
|
||||||
|
'--base_dir',
|
||||||
|
type=str,
|
||||||
|
default=default_base_dir,
|
||||||
|
help=('Base directory to to strip from file names referenced in docs. '
|
||||||
|
'Defaults to three directories up from the location of this file.')
|
||||||
|
)
|
||||||
|
|
||||||
|
flags, _ = argument_parser.parse_known_args()
|
||||||
|
|
||||||
|
if os.path.exists(flags.output_dir):
|
||||||
|
raise RuntimeError('output_dir %s exists.\n'
|
||||||
|
'Cowardly refusing to wipe it, please do that yourself.'
|
||||||
|
% flags.output_dir)
|
||||||
|
|
||||||
|
write(flags.output_dir, flags.base_dir, extract())
|
||||||
114
tensorflow/tools/docs/generate_test.py
Normal file
114
tensorflow/tools/docs/generate_test.py
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
# Copyright 2015 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.
|
||||||
|
# ==============================================================================
|
||||||
|
"""Tests for doc generator traversal."""
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from __future__ import division
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
from tensorflow.python.platform import googletest
|
||||||
|
from tensorflow.tools.docs import generate
|
||||||
|
|
||||||
|
|
||||||
|
def test_function():
|
||||||
|
"""Docstring for test_function."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TestClass(object):
|
||||||
|
"""Docstring for TestClass itself."""
|
||||||
|
|
||||||
|
class ChildClass(object):
|
||||||
|
"""Docstring for a child class."""
|
||||||
|
|
||||||
|
class GrandChildClass(object):
|
||||||
|
"""Docstring for a child of a child class."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class GenerateTest(googletest.TestCase):
|
||||||
|
|
||||||
|
def test_extraction(self):
|
||||||
|
try:
|
||||||
|
generate.extract()
|
||||||
|
except RuntimeError:
|
||||||
|
print('*****************************************************************')
|
||||||
|
print('If this test fails, you have most likely introduced an unsealed')
|
||||||
|
print('module. Make sure to use remove_undocumented or similar utilities')
|
||||||
|
print('to avoid leaking symbols. See below for more information on the')
|
||||||
|
print('failure.')
|
||||||
|
print('*****************************************************************')
|
||||||
|
raise
|
||||||
|
|
||||||
|
def test_write(self):
|
||||||
|
module = sys.modules[__name__]
|
||||||
|
|
||||||
|
index = {
|
||||||
|
'': sys, # Can be any module, this test doesn't care about content.
|
||||||
|
'TestModule': module,
|
||||||
|
'test_function': test_function,
|
||||||
|
'TestModule.test_function': test_function,
|
||||||
|
'TestModule.TestClass': TestClass,
|
||||||
|
'TestModule.TestClass.ChildClass': TestClass.ChildClass,
|
||||||
|
'TestModule.TestClass.ChildClass.GrandChildClass':
|
||||||
|
TestClass.ChildClass.GrandChildClass,
|
||||||
|
}
|
||||||
|
|
||||||
|
tree = {
|
||||||
|
'': ['TestModule', 'test_function'],
|
||||||
|
'TestModule': ['test_function', 'TestClass'],
|
||||||
|
'TestModule.TestClass': ['ChildClass'],
|
||||||
|
'TestModule.TestClass.ChildClass': ['GrandChildClass'],
|
||||||
|
'TestModule.TestClass.ChildClass.GrandChildClass': []
|
||||||
|
}
|
||||||
|
|
||||||
|
duplicate_of = {
|
||||||
|
'TestModule.test_function': 'test_function'
|
||||||
|
}
|
||||||
|
|
||||||
|
duplicates = {
|
||||||
|
'test_function': ['test_function', 'TestModule.test_function']
|
||||||
|
}
|
||||||
|
|
||||||
|
output_dir = tempfile.mkdtemp()
|
||||||
|
base_dir = os.path.dirname(__file__)
|
||||||
|
|
||||||
|
generate.write_docs(output_dir, base_dir,
|
||||||
|
duplicate_of, duplicates,
|
||||||
|
index, tree)
|
||||||
|
|
||||||
|
# Make sure that the right files are written to disk.
|
||||||
|
self.assertTrue(os.path.exists(os.path.join(output_dir, 'index.md')))
|
||||||
|
self.assertTrue(os.path.exists(os.path.join(output_dir, 'full_index.md')))
|
||||||
|
self.assertTrue(os.path.exists(os.path.join(output_dir, 'TestModule.md')))
|
||||||
|
self.assertTrue(os.path.exists(os.path.join(
|
||||||
|
output_dir, 'test_function.md')))
|
||||||
|
self.assertTrue(os.path.exists(os.path.join(
|
||||||
|
output_dir, 'TestModule/TestClass.md')))
|
||||||
|
self.assertTrue(os.path.exists(os.path.join(
|
||||||
|
output_dir, 'TestModule/TestClass/ChildClass.md')))
|
||||||
|
self.assertTrue(os.path.exists(os.path.join(
|
||||||
|
output_dir, 'TestModule/TestClass/ChildClass/GrandChildClass.md')))
|
||||||
|
# Make sure that duplicates are not written
|
||||||
|
self.assertFalse(os.path.exists(os.path.join(
|
||||||
|
output_dir, 'TestModule/test_function.md')))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
googletest.main()
|
||||||
634
tensorflow/tools/docs/parser.py
Normal file
634
tensorflow/tools/docs/parser.py
Normal file
@ -0,0 +1,634 @@
|
|||||||
|
# Copyright 2015 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.
|
||||||
|
# ==============================================================================
|
||||||
|
"""Turn Python docstrings into Markdown for TensorFlow documentation."""
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from __future__ import division
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import functools
|
||||||
|
import inspect
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
# A regular expression capturing a python indentifier.
|
||||||
|
IDENTIFIER_RE = '[a-zA-Z_][a-zA-Z0-9_]*'
|
||||||
|
|
||||||
|
|
||||||
|
def documentation_path(full_name):
|
||||||
|
"""Returns the file path for the documentation for the given API symbol.
|
||||||
|
|
||||||
|
Given the fully qualified name of a library symbol, compute the path to which
|
||||||
|
to write the documentation for that symbol (relative to a base directory).
|
||||||
|
Documentation files are organized into directories that mirror the python
|
||||||
|
module/class structure. The path for the top-level module (whose full name is
|
||||||
|
'') is 'index.md'.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
full_name: Fully qualified name of a library symbol.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The file path to which to write the documentation for `full_name`.
|
||||||
|
"""
|
||||||
|
# The main page is special, since it has no name in here.
|
||||||
|
if not full_name:
|
||||||
|
dirs = ['index']
|
||||||
|
else:
|
||||||
|
dirs = full_name.split('.')
|
||||||
|
|
||||||
|
return os.path.join(*dirs) + '.md'
|
||||||
|
|
||||||
|
|
||||||
|
def _get_raw_docstring(py_object):
|
||||||
|
"""Get the docs for a given python object.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
py_object: A python object to retrieve the docs for (class, function/method,
|
||||||
|
or module).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The docstring, or the empty string if no docstring was found.
|
||||||
|
"""
|
||||||
|
# For object instances, inspect.getdoc does give us the docstring of their
|
||||||
|
# type, which is not what we want. Only return the docstring if it is useful.
|
||||||
|
if (inspect.isclass(py_object) or inspect.ismethod(py_object) or
|
||||||
|
inspect.isfunction(py_object) or inspect.ismodule(py_object) or
|
||||||
|
isinstance(py_object, property)):
|
||||||
|
return inspect.getdoc(py_object) or ''
|
||||||
|
else:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
|
||||||
|
def _get_brief_docstring(py_object):
|
||||||
|
"""Gets the one line docstring of a python object."""
|
||||||
|
return _get_raw_docstring(py_object).split('\n')[0]
|
||||||
|
|
||||||
|
|
||||||
|
def _reference_to_link(ref_full_name, relative_path_to_root, duplicate_of):
|
||||||
|
"""Resolve a "@{symbol}" reference to a relative path, respecting duplicates.
|
||||||
|
|
||||||
|
The input to this function should already be stripped of the '@' and '{}', and
|
||||||
|
its output is only the link, not the full Markdown.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ref_full_name: The fully qualified name of the symbol to link to.
|
||||||
|
relative_path_to_root: The relative path from the location of the current
|
||||||
|
document to the root of the API documentation.
|
||||||
|
duplicate_of: A map from duplicate full names to master names.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A relative path that links from the documentation page of `from_full_name`
|
||||||
|
to the documentation page of `ref_full_name`.
|
||||||
|
"""
|
||||||
|
master_name = duplicate_of.get(ref_full_name, ref_full_name)
|
||||||
|
ref_path = documentation_path(master_name)
|
||||||
|
return os.path.join(relative_path_to_root, ref_path)
|
||||||
|
|
||||||
|
|
||||||
|
def _markdown_link(link_text, ref_full_name, relative_path_to_root,
|
||||||
|
duplicate_of):
|
||||||
|
"""Resolve a "@{symbol}" reference to a Markdown link, respecting duplicates.
|
||||||
|
|
||||||
|
The input to this function should already be stripped of the '@' and '{}'.
|
||||||
|
This function returns a Markdown link. It is assumed that this is a code
|
||||||
|
reference, so the link text will always be rendered as code (using backticks).
|
||||||
|
|
||||||
|
`link_text` should refer to a library symbol. You can either refer to it with
|
||||||
|
or without the `tf.` prefix.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
link_text: The text of the Markdown link.
|
||||||
|
ref_full_name: The fully qualified name of the symbol to link to
|
||||||
|
(may optionally include 'tf.').
|
||||||
|
relative_path_to_root: The relative path from the location of the current
|
||||||
|
document to the root of the API documentation.
|
||||||
|
duplicate_of: A map from duplicate full names to master names.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A markdown link from the documentation page of `from_full_name`
|
||||||
|
to the documentation page of `ref_full_name`.
|
||||||
|
"""
|
||||||
|
if ref_full_name.startswith('tf.'):
|
||||||
|
ref_full_name = ref_full_name[3:]
|
||||||
|
|
||||||
|
return '[`%s`](%s)' % (
|
||||||
|
link_text,
|
||||||
|
_reference_to_link(ref_full_name, relative_path_to_root, duplicate_of))
|
||||||
|
|
||||||
|
|
||||||
|
def replace_references(string, relative_path_to_root, duplicate_of):
|
||||||
|
"""Replace "@{symbol}" references with links to symbol's documentation page.
|
||||||
|
|
||||||
|
This functions finds all occurrences of "@{symbol}" in `string` and replaces
|
||||||
|
them with markdown links to the documentation page for "symbol".
|
||||||
|
|
||||||
|
`relative_path_to_root` is the relative path from the document that contains
|
||||||
|
the "@{symbol}" reference to the root of the API documentation that is linked
|
||||||
|
to. If the containing page is part of the same API docset,
|
||||||
|
`relative_path_to_root` can be set to
|
||||||
|
`os.path.dirname(documentation_path(name))`, where `name` is the python name
|
||||||
|
of the object whose documentation page the reference lives on.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
string: A string in which "@{symbol}" references should be replaced.
|
||||||
|
relative_path_to_root: The relative path from the contianing document to the
|
||||||
|
root of the API documentation that is being linked to.
|
||||||
|
duplicate_of: A map from duplicate names to preferred names of API symbols.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
`string`, with "@{symbol}" references replaced by Markdown links.
|
||||||
|
"""
|
||||||
|
full_name_re = '%s(.%s)*' % (IDENTIFIER_RE, IDENTIFIER_RE)
|
||||||
|
symbol_reference_re = re.compile(r'@\{(' + full_name_re + r')\}')
|
||||||
|
match = symbol_reference_re.search(string)
|
||||||
|
while match:
|
||||||
|
symbol_name = match.group(1)
|
||||||
|
link_text = _markdown_link(symbol_name, symbol_name,
|
||||||
|
relative_path_to_root, duplicate_of)
|
||||||
|
|
||||||
|
# Remove only the '@symbol' part of the match, and replace with the link.
|
||||||
|
string = string[:match.start()] + link_text + string[match.end():]
|
||||||
|
match = symbol_reference_re.search(string,
|
||||||
|
pos=match.start() + len(link_text))
|
||||||
|
return string
|
||||||
|
|
||||||
|
|
||||||
|
def _md_docstring(py_object, relative_path_to_root, duplicate_of):
|
||||||
|
"""Get the docstring from an object and make it into nice Markdown.
|
||||||
|
|
||||||
|
For links within the same set of docs, the `relative_path_to_root` for a
|
||||||
|
docstring on the page for `full_name` can be set to
|
||||||
|
|
||||||
|
```python
|
||||||
|
relative_path_to_root = os.path.relpath(
|
||||||
|
os.path.dirname(documentation_path(full_name)) or '.', '.')
|
||||||
|
```
|
||||||
|
|
||||||
|
Args:
|
||||||
|
py_object: A python object to retrieve the docs for (class, function/method,
|
||||||
|
or module).
|
||||||
|
relative_path_to_root: The relative path from the location of the current
|
||||||
|
document to the root of the API documentation. This is used to compute
|
||||||
|
links for "@symbol" references.
|
||||||
|
duplicate_of: A map from duplicate symbol names to master names. Used to
|
||||||
|
resolve "@symbol" references.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The docstring, or the empty string if no docstring was found.
|
||||||
|
"""
|
||||||
|
# TODO(wicke): If this is a partial, use the .func docstring and add a note.
|
||||||
|
raw_docstring = _get_raw_docstring(py_object)
|
||||||
|
raw_lines = raw_docstring.split('\n')
|
||||||
|
|
||||||
|
# Define regular expressions used during parsing below.
|
||||||
|
symbol_list_item_re = re.compile(r'^ (%s): ' % IDENTIFIER_RE)
|
||||||
|
section_re = re.compile(r'^(\w+):\s*$')
|
||||||
|
|
||||||
|
# Translate docstring line by line.
|
||||||
|
in_special_section = False
|
||||||
|
lines = []
|
||||||
|
|
||||||
|
def is_section_start(i):
|
||||||
|
# Previous line is empty, line i is "Word:", and next line is indented.
|
||||||
|
return (i > 0 and not raw_lines[i-1].strip() and
|
||||||
|
re.match(section_re, raw_lines[i]) and
|
||||||
|
len(raw_lines) > i+1 and raw_lines[i+1].startswith(' '))
|
||||||
|
|
||||||
|
for i, line in enumerate(raw_lines):
|
||||||
|
if not in_special_section and is_section_start(i):
|
||||||
|
in_special_section = True
|
||||||
|
lines.append('#### ' + section_re.sub(r'\1:', line))
|
||||||
|
lines.append('')
|
||||||
|
continue
|
||||||
|
|
||||||
|
# If the next line starts a new section, this one ends. Add an extra line.
|
||||||
|
if in_special_section and is_section_start(i+1):
|
||||||
|
in_special_section = False
|
||||||
|
lines.append('')
|
||||||
|
|
||||||
|
if in_special_section:
|
||||||
|
# Translate symbols in 'Args:', 'Parameters:', 'Raises:', etc. sections.
|
||||||
|
lines.append(symbol_list_item_re.sub(r'* <b>`\1`</b>: ', line))
|
||||||
|
else:
|
||||||
|
lines.append(line)
|
||||||
|
|
||||||
|
docstring = '\n'.join(lines)
|
||||||
|
|
||||||
|
# TODO(deannarubin): Improve formatting for devsite
|
||||||
|
# TODO(deannarubin): Interpret @compatibility and other formatting notes.
|
||||||
|
|
||||||
|
return replace_references(docstring, relative_path_to_root, duplicate_of)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_arg_spec(func):
|
||||||
|
"""Extracts signature information from a function or functools.partial object.
|
||||||
|
|
||||||
|
For functions, uses `inspect.getargspec`. For `functools.partial` objects,
|
||||||
|
corrects the signature of the underlying function to take into account the
|
||||||
|
removed arguments.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
func: A function whose signature to extract.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
An `ArgSpec` namedtuple `(args, varargs, keywords, defaults)`, as returned
|
||||||
|
by `inspect.getargspec`.
|
||||||
|
"""
|
||||||
|
# getargspec does not work for functools.partial objects directly.
|
||||||
|
if isinstance(func, functools.partial):
|
||||||
|
argspec = inspect.getargspec(func.func)
|
||||||
|
# Remove the args from the original function that have been used up.
|
||||||
|
first_default_arg = (
|
||||||
|
len(argspec.args or []) - len(argspec.defaults or []))
|
||||||
|
partial_args = len(func.args)
|
||||||
|
argspec_args = []
|
||||||
|
|
||||||
|
if argspec.args:
|
||||||
|
argspec_args = list(argspec.args[partial_args:])
|
||||||
|
|
||||||
|
argspec_defaults = list(argspec.defaults or ())
|
||||||
|
if argspec.defaults and partial_args > first_default_arg:
|
||||||
|
argspec_defaults = list(argspec.defaults[partial_args-first_default_arg:])
|
||||||
|
|
||||||
|
first_default_arg = max(0, first_default_arg - partial_args)
|
||||||
|
for kwarg in func.keywords:
|
||||||
|
if kwarg in argspec.args:
|
||||||
|
i = argspec_args.index(kwarg)
|
||||||
|
argspec_args.pop(i)
|
||||||
|
if i >= first_default_arg:
|
||||||
|
argspec_defaults.pop(i-first_default_arg)
|
||||||
|
else:
|
||||||
|
first_default_arg -= 1
|
||||||
|
return inspect.ArgSpec(args=argspec_args,
|
||||||
|
varargs=argspec.varargs,
|
||||||
|
keywords=argspec.keywords,
|
||||||
|
defaults=tuple(argspec_defaults))
|
||||||
|
else: # Regular function or method, getargspec will work fine.
|
||||||
|
return inspect.getargspec(func)
|
||||||
|
|
||||||
|
|
||||||
|
def _generate_signature(func):
|
||||||
|
"""Given a function, returns a string representing its args.
|
||||||
|
|
||||||
|
This function produces a string representing the arguments to a python
|
||||||
|
function, including surrounding parentheses. It uses inspect.getargspec, which
|
||||||
|
does not generalize well to Python 3.x, which is more flexible in how *args
|
||||||
|
and **kwargs are handled. This is not a problem in TF, since we have to remain
|
||||||
|
compatible to Python 2.7 anyway.
|
||||||
|
|
||||||
|
This function uses `__name__` for callables if it is available. This can lead
|
||||||
|
to poor results for functools.partial and other callable objects.
|
||||||
|
|
||||||
|
The returned string is Python code, so if it is included in a Markdown
|
||||||
|
document, it should be typeset as code (using backticks), or escaped.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
func: A function of method to extract the signature for (anything
|
||||||
|
`inspect.getargspec` will accept).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A string representing the signature of `func` as python code.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# This produces poor signatures for decorated functions.
|
||||||
|
# TODO(wicke): We need to use something like the decorator module to fix it.
|
||||||
|
|
||||||
|
args_list = []
|
||||||
|
|
||||||
|
argspec = _get_arg_spec(func)
|
||||||
|
first_arg_with_default = (
|
||||||
|
len(argspec.args or []) - len(argspec.defaults or []))
|
||||||
|
|
||||||
|
# Python documentation skips `self` when printing method signatures.
|
||||||
|
first_arg = 1 if inspect.ismethod(func) and 'self' in argspec.args[:1] else 0
|
||||||
|
|
||||||
|
# Add all args without defaults.
|
||||||
|
for arg in argspec.args[first_arg:first_arg_with_default]:
|
||||||
|
args_list.append(arg)
|
||||||
|
|
||||||
|
# Add all args with defaults.
|
||||||
|
if argspec.defaults:
|
||||||
|
for arg, default in zip(
|
||||||
|
argspec.args[first_arg_with_default:], argspec.defaults):
|
||||||
|
# Some callables don't have __name__, fall back to including their repr.
|
||||||
|
# TODO(wicke): This could be improved at least for common cases.
|
||||||
|
if callable(default) and hasattr(default, '__name__'):
|
||||||
|
args_list.append('%s=%s' % (arg, default.__name__))
|
||||||
|
else:
|
||||||
|
args_list.append('%s=%r' % (arg, default))
|
||||||
|
|
||||||
|
# Add *args and *kwargs.
|
||||||
|
if argspec.varargs:
|
||||||
|
args_list.append('*' + argspec.varargs)
|
||||||
|
if argspec.keywords:
|
||||||
|
args_list.append('**' + argspec.keywords)
|
||||||
|
|
||||||
|
return '(%s)' % ', '.join(args_list)
|
||||||
|
|
||||||
|
|
||||||
|
def _generate_markdown_for_function(full_name, duplicate_names,
|
||||||
|
function, duplicate_of):
|
||||||
|
"""Generate Markdown docs for a function or method.
|
||||||
|
|
||||||
|
This function creates a documentation page for a function. It uses the
|
||||||
|
function name (incl. signature) as the title, followed by a list of duplicate
|
||||||
|
names (if there are any), and the Markdown formatted docstring of the
|
||||||
|
function.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
full_name: The preferred name of the function. Used in the title. Must not
|
||||||
|
be present in `duplicate_of` (master names never are).
|
||||||
|
duplicate_names: A sorted list of alternative names (incl. `full_name`).
|
||||||
|
function: The python object referenced by `full_name`.
|
||||||
|
duplicate_of: A map of duplicate full names to master names. Used to resolve
|
||||||
|
@{symbol} references in the docstring.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A string that can be written to a documentation file for this function.
|
||||||
|
"""
|
||||||
|
# TODO(wicke): Make sure this works for partials.
|
||||||
|
relative_path = os.path.relpath(
|
||||||
|
os.path.dirname(documentation_path(full_name)) or '.', '.')
|
||||||
|
docstring = _md_docstring(function, relative_path, duplicate_of)
|
||||||
|
signature = _generate_signature(function)
|
||||||
|
|
||||||
|
if duplicate_names:
|
||||||
|
aliases = '\n'.join(['### `%s`' % (name + signature)
|
||||||
|
for name in duplicate_names])
|
||||||
|
aliases += '\n\n'
|
||||||
|
else:
|
||||||
|
aliases = ''
|
||||||
|
|
||||||
|
return '#`%s%s`\n\n%s%s' % (full_name, signature, aliases, docstring)
|
||||||
|
|
||||||
|
|
||||||
|
def _generate_markdown_for_class(full_name, duplicate_names, py_class,
|
||||||
|
duplicate_of, index, tree):
|
||||||
|
"""Generate Markdown docs for a class.
|
||||||
|
|
||||||
|
This function creates a documentation page for a class. It uses the
|
||||||
|
class name as the title, followed by a list of duplicate
|
||||||
|
names (if there are any), the Markdown formatted docstring of the
|
||||||
|
class, a list of links to all child class docs, a list of all properties
|
||||||
|
including their docstrings, a list of all methods incl. their docstrings, and
|
||||||
|
a list of all class member names (public fields).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
full_name: The preferred name of the class. Used in the title. Must not
|
||||||
|
be present in `duplicate_of` (master names never are).
|
||||||
|
duplicate_names: A sorted list of alternative names (incl. `full_name`).
|
||||||
|
py_class: The python object referenced by `full_name`.
|
||||||
|
duplicate_of: A map of duplicate full names to master names. Used to resolve
|
||||||
|
@{symbol} references in the docstrings.
|
||||||
|
index: A map from full names to python object references.
|
||||||
|
tree: A map from full names to the names of all documentable child objects.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A string that can be written to a documentation file for this class.
|
||||||
|
"""
|
||||||
|
relative_path = os.path.relpath(
|
||||||
|
os.path.dirname(documentation_path(full_name)) or '.', '.')
|
||||||
|
docstring = _md_docstring(py_class, relative_path, duplicate_of)
|
||||||
|
if duplicate_names:
|
||||||
|
aliases = '\n'.join(['### `class %s`' % name for name in duplicate_names])
|
||||||
|
aliases += '\n\n'
|
||||||
|
else:
|
||||||
|
aliases = ''
|
||||||
|
|
||||||
|
docs = '# `%s`\n\n%s%s\n\n' % (full_name, aliases, docstring)
|
||||||
|
|
||||||
|
field_names = []
|
||||||
|
properties = []
|
||||||
|
methods = []
|
||||||
|
class_links = []
|
||||||
|
for member in tree[full_name]:
|
||||||
|
child_name = '.'.join([full_name, member])
|
||||||
|
child = index[child_name]
|
||||||
|
|
||||||
|
if isinstance(child, property):
|
||||||
|
properties.append((member, child))
|
||||||
|
elif inspect.isclass(child):
|
||||||
|
class_links.append(_markdown_link('class ' + member, child_name,
|
||||||
|
relative_path, duplicate_of))
|
||||||
|
elif inspect.ismethod(child) or inspect.isfunction(child):
|
||||||
|
methods.append((member, child))
|
||||||
|
else:
|
||||||
|
# TODO(wicke): We may want to also remember the object itself.
|
||||||
|
field_names.append(member)
|
||||||
|
|
||||||
|
if class_links:
|
||||||
|
docs += '## Child Classes\n'
|
||||||
|
docs += '\n\n'.join(sorted(class_links))
|
||||||
|
docs += '\n\n'
|
||||||
|
|
||||||
|
if properties:
|
||||||
|
docs += '## Properties\n\n'
|
||||||
|
for property_name, prop in sorted(properties, key=lambda x: x[0]):
|
||||||
|
docs += '### `%s`\n\n%s\n\n' % (
|
||||||
|
property_name, _md_docstring(prop, relative_path, duplicate_of))
|
||||||
|
docs += '\n\n'
|
||||||
|
|
||||||
|
if methods:
|
||||||
|
docs += '## Methods\n\n'
|
||||||
|
for method_name, method in sorted(methods, key=lambda x: x[0]):
|
||||||
|
method_signature = method_name + _generate_signature(method)
|
||||||
|
docs += '### `%s`\n\n%s\n\n' % (method_signature,
|
||||||
|
_md_docstring(method, relative_path,
|
||||||
|
duplicate_of))
|
||||||
|
docs += '\n\n'
|
||||||
|
|
||||||
|
if field_names:
|
||||||
|
docs += '## Class Members\n\n'
|
||||||
|
# TODO(wicke): Document the value of the members, at least for basic types.
|
||||||
|
docs += '\n\n'.join(sorted(field_names))
|
||||||
|
docs += '\n\n'
|
||||||
|
|
||||||
|
return docs
|
||||||
|
|
||||||
|
|
||||||
|
def _generate_markdown_for_module(full_name, duplicate_names, module,
|
||||||
|
duplicate_of, index, tree):
|
||||||
|
"""Generate Markdown docs for a module.
|
||||||
|
|
||||||
|
This function creates a documentation page for a module. It uses the
|
||||||
|
module name as the title, followed by a list of duplicate
|
||||||
|
names (if there are any), the Markdown formatted docstring of the
|
||||||
|
class, and a list of links to all members of this module.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
full_name: The preferred name of the module. Used in the title. Must not
|
||||||
|
be present in `duplicate_of` (master names never are).
|
||||||
|
duplicate_names: A sorted list of alternative names (incl. `full_name`).
|
||||||
|
module: The python object referenced by `full_name`.
|
||||||
|
duplicate_of: A map of duplicate full names to master names. Used to resolve
|
||||||
|
@{symbol} references in the docstrings.
|
||||||
|
index: A map from full names to python object references.
|
||||||
|
tree: A map from full names to the names of all documentable child objects.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A string that can be written to a documentation file for this module.
|
||||||
|
"""
|
||||||
|
relative_path = os.path.relpath(
|
||||||
|
os.path.dirname(documentation_path(full_name)) or '.', '.')
|
||||||
|
docstring = _md_docstring(module, relative_path, duplicate_of)
|
||||||
|
if duplicate_names:
|
||||||
|
aliases = '\n'.join(['### Module `%s`' % name for name in duplicate_names])
|
||||||
|
aliases += '\n\n'
|
||||||
|
else:
|
||||||
|
aliases = ''
|
||||||
|
|
||||||
|
member_names = tree.get(full_name, [])
|
||||||
|
|
||||||
|
# Make links to all members.
|
||||||
|
member_links = []
|
||||||
|
for name in member_names:
|
||||||
|
member_full_name = full_name + '.' + name if full_name else name
|
||||||
|
member = index[member_full_name]
|
||||||
|
|
||||||
|
if inspect.isclass(member):
|
||||||
|
link_text = 'class ' + name
|
||||||
|
elif inspect.isfunction(member):
|
||||||
|
link_text = name + _generate_signature(member)
|
||||||
|
else:
|
||||||
|
link_text = name
|
||||||
|
|
||||||
|
member_links.append(_markdown_link(link_text, member_full_name,
|
||||||
|
relative_path, duplicate_of))
|
||||||
|
|
||||||
|
# TODO(deannarubin): Make this list into a table and add the brief docstring.
|
||||||
|
# (use _get_brief_docstring)
|
||||||
|
|
||||||
|
return '# Module `%s`\n\n%s%s\n\n## Members\n\n%s' % (
|
||||||
|
full_name, aliases, docstring, '\n\n'.join(member_links))
|
||||||
|
|
||||||
|
|
||||||
|
def generate_markdown(full_name, py_object,
|
||||||
|
duplicate_of, duplicates,
|
||||||
|
index, tree, base_dir):
|
||||||
|
"""Generate Markdown docs for a given object that's part of the TF API.
|
||||||
|
|
||||||
|
This function uses _md_docstring to obtain the docs pertaining to
|
||||||
|
`object`.
|
||||||
|
|
||||||
|
This function resolves '@symbol' references in the docstrings into links to
|
||||||
|
the appropriate location. It also adds a list of alternative names for the
|
||||||
|
symbol automatically.
|
||||||
|
|
||||||
|
It assumes that the docs for each object live in a file given by
|
||||||
|
`documentation_path`, and that relative links to files within the
|
||||||
|
documentation are resolvable.
|
||||||
|
|
||||||
|
The output is Markdown that can be written to file and published.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
full_name: The fully qualified name (excl. "tf.") of the symbol to be
|
||||||
|
documented.
|
||||||
|
py_object: The Python object to be documented. Its documentation is sourced
|
||||||
|
from `py_object`'s docstring.
|
||||||
|
duplicate_of: A `dict` mapping fully qualified names to "master" names. This
|
||||||
|
is used to resolve "@{symbol}" references to the "master" name.
|
||||||
|
duplicates: A `dict` mapping fully qualified names to a set of all
|
||||||
|
aliases of this name. This is used to automatically generate a list of all
|
||||||
|
aliases for each name.
|
||||||
|
index: A `dict` mapping fully qualified names to the corresponding Python
|
||||||
|
objects. Used to produce docs for child objects, and to check the validity
|
||||||
|
of "@{symbol}" references.
|
||||||
|
tree: A `dict` mapping a fully qualified name to the names of all its
|
||||||
|
members. Used to populate the members section of a class or module page.
|
||||||
|
base_dir: A base path that is stripped from file locations written to the
|
||||||
|
docs.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A string containing the Markdown docs for `py_object`.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RuntimeError: If an object is encountered for which we don't know how
|
||||||
|
to make docs.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Which other aliases exist for the object referenced by full_name?
|
||||||
|
master_name = duplicate_of.get(full_name, full_name)
|
||||||
|
duplicate_names = duplicates.get(master_name, [full_name])
|
||||||
|
|
||||||
|
# TODO(wicke): Once other pieces are ready, enable this also for partials.
|
||||||
|
if (inspect.ismethod(py_object) or inspect.isfunction(py_object) or
|
||||||
|
# Some methods in classes from extensions come in as routines.
|
||||||
|
inspect.isroutine(py_object)):
|
||||||
|
markdown = _generate_markdown_for_function(master_name, duplicate_names,
|
||||||
|
py_object, duplicate_of)
|
||||||
|
elif inspect.isclass(py_object):
|
||||||
|
markdown = _generate_markdown_for_class(master_name, duplicate_names,
|
||||||
|
py_object, duplicate_of,
|
||||||
|
index, tree)
|
||||||
|
elif inspect.ismodule(py_object):
|
||||||
|
markdown = _generate_markdown_for_module(master_name, duplicate_names,
|
||||||
|
py_object, duplicate_of,
|
||||||
|
index, tree)
|
||||||
|
else:
|
||||||
|
raise RuntimeError('Cannot make docs for object %s: %r' % (full_name,
|
||||||
|
py_object))
|
||||||
|
|
||||||
|
# Every page gets a note on the bottom about where this object is defined
|
||||||
|
# TODO(wicke): If py_object is decorated, get the decorated object instead.
|
||||||
|
# TODO(wicke): Only use decorators that support this in TF.
|
||||||
|
|
||||||
|
try:
|
||||||
|
path = os.path.relpath(inspect.getfile(py_object), base_dir)
|
||||||
|
|
||||||
|
# TODO(wicke): If this is a generated file, point to the source instead.
|
||||||
|
|
||||||
|
# Never include links outside this code base.
|
||||||
|
if not path.startswith('..'):
|
||||||
|
# TODO(wicke): Make this a link to github.
|
||||||
|
markdown += '\n\ndefined in %s\n\n' % path
|
||||||
|
except TypeError: # getfile throws TypeError if py_object is a builtin.
|
||||||
|
markdown += '\n\nthis is an alias for a Python built-in.'
|
||||||
|
|
||||||
|
return markdown
|
||||||
|
|
||||||
|
|
||||||
|
def generate_global_index(library_name, root_name, index, duplicate_of):
|
||||||
|
"""Given a dict of full names to python objects, generate an index page.
|
||||||
|
|
||||||
|
The index page generated contains a list of links for all symbols in `index`
|
||||||
|
that have their own documentation page.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
library_name: The name for the documented library to use in the title.
|
||||||
|
root_name: The name to use for the root module.
|
||||||
|
index: A dict mapping full names to python objects.
|
||||||
|
duplicate_of: A map of duplicate names to preferred names.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A string containing an index page as Markdown.
|
||||||
|
"""
|
||||||
|
symbol_links = []
|
||||||
|
for full_name, py_object in index.iteritems():
|
||||||
|
index_name = full_name or root_name
|
||||||
|
if (inspect.ismodule(py_object) or inspect.isfunction(py_object) or
|
||||||
|
inspect.isclass(py_object)):
|
||||||
|
symbol_links.append((index_name,
|
||||||
|
_markdown_link(index_name, full_name,
|
||||||
|
'.', duplicate_of)))
|
||||||
|
|
||||||
|
lines = ['# All symbols in %s' % library_name, '']
|
||||||
|
for _, link in sorted(symbol_links, key=lambda x: x[0]):
|
||||||
|
lines.append('* %s' % link)
|
||||||
|
|
||||||
|
# TODO(deannarubin): Make this list into a table and add the brief docstring.
|
||||||
|
# (use _get_brief_docstring)
|
||||||
|
|
||||||
|
return '\n'.join(lines)
|
||||||
349
tensorflow/tools/docs/parser_test.py
Normal file
349
tensorflow/tools/docs/parser_test.py
Normal file
@ -0,0 +1,349 @@
|
|||||||
|
# Copyright 2015 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.
|
||||||
|
# ==============================================================================
|
||||||
|
"""Tests for documentation parser."""
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from __future__ import division
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import functools
|
||||||
|
import inspect
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from tensorflow.python.platform import googletest
|
||||||
|
from tensorflow.tools.docs import parser
|
||||||
|
|
||||||
|
|
||||||
|
def test_function_for_markdown_reference(unused_arg):
|
||||||
|
"""Docstring with reference to @{test_function}."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def test_function(unused_arg, unused_kwarg='default'):
|
||||||
|
"""Docstring for test function."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def test_function_with_args_kwargs(unused_arg, *unused_args, **unused_kwargs):
|
||||||
|
"""Docstring for second test function."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def test_function_with_fancy_docstring(arg):
|
||||||
|
"""Function with a fancy docstring.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
arg: An argument.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
arg: the input, and
|
||||||
|
arg: the input, again.
|
||||||
|
"""
|
||||||
|
return arg, arg
|
||||||
|
|
||||||
|
|
||||||
|
class TestClass(object):
|
||||||
|
"""Docstring for TestClass itself."""
|
||||||
|
|
||||||
|
def a_method(self, arg='default'):
|
||||||
|
"""Docstring for a method."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class ChildClass(object):
|
||||||
|
"""Docstring for a child class."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def a_property(self):
|
||||||
|
"""Docstring for a property."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
CLASS_MEMBER = 'a class member'
|
||||||
|
|
||||||
|
|
||||||
|
class ParserTest(googletest.TestCase):
|
||||||
|
|
||||||
|
def test_documentation_path(self):
|
||||||
|
self.assertEqual('test.md', parser.documentation_path('test'))
|
||||||
|
self.assertEqual('test/module.md', parser.documentation_path('test.module'))
|
||||||
|
|
||||||
|
def test_documentation_path_empty(self):
|
||||||
|
self.assertEqual('index.md', parser.documentation_path(''))
|
||||||
|
|
||||||
|
def test_replace_references(self):
|
||||||
|
string = 'A @{reference}, another @{tf.reference}, and a @{third}.'
|
||||||
|
duplicate_of = {'third': 'fourth'}
|
||||||
|
result = parser.replace_references(string, '../..', duplicate_of)
|
||||||
|
self.assertEqual(
|
||||||
|
'A [`reference`](../../reference.md), another '
|
||||||
|
'[`tf.reference`](../../reference.md), '
|
||||||
|
'and a [`third`](../../fourth.md).',
|
||||||
|
result)
|
||||||
|
|
||||||
|
def test_generate_markdown_for_class(self):
|
||||||
|
|
||||||
|
index = {
|
||||||
|
'TestClass': TestClass,
|
||||||
|
'TestClass.a_method': TestClass.a_method,
|
||||||
|
'TestClass.a_property': TestClass.a_property,
|
||||||
|
'TestClass.ChildClass': TestClass.ChildClass,
|
||||||
|
'TestClass.CLASS_MEMBER': TestClass.CLASS_MEMBER
|
||||||
|
}
|
||||||
|
|
||||||
|
tree = {
|
||||||
|
'TestClass': ['a_method', 'a_property', 'ChildClass', 'CLASS_MEMBER']
|
||||||
|
}
|
||||||
|
|
||||||
|
docs = parser.generate_markdown(full_name='TestClass', py_object=TestClass,
|
||||||
|
duplicate_of={}, duplicates={},
|
||||||
|
index=index, tree=tree, base_dir='/')
|
||||||
|
|
||||||
|
# Make sure all required docstrings are present.
|
||||||
|
self.assertTrue(inspect.getdoc(TestClass) in docs)
|
||||||
|
self.assertTrue(inspect.getdoc(TestClass.a_method) in docs)
|
||||||
|
self.assertTrue(inspect.getdoc(TestClass.a_property) in docs)
|
||||||
|
|
||||||
|
# Make sure that the signature is extracted properly and omits self.
|
||||||
|
self.assertTrue('a_method(arg=\'default\')' in docs)
|
||||||
|
|
||||||
|
# Make sure there is a link to the child class and it points the right way.
|
||||||
|
self.assertTrue('[`class ChildClass`](./TestClass/ChildClass.md)' in docs)
|
||||||
|
|
||||||
|
# Make sure CLASS_MEMBER is mentioned.
|
||||||
|
self.assertTrue('CLASS_MEMBER' in docs)
|
||||||
|
|
||||||
|
# Make sure this file is contained as the definition location.
|
||||||
|
self.assertTrue(os.path.relpath(__file__, '/') in docs)
|
||||||
|
|
||||||
|
def test_generate_markdown_for_module(self):
|
||||||
|
module = sys.modules[__name__]
|
||||||
|
|
||||||
|
index = {
|
||||||
|
'TestModule': module,
|
||||||
|
'TestModule.test_function': test_function,
|
||||||
|
'TestModule.test_function_with_args_kwargs':
|
||||||
|
test_function_with_args_kwargs,
|
||||||
|
'TestModule.TestClass': TestClass,
|
||||||
|
}
|
||||||
|
|
||||||
|
tree = {
|
||||||
|
'TestModule': ['TestClass', 'test_function',
|
||||||
|
'test_function_with_args_kwargs']
|
||||||
|
}
|
||||||
|
|
||||||
|
docs = parser.generate_markdown(full_name='TestModule', py_object=module,
|
||||||
|
duplicate_of={}, duplicates={},
|
||||||
|
index=index, tree=tree, base_dir='/')
|
||||||
|
|
||||||
|
# Make sure all required docstrings are present.
|
||||||
|
self.assertTrue(inspect.getdoc(module) in docs)
|
||||||
|
|
||||||
|
# Make sure that links to the members are there (not asserting on exact link
|
||||||
|
# text for functions).
|
||||||
|
self.assertTrue('./TestModule/test_function.md' in docs)
|
||||||
|
self.assertTrue('./TestModule/test_function_with_args_kwargs.md' in docs)
|
||||||
|
|
||||||
|
# Make sure there is a link to the child class and it points the right way.
|
||||||
|
self.assertTrue('[`class TestClass`](./TestModule/TestClass.md)' in docs)
|
||||||
|
|
||||||
|
# Make sure this file is contained as the definition location.
|
||||||
|
self.assertTrue(os.path.relpath(__file__, '/') in docs)
|
||||||
|
|
||||||
|
def test_generate_markdown_for_function(self):
|
||||||
|
index = {
|
||||||
|
'test_function': test_function
|
||||||
|
}
|
||||||
|
|
||||||
|
tree = {
|
||||||
|
'': ['test_function']
|
||||||
|
}
|
||||||
|
|
||||||
|
docs = parser.generate_markdown(full_name='test_function',
|
||||||
|
py_object=test_function,
|
||||||
|
duplicate_of={}, duplicates={},
|
||||||
|
index=index, tree=tree, base_dir='/')
|
||||||
|
|
||||||
|
# Make sure docstring shows up.
|
||||||
|
self.assertTrue(inspect.getdoc(test_function) in docs)
|
||||||
|
|
||||||
|
# Make sure the extracted signature is good.
|
||||||
|
self.assertTrue(
|
||||||
|
'test_function(unused_arg, unused_kwarg=\'default\')' in docs)
|
||||||
|
|
||||||
|
# Make sure this file is contained as the definition location.
|
||||||
|
self.assertTrue(os.path.relpath(__file__, '/') in docs)
|
||||||
|
|
||||||
|
def test_generate_markdown_for_function_with_kwargs(self):
|
||||||
|
index = {
|
||||||
|
'test_function_with_args_kwargs': test_function_with_args_kwargs
|
||||||
|
}
|
||||||
|
|
||||||
|
tree = {
|
||||||
|
'': ['test_function_with_args_kwargs']
|
||||||
|
}
|
||||||
|
|
||||||
|
docs = parser.generate_markdown(full_name='test_function_with_args_kwargs',
|
||||||
|
py_object=test_function_with_args_kwargs,
|
||||||
|
duplicate_of={}, duplicates={},
|
||||||
|
index=index, tree=tree, base_dir='/')
|
||||||
|
|
||||||
|
# Make sure docstring shows up.
|
||||||
|
self.assertTrue(inspect.getdoc(test_function_with_args_kwargs) in docs)
|
||||||
|
|
||||||
|
# Make sure the extracted signature is good.
|
||||||
|
self.assertTrue(
|
||||||
|
'test_function_with_args_kwargs(unused_arg,'
|
||||||
|
' *unused_args, **unused_kwargs)' in docs)
|
||||||
|
|
||||||
|
def test_references_replaced_in_generated_markdown(self):
|
||||||
|
index = {
|
||||||
|
'test_function_for_markdown_reference':
|
||||||
|
test_function_for_markdown_reference
|
||||||
|
}
|
||||||
|
|
||||||
|
tree = {
|
||||||
|
'': ['test_function_for_markdown_reference']
|
||||||
|
}
|
||||||
|
|
||||||
|
docs = parser.generate_markdown(
|
||||||
|
full_name='test_function_for_markdown_reference',
|
||||||
|
py_object=test_function_for_markdown_reference,
|
||||||
|
duplicate_of={}, duplicates={},
|
||||||
|
index=index, tree=tree, base_dir='/')
|
||||||
|
|
||||||
|
# Make sure docstring shows up and is properly processed.
|
||||||
|
expected_docs = parser.replace_references(
|
||||||
|
inspect.getdoc(test_function_for_markdown_reference),
|
||||||
|
relative_path_to_root='.', duplicate_of={})
|
||||||
|
|
||||||
|
self.assertTrue(expected_docs in docs)
|
||||||
|
|
||||||
|
def test_docstring_special_section(self):
|
||||||
|
index = {
|
||||||
|
'test_function': test_function_with_fancy_docstring
|
||||||
|
}
|
||||||
|
|
||||||
|
tree = {
|
||||||
|
'': 'test_function'
|
||||||
|
}
|
||||||
|
|
||||||
|
docs = parser.generate_markdown(
|
||||||
|
full_name='test_function',
|
||||||
|
py_object=test_function_with_fancy_docstring,
|
||||||
|
duplicate_of={}, duplicates={},
|
||||||
|
index=index, tree=tree, base_dir='/')
|
||||||
|
|
||||||
|
expected = '\n'.join([
|
||||||
|
'Function with a fancy docstring.',
|
||||||
|
'',
|
||||||
|
'#### Args:',
|
||||||
|
'',
|
||||||
|
'* <b>`arg`</b>: An argument.',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'#### Returns:',
|
||||||
|
'',
|
||||||
|
'* <b>`arg`</b>: the input, and',
|
||||||
|
'* <b>`arg`</b>: the input, again.',
|
||||||
|
''])
|
||||||
|
self.assertTrue(expected in docs)
|
||||||
|
|
||||||
|
def test_generate_index(self):
|
||||||
|
module = sys.modules[__name__]
|
||||||
|
|
||||||
|
index = {
|
||||||
|
'TestModule': module,
|
||||||
|
'test_function': test_function,
|
||||||
|
'TestModule.test_function': test_function,
|
||||||
|
'TestModule.TestClass': TestClass,
|
||||||
|
'TestModule.TestClass.a_method': TestClass.a_method,
|
||||||
|
'TestModule.TestClass.a_property': TestClass.a_property,
|
||||||
|
'TestModule.TestClass.ChildClass': TestClass.ChildClass,
|
||||||
|
}
|
||||||
|
|
||||||
|
duplicate_of = {
|
||||||
|
'TestModule.test_function': 'test_function'
|
||||||
|
}
|
||||||
|
|
||||||
|
docs = parser.generate_global_index('TestLibrary', 'test',
|
||||||
|
index=index,
|
||||||
|
duplicate_of=duplicate_of)
|
||||||
|
|
||||||
|
# Make sure duplicates and non-top-level symbols are in the index, but
|
||||||
|
# methods and properties are not.
|
||||||
|
self.assertTrue('a_method' not in docs)
|
||||||
|
self.assertTrue('a_property' not in docs)
|
||||||
|
self.assertTrue('TestModule.TestClass' in docs)
|
||||||
|
self.assertTrue('TestModule.TestClass.ChildClass' in docs)
|
||||||
|
self.assertTrue('TestModule.test_function' in docs)
|
||||||
|
# Leading backtick to make sure it's included top-level.
|
||||||
|
# This depends on formatting, but should be stable.
|
||||||
|
self.assertTrue('`test_function' in docs)
|
||||||
|
|
||||||
|
def test_argspec_for_functoos_partial(self):
|
||||||
|
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
def test_function_for_partial1(arg1, arg2, kwarg1=1, kwarg2=2):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_function_for_partial2(arg1, arg2, *my_args, **my_kwargs):
|
||||||
|
pass
|
||||||
|
# pylint: enable=unused-argument
|
||||||
|
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
# Make sure everything works for regular functions.
|
||||||
|
expected = inspect.ArgSpec(['arg1', 'arg2', 'kwarg1', 'kwarg2'], None, None,
|
||||||
|
(1, 2))
|
||||||
|
self.assertEqual(expected, parser._get_arg_spec(test_function_for_partial1))
|
||||||
|
|
||||||
|
# Make sure doing nothing works.
|
||||||
|
expected = inspect.ArgSpec(['arg1', 'arg2', 'kwarg1', 'kwarg2'], None, None,
|
||||||
|
(1, 2))
|
||||||
|
partial = functools.partial(test_function_for_partial1)
|
||||||
|
self.assertEqual(expected, parser._get_arg_spec(partial))
|
||||||
|
|
||||||
|
# Make sure setting args from the front works.
|
||||||
|
expected = inspect.ArgSpec(['arg2', 'kwarg1', 'kwarg2'], None, None, (1, 2))
|
||||||
|
partial = functools.partial(test_function_for_partial1, 1)
|
||||||
|
self.assertEqual(expected, parser._get_arg_spec(partial))
|
||||||
|
|
||||||
|
expected = inspect.ArgSpec(['kwarg2',], None, None, (2,))
|
||||||
|
partial = functools.partial(test_function_for_partial1, 1, 2, 3)
|
||||||
|
self.assertEqual(expected, parser._get_arg_spec(partial))
|
||||||
|
|
||||||
|
# Make sure setting kwargs works.
|
||||||
|
expected = inspect.ArgSpec(['arg1', 'arg2', 'kwarg2'], None, None, (2,))
|
||||||
|
partial = functools.partial(test_function_for_partial1, kwarg1=0)
|
||||||
|
self.assertEqual(expected, parser._get_arg_spec(partial))
|
||||||
|
|
||||||
|
expected = inspect.ArgSpec(['arg1', 'arg2', 'kwarg1'], None, None, (1,))
|
||||||
|
partial = functools.partial(test_function_for_partial1, kwarg2=0)
|
||||||
|
self.assertEqual(expected, parser._get_arg_spec(partial))
|
||||||
|
|
||||||
|
expected = inspect.ArgSpec(['arg1'], None, None, ())
|
||||||
|
partial = functools.partial(test_function_for_partial1,
|
||||||
|
arg2=0, kwarg1=0, kwarg2=0)
|
||||||
|
self.assertEqual(expected, parser._get_arg_spec(partial))
|
||||||
|
|
||||||
|
# Make sure *args, *kwargs is accounted for.
|
||||||
|
expected = inspect.ArgSpec([], 'my_args', 'my_kwargs', ())
|
||||||
|
partial = functools.partial(test_function_for_partial2, 0, 1)
|
||||||
|
self.assertEqual(expected, parser._get_arg_spec(partial))
|
||||||
|
|
||||||
|
# pylint: enable=protected-access
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
googletest.main()
|
||||||
Loading…
x
Reference in New Issue
Block a user