diff --git a/py/apigen/html.py b/py/apigen/html.py index d7d836185..1ee0f8f19 100644 --- a/py/apigen/html.py +++ b/py/apigen/html.py @@ -47,12 +47,21 @@ class H(html): super(H.FunctionInfo, self).__init__( H.Hideable('funcinfo', 'funcinfo', valuedesc, csource, callstack)) + + class PropertyDescription(html.div): + def __init__(self, name, value): + if type(value) not in [str, unicode]: + value = str(value) + if len(value) > 100: + value = value[:100] + '...' + super(H.PropertyDescription, self).__init__(H.strong(name), ': ', + H.em(value), + class_='property') class ParameterDescription(html.div): pass class Docstring(html.pre): - style = html.Style(width='100%') pass class Navigation(html.div): diff --git a/py/apigen/htmlgen.py b/py/apigen/htmlgen.py index 62a0207d6..4c1aedcb8 100644 --- a/py/apigen/htmlgen.py +++ b/py/apigen/htmlgen.py @@ -17,6 +17,17 @@ raw = py.xml.raw def is_navigateable(name): return (not is_private(name) and name != '__doc__') +def show_property(name): + if not name.startswith('_'): + return True + if name.startswith('__') and name.endswith('__'): + # XXX do we need to skip more manually here? + if (name not in dir(object) and + name not in ['__doc__', '__dict__', '__name__', '__module__', + '__weakref__']): + return True + return False + def deindent(str, linesep='\n'): """ de-indent string @@ -351,7 +362,24 @@ class ApiPageBuilder(AbstractPageBuilder): docstring = cls.__doc__ if docstring: docstring = deindent(docstring) - methods = self.dsa.get_class_methods(dotted_name) + if not hasattr(cls, '__name__'): + clsname = 'instance of %s' % (cls.__class__.__name__,) + else: + clsname = cls.__name__ + bases = self.build_bases(dotted_name) + properties = self.build_properties(cls) + methods = self.build_methods(dotted_name) + snippet = H.ClassDescription( + # XXX bases HTML + H.ClassDef('%s(' % (clsname,), *bases), + H.Docstring(docstring or '*no docstring available*'), + sourcelink, + *(properties+methods) + ) + + return snippet + + def build_bases(self, dotted_name): basehtml = [] bases = self.dsa.get_possible_base_classes(dotted_name) for base in bases: @@ -366,23 +394,33 @@ class ApiPageBuilder(AbstractPageBuilder): if basehtml: basehtml.pop() basehtml.append('):') - if not hasattr(cls, '__name__'): - clsname = 'instance of %s' % (cls.__class__.__name__,) - else: - clsname = cls.__name__ - snippet = H.ClassDescription( - # XXX bases HTML - H.ClassDef('%s(' % (clsname,), *basehtml), - H.Docstring(docstring or '*no docstring available*'), - sourcelink, - ) + return basehtml + + def build_properties(self, cls): + properties = [] + for attr in dir(cls): + val = getattr(cls, attr) + if show_property(attr) and not callable(val): + if isinstance(val, property): + val = '' + properties.append((attr, val)) + properties.sort(key=lambda a: a[0]) # sort on name + ret = [] + if properties: + ret.append(H.h2('properties:')) + for name, val in properties: + ret.append(H.PropertyDescription(name, val)) + return ret + + def build_methods(self, dotted_name): + ret = [] + methods = self.dsa.get_class_methods(dotted_name) if methods: - snippet.append(H.h2('methods:')) + ret.append(H.h2('methods:')) for method in methods: - snippet += self.build_callable_view('%s.%s' % (dotted_name, - method)) - # XXX properties - return snippet + ret += self.build_callable_view('%s.%s' % (dotted_name, + method)) + return ret def build_namespace_view(self, namespace_dotted_name, item_dotted_names): """ build the html for a namespace (module) """ diff --git a/py/apigen/layout.py b/py/apigen/layout.py index 92c6b062b..ef9e012be 100644 --- a/py/apigen/layout.py +++ b/py/apigen/layout.py @@ -26,9 +26,17 @@ class LayoutPage(confrest.PyPage): def fill(self): super(LayoutPage, self).fill() - #self.menubar[:] = [] + self.update_menubar_links(self.menubar) self.body.insert(0, self.nav) + def update_menubar_links(self, node): + for item in node: + if not isinstance(item, py.xml.Tag): + continue + if (item.__class__.__name__ == 'a' and hasattr(item.attr, 'href') + and not item.attr.href.startswith('http://')): + item.attr.href = self.relpath + '../py/doc/' + item.attr.href + def setup_scripts_styles(self, copyto=None): for path, name in self.stylesheets: if copyto: diff --git a/py/apigen/style.css b/py/apigen/style.css index b84c5b5e8..991767005 100644 --- a/py/apigen/style.css +++ b/py/apigen/style.css @@ -18,7 +18,6 @@ div.sidebar .selected a { #content { border: 0px; height: 95%; - width: 100%; } ul { @@ -76,6 +75,10 @@ ul li { background-color: white; } +.property { + font-size: 1.2em; +} + .callstackitem { border: 1px solid black; margin-bottom: 1em; diff --git a/py/apigen/testing/test_apigen_functional.py b/py/apigen/testing/test_apigen_functional.py index 8d2def970..17109dd26 100644 --- a/py/apigen/testing/test_apigen_functional.py +++ b/py/apigen/testing/test_apigen_functional.py @@ -19,6 +19,7 @@ def setup_fs_project(name): temp.ensure('pak/sometestclass.py').write(py.code.Source("""\ class SomeTestClass(object): " docstring sometestclass " + someattr = 'somevalue' def __init__(self, somevar): self.somevar = somevar @@ -129,12 +130,10 @@ def test_apigen_functional(): sometestclass_api = apidir.join('main.SomeTestClass.html') assert sometestclass_api.check(file=True) html = sometestclass_api.read() + print html assert 'SomeTestClass' in html - # XXX not linking to method files anymore - #sometestclass_init_api = apidir.join('main.SomeTestClass.__init__.html') - #assert sometestclass_init_api.check(file=True) - #assert sometestclass_init_api.read().find( - # '__init__') > -1 + assert 'someattr: somevalue' in html + namespace_api = apidir.join('main.html') assert namespace_api.check(file=True) html = namespace_api.read() @@ -156,4 +155,5 @@ def test_apigen_functional(): html = index.read() print html assert 'test' in html + assert 'href="../../py/doc/home.html"' diff --git a/py/apigen/testing/test_htmlgen.py b/py/apigen/testing/test_htmlgen.py index ec3893293..f1953f8d7 100644 --- a/py/apigen/testing/test_htmlgen.py +++ b/py/apigen/testing/test_htmlgen.py @@ -55,3 +55,12 @@ def test_enumerate_and_color(): ' "bar"\n' '') +def test_show_property(): + assert htmlgen.show_property('foo') + assert not htmlgen.show_property('_foo') + assert htmlgen.show_property('__foo__') + assert not htmlgen.show_property('__doc__') + assert not htmlgen.show_property('__dict__') + assert not htmlgen.show_property('__name__') + assert not htmlgen.show_property('__class__') + diff --git a/py/apigen/todo-apigen.txt b/py/apigen/todo-apigen.txt index 03d4a98cd..7676df8e5 100644 --- a/py/apigen/todo-apigen.txt +++ b/py/apigen/todo-apigen.txt @@ -1,5 +1,7 @@ -* format docstrings more nicely (with tests) - DONE I guess +* format docstrings more nicely (with tests) + + DONE I guess * have the API function view be as informative as possible without having to go to the "single method" view @@ -10,10 +12,12 @@ viewed. method views (when navigating there through the class view) should also have the source there - DONE I think + DONE I think, single method view is gone * have class-level attributes be displayed + DONE + * use "inherited" doc strings, i.e. for class A: def meth(self): @@ -25,8 +29,12 @@ B.meth should display the A.meth docstring, probably with special formatting (italics or so). + NOT YET DONE (later?) + * factor out some common code in the build_* functions + (mostly) DONE + * refactor the apigen/rsession interaction to become cleaner (e.g. apigen's get_documentable_items should be separately tested and the caller should not need @@ -76,9 +84,15 @@ * add syntax coloring for Python source snippets + DONE + * remove py.test/apigen cruft from stack traces + DONE, thanks to fijal + * fix non-ascii source encoding support + DONE + * XXX