# 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 collections import functools import os import sys from tensorflow.python.platform import googletest from tensorflow.python.util import tf_inspect from tensorflow.tools.docs import doc_controls from tensorflow.tools.docs import parser # The test needs a real module. `types.ModuleType()` doesn't work, as the result # is a `builtin` module. Using "parser" here is arbitraty. The tests don't # depend on the module contents. At this point in the process the public api # has already been extracted. test_module = parser 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 class ParentClass(object): @doc_controls.do_not_doc_inheritable def hidden_method(self): pass class TestClass(ParentClass): """Docstring for TestClass itself.""" def a_method(self, arg='default'): """Docstring for a method.""" pass def hidden_method(self): pass @doc_controls.do_not_generate_docs def hidden_method2(self): 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 DummyVisitor(object): def __init__(self, index, duplicate_of): self.index = index self.duplicate_of = duplicate_of 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_replace_references(self): class HasOneMember(object): def foo(self): pass string = ( 'A @{tf.reference}, another @{tf.reference$with\nnewline}, a member ' '@{tf.reference.foo}, and a @{tf.third$link `text` with `code` in ' 'it}.') duplicate_of = {'tf.third': 'tf.fourth'} index = {'tf.reference': HasOneMember, 'tf.reference.foo': HasOneMember.foo, 'tf.third': HasOneMember, 'tf.fourth': HasOneMember} visitor = DummyVisitor(index, duplicate_of) reference_resolver = parser.ReferenceResolver.from_visitor( visitor=visitor, doc_index={}, py_module_names=['tf']) result = reference_resolver.replace_references(string, '../..') self.assertEqual('A ' 'tf.reference, ' 'another ' 'with\nnewline, ' 'a member ' 'tf.reference.foo, ' 'and a link ' 'text with ' 'code in it.', result) def test_doc_replace_references(self): string = '@{$doc1} @{$doc1#abc} @{$doc1$link} @{$doc1#def$zelda} @{$do/c2}' class DocInfo(object): pass doc1 = DocInfo() doc1.title = 'Title1' doc1.url = 'URL1' doc2 = DocInfo() doc2.title = 'Two words' doc2.url = 'somewhere/else' doc_index = {'doc1': doc1, 'do/c2': doc2} visitor = DummyVisitor(index={}, duplicate_of={}) reference_resolver = parser.ReferenceResolver.from_visitor( visitor=visitor, doc_index=doc_index, py_module_names=['tf']) result = reference_resolver.replace_references(string, 'python') self.assertEqual('Title1 ' 'Title1 ' 'link ' 'zelda ' 'Two words', result) def test_docs_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 } visitor = DummyVisitor(index=index, duplicate_of={}) reference_resolver = parser.ReferenceResolver.from_visitor( visitor=visitor, doc_index={}, py_module_names=['tf']) tree = { 'TestClass': ['a_method', 'a_property', 'ChildClass', 'CLASS_MEMBER'] } parser_config = parser.ParserConfig( reference_resolver=reference_resolver, duplicates={}, duplicate_of={}, tree=tree, index=index, reverse_index={}, guide_index={}, base_dir='/') page_info = parser.docs_for_object( full_name='TestClass', py_object=TestClass, parser_config=parser_config) # Make sure the brief docstring is present self.assertEqual( tf_inspect.getdoc(TestClass).split('\n')[0], page_info.doc.brief) # Make sure the method is present self.assertEqual(TestClass.a_method, page_info.methods[0].obj) # Make sure that the signature is extracted properly and omits self. self.assertEqual(["arg='default'"], page_info.methods[0].signature) # Make sure the property is present self.assertIs(TestClass.a_property, page_info.properties[0].obj) # Make sure there is a link to the child class and it points the right way. self.assertIs(TestClass.ChildClass, page_info.classes[0].obj) # Make sure this file is contained as the definition location. self.assertEqual(os.path.relpath(__file__, '/'), page_info.defined_in.path) def test_namedtuple_field_order(self): namedtupleclass = collections.namedtuple('namedtupleclass', {'z', 'y', 'x', 'w', 'v', 'u'}) index = { 'namedtupleclass': namedtupleclass, 'namedtupleclass.u': namedtupleclass.u, 'namedtupleclass.v': namedtupleclass.v, 'namedtupleclass.w': namedtupleclass.w, 'namedtupleclass.x': namedtupleclass.x, 'namedtupleclass.y': namedtupleclass.y, 'namedtupleclass.z': namedtupleclass.z, } visitor = DummyVisitor(index=index, duplicate_of={}) reference_resolver = parser.ReferenceResolver.from_visitor( visitor=visitor, doc_index={}, py_module_names=['tf']) tree = {'namedtupleclass': {'u', 'v', 'w', 'x', 'y', 'z'}} parser_config = parser.ParserConfig( reference_resolver=reference_resolver, duplicates={}, duplicate_of={}, tree=tree, index=index, reverse_index={}, guide_index={}, base_dir='/') page_info = parser.docs_for_object( full_name='namedtupleclass', py_object=namedtupleclass, parser_config=parser_config) # Each namedtiple field has a docstring of the form: # 'Alias for field number ##'. These props are returned sorted. def sort_key(prop_info): return int(prop_info.obj.__doc__.split(' ')[-1]) self.assertSequenceEqual(page_info.properties, sorted(page_info.properties, key=sort_key)) def test_docs_for_class_should_skip(self): class Parent(object): @doc_controls.do_not_doc_inheritable def a_method(self, arg='default'): pass class Child(Parent): def a_method(self, arg='default'): pass index = { 'Child': Child, 'Child.a_method': Child.a_method, } visitor = DummyVisitor(index=index, duplicate_of={}) reference_resolver = parser.ReferenceResolver.from_visitor( visitor=visitor, doc_index={}, py_module_names=['tf']) tree = { 'Child': ['a_method'], } parser_config = parser.ParserConfig( reference_resolver=reference_resolver, duplicates={}, duplicate_of={}, tree=tree, index=index, reverse_index={}, guide_index={}, base_dir='/') page_info = parser.docs_for_object( full_name='Child', py_object=Child, parser_config=parser_config) # Make sure the `a_method` is not present self.assertEqual(0, len(page_info.methods)) def test_docs_for_message_class(self): class CMessage(object): def hidden(self): pass class Message(object): def hidden2(self): pass class MessageMeta(object): def hidden3(self): pass class ChildMessage(CMessage, Message, MessageMeta): def my_method(self): pass index = { 'ChildMessage': ChildMessage, 'ChildMessage.hidden': ChildMessage.hidden, 'ChildMessage.hidden2': ChildMessage.hidden2, 'ChildMessage.hidden3': ChildMessage.hidden3, 'ChildMessage.my_method': ChildMessage.my_method, } visitor = DummyVisitor(index=index, duplicate_of={}) reference_resolver = parser.ReferenceResolver.from_visitor( visitor=visitor, doc_index={}, py_module_names=['tf']) tree = {'ChildMessage': ['hidden', 'hidden2', 'hidden3', 'my_method']} parser_config = parser.ParserConfig( reference_resolver=reference_resolver, duplicates={}, duplicate_of={}, tree=tree, index=index, reverse_index={}, guide_index={}, base_dir='/') page_info = parser.docs_for_object( full_name='ChildMessage', py_object=ChildMessage, parser_config=parser_config) self.assertEqual(1, len(page_info.methods)) self.assertEqual('my_method', page_info.methods[0].short_name) def test_docs_for_module(self): index = { 'TestModule': test_module, 'TestModule.test_function': test_function, 'TestModule.test_function_with_args_kwargs': test_function_with_args_kwargs, 'TestModule.TestClass': TestClass, } visitor = DummyVisitor(index=index, duplicate_of={}) reference_resolver = parser.ReferenceResolver.from_visitor( visitor=visitor, doc_index={}, py_module_names=['tf']) tree = { 'TestModule': ['TestClass', 'test_function', 'test_function_with_args_kwargs'] } parser_config = parser.ParserConfig( reference_resolver=reference_resolver, duplicates={}, duplicate_of={}, tree=tree, index=index, reverse_index={}, guide_index={}, base_dir='/') page_info = parser.docs_for_object( full_name='TestModule', py_object=test_module, parser_config=parser_config) # Make sure the brief docstring is present self.assertEqual( tf_inspect.getdoc(test_module).split('\n')[0], page_info.doc.brief) # Make sure that the members are there funcs = {f_info.obj for f_info in page_info.functions} self.assertEqual({test_function, test_function_with_args_kwargs}, funcs) classes = {cls_info.obj for cls_info in page_info.classes} self.assertEqual({TestClass}, classes) # Make sure the module's file is contained as the definition location. self.assertEqual( os.path.relpath(test_module.__file__, '/'), page_info.defined_in.path) def test_docs_for_function(self): index = { 'test_function': test_function } visitor = DummyVisitor(index=index, duplicate_of={}) reference_resolver = parser.ReferenceResolver.from_visitor( visitor=visitor, doc_index={}, py_module_names=['tf']) tree = { '': ['test_function'] } parser_config = parser.ParserConfig( reference_resolver=reference_resolver, duplicates={}, duplicate_of={}, tree=tree, index=index, reverse_index={}, guide_index={}, base_dir='/') page_info = parser.docs_for_object( full_name='test_function', py_object=test_function, parser_config=parser_config) # Make sure the brief docstring is present self.assertEqual( tf_inspect.getdoc(test_function).split('\n')[0], page_info.doc.brief) # Make sure the extracted signature is good. self.assertEqual(['unused_arg', "unused_kwarg='default'"], page_info.signature) # Make sure this file is contained as the definition location. self.assertEqual(os.path.relpath(__file__, '/'), page_info.defined_in.path) def test_docs_for_function_with_kwargs(self): index = { 'test_function_with_args_kwargs': test_function_with_args_kwargs } visitor = DummyVisitor(index=index, duplicate_of={}) reference_resolver = parser.ReferenceResolver.from_visitor( visitor=visitor, doc_index={}, py_module_names=['tf']) tree = { '': ['test_function_with_args_kwargs'] } parser_config = parser.ParserConfig( reference_resolver=reference_resolver, duplicates={}, duplicate_of={}, tree=tree, index=index, reverse_index={}, guide_index={}, base_dir='/') page_info = parser.docs_for_object( full_name='test_function_with_args_kwargs', py_object=test_function_with_args_kwargs, parser_config=parser_config) # Make sure the brief docstring is present self.assertEqual( tf_inspect.getdoc(test_function_with_args_kwargs).split('\n')[0], page_info.doc.brief) # Make sure the extracted signature is good. self.assertEqual(['unused_arg', '*unused_args', '**unused_kwargs'], page_info.signature) def test_parse_md_docstring(self): def test_function_with_fancy_docstring(arg): """Function with a fancy docstring. And a bunch of references: @{tf.reference}, another @{tf.reference}, a member @{tf.reference.foo}, and a @{tf.third}. Args: arg: An argument. Raises: an exception Returns: arg: the input, and arg: the input, again. @compatibility(numpy) NumPy has nothing as awesome as this function. @end_compatibility @compatibility(theano) Theano has nothing as awesome as this function. Check it out. @end_compatibility """ return arg, arg class HasOneMember(object): def foo(self): pass duplicate_of = {'tf.third': 'tf.fourth'} index = { 'tf': test_module, 'tf.fancy': test_function_with_fancy_docstring, 'tf.reference': HasOneMember, 'tf.reference.foo': HasOneMember.foo, 'tf.third': HasOneMember, 'tf.fourth': HasOneMember } visitor = DummyVisitor(index=index, duplicate_of=duplicate_of) reference_resolver = parser.ReferenceResolver.from_visitor( visitor=visitor, doc_index={}, py_module_names=['tf']) doc_info = parser._parse_md_docstring(test_function_with_fancy_docstring, '../..', reference_resolver) self.assertNotIn('@', doc_info.docstring) self.assertNotIn('compatibility', doc_info.docstring) self.assertNotIn('Raises:', doc_info.docstring) self.assertEqual(len(doc_info.function_details), 3) self.assertEqual(set(doc_info.compatibility.keys()), {'numpy', 'theano'}) self.assertEqual(doc_info.compatibility['numpy'], 'NumPy has nothing as awesome as this function.\n') def test_generate_index(self): index = { 'tf': test_module, 'tf.TestModule': test_module, 'tf.test_function': test_function, 'tf.TestModule.test_function': test_function, 'tf.TestModule.TestClass': TestClass, 'tf.TestModule.TestClass.a_method': TestClass.a_method, 'tf.TestModule.TestClass.a_property': TestClass.a_property, 'tf.TestModule.TestClass.ChildClass': TestClass.ChildClass, } duplicate_of = {'tf.TestModule.test_function': 'tf.test_function'} visitor = DummyVisitor(index=index, duplicate_of=duplicate_of) reference_resolver = parser.ReferenceResolver.from_visitor( visitor=visitor, doc_index={}, py_module_names=['tf']) docs = parser.generate_global_index('TestLibrary', index=index, reference_resolver=reference_resolver) # Make sure duplicates and non-top-level symbols are in the index, but # methods and properties are not. self.assertNotIn('a_method', docs) self.assertNotIn('a_property', docs) self.assertIn('TestModule.TestClass', docs) self.assertIn('TestModule.TestClass.ChildClass', docs) self.assertIn('TestModule.test_function', docs) # Leading backtick to make sure it's included top-level. # This depends on formatting, but should be stable. self.assertIn('tf.test_function', docs) def test_argspec_for_functools_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 = tf_inspect.FullArgSpec( args=['arg1', 'arg2', 'kwarg1', 'kwarg2'], varargs=None, varkw=None, defaults=(1, 2), kwonlyargs=[], kwonlydefaults=None, annotations={}) self.assertEqual(expected, parser._get_arg_spec(test_function_for_partial1)) # Make sure doing nothing works. expected = tf_inspect.FullArgSpec( args=['arg1', 'arg2', 'kwarg1', 'kwarg2'], varargs=None, varkw=None, defaults=(1, 2), kwonlyargs=[], kwonlydefaults=None, annotations={}) partial = functools.partial(test_function_for_partial1) self.assertEqual(expected, parser._get_arg_spec(partial)) # Make sure setting args from the front works. expected = tf_inspect.FullArgSpec( args=['arg2', 'kwarg1', 'kwarg2'], varargs=None, varkw=None, defaults=(1, 2), kwonlyargs=[], kwonlydefaults=None, annotations={}) partial = functools.partial(test_function_for_partial1, 1) self.assertEqual(expected, parser._get_arg_spec(partial)) expected = tf_inspect.FullArgSpec( args=['kwarg2'], varargs=None, varkw=None, defaults=(2,), kwonlyargs=[], kwonlydefaults=None, annotations={}) partial = functools.partial(test_function_for_partial1, 1, 2, 3) self.assertEqual(expected, parser._get_arg_spec(partial)) # Make sure setting kwargs works. expected = tf_inspect.FullArgSpec( args=['arg1', 'arg2', 'kwarg2'], varargs=None, varkw=None, defaults=(2,), kwonlyargs=[], kwonlydefaults=None, annotations={}) partial = functools.partial(test_function_for_partial1, kwarg1=0) self.assertEqual(expected, parser._get_arg_spec(partial)) expected = tf_inspect.FullArgSpec( args=['arg1', 'arg2', 'kwarg1'], varargs=None, varkw=None, defaults=(1,), kwonlyargs=[], kwonlydefaults=None, annotations={}) partial = functools.partial(test_function_for_partial1, kwarg2=0) self.assertEqual(expected, parser._get_arg_spec(partial)) expected = tf_inspect.FullArgSpec( args=['arg1'], varargs=None, varkw=None, defaults=(), kwonlyargs=[], kwonlydefaults=None, annotations={}) 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 = tf_inspect.FullArgSpec( args=[], varargs='my_args', varkw='my_kwargs', defaults=(), kwonlyargs=[], kwonlydefaults=None, annotations={}) partial = functools.partial(test_function_for_partial2, 0, 1) self.assertEqual(expected, parser._get_arg_spec(partial)) # pylint: enable=protected-access def testSaveReferenceResolver(self): you_cant_serialize_this = object() duplicate_of = {'AClass': ['AClass2']} doc_index = {'doc': you_cant_serialize_this} is_fragment = { 'tf': False, 'tf.VERSION': True, 'tf.AClass': False, 'tf.AClass.method': True, 'tf.AClass2': False, 'tf.function': False } py_module_names = ['tf', 'tfdbg'] resolver = parser.ReferenceResolver(duplicate_of, doc_index, is_fragment, py_module_names) outdir = googletest.GetTempDir() filepath = os.path.join(outdir, 'resolver.json') resolver.to_json_file(filepath) resolver2 = parser.ReferenceResolver.from_json_file(filepath, doc_index) # There are no __slots__, so all fields are visible in __dict__. self.assertEqual(resolver.__dict__, resolver2.__dict__) def testIsFreeFunction(self): result = parser.is_free_function(test_function, 'test_module.test_function', {'test_module': test_module}) self.assertTrue(result) result = parser.is_free_function(test_function, 'TestClass.test_function', {'TestClass': TestClass}) self.assertFalse(result) result = parser.is_free_function(TestClass, 'TestClass', {}) self.assertFalse(result) result = parser.is_free_function(test_module, 'test_module', {}) self.assertFalse(result) RELU_DOC = """Computes rectified linear: `max(features, 0)` Args: features: A `Tensor`. Must be one of the following types: `float32`, `float64`, `int32`, `int64`, `uint8`, `int16`, `int8`, `uint16`, `half`. name: A name for the operation (optional) Returns: A `Tensor`. Has the same type as `features` """ class TestParseFunctionDetails(googletest.TestCase): def test_parse_function_details(self): docstring, function_details = parser._parse_function_details(RELU_DOC) self.assertEqual(len(function_details), 2) args = function_details[0] self.assertEqual(args.keyword, 'Args') self.assertEqual(len(args.header), 0) self.assertEqual(len(args.items), 2) self.assertEqual(args.items[0][0], 'features') self.assertEqual(args.items[1][0], 'name') self.assertEqual(args.items[1][1], 'A name for the operation (optional)\n\n') returns = function_details[1] self.assertEqual(returns.keyword, 'Returns') relu_doc_lines = RELU_DOC.split('\n') self.assertEqual(docstring, relu_doc_lines[0] + '\n\n') self.assertEqual(returns.header, relu_doc_lines[-2] + '\n') self.assertEqual( RELU_DOC, docstring + ''.join(str(detail) for detail in function_details)) class TestGenerateSignature(googletest.TestCase): def test_known_object(self): known_object = object() reverse_index = {id(known_object): 'location.of.object.in.api'} def example_fun(arg=known_object): # pylint: disable=unused-argument pass sig = parser._generate_signature(example_fun, reverse_index) self.assertEqual(sig, ['arg=location.of.object.in.api']) def test_literals(self): if sys.version_info >= (3, 0): print('Warning: Doc generation is not supported from python3.') return def example_fun(a=5, b=5.0, c=None, d=True, e='hello', f=(1, (2, 3))): # pylint: disable=g-bad-name, unused-argument pass sig = parser._generate_signature(example_fun, reverse_index={}) self.assertEqual( sig, ['a=5', 'b=5.0', 'c=None', 'd=True', "e='hello'", 'f=(1, (2, 3))']) def test_dotted_name(self): if sys.version_info >= (3, 0): print('Warning: Doc generation is not supported from python3.') return # pylint: disable=g-bad-name class a(object): class b(object): class c(object): class d(object): def __init__(self, *args): pass # pylint: enable=g-bad-name e = {'f': 1} def example_fun(arg1=a.b.c.d, arg2=a.b.c.d(1, 2), arg3=e['f']): # pylint: disable=unused-argument pass sig = parser._generate_signature(example_fun, reverse_index={}) self.assertEqual(sig, ['arg1=a.b.c.d', 'arg2=a.b.c.d(1, 2)', "arg3=e['f']"]) if __name__ == '__main__': googletest.main()