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.
|
||||
|
||||
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:
|
||||
op_seed: integer.
|
||||
|
@ -40,16 +40,28 @@ class PublicAPIVisitor(object):
|
||||
# Each entry maps a module path to a name to ignore in traversal.
|
||||
_do_not_descend_map = {
|
||||
# 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.
|
||||
'flags': ['cpp_flags'],
|
||||
|
||||
# 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.
|
||||
}
|
||||
|
||||
@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):
|
||||
"""Return whether a name is private."""
|
||||
return name.startswith('_')
|
||||
@ -61,6 +73,8 @@ class PublicAPIVisitor(object):
|
||||
|
||||
def __call__(self, path, parent, children):
|
||||
"""Visitor interface, see `traverse` for details."""
|
||||
|
||||
# Avoid long waits in cases of pretty unambiguous failure.
|
||||
if inspect.ismodule(parent) and len(path.split('.')) > 10:
|
||||
raise RuntimeError('Modules nested too deep:\n%s\n\nThis is likely a '
|
||||
'problem with an accidental public import.' % path)
|
||||
|
@ -16,6 +16,74 @@ py_binary(
|
||||
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(
|
||||
name = "doxy_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…
Reference in New Issue
Block a user