diff --git a/tests/test_sphinxext.py b/tests/test_sphinxext.py index a86e11f..62d0ee1 100644 --- a/tests/test_sphinxext.py +++ b/tests/test_sphinxext.py @@ -3,6 +3,7 @@ import sphinx import os.path import wsme.types +from wsme import sphinxext docpath = os.path.join( os.path.dirname(__file__), @@ -24,3 +25,21 @@ class TestSphinxExt(unittest.TestCase): '-d', '.test_sphinxext/doctree', docpath, '.test_sphinxext/html']) == 0 + + +class TestDataTypeName(unittest.TestCase): + def test_user_type(self): + self.assertEqual(sphinxext.datatypename(ASampleType), + 'ASampleType') + + def test_dict_type(self): + d = wsme.types.DictType(str, str) + self.assertEqual(sphinxext.datatypename(d), 'dict(str: str)') + d = wsme.types.DictType(str, ASampleType) + self.assertEqual(sphinxext.datatypename(d), 'dict(str: ASampleType)') + + def test_array_type(self): + d = wsme.types.ArrayType(str) + self.assertEqual(sphinxext.datatypename(d), 'list(str)') + d = wsme.types.ArrayType(ASampleType) + self.assertEqual(sphinxext.datatypename(d), 'list(ASampleType)') diff --git a/wsme/pecan.py b/wsme/pecan.py index 7a478a8..e9ba454 100644 --- a/wsme/pecan.py +++ b/wsme/pecan.py @@ -1,5 +1,6 @@ from __future__ import absolute_import +import functools import inspect import sys @@ -57,6 +58,7 @@ def wsexpose(*args, **kwargs): funcdef = wsme.api.FunctionDefinition.get(f) funcdef.resolve_types(wsme.types.registry) + @functools.wraps(f) def callfunction(self, *args, **kwargs): try: args, kwargs = wsme.rest.args.get_args( diff --git a/wsme/sphinxext.py b/wsme/sphinxext.py index 5a6d02f..898b333 100644 --- a/wsme/sphinxext.py +++ b/wsme/sphinxext.py @@ -27,6 +27,11 @@ field_re = re.compile(r':(?P\w+)(\s+(?P\w+))?:') def datatypename(datatype): if isinstance(datatype, wsme.types.UserType): return datatype.name + if isinstance(datatype, wsme.types.DictType): + return 'dict(%s: %s)' % (datatypename(datatype.key_type), + datatypename(datatype.value_type)) + if isinstance(datatype, wsme.types.ArrayType): + return 'list(%s)' % datatypename(datatype.item_type) return datatype.__name__ @@ -157,16 +162,37 @@ class AttributeDirective(PyClassmember): ] +def check_samples_slot(value): + """Validate the samples_slot option to the TypeDocumenter. + + Valid positions are 'before-docstring' and + 'after-docstring'. Using the explicit 'none' disables sample + output. The default is after-docstring. + """ + if not value: + return 'after-docstring' + val = directives.choice( + value, + ('none', # do not include + 'before-docstring', # show samples then docstring + 'after-docstring', # show docstring then samples + )) + return val + + class TypeDocumenter(autodoc.ClassDocumenter): objtype = 'type' directivetype = 'type' domain = 'wsme' required_arguments = 1 + default_samples_slot = 'after-docstring' - option_spec = dict(autodoc.ClassDocumenter.option_spec, **{ - 'protocols': lambda l: [v.strip() for v in l.split(',')] - }) + option_spec = dict( + autodoc.ClassDocumenter.option_spec, + **{'protocols': lambda l: [v.strip() for v in l.split(',')], + 'samples-slot': check_samples_slot, + }) @classmethod def can_document_member(cls, member, membername, isattr, parent): @@ -194,34 +220,51 @@ class TypeDocumenter(autodoc.ClassDocumenter): return False def add_content(self, more_content, no_docstring=False): - protocols = get_protocols( - self.options.protocols or self.env.app.config.wsme_protocols - ) - content = [] - if protocols: - sample_obj = make_sample_object(self.object) - content.extend([ - l_(u'Data samples:'), - u'', - u'.. cssclass:: toggle', - u'' - ]) - for name, protocol in protocols: - language, sample = protocol.encode_sample_value( - self.object, sample_obj, format=True) - content.extend([ - name, - u' .. code-block:: ' + language, - u'', - ]) - content.extend(( - u' ' * 8 + line for line in sample.split('\n'))) - for line in content: - self.add_line(line, u'') - super(TypeDocumenter, self).add_content( - more_content, no_docstring) + print 'SAMPLES SLOT:', self.options.samples_slot + + def add_docstring(): + super(TypeDocumenter, self).add_content( + more_content, no_docstring) + + def add_samples(): + protocols = get_protocols( + self.options.protocols or self.env.app.config.wsme_protocols + ) + content = [] + if protocols: + sample_obj = make_sample_object(self.object) + content.extend([ + l_(u'Data samples:'), + u'', + u'.. cssclass:: toggle', + u'' + ]) + for name, protocol in protocols: + language, sample = protocol.encode_sample_value( + self.object, sample_obj, format=True) + content.extend([ + name, + u' .. code-block:: ' + language, + u'', + ]) + content.extend(( + u' ' * 8 + line for line in sample.split('\n'))) + for line in content: + self.add_line(line, u'') + + if samples_slot == 'after-docstring': + add_docstring() + add_samples() + elif samples_slot == 'before-docstring': + add_samples() + add_docstring() + else: + add_docstring() class AttributeDocumenter(autodoc.AttributeDocumenter): @@ -341,13 +384,14 @@ class FunctionDocumenter(autodoc.MethodDocumenter): @classmethod def can_document_member(cls, member, membername, isattr, parent): - return wsme.api.iswsmefunction(member) + return (isinstance(parent, ServiceDocumenter) + and wsme.api.iswsmefunction(member)) def import_object(self): ret = super(FunctionDocumenter, self).import_object() self.directivetype = 'function' self.wsme_fd = wsme.api.FunctionDefinition.get(self.object) - self.retann = self.wsme_fd.return_type.__name__ + self.retann = datatypename(self.wsme_fd.return_type) return ret def format_args(self): @@ -360,6 +404,10 @@ class FunctionDocumenter(autodoc.MethodDocumenter): """Inject the type and param fields into the docstrings so that the user can add its own param fields to document the parameters""" docstrings = super(FunctionDocumenter, self).get_doc(encoding) + # If the function doesn't have a docstring, add an empty list + # so the default behaviors below work correctly. + if not docstrings: + docstrings.append([]) found_params = set() protocols = get_protocols( @@ -390,14 +438,13 @@ class FunctionDocumenter(autodoc.MethodDocumenter): and m.group('name') == arg.name: pos = (si, i + 1) break - break docstring = docstrings[pos[0]] docstring[pos[1]:pos[1]] = content next_param_pos = (pos[0], pos[1] + len(content)) if self.wsme_fd.return_type: content = [ - u':rtype: %s' % self.wsme_fd.return_type.__name__ + u':rtype: %s' % datatypename(self.wsme_fd.return_type) ] pos = None for si, docstring in enumerate(docstrings): @@ -406,8 +453,7 @@ class FunctionDocumenter(autodoc.MethodDocumenter): if m and m.group('field') == 'return': pos = (si, i + 1) break - break - if pos is None: + else: pos = next_param_pos docstring = docstrings[pos[0]] docstring[pos[1]:pos[1]] = content diff --git a/wsme/tests/test_types.py b/wsme/tests/test_types.py index cbfb45d..0784aa5 100644 --- a/wsme/tests/test_types.py +++ b/wsme/tests/test_types.py @@ -355,3 +355,15 @@ class TestTypes(unittest.TestCase): def test_array_eq(self): l = [types.ArrayType(str)] assert types.ArrayType(str) in l + + def test_array_sample(self): + s = types.ArrayType(str).sample() + assert isinstance(s, list) + assert s + assert s[0] == '' + + def test_dict_sample(self): + s = types.DictType(str, str).sample() + assert isinstance(s, dict) + assert s + assert s == {'': ''} diff --git a/wsme/types.py b/wsme/types.py index c88acf9..8758569 100644 --- a/wsme/types.py +++ b/wsme/types.py @@ -34,6 +34,9 @@ class ArrayType(object): return isinstance(other, ArrayType) \ and self.item_type == other.item_type + def sample(self): + return [getattr(self.item_type, 'sample', self.item_type)()] + @property def item_type(self): if isinstance(self._item_type, weakref.ref): @@ -67,6 +70,11 @@ class DictType(object): def __hash__(self): return hash((self.key_type, self.value_type)) + def sample(self): + key = getattr(self.key_type, 'sample', self.key_type)() + value = getattr(self.value_type, 'sample', self.value_type)() + return {key: value} + @property def value_type(self): if isinstance(self._value_type, weakref.ref):