#! /usr/bin/env python2.2 # # Syntax Hilight a python source file using CSS. # Copyright (C) 2002 Michael Urman # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # """ Create an HTML summary of a python module. Unlike pydoc's output, this is "nice" CSS HTML4 code. """ __version__ = '0.2' __date__ = '2003/05/03' __author__ = 'Michael Urman (mu on irc.freenode.net)' from __future__ import generators import sys, types, os class Object: """Base class for the Object Hierarchy""" def __init__(self, obj, name=None): """__init_(self, obj,name=None) -> Object Initialize an Object with optional parameter name""" self._obj = obj self._name = name self._dir = dir(obj) self._dir.sort() def doc(self): """doc(self) -> docstring Return the object's docstring, or None if not available""" if hasattr(self._obj, '__doc__'): if type(self._obj) not in (type(4), type(0.0), type(''), type(()), type([]), type({})): doc = getattr(self._obj, '__doc__') return doc if doc is not None: return doc.split('\n')[0] def name(self): """name(self) -> str Return the object's name, either as specified, or stored""" if self._name is not None: return self._name elif hasattr(self._obj, '__name__'): return getattr(self._obj, '__name__') else: return None def __str__(self): """__str__(self) -> str Represent an object with its name (by default)""" if self._name is not None: return self._name else: try: return self._obj.__name__ except AttributeError: return '<%s>' % self.__class__ class Var(Object): "Represent variables" def val(self): return repr(self._obj) class Property(Object): "Represent properties" def val(self): o = self._obj fget = o.fget and MethodRef(o.fget) fset = o.fset and MethodRef(o.fset) fdel = o.fdel and MethodRef(o.fdel) doc = o.__doc__ if doc is not None: return 'property(fget=%s, fset=%s, fdel=%s, doc=%s)' % (fget, fset, fdel, doc) elif fdel is not None: return 'property(fget=%s, fset=%s, fdel=%s)' % (fget, fset, fdel) elif fset is not None: return 'property(fget=%s, fset=%s)' % (fget, fset) elif fget is not None: return 'property(fget=%s)' % (fget) else: return 'property()' class Arg(Var): "Represent arguments to a function" pass class DArg(Arg): "Represent arguments with a default value" def __init__(self, obj, name=None, default=None): """__init__(self, obj, name=None, default=None) -> DArg Initialize a DArg with default value""" Arg.__init__(self, obj, name) self._default = default def __str__(self): return self.name() + '=' + str(self._default) def default(self): """default(self) -> str Return a stringified version of the default value""" return str(self._default) class VArg(Arg): "Represent an argument grabbing list" def __str__(self): return '*' + self.name() class KWArg(Arg): "Represent an argument grabbing dictionary" def __str__(self): return '**' + self.name() class Function(Object): "Represent a function" def getcode(self): return self._obj.func_code def getdefaults(self): return self._obj.func_defaults def args(self): """args(self) -> list of Arg (and Arg derived) objects Return a list generator of function arguments""" code = self.getcode() argnames = code.co_varnames nargs = code.co_argcount flags = code.co_flags defaults = self.getdefaults() if defaults is None: ndef = 0 else: ndef = len(defaults) for arg, ind in zip(argnames[:nargs], range(nargs)): if ind >= nargs - ndef: yield DArg(self._obj, arg, defaults[ndef-nargs+ind]) else: yield Arg(self._obj, arg) if flags & 4: # has * argument yield VArg(self._obj, argnames[nargs]) if flags & 8: # has ** argument yield KWArg(self._obj, argnames[nargs+((flags&4)>>2)]) def __str__(self): return '%s(%s)' % (self.name(), ', '.join([str(arg) for arg in self.args()])) class Method(Function): "Represent a method" def __init__(self, obj, name=None, class_=None): """__init__(self, obj, name=None, class_=None) -> Method Initialize an Method with its class""" Function.__init__(self, obj, name) self._class = class_ def getcode(self): return self._obj.im_func.func_code def getdefaults(self): return self._obj.im_func.func_defaults def cls(self): """cls(self) -> Clas Return this method's Class""" return self._class class InheritedMethod(Method): "Represent a method inherited from a parent class" def __init__(self, obj, name=None, class_=None, from_=None): """__init__(self, obj, name=None, class_=None, from_=None) -> InheritedMethod Initialize an InheritedMethod with class from which it inherits""" Method.__init__(self, obj, name, class_) self._from = from_ def inherited(self): """inherited(self) -> str Return the name of the class from which the method is inherited""" return self._from.name() class MethodRef(Method): "Represent some sort of method reference, such as used in a property" def __str__(self): if hasattr(self._obj, '__name__'): return self._obj.__name__ elif isinstance(self._obj, classmethod): # this hack is really really gross. but i can't get the function # name without doing something wacky like this... class tmp(object): cm = self._obj cm = tmp.cm return 'classmethod(%s)' % cm.__name__ elif isinstance(self._obj, staticmethod): class tmp(object): sm = self._obj sm = tmp.sm return 'staticmethod(%s)' % sm.__name__ else: return '[...]' class Class(Object): "Represent a class" def vars(self): """vars(self) -> list of Var objects Return a list generator of class variables""" for name in self._dir: if not name.startswith('__'): var = getattr(self._obj, name) if not isinstance(var, (types.UnboundMethodType, property)): yield Var(var, name) def properties(self): """properties(self) -> list of property objects Return a list generator of properties of the class""" for name in self._dir: if not name.startswith('__'): var = getattr(self._obj, name) if isinstance(var, property): yield Property(var, name) def methods(self): """methods(self) -> list of Method objects Return a list generator of methods in the class""" for name in self._dir: #if not name.startswith('__'): meth = getattr(self._obj, name) if isinstance(meth, types.UnboundMethodType): # figure out if inherited fc = meth.im_func.func_code found = 0 curbase = self._obj while not found: for base in curbase.__bases__: try: if getattr(base, name).im_func.func_code == fc: curbase = base break except AttributeError: pass else: found = 1 if curbase == self._obj: yield Method(meth, class_=self.name()) else: yield InheritedMethod(meth, class_=self.name(), from_=Class(curbase)) def const(self): """const(self) -> Method Return the Method corresponding to __init__()""" if '__init__' in self._dir: meth = getattr(self._obj, '__init__') yield Method(meth, self.name(), class_=self.name()) def bases(self): """bases(self) -> list of Class objects Return a list generator of Class self's base classes""" for base in self._obj.__bases__: yield Class(base) #return [Class(b) for b in self._obj.__bases__] def subclasses(self): import sys module = Module(sys.modules[self._obj.__module__]) for cls in module.classes(): if self.name() in [c.name() for c in cls.bases()]: yield cls class Module(Object): "Represent a module" def modules(self): """modules(self) -> list of Module objects Return a list of modules defined (imported into) the module""" for name in self._dir: if not name.startswith('__'): mod = getattr(self._obj, name) if isinstance(mod, types.ModuleType): yield Module(mod) def classes(self): """classes(self) -> list of Class objects Return a list generator of all classes in the module""" for name in self._dir: cls = getattr(self._obj, name) if isinstance(cls, (types.ClassType, types.TypeType)): yield Class(cls) def functions(self): """functions(self) -> list of Function objects Return a list generator of all module level functions""" for name in self._dir: func = getattr(self._obj, name) if isinstance(func, types.FunctionType): yield Function(func) def vars(self): """vars(self) -> list of Var objects Return a list generator of module level variables""" for name in self._dir: if not name.startswith('__'): var = getattr(self._obj, name) if not (isinstance(var, (types.UnboundMethodType, types.ModuleType, types.FunctionType, types.ClassType, types.TypeType))): yield Var(var, name) def baseclasses(self): """baseclasses(self) -> list of Class objects Return a list generator of base classes; classes which have no parent classes in the module""" for cls in self.classes(): if genlen(cls.bases()) == 0: yield cls def author(self): """Return module.__author__ or None""" if '__author__' in self._dir: return getattr(self._obj, '__author__') def date(self): """Return module.__date__ or None""" if '__date__' in self._dir: return getattr(self._obj, '__date__') def version(self): """Return module.__version__ or None""" if '__version__' in self._dir: return getattr(self._obj, '__version__') class Attribute: "Handle CSS attributes somewhat python style" def __init__(self, sel, bg=None, fg=None, **kw): self.sel = sel for k, v in kw.items()[:]: if k.find('_') >= 0: newk = '-'.join(k.split('_')) del kw[k] kw[newk] = v if bg: kw['background-color'] = bg if fg: kw['color'] = fg self.styledict = kw def __str__(self): return ' '.join([self.sel, '{']+[ '%s: %s;'%(k,v) for k,v in self.styledict.items()]+['}']) + '\n' def __repr__(self): return "<Attribute sel='%s'>" % self.sel attributes = [ Attribute('body', '#f8f8f8', '#000'), Attribute('a', text_decoration='None'), Attribute('a:hover', text_decoration='underline'), Attribute('.doc', font_family='monospace', padding_left='20px'), Attribute('.list', font_size='x-small', padding_left='20px', word_spacing='1em'), Attribute('.head > .info', font_size='small'), #Attribute('.super'), #Attribute('.inherit'), Attribute('.module > .head', 'green', 'white', padding='5px', padding_top='25px', margin_bottom='5px', font_weight='bold', font_size='xx-large'), Attribute('.module > .doc', white_space='pre'), Attribute('.module > .info', padding_left='20px', margin_top='10px'), Attribute('.usemodules', border_left='solid 30px blue', border_top='solid 30px blue', padding_left='5px', margin_top='5px', margin_bottom='5px'), Attribute('.usemodules > .head', 'blue', 'white', font_weight='bold', font_size='x-large', padding_top='0px', padding_bottom='5px', margin_left='-15px', margin_bottom='5px'), Attribute('.classes', border_left='solid 30px red', border_top='solid 30px red', padding_left='5px', margin_top='5px', margin_bottom='5px'), Attribute('.classtree', margin_left='10px'), Attribute('.subclasstree', margin_left='20px'), Attribute('a.tree', font_weight='bold', display='block'), Attribute('.classtree > .tree:before', content='"* "'), Attribute('.subclasstree > .tree:before', content='"+ "'), Attribute('.classes > .head', 'red', 'white', font_weight='bold', font_size='x-large', padding_top='0px', padding_bottom='5px', margin_left='-15px', margin_bottom='5px'), Attribute('.class', border_left='solid 20px pink', border_top='solid 20px pink', padding_left='5px', margin_top='5px', margin_bottom='5px'), Attribute('.class > .head', 'pink', 'black', font_weight='bold', font_size='large', padding_top='0px', padding_bottom='5px', margin_left='-10px', margin_bottom='5px'), Attribute('.class > .doc', bg='#bbf', padding='3px 20px', font_weight='bold'), Attribute('.vars', border_left='solid 10px #ff8', border_top='solid 10px #ff8', padding_left='5px', margin_top='5px', margin_bottom='5px'), Attribute('.vars > .head', '#ff8', font_weight='bold', font_size='large', padding_top='0px', padding_bottom='5px', margin_left='-5px', margin_bottom='5px'), #Attribute('.varinfo'), Attribute('.varinfo > .var', font_weight='bold'), Attribute('.varinfo > .val', font_family='monospace'), Attribute('.methods', border_left='solid 10px #ff8', border_top='solid 10px #ff8', padding_left='5px', margin_top='5px', margin_bottom='5px'), Attribute('.methods > .head', '#ff8', font_weight='bold', font_size='large', padding_top='0px', padding_bottom='5px', margin_left='-5px', margin_bottom='5px'), Attribute('.method, .function', margin_top='5px', margin_bottom='5px', border_style='solid', border_color='black', border_width='2px 1px 1px 2px', padding='3px'), #Attribute('.method > .head', margin_bottom='3px'), Attribute('.method > .head + .doc, .function > .head + .doc', margin_top='3px'), Attribute('.method > .doc, .function > .doc', fg='maroon', border_style='solid', border_color='grey', border_width='1px 0px', padding='2px 2px', margin='0px 20px'), Attribute('.funcname', font_weight='bold'), Attribute('.argname', font_family='monospace'), Attribute('.argdef', fg='#888', font_style='italic'), Attribute('.functions', border_left='solid 30px brown', border_top='solid 30px brown', padding_left='5px', margin_top='5px', margin_bottom='5px'), Attribute('.functions > .head', 'brown', 'white', font_weight='bold', font_size='x-large', padding_top='0px', padding_bottom='5px', margin_left='-15px', margin_bottom='5px'), #Attribute('.function > .head'), Attribute('.data', border_left='solid 30px purple', border_top='solid 30px purple', padding_left='5px', margin_top='5px', margin_bottom='5px'), Attribute('.data > .head', 'purple', 'white', font_weight='bold', font_size='x-large', padding_top='0px', padding_bottom='5px', margin_left='-15px', margin_bottom='5px'), Attribute('.datum', margin='5px 5px 5px 20px', padding='3px'), #Attribute('.datum:first-line', text_indent='-15px'), Attribute('.datum > .var', font_weight='bold', margin_left='-20px'), Attribute('.datum > .val', font_family='monospace'), ] def writecss(dst): for attr in attributes: dst.write(str(attr)) def esc(s, nl='br'): r"""escape a string for html safety. nl can be one of the following: 'br': use <br /> tags to replace \n 'p': use </p><p> tags to replace \n and wrap in a <p>...</p> 'rbr': usr <br /><br /> tags to replace \n\s*\n 'rp: use </p><p> tags to replace \n\s*\n and wrap in a <p>...</p> """ if s is None: return '' elif not isinstance(s, type('')): s = str(s) s = '&'.join(s.split('&')) s = '<'.join(s.split('<')) s = '>'.join(s.split('>')) s = '"'.join(s.split('"')) if nl == 'br': s = '<br>'.join(s.split('\n')) elif nl == 'p': s = '<p>' + '</p><p>'.join(s.split('\n')) + '</p>' elif nl == 'rbr': import re s = re.sub('\n\\s+\n', '\n\n', s) s = '<br><br>'.join(s.split('\n\n')) elif nl == 'rp': import re s = re.sub('\\s+\n', '\n', s) s = '<p>' + '</p><p>'.join(s.split('\n\n')) + '</p>' return s indentstr = ' ' def writefuncs(out, title, indent=0, css=('functions', 'function'), *funcs): count = 0 allfuncs = [] for gen in funcs: for func in gen: allfuncs.append(func) if len(allfuncs) == 0: return strdict = { 'i0': indentstr * indent, 'i1': indentstr * (indent+1), 'i2': indentstr * (indent+2), 'i3': indentstr * (indent+3), 'functions': css[0], 'function': css[1], 'title': title, } # \functions out('\n%(i0)s<div class="%(functions)s">' % strdict) out('\n%(i1)s<div class="head">%(title)s</div>' % strdict) if isinstance(allfuncs[0], Method): out('\n%(i1)s<div class="list">' % strdict) for func in allfuncs: strdict['cls'] = esc(func.cls()) strdict['name'] = esc(func.name()) out('\n%(i2)s<a href="#method-%(cls)s-%(name)s">%(name)s</a>' % strdict) out('\n%(i1)s</div>' % strdict) elif isinstance(allfuncs[0], Function): out('\n%(i1)s<div class="list">' % strdict) for func in allfuncs: strdict['name'] = esc(func.name()) out('\n%(i1)s<a href="#function-%(name)s">%(name)s</a>' % strdict) out('\n%(i1)s</div>' % strdict) for func in allfuncs: strdict['name'] = esc(func.name()) # \function out('\n%(i1)s<div class="%(function)s">' % strdict) if isinstance(func, Method): out('\n%(i2)s<a name="method-%(cls)s-%(name)s"></a>' % strdict) elif isinstance(func, Function): out('\n%(i2)s<a name="function-%(name)s"></a>' % strdict) out('\n%(i2)s<div class="head">' % strdict) out('<span class="funcname">%(name)s</span>(' % strdict) sep = '' for arg in func.args(): out(sep) if isinstance(arg, VArg): out('<span class="argdef">*</span>') elif isinstance(arg, KWArg): out('<span class="argdef">**</span>') out('<span class="argname">%s</span>' % esc(arg.name())) sep = ', ' if isinstance(arg, DArg): out('<span class="argdef">=%s</span>' % esc(arg.default())) if isinstance(func, InheritedMethod): super = { 'class': esc(func.inherited()), 'method': esc(func.name()) } out(') from <a class="inherit" href=') out('"#method-%(class)s-%(method)s">%(class)s</a></div>' % super) else: out(')</div>') if func.doc(): strdict['doc'] = esc(func.doc(), nl='rbr') out('\n%(i2)s<div class="doc">%(doc)s</div>' % strdict) # /function out('\n%(i1)s</div>' % strdict) # /functions out('\n%(i0)s</div>' % strdict) def writevars(out, title, indent=0, css=('data', 'datum'), *vars): allvars = [] for gen in vars: for var in gen: allvars.append(var) if len(allvars) == 0: return strdict = { 'i0': indentstr * indent, 'i1': indentstr * (indent+1), 'i2': indentstr * (indent+2), 'i3': indentstr * (indent+3), 'vars': css[0], 'var': css[1], 'title': title, } # \vars out('\n%(i0)s<div class="%(vars)s">' % strdict) out('\n%(i1)s<div class="head">%(title)s</div>' % strdict) for var in allvars: out('\n%(i2)s<div class="%(var)s">' % strdict) out('<span class="var">%s</span>' % esc(var.name())) out(' = <span class="val">%s</span>' % esc(var.val())) if var.doc(): out(' <span class=".doc">%s</span>' % esc(var.doc())) out('</div>') #if count == 0: # out('\n <div class="datum">None</div>') # /vars out('\n</div>') def writehtml(module, dst, css, title='Syntax Hilight'): out = dst.write out('<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"') out('\n "http://www.w3.org/TR/html4/strict.dtd">') out('\n<html>') out('\n <head>''') out('\n <title>%s</title>' % title) if css: out('\n <link rel="stylesheet" href="%s" />' % css) else: out('\n <style type="text/css"><!--') writecss(dst) out('\n --></style>') out('\n </head>') out('\n <body>') # Module name out('\n<div class="module">') out('\n <div class="head">%s <span class="info">%s %s</span></div>' % (esc(module.name()), esc(module.version()), esc(module.date()))) # Module __doc__ if module.doc(): out('\n <div class="doc">' + esc(module.doc()) + '</div>') authors = module.author() if isinstance(authors, type('')): out('\n <div class="info">Written by %s</div>' % esc(authors)) elif isinstance(authors, type(())) or isinstance(authors, type([])): if len(authors) == 1: out('\n <div class="author">Author: %s</div>' % esc(authors[0])) else: out('\n <div class="author">Authors:') for author in authors: out(' %s' % esc(author)) out('</div>') out('\n</div>') # Used modules if genlen(module.modules()): out('\n<div class="usemodules">') out('\n <div class="head">Uses Modules</div>') for mod in module.modules(): out('\n %s' % esc(mod.name())) out('\n</div>') if genlen(module.classes()): # Classes out('\n<div class="classes">') out('\n <div class="head">Classes</div>') # list them #out('\n <div class="list">') #for cls in module.classes(): # name = esc(cls.name()) # out('\n <a href="#class-%s">%s</a>' % (name, name)) #out('\n </div>') # show a tree queue = [] queue.extend(module.baseclasses()) out('\n <div class="classtree">') while len(queue): cur = queue.pop(0) if cur == 'pop': out('\n </div>') else: name = esc(cur.name()) out('\n <a class="tree" href="#class-%s">%s</a>' % (name,name)) subs = [] subs.extend(cur.subclasses()) if len(subs): out('\n <div class="subclasstree">') subs.append('pop') subs.extend(queue) queue = subs out('\n </div>') # display actual class info for cls in module.classes(): name = esc(cls.name()) bases = ', '.join(['<a class="super" href="#class-%s">%s</a>' % (esc(b.name()), esc(b.name())) for b in cls.bases()]) if len(bases): bases = '(%s)' % bases out('\n <div class="class"><a name="class-%s"></a>' % name) out('\n <div class="head">class ') out('<span class="name">%s</span>%s</div>' % (name, bases)) if cls.doc(): out('\n <div class="doc">%s</div>' % esc(cls.doc(), nl='rbr')) # Class Variables writevars(out, 'Variables', 2, ('vars', 'varinfo'), cls.vars()) writevars(out, 'Properties', 2, ('vars', 'varinfo'), cls.properties()) # Methods writefuncs(out, 'Methods', 2, ('methods','method'), cls.methods()) # /class out('\n </div>') # /classes out('''\n</div>''') if genlen(module.functions()): writefuncs(out, 'Functions', 0, ('functions','function'), module.functions()) if genlen(module.vars()): writevars(out, 'Global Data', 0, ('data', 'datum'), module.vars()) out('\n</div>') out('\n </body>') out('\n</html>\n') dst.flush() def genlen(gen): """genlen(gen) -> bool Return whether generator has any elements by testing for StopIteration""" try: x = gen.next() except StopIteration: return 0 return 1 def writetext(module, dst): print 'MODULE', module.name() doc = module.doc() if doc: print '+DOC', doc print ' USEMOD', for mod in module.modules(): print mod.name(), print for cls in module.classes(): print ' CLASS', cls.name() doc = cls.doc() if doc: print ' +DOC', doc for var in cls.vars(): print ' VAR', var.name(), '=', var.val() for const in cls.const(): print ' CONST', const.name() doc = const.doc() if doc: print ' +DOC', doc for meth in cls.methods(): print ' METH', meth doc = meth.doc() if doc: print ' +DOC', doc for func in module.functions(): print ' FUNC', func doc = func.doc() if doc: print ' +DOC', doc for var in module.vars(): print ' VAR', var.name(), '=', var.val() def main(): sys.path.insert(0, os.getcwd()) if len(sys.argv) == 2: src = __import__(sys.argv[1]) dst = sys.stdout css = None elif len(sys.argv) == 3: src = __import__(sys.argv[1]) if sys.argv[2] == '-': dst = sys.stdout css = None else: css = os.path.join(os.path.split(sys.argv[2])[0], 'python.css') dst = open(css, 'w', 1) writecss(dst) dst.close() css = 'python.css' dst = open(sys.argv[2], 'w', 1) module = Module(src) writehtml(module, dst, css, 'Module %s Documentation' % os.path.basename(sys.argv[1])) if __name__ == '__main__': main()