diff --git a/py/apigen/html.py b/py/apigen/html.py index 9149e9dfc..0d7ef9bbf 100644 --- a/py/apigen/html.py +++ b/py/apigen/html.py @@ -61,6 +61,7 @@ class H(html): infoid = 'info_%s' % (localname.replace('.', '_dot_'),) docstringid = 'docstring_%s' % (localname.replace('.', '_dot_'),) fd = H.FunctionDef(localname, argdesc, + title='click to view details', onclick=('showhideel(' 'document.getElementById("%s")); ' % (infoid,))) diff --git a/py/apigen/htmlgen.py b/py/apigen/htmlgen.py index af51eaf1e..72aa7fb54 100644 --- a/py/apigen/htmlgen.py +++ b/py/apigen/htmlgen.py @@ -18,6 +18,25 @@ raw = py.xml.raw REDUCE_CALLSITES = True +def find_method_origin(meth): + cls = getattr(meth, 'im_class', None) + if cls is None: + return None # XXX unknown origin (built-in function or method or sth) + name = meth.im_func.func_name + origin = cls + # XXX old-style classes support required? :| + mro = inspect.getmro(cls) + for base in mro: + m = getattr(base, name, None) + if m is None: + continue + if not hasattr(m, 'im_func'): + # builtin + return None + if m.im_func is meth.im_func: + origin = base + return origin + def is_navigateable(name): return (not is_private(name) and name != '__doc__') @@ -135,10 +154,17 @@ def enumerate_and_color(codelines, firstlineno, enc): source_html.prepare_line([line], tokenizer, enc)) except py.error.ENOENT: # error reading source code, giving up - snippet = org + snippet = codelines break return snippet +def enumerate_and_color_module(path, enc): + snippet = H.SourceBlock() + tokenizer = source_color.Tokenizer(source_color.PythonSchema) + for i, text in enumerate(source_html.prepare_module(path, tokenizer, enc)): + snippet.add_line(i + 1, text) + return snippet + _get_obj_cache = {} def get_obj(dsa, pkg, dotted_name): full_dotted_name = '%s.%s' % (pkg.__name__, dotted_name) @@ -258,9 +284,16 @@ class SourcePageBuilder(AbstractPageBuilder): # XXX two reads of the same file here... not very bad (disk caches # and such) but also not very nice... enc = source_html.get_module_encoding(fspath.strpath) - source = fspath.read() - sep = get_linesep(source) - colored = [enumerate_and_color(source.split(sep), 0, enc)] + try: + colored = [enumerate_and_color_module(fspath, enc)] + except (KeyboardInterrupt, SystemExit): + raise + except Exception, e: + #self.capture.err.writeorg('\ncompilation exception: %s\n' % (e,)) + # problem building HTML with anchors; let's try without... + source = fspath.read() + sep = get_linesep(source) + colored = [enumerate_and_color(source.split(sep), 0, enc)] tag = H.PythonSource(colored) nav = self.build_navigation(fspath) return tag, nav @@ -316,7 +349,7 @@ class SourcePageBuilder(AbstractPageBuilder): if fspath.check(ext='.py'): try: tag, nav = self.build_python_page(fspath) - except (KeyboardInterrupt, SystemError): + except (KeyboardInterrupt, SystemExit): raise except: # XXX strange stuff going wrong at times... need to fix raise @@ -398,8 +431,8 @@ class ApiPageBuilder(AbstractPageBuilder): relpath = get_rel_sourcepath(self.projroot, sourcefile, sourcefile) text = 'source: %s' % (relpath,) if is_in_pkg: - href = self.linker.get_lazyhref(sourcefile) - + href = self.linker.get_lazyhref(sourcefile, + self.get_anchor(func)) csource = H.SourceSnippet(text, href, colored) cslinks = self.build_callsites(dotted_name) snippet = H.FunctionDescription(localname, argdesc, docstring, @@ -432,7 +465,8 @@ class ApiPageBuilder(AbstractPageBuilder): if sourcefile[-1] in ['o', 'c']: sourcefile = sourcefile[:-1] sourcelink = H.div(H.a('view source', - href=self.linker.get_lazyhref(sourcefile))) + href=self.linker.get_lazyhref(sourcefile, + self.get_anchor(cls)))) snippet = H.ClassDescription( # XXX bases HTML @@ -651,6 +685,10 @@ class ApiPageBuilder(AbstractPageBuilder): return lst name, _desc_type, is_degenerated = data if not is_degenerated: + try: + obj = self.dsa.get_obj(name) + except KeyError: + obj = None linktarget = self.linker.get_lazyhref(name) lst.append(H.a(str(_type), href=linktarget)) else: @@ -695,6 +733,7 @@ class ApiPageBuilder(AbstractPageBuilder): _reg_source = py.std.re.compile(r'([^>]*)<(.*)>') def gen_traceback(self, dotted_name, call_site): tbtag = H.CallStackDescription() + obj = self.dsa.get_obj(dotted_name) for frame in call_site: lineno = frame.lineno - frame.firstlineno source = frame.source @@ -772,3 +811,20 @@ class ApiPageBuilder(AbstractPageBuilder): self._revcache[dotted_name] = rev return rev + def get_anchor(self, obj): + # XXX may not always return the right results... + anchor = None + if hasattr(obj, 'im_func'): + # method + origin = find_method_origin(obj) + if origin: + anchor = '%s.%s' % (origin.__name__, + obj.im_func.func_name) + elif hasattr(obj, 'func_name'): + anchor = obj.func_name + elif hasattr(obj, '__name__'): + anchor = obj.__name__ + elif hasattr(obj, '__class__'): + anchor = obj.__class__.__name__ + return anchor + diff --git a/py/apigen/linker.py b/py/apigen/linker.py index 9655467e5..e8a80b533 100644 --- a/py/apigen/linker.py +++ b/py/apigen/linker.py @@ -63,8 +63,11 @@ class TempLinker(object): def __init__(self): self._linkid2target = {} - def get_lazyhref(self, linkid): - return '%s://%s' % (TEMPLINK_PROTO, linkid) + def get_lazyhref(self, linkid, anchor=None): + href = '%s://%s' % (TEMPLINK_PROTO, linkid) + if anchor: + href += '#' + anchor + return href def set_link(self, linkid, target): assert linkid not in self._linkid2target @@ -72,13 +75,18 @@ class TempLinker(object): def get_target(self, tempurl, fromlocation=None): assert tempurl.startswith('%s://' % (TEMPLINK_PROTO,)) - linkid = '://'.join(tempurl.split('://')[1:]) + anchor = None + if '#' in tempurl: + tempurl, anchor = tempurl.split('#', 1) + linkid = tempurl.split('://', 1)[1] linktarget = self._linkid2target[linkid] if fromlocation is not None: linktarget = relpath(fromlocation, linktarget) + if anchor is not None: + linktarget += '#' + anchor return linktarget - _reg_tempurl = py.std.re.compile('["\'](%s:\/\/[^"\s]*)["\']' % ( + _reg_tempurl = py.std.re.compile('(["\'])(%s:\/\/[^"\'\s#]*)(["\'#])' % ( TEMPLINK_PROTO,)) def replace_dirpath(self, dirpath, stoponerrors=True): """ replace temporary links in all html files in dirpath and below """ @@ -88,16 +96,19 @@ class TempLinker(object): match = self._reg_tempurl.search(html) if not match: break - tempurl = match.group(1) + tempurl = match.group(2) + pre = match.group(1) + post = match.group(3) try: - html = html.replace('"' + tempurl + '"', - '"' + self.get_target(tempurl, - fpath.relto(dirpath)) + '"') + html = html.replace(match.group(0), pre + + self.get_target(tempurl, + fpath.relto(dirpath)) + post) except KeyError: if stoponerrors: raise - html = html.replace('"' + tempurl + '"', - '"apigen.notfound://%s"' % (tempurl,)) + html = html.replace(match.group(0), pre + + 'apigen.notfound://%s' % (tempurl,) + + post) fpath.write(html) diff --git a/py/apigen/source/html.py b/py/apigen/source/html.py index dbcd7cbc6..a63ac7682 100644 --- a/py/apigen/source/html.py +++ b/py/apigen/source/html.py @@ -7,6 +7,10 @@ from py.xml import html, raw from compiler import ast import time from py.__.apigen.source.color import Tokenizer, PythonSchema +from py.__.apigen.source.browser import parse_path + +class CompilationException(Exception): + """ raised when something goes wrong while importing a module """ class HtmlEnchanter(object): def __init__(self, mod): @@ -59,6 +63,30 @@ def prepare_line(text, tokenizer, encoding): ret.append(item) return ret +def prepare_module(path, tokenizer, encoding): + path = py.path.local(path) + try: + mod = parse_path(path) + except: + # XXX don't try to catch SystemExit: it's actually raised by one + # of the modules in the py lib on import :( + exc, e, tb = py.std.sys.exc_info() + del tb + raise CompilationException('while compiling %s: %s - %s' % ( + path, e.__class__.__name__, e)) + lines = [unicode(l, encoding) for l in path.readlines()] + + enchanter = HtmlEnchanter(mod) + ret = [] + for i, line in enumerate(lines): + text = enchanter.enchant_row(i + 1, line) + if text == ['']: + text = [raw(' ')] + else: + text = prepare_line(text, tokenizer, encoding) + ret.append(text) + return ret + class HTMLDocument(object): def __init__(self, encoding, tokenizer=None): self.encoding = encoding diff --git a/py/apigen/testing/test_apigen_example.py b/py/apigen/testing/test_apigen_example.py index bea6c247a..7c692cd79 100644 --- a/py/apigen/testing/test_apigen_example.py +++ b/py/apigen/testing/test_apigen_example.py @@ -198,12 +198,10 @@ class TestApiPageBuilder(AbstractBuilderTest): self.apb.build_class_pages(['main.SomeSubClass', 'main.SomeClass']) self.apb.build_namespace_pages() - # fake some stuff that would be built from other methods self.linker.replace_dirpath(self.base, False) clsfile = self.base.join('api/main.SomeClass.html') assert clsfile.check() html = clsfile.read() - print html run_string_sequence_test(html, [ 'href="../style.css"', 'href="../apigen_style.css"', @@ -237,7 +235,8 @@ class TestApiPageBuilder(AbstractBuilderTest): self.spb.build_pages(self.fs_root) self.linker.replace_dirpath(self.base, False) funchtml = self.base.join('api/main.SomeClass.html').read() - assert funchtml.find('href="../source/pkg/someclass.py.html"') > -1 + print funchtml + assert funchtml.find('href="../source/pkg/someclass.py.html#SomeClass"') > -1 _checkhtml(funchtml) def test_build_namespace_pages(self): @@ -433,7 +432,8 @@ class TestSourcePageBuilder(AbstractBuilderTest): assert funcsource.check(file=True) html = funcsource.read() print html - assert ('def func(arg1)') in html + assert ('def ' + 'func(arg1)') in html def test_build_navigation_root(self): self.spb.build_pages(self.fs_root) diff --git a/py/apigen/testing/test_htmlgen.py b/py/apigen/testing/test_htmlgen.py index 9ca27652c..fc964c42c 100644 --- a/py/apigen/testing/test_htmlgen.py +++ b/py/apigen/testing/test_htmlgen.py @@ -165,3 +165,30 @@ def test_get_rel_sourcepath(): assert (htmlgen.get_rel_sourcepath(projpath, py.path.local('')) is None) +def test_find_method_origin(): + class Foo(object): + def foo(self): + pass + class Bar(Foo): + def bar(self): + pass + class Baz(Bar): + pass + assert htmlgen.find_method_origin(Baz.bar) is Bar + assert htmlgen.find_method_origin(Baz.foo) is Foo + assert htmlgen.find_method_origin(Bar.bar) is Bar + assert htmlgen.find_method_origin(Baz.__init__) is None + +def test_find_method_origin_old_style(): + class Foo: + def foo(self): + pass + class Bar(Foo): + def bar(self): + pass + class Baz(Bar): + pass + assert htmlgen.find_method_origin(Baz.bar) is Bar + assert htmlgen.find_method_origin(Baz.foo) is Foo + assert htmlgen.find_method_origin(Bar.bar) is Bar + diff --git a/py/apigen/testing/test_linker.py b/py/apigen/testing/test_linker.py index 3522841e9..6b345dff6 100644 --- a/py/apigen/testing/test_linker.py +++ b/py/apigen/testing/test_linker.py @@ -47,6 +47,13 @@ class TestTempLinker(object): l.replace_dirpath(temp) assert bar.read() == 'baz' + def test_with_anchor(self): + linker = TempLinker() + temphref = linker.get_lazyhref('py.path.local', 'LocalPath.join') + linker.set_link('py.path.local', 'py/path/local.html') + relpath = linker.get_target(temphref) + assert relpath == 'py/path/local.html#LocalPath.join' + def gen_check(frompath, topath, sep, expected): result = relpath(frompath, topath, sep=sep) assert result == expected