[Epydoc-commits] SF.net SVN: epydoc: [1396] trunk/epydoc/src/epydoc
Brought to you by:
edloper
From: <dva...@us...> - 2006-09-16 13:48:40
|
Revision: 1396 http://svn.sourceforge.net/epydoc/?rev=1396&view=rev Author: dvarrazzo Date: 2006-09-16 06:48:32 -0700 (Sat, 16 Sep 2006) Log Message: ----------- - Class docstrings can describe the constructor signature, parameters and raised exceptions. Merged revisions 1373-1375,1377-1388,1390-1395 via svnmerge from https://svn.sourceforge.net/svnroot/epydoc/branches/exp-args_in_class ........ r1373 | dvarrazzo | 2006-09-08 12:38:55 +0200 (Fri, 08 Sep 2006) | 10 lines - Allow parameter-related fields in a class docstring: use them to extend the matching __init__ docstring. - Enforce docstring parsing in dotted name order to ensure class docstring have already been parsed at __init__ docstring parsing time. - "type" fields are used to specify both class/instance variables types and constructor arguments types. In case a type is expressed in both class and __init__ docstring, the latter wins (e.g. the __init__ may receive an arg, parse it and store it in the instance state with the same name but different type) ........ r1374 | dvarrazzo | 2006-09-08 17:40:13 +0200 (Fri, 08 Sep 2006) | 8 lines - __init__ signature can be described in the class docstring also when there is no __init__.__doc__ at all. - parse_function_signature() can receive two arguments: one whose docstring is to be parsed and one to be populated. So the constructor signature can be parsed from the class docstring. - Dropped generation of variables when there is a type w/o matching var. The issue is still to be defined consistently anyway. ........ r1390 | dvarrazzo | 2006-09-15 03:47:53 +0200 (Fri, 15 Sep 2006) | 5 lines - Checked in a new algorithm to split fields in the class docstring between class documentation and constructor documentation. The algorithm requires the `type` fields to appear after the related fields. - Added more test. ........ r1391 | dvarrazzo | 2006-09-15 04:24:11 +0200 (Fri, 15 Sep 2006) | 2 lines - Splitting argument refactored into a single distinct function. ........ r1392 | dvarrazzo | 2006-09-16 01:36:23 +0200 (Sat, 16 Sep 2006) | 1 line - Be more lenient about types specified before matching objects. ........ r1393 | dvarrazzo | 2006-09-16 02:02:10 +0200 (Sat, 16 Sep 2006) | 5 lines - Reverted updates to `process_arg_field()` handler: is context is always a function, not the class docstring. - Exceptions are handled in the class docstring as well. - More explicit name of some variables. ........ r1394 | dvarrazzo | 2006-09-16 02:20:06 +0200 (Sat, 16 Sep 2006) | 1 line - Reverting type fields handling to trunk behavior. ........ r1395 | dvarrazzo | 2006-09-16 14:57:14 +0200 (Sat, 16 Sep 2006) | 4 lines - Class docstring fields are passed to __init__ without using an extra ClassDoc attribute. - Some docstring fixed. ........ Modified Paths: -------------- trunk/epydoc/src/epydoc/docstringparser.py trunk/epydoc/src/epydoc/test/docbuilder.doctest Modified: trunk/epydoc/src/epydoc/docstringparser.py =================================================================== --- trunk/epydoc/src/epydoc/docstringparser.py 2006-09-16 12:57:14 UTC (rev 1395) +++ trunk/epydoc/src/epydoc/docstringparser.py 2006-09-16 13:48:32 UTC (rev 1396) @@ -168,7 +168,9 @@ user docfields defined by containing objects. """ if api_doc.metadata is not UNKNOWN: - log.debug("%s's docstring processed twice" % api_doc.canonical_name) + if not (isinstance(api_doc, RoutineDoc) + and api_doc.canonical_name[-1] == '__init__'): + log.debug("%s's docstring processed twice" % api_doc.canonical_name) return initialize_api_doc(api_doc) @@ -197,8 +199,30 @@ descr, fields = parsed_docstring.split_fields(parse_errors) api_doc.descr = descr + field_warnings = [] + + # Handle the constructor fields that have been defined in the class + # docstring. This code assumes that a class docstring is parsed before + # the same class __init__ docstring. + if isinstance(api_doc, ClassDoc): + + # Parse ahead the __init__ docstring for this class + initvar = api_doc.variables.get('__init__') + if initvar and initvar.value not in (None, UNKNOWN): + init_api_doc = initvar.value + parse_docstring(init_api_doc, docindex) + + parse_function_signature(init_api_doc, api_doc) + init_fields = split_init_fields(fields, field_warnings) + + # Process fields + for field in init_fields: + try: + process_field(init_api_doc, docindex, field.tag(), + field.arg(), field.body()) + except ValueError, e: field_warnings.append(str(e)) + # Process fields - field_warnings = [] for field in fields: try: process_field(api_doc, docindex, field.tag(), @@ -254,6 +278,91 @@ if api_doc.sort_spec is UNKNOWN: api_doc.sort_spec = [] +def split_init_fields(fields, warnings): + """ + Remove the fields related to the constructor from a class docstring + fields list. + + @param fields: The fields to process. The list will be modified in place + @type fields: C{list} of L{markup.Field} + @param warnings: A list to emit processing warnings + @type warnings: C{list} + @return: The C{fields} items to be applied to the C{__init__} method + @rtype: C{list} of L{markup.Field} + """ + init_fields = [] + + # Split fields in lists according to their argument, keeping order. + arg_fields = {} + args_order = [] + i = 0 + while i < len(fields): + field = fields[i] + + # gather together all the fields with the same arg + if field.arg() is not None: + arg_fields.setdefault(field.arg(), []).append(fields.pop(i)) + args_order.append(field.arg()) + else: + i += 1 + + # Now check that for each argument there is at most a single variable + # and a single parameter, and at most a single type for each of them. + for arg in args_order: + ff = arg_fields.pop(arg, None) + if ff is None: + continue + + var = tvar = par = tpar = None + for field in ff: + if field.tag() in VARIABLE_TAGS: + if var is None: + var = field + fields.append(field) + else: + warnings.append( + "There is more than one variable named '%s'" + % arg) + elif field.tag() in PARAMETER_TAGS: + if par is None: + par = field + init_fields.append(field) + else: + warnings.append( + "There is more than one parameter named '%s'" + % arg) + + elif field.tag() == 'type': + if var is None and par is None: + # type before obj + tvar = tpar = field + else: + if var is not None and tvar is None: + tvar = field + if par is not None and tpar is None: + tpar = field + + elif field.tag() in EXCEPTION_TAGS: + init_fields.append(field) + + else: # Unespected field + fields.append(field) + + # Put selected types into the proper output lists + if tvar is not None: + if var is not None: + fields.append(tvar) + else: + pass # [xx] warn about type w/o object? + + if tpar is not None: + if par is not None: + init_fields.append(tpar) + else: + pass # [xx] warn about type w/o object? + + return init_fields + def report_errors(api_doc, docindex, parse_errors, field_warnings): """A helper function for L{parse_docstring()} that reports any markup warnings and field warnings that we encountered while @@ -620,6 +729,16 @@ register_field_handler(process_raise_field, 'raise', 'raises', 'except', 'exception') +# Tags related to function parameters +PARAMETER_TAGS = ('arg', 'argument', 'parameter', 'param', + 'kwarg', 'keyword', 'kwparam') + +# Tags related to variables in a class +VARIABLE_TAGS = ('cvar', 'cvariable', 'ivar', 'ivariable') + +# Tags related to exceptions +EXCEPTION_TAGS = ('raise', 'raises', 'except', 'exception') + ###################################################################### #{ Helper Functions ###################################################################### @@ -697,7 +816,7 @@ # [xx] copied from inspect.getdoc(); we can't use inspect.getdoc() # itself, since it expects an object, not a string. - if docstring == '': return '' + if not docstring: return '' lines = docstring.expandtabs().split('\n') # Find minimum indentation of any non-blank lines after first line. @@ -780,7 +899,7 @@ """A regular expression that is used to extract signatures from docstrings.""" -def parse_function_signature(func_doc): +def parse_function_signature(func_doc, doc_source=None): """ Construct the signature for a builtin function or method from its docstring. If the docstring uses the standard convention @@ -790,15 +909,26 @@ Otherwise, the signature will be set to a single varargs variable named C{"..."}. + @param func_doc: The target object where to store parsed signature. Also + container of the docstring to parse if doc_source is C{None} + @type func_doc: L{RoutineDoc} + @param doc_source: Contains the docstring to parse. If C{None}, parse + L{func_doc} docstring instead + @type doc_source: L{APIDoc} @rtype: C{None} """ + if doc_source is None: + doc_source = func_doc + # If there's no docstring, then don't do anything. - if not func_doc.docstring: return False + if not doc_source.docstring: return False - m = _SIGNATURE_RE.match(func_doc.docstring) + m = _SIGNATURE_RE.match(doc_source.docstring) if m is None: return False # Do I want to be this strict? + # Notice that __init__ must match the class name instead, if the signature + # comes from the class docstring # if not (m.group('func') == func_doc.canonical_name[-1] or # '_'+m.group('func') == func_doc.canonical_name[-1]): # log.warning("Not extracting function signature from %s's " @@ -857,7 +987,7 @@ func_doc.posarg_defaults.insert(0, None) # Remove the signature from the docstring. - func_doc.docstring = func_doc.docstring[m.end():] + doc_source.docstring = doc_source.docstring[m.end():] # We found a signature. return True Modified: trunk/epydoc/src/epydoc/test/docbuilder.doctest =================================================================== --- trunk/epydoc/src/epydoc/test/docbuilder.doctest 2006-09-16 12:57:14 UTC (rev 1395) +++ trunk/epydoc/src/epydoc/test/docbuilder.doctest 2006-09-16 13:48:32 UTC (rev 1396) @@ -1,5 +1,5 @@ Regression Testing for epydoc.docbuilder -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Test Function ============= @@ -14,6 +14,22 @@ >>> import tempfile, re, os, os.path, textwrap, sys >>> from epydoc.docbuilder import build_doc + >>> from epydoc.apidoc import ClassDoc, RoutineDoc + >>> from epydoc.markup import ParsedDocstring + + >>> def to_plain(docstring): + ... """Conver a parsed docstring into plain text""" + ... if isinstance(docstring, ParsedDocstring): + ... docstring = docstring.to_plaintext(None) + ... return docstring.rsplit() + + >>> def fun_to_plain(val_doc): + ... """Convert parsed docstrings in text from a RoutineDoc""" + ... for k, v in val_doc.arg_types.items(): + ... val_doc.arg_types[k] = to_plain(v) + ... for i, (k, v) in enumerate(val_doc.arg_descrs): + ... val_doc.arg_descrs[i] = (k, to_plain(v)) + >>> def runbuilder(s, attribs='', build=None, exclude=''): ... # Write it to a temp file. ... tmp_dir = tempfile.mkdtemp() @@ -24,6 +40,10 @@ ... val_doc = build_doc(os.path.join(tmp_dir, 'epydoc_test.py')) ... if build: val_doc = val_doc.variables[build].value ... # Display it. + ... if isinstance(val_doc, ClassDoc): + ... for val in val_doc.variables.values(): + ... if isinstance(val.value, RoutineDoc): + ... fun_to_plain(val.value) ... s = val_doc.pp(include=attribs.split(),exclude=exclude.split()) ... s = re.sub(r"(filename = ).*", r"\1...", s) ... s = re.sub(r"(<module 'epydoc_test' from ).*", r'\1...', s) @@ -115,3 +135,291 @@ +- PropertyDoc for epydoc_test.Foo.y [9] +- descr = u'A property has no defining module' +- type_descr = u'int' + +Stuff from future doesn't appear as variable. + + >>> runbuilder(s=""" + ... from __future__ import division + ... from re import match + ... """, + ... attribs='variables value') + ModuleDoc for epydoc_test [0] + +- variables + +- match => VariableDoc for epydoc_test.match [1] + +- value + +- ValueDoc for sre.match [2] + + +Specifying constructor signature in class docstring +=================================================== + +The class signature can be specified in the class docstring instead of __init__ + + >>> runbuilder(s=''' + ... class Foo: + ... """This is the object docstring + ... + ... @param a: init param. + ... @ivar a: instance var. + ... @type a: date + ... """ + ... def __init__(self, a): + ... """The ctor docstring. + ... + ... @type a: str + ... """ + ... pass + ... ''', + ... build="Foo", + ... attribs="variables name value " + ... "posargs vararg kwarg type arg_types arg_descrs") + ClassDoc for epydoc_test.Foo [0] + +- variables + +- __init__ => VariableDoc for epydoc_test.Foo.__init__ [1] + | +- name = '__init__' + | +- value + | +- RoutineDoc for epydoc_test.Foo.__init__ [2] + | +- arg_descrs = [([u'a'], ... + | +- arg_types = {u'a': ... + | +- kwarg = None + | +- posargs = ['self', 'a'] + | +- vararg = None + +- a => VariableDoc for epydoc_test.Foo.a [3] + +- name = u'a' + +- value = <UNKNOWN> + +Also keywords arguments can be put in the constructor + + >>> runbuilder(s=''' + ... class Foo: + ... """This is the object docstring + ... + ... @keyword a: a kwarg. + ... @type a: str + ... """ + ... def __init__(self, **kwargs): + ... """The ctor docstring. + ... + ... @type a: str + ... """ + ... ''', + ... build="Foo", + ... attribs="variables name value " + ... "posargs vararg kwarg type arg_types arg_descrs") + ClassDoc for epydoc_test.Foo [0] + +- variables + +- __init__ => VariableDoc for epydoc_test.Foo.__init__ [1] + +- name = '__init__' + +- value + +- RoutineDoc for epydoc_test.Foo.__init__ [2] + +- arg_descrs = [([u'a'], ... + +- arg_types = {u'a': ... + +- kwarg = 'kwargs' + +- posargs = ['self'] + +- vararg = None + +A missing docstring on the __init__ is not an issue. + + >>> runbuilder(s=''' + ... class Foo: + ... """This is the object docstring + ... + ... @param a: a param. + ... @type a: str + ... """ + ... def __init__(self): + ... pass + ... ''', + ... build="Foo", + ... attribs="variables name value " + ... "posargs vararg kwarg type arg_types arg_descrs") + ClassDoc for epydoc_test.Foo [0] + +- variables + +- __init__ => VariableDoc for epydoc_test.Foo.__init__ [1] + +- name = '__init__' + +- value + +- RoutineDoc for epydoc_test.Foo.__init__ [2] + +- arg_descrs = [([u'a'], ... + +- arg_types = {u'a': ... + +- kwarg = None + +- posargs = ['self'] + +- vararg = None + +Exceptions can be put in the docstring class, and they are assigned to the +constructor too. + >>> runbuilder(s=''' + ... class Foo: + ... """Foo(x, y) + ... + ... A class to ship rockets in outer space. + ... + ... @param x: first param + ... @param y: second param + ... @except ValueError: frobnication error + ... """ + ... def __init__(self, a, b): + ... """__init__ doc""" + ... pass + ... ''', + ... build="Foo", + ... attribs="variables name value exception_descrs " + ... "posargs vararg kwarg type arg_types arg_descrs docstring") + ClassDoc for epydoc_test.Foo [0] + +- docstring = u'A class to ship rockets in outer sp... + +- variables + +- __init__ => VariableDoc for epydoc_test.Foo.__init__ [1] + +- docstring = <UNKNOWN> + +- name = '__init__' + +- value + +- RoutineDoc for epydoc_test.Foo.__init__ [2] + +- arg_descrs = [([u'x'], [u'first', u'param']), ... + +- arg_types = {} + +- docstring = u'__init__ doc' + +- exception_descrs = [(DottedName(u'ValueError'), ... + +- kwarg = None + +- posargs = [u'x', u'y'] + +- vararg = None + + +Epydoc can also grok the constructor signature from the class docstring + + >>> runbuilder(s=''' + ... class Foo: + ... """Foo(x, y) + ... + ... A class to ship rockets in outer space. + ... + ... @param x: first param + ... @param y: second param + ... """ + ... def __init__(self, a, b): + ... """__init__ doc""" + ... pass + ... ''', + ... build="Foo", + ... attribs="variables name value " + ... "posargs vararg kwarg type arg_types arg_descrs docstring") + ClassDoc for epydoc_test.Foo [0] + +- docstring = u'A class to ship rockets ... + +- variables + +- __init__ => VariableDoc for epydoc_test.Foo.__init__ [1] + +- docstring = <UNKNOWN> + +- name = '__init__' + +- value + +- RoutineDoc for epydoc_test.Foo.__init__ [2] + +- arg_descrs = [([u'x'], ... + +- arg_types = {} + +- docstring = u'__init__ doc' + +- kwarg = None + +- posargs = [u'x', u'y'] + +- vararg = None + +A type can apply to both a param and a variable + + >>> runbuilder(s=''' + ... class Foo: + ... """This is the object docstring + ... + ... @param a: init param. + ... @ivar a: instance var. + ... @type a: date + ... """ + ... def __init__(self, a): + ... pass + ... ''', + ... build="Foo", + ... attribs="variables name value " + ... "posargs vararg kwarg type_descr arg_types arg_descrs") + ClassDoc for epydoc_test.Foo [0] + +- variables + +- __init__ => VariableDoc for epydoc_test.Foo.__init__ [1] + | +- name = '__init__' + | +- type_descr = None + | +- value + | +- RoutineDoc for epydoc_test.Foo.__init__ [2] + | +- arg_descrs = [([u'a'], [u'init', u'param.'])] + | +- arg_types = {u'a': [u'date']} + | +- kwarg = None + | +- posargs = ['self', 'a'] + | +- vararg = None + +- a => VariableDoc for epydoc_test.Foo.a [3] + +- name = u'a' + +- type_descr = u'date\n\n' + +- value = <UNKNOWN> + +But there can also be two different types + + >>> runbuilder(s=''' + ... class Foo: + ... """This is the object docstring + ... + ... @param a: init param. + ... @type a: string + ... @ivar a: instance var. + ... @type a: date + ... """ + ... def __init__(self, a): + ... pass + ... ''', + ... build="Foo", + ... attribs="variables name value " + ... "posargs vararg kwarg type_descr arg_types arg_descrs") + ClassDoc for epydoc_test.Foo [0] + +- variables + +- __init__ => VariableDoc for epydoc_test.Foo.__init__ [1] + | +- name = '__init__' + | +- type_descr = None + | +- value + | +- RoutineDoc for epydoc_test.Foo.__init__ [2] + | +- arg_descrs = [([u'a'], [u'init', u'param.'])] + | +- arg_types = {u'a': [u'string']} + | +- kwarg = None + | +- posargs = ['self', 'a'] + | +- vararg = None + +- a => VariableDoc for epydoc_test.Foo.a [3] + +- name = u'a' + +- type_descr = u'date\n\n' + +- value = <UNKNOWN> + +Also reST consolidated fields are not a problem. + + >>> runbuilder(s=''' + ... __docformat__ = 'restructuredtext' + ... class Foo: + ... """This is the object docstring + ... + ... :Parameters: + ... `a` : string + ... init param. + ... + ... :Exceptions: + ... * `ValueError`: frobnication error + ... init param. + ... + ... :IVariables: + ... `a` : date + ... instance var. + ... """ + ... def __init__(self, a): + ... pass + ... ''', + ... build="Foo", + ... attribs="variables name value exception_descrs " + ... "posargs vararg kwarg type_descr arg_types arg_descrs") + ClassDoc for epydoc_test.Foo [0] + +- variables + +- __init__ => VariableDoc for epydoc_test.Foo.__init__ [1] + | +- name = '__init__' + | +- type_descr = None + | +- value + | +- RoutineDoc for epydoc_test.Foo.__init__ [2] + | +- arg_descrs = [([u'a'], [u'init', u'param.'])] + | +- arg_types = {u'a': [u'string']} + | +- exception_descrs = [(DottedName(u'ValueError'), ... + | +- kwarg = None + | +- posargs = ['self', 'a'] + | +- vararg = None + +- a => VariableDoc for epydoc_test.Foo.a [3] + +- name = u'a' + +- type_descr = u'date' + +- value = <UNKNOWN> This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |