diff --git a/py/apigen/apigen.py b/py/apigen/apigen.py index b7ee295ae..6a527d8c3 100644 --- a/py/apigen/apigen.py +++ b/py/apigen/apigen.py @@ -30,6 +30,7 @@ def get_documentable_items(pkgdir): pkgname, pkgdict = get_documentable_items_pkgdir(pkgdir) from py.__.execnet.channel import Channel pkgdict['execnet.Channel'] = Channel + Channel.__apigen_hide_from_nav__ = True return pkgname, pkgdict def build(pkgdir, dsa, capture): diff --git a/py/apigen/html.py b/py/apigen/html.py index 6043d7801..3681f3e44 100644 --- a/py/apigen/html.py +++ b/py/apigen/html.py @@ -25,7 +25,7 @@ class H(html): class ClassDef(html.div): def __init__(self, classname, bases, docstring, sourcelink, - properties, methods): + attrs, methods): header = H.h1('class %s(' % (classname,)) for i, (name, href) in py.builtin.enumerate(bases): if i > 0: @@ -40,9 +40,9 @@ class H(html): '*no docstring available*'), sourcelink, class_='classdoc')) - if properties: - self.append(H.h2('properties:')) - for name, val in properties: + if attrs: + self.append(H.h2('class attributes and properties:')) + for name, val in attrs: self.append(H.PropertyDescription(name, val)) if methods: self.append(H.h2('methods:')) @@ -58,20 +58,32 @@ class H(html): class FunctionDescription(Description): def __init__(self, localname, argdesc, docstring, valuedesc, csource, callstack): - fd = H.FunctionDef(localname, argdesc) - ds = H.Docstring(docstring or '*no docstring available*') - fi = H.FunctionInfo(valuedesc, csource, callstack) + infoid = 'info_%s' % (localname.replace('.', '_dot_'),) + docstringid = 'docstring_%s' % (localname.replace('.', '_dot_'),) + fd = H.FunctionDef(localname, argdesc, + onclick=('showhideel(' + 'document.getElementById("%s")); ' + 'showhideel(' + 'document.getElementById("%s")); ' + 'this.scrollIntoView()' % ( + infoid, docstringid))) + ds = H.Docstring(docstring or '*no docstring available*', + id=docstringid) + fi = H.FunctionInfo(valuedesc, csource, callstack, + id=infoid, style="display: none") super(H.FunctionDescription, self).__init__(fd, ds, fi) class FunctionDef(html.h2): - def __init__(self, name, argdesc): - super(H.FunctionDef, self).__init__('def %s%s:' % (name, argdesc)) + style = html.Style(cursor='pointer', color='blue') + def __init__(self, name, argdesc, **kwargs): + super(H.FunctionDef, self).__init__('def %s%s:' % (name, argdesc), + **kwargs) class FunctionInfo(html.div): - def __init__(self, valuedesc, csource, callstack): - super(H.FunctionInfo, self).__init__( - H.Hideable('funcinfo', 'funcinfo', valuedesc, H.br(), csource, - callstack)) + def __init__(self, valuedesc, csource, callstack, **kwargs): + super(H.FunctionInfo, self).__init__(valuedesc, H.br(), csource, + callstack, class_='funcinfo', + **kwargs) class PropertyDescription(html.div): def __init__(self, name, value): @@ -86,8 +98,9 @@ class H(html): class ParameterDescription(html.div): pass - class Docstring(html.pre): - pass + class Docstring(html.div): + style = html.Style(white_space='pre', color='#666', + margin_left='1em', margin_bottom='1em') class Navigation(html.div): #style = html.Style(min_height='99%', float='left', margin_top='1.2em', diff --git a/py/apigen/htmlgen.py b/py/apigen/htmlgen.py index c9e09577c..ba64ca222 100644 --- a/py/apigen/htmlgen.py +++ b/py/apigen/htmlgen.py @@ -14,6 +14,8 @@ sorted = py.builtin.sorted html = py.xml.html raw = py.xml.raw +REDUCE_CALLSITES = True + def is_navigateable(name): return (not is_private(name) and name != '__doc__') @@ -24,7 +26,7 @@ def show_property(name): # XXX do we need to skip more manually here? if (name not in dir(object) and name not in ['__doc__', '__dict__', '__name__', '__module__', - '__weakref__']): + '__weakref__', '__apigen_hide_from_nav__']): return True return False @@ -136,10 +138,15 @@ def enumerate_and_color(codelines, firstlineno, enc): break return snippet +_get_obj_cache = {} def get_obj(dsa, pkg, dotted_name): full_dotted_name = '%s.%s' % (pkg.__name__, dotted_name) if dotted_name == '': return pkg + try: + return _get_obj_cache[dotted_name] + except KeyError: + pass path = dotted_name.split('.') ret = pkg for item in path: @@ -147,10 +154,13 @@ def get_obj(dsa, pkg, dotted_name): ret = getattr(ret, item, marker) if ret is marker: try: - return dsa.get_obj(dotted_name) + ret = dsa.get_obj(dotted_name) except KeyError: raise NameError('can not access %s in %s' % (item, full_dotted_name)) + else: + break + _get_obj_cache[dotted_name] = ret return ret def get_rel_sourcepath(projpath, filename, default=None): @@ -419,6 +429,10 @@ class ApiPageBuilder(AbstractPageBuilder): def build_methods(self, dotted_name): ret = [] methods = self.dsa.get_class_methods(dotted_name) + # move all __*__ methods to the back + methods = ([m for m in methods if not m.startswith('_')] + + [m for m in methods if m.startswith('_')]) + # except for __init__, which should be first if '__init__' in methods: methods.remove('__init__') methods.insert(0, '__init__') @@ -437,7 +451,8 @@ class ApiPageBuilder(AbstractPageBuilder): ) for dotted_name in sorted(item_dotted_names): itemname = dotted_name.split('.')[-1] - if not is_navigateable(itemname): + if (not is_navigateable(itemname) or + self.is_hidden_from_nav(dotted_name)): continue snippet.append( H.NamespaceItem( @@ -463,7 +478,10 @@ class ApiPageBuilder(AbstractPageBuilder): nav = self.build_navigation(dotted_name, False) reltargetpath = "api/%s.html" % (dotted_name,) self.linker.set_link(dotted_name, reltargetpath) - title = 'api documentation for %s' % (dotted_name,) + title = '%s API documentation' % (dotted_name,) + rev = self.get_revision(dotted_name) + if rev: + title += ' [rev. %s]' % (rev,) self.write_page(title, reltargetpath, tag, nav) return passed @@ -479,7 +497,10 @@ class ApiPageBuilder(AbstractPageBuilder): nav = self.build_navigation(dotted_name, False) reltargetpath = "api/%s.html" % (dotted_name,) self.linker.set_link(dotted_name, reltargetpath) - title = 'api documentation for %s' % (dotted_name,) + title = '%s API documentation' % (dotted_name,) + rev = self.get_revision(dotted_name) + if rev: + title += ' [rev. %s]' % (rev,) self.write_page(title, reltargetpath, tag, nav) return passed @@ -528,6 +549,8 @@ class ApiPageBuilder(AbstractPageBuilder): sibname = sibpath[-1] if not is_navigateable(sibname): continue + if self.is_hidden_from_nav(dn): + continue navitems.append(H.NavigationItem(self.linker, dn, sibname, depth, selected)) if selected: @@ -595,10 +618,18 @@ class ApiPageBuilder(AbstractPageBuilder): def is_in_pkg(self, sourcefile): return py.path.local(sourcefile).relto(self.projpath) + _processed_callsites = {} def build_callsites(self, dotted_name): callstack = self.dsa.get_function_callpoints(dotted_name) cslinks = [] for i, (cs, _) in enumerate(callstack): + if REDUCE_CALLSITES: + key = (cs[0].filename, cs[0].lineno) + if key in self._processed_callsites: + # process one call site per line of test code when + # REDUCE_CALLSITES is set to True + continue + self._processed_callsites[key] = 1 link = self.build_callsite(dotted_name, cs, i) cslinks.append(link) return cslinks @@ -660,3 +691,22 @@ class ApiPageBuilder(AbstractPageBuilder): tbtag.append(H.div(*colored)) return tbtag + def is_hidden_from_nav(self, dotted_name): + obj = get_obj(self.dsa, self.pkg, dotted_name) + return getattr(obj, '__apigen_hide_from_nav__', False) + + def get_revision(self, dotted_name): + obj = get_obj(self.dsa, self.pkg, dotted_name) + try: + sourcefile = inspect.getsourcefile(obj) + except TypeError: + return None + if sourcefile is None: + return None + if sourcefile[-1] in ['o', 'c']: + sourcefile = sourcefile[:-1] + wc = py.path.svnwc(sourcefile) + if wc.check(versioned=True): + return wc.status().rev + return None + diff --git a/py/apigen/layout.py b/py/apigen/layout.py index ef9e012be..a786252cb 100644 --- a/py/apigen/layout.py +++ b/py/apigen/layout.py @@ -20,6 +20,7 @@ class LayoutPage(confrest.PyPage): self.nav = kwargs.pop('nav') self.relpath = kwargs.pop('relpath') super(LayoutPage, self).__init__(*args, **kwargs) + self.project.logo.attr.id = 'logo' def set_content(self, contentel): self.contentspace.append(contentel) diff --git a/py/apigen/style.css b/py/apigen/style.css index 71405c748..533b543cd 100644 --- a/py/apigen/style.css +++ b/py/apigen/style.css @@ -2,6 +2,11 @@ font-size: 0.8em; } +#logo { + position: relative; + position: fixed; +} + div.sidebar { font-family: Verdana, Helvetica, Arial, sans-serif; font-size: 0.9em; @@ -9,6 +14,7 @@ div.sidebar { vertical-align: top; margin-top: 0.5em; position: absolute; + position: fixed; top: 130px; left: 4px; bottom: 4px; @@ -34,6 +40,10 @@ ul li { list-style-type: none; } +h2 { + padding-top: 0.5em; +} + .code a { color: blue; font-weight: bold; @@ -42,6 +52,7 @@ ul li { .lineno { line-height: 1.4em; + height: 1.4em; text-align: right; color: #555; width: 3em; @@ -52,6 +63,7 @@ ul li { .code { line-height: 1.4em; + height: 1.4em; padding-left: 1em; white-space: pre; font-family: monospace, Monaco; diff --git a/py/apigen/testing/test_apigen_example.py b/py/apigen/testing/test_apigen_example.py index 6a02c39c8..af5851e8b 100644 --- a/py/apigen/testing/test_apigen_example.py +++ b/py/apigen/testing/test_apigen_example.py @@ -37,6 +37,9 @@ def setup_fs_project(): " get_somevar docstring " return self.somevar SomeInstance = SomeClass(10) + class SomeHiddenClass(object): + " docstring somehiddenclass " + __apigen_hide_from_nav__ = True # hide it from the navigation """)) temp.ensure('pkg/somesubclass.py').write(py.code.Source("""\ from someclass import SomeClass @@ -59,6 +62,7 @@ def setup_fs_project(): 'main.SomeInstance': ('./someclass.py', 'SomeInstance'), 'main.SomeSubClass': ('./somesubclass.py', 'SomeSubClass'), 'main.SomeSubClass': ('./somesubclass.py', 'SomeSubClass'), + 'main.SomeHiddenClass': ('./someclass.py', 'SomeHiddenClass'), 'other': ('./somenamespace.py', '*'), '_test': ('./somenamespace.py', '*'), }) @@ -105,8 +109,9 @@ class AbstractBuilderTest(object): 'main.SomeClass', 'main.SomeSubClass', 'main.SomeInstance', + 'main.SomeHiddenClass', 'other.foo', - 'other.bar', + 'other.baz', '_test']) self.namespace_tree = namespace_tree self.apb = ApiPageBuilder(base, linker, self.dsa, @@ -284,7 +289,8 @@ class TestApiPageBuilder(AbstractBuilderTest): self.apb.build_function_pages(['main.sub.func']) self.apb.build_class_pages(['main.SomeClass', 'main.SomeSubClass', - 'main.SomeInstance']) + 'main.SomeInstance', + 'main.SomeHiddenClass']) self.linker.replace_dirpath(self.base, False) html = self.base.join('api/main.sub.func.html').read() print html diff --git a/py/apigen/todo.txt b/py/apigen/todo.txt index 7f649f11f..d9a1c143b 100644 --- a/py/apigen/todo.txt +++ b/py/apigen/todo.txt @@ -3,6 +3,8 @@ special "__*__" methods should come last except for __init__ which comes first + DONE + * the page header should read: py.path.local API documentation [rev XYZ] @@ -11,10 +13,15 @@ api documentation for path.local + DONE, title changed and if possible (read: if source file in SVN) rev is + retrieved and added + * have the py/doc/ and apigen page layout have an api and source link in the menu bar (e.g.: home doc api source contact getting-started issue) + DONE + * function view: def __init__(self, rawcode): @@ -28,13 +35,21 @@ be "sticking" out (the show/hide info link IMO disrupts this and it's not visually clear it belongs to the function above it) + DONE, but please review if you like it like this... + * can it be avoided that py.execnet.Channel shows up as a primary object but still have it documented/linked from remote_exec()'s "return value"? + DONE: if you set an attribute __hide_from_nav__ to True on an + object somehow, it is hidden from the navigation + * class attributes are not "properties". can they get their section? + DONE: renamed title to 'class attributes and properties' + (as discussed) + * stacktraces: a lot are "duplicates" like: /home/hpk/py-trunk/py/test/rsession/hostmanage.py - line 37 @@ -45,6 +60,12 @@ i think we should by default strip out these duplicates, this would also reduce the generated html files, right? + DONE, although I'm not happy with it... I'd rather only display call sites + from calls in the test somehow or something... + * allow for flexibility regarding linking from py/doc/*.txt documents to apigen with respect to where apigen/ docs are located. + + LATER, as discussed + diff --git a/py/doc/confrest.py b/py/doc/confrest.py index 5de036ea6..94fb8b9ab 100644 --- a/py/doc/confrest.py +++ b/py/doc/confrest.py @@ -33,6 +33,10 @@ class Page(object): self.menubar = html.div( html.a("home", href="home.html", class_="menu"), " ", html.a("doc", href="index.html", class_="menu"), " ", + html.a("api", href="../../apigen/api/index.html", class_="menu"), + " ", + html.a("source", href="../../apigen/source/index.html", + class_="menu"), " ", html.a("contact", href="contact.html", class_="menu"), " ", html.a("getting-started", href="getting-started.html", class_="menu"), " ", id="menubar",