diff options
author | Ka-Ping Yee <ping@zesty.ca> | 2001-02-27 14:43:46 (GMT) |
---|---|---|
committer | Ka-Ping Yee <ping@zesty.ca> | 2001-02-27 14:43:46 (GMT) |
commit | dd1753434a145a2a73abf6f733daba76a6b4a862 (patch) | |
tree | 28f498eb000c7c20b7fc950763d209532d9f306c | |
parent | 6397c7c9a96de6a253da0210239a6fdeafe42a18 (diff) | |
download | cpython-dd1753434a145a2a73abf6f733daba76a6b4a862.zip cpython-dd1753434a145a2a73abf6f733daba76a6b4a862.tar.gz cpython-dd1753434a145a2a73abf6f733daba76a6b4a862.tar.bz2 |
pydoc: text and HTML documentation generator for interactive use
-rwxr-xr-x | Lib/pydoc.py | 1158 |
1 files changed, 1158 insertions, 0 deletions
diff --git a/Lib/pydoc.py b/Lib/pydoc.py new file mode 100755 index 0000000..45e0966 --- /dev/null +++ b/Lib/pydoc.py @@ -0,0 +1,1158 @@ +#!/usr/bin/env python +"""Generate Python documentation in HTML or as text for interactive use. + +At the shell command line outside of Python, run "pydoc <name>" to show +documentation on something. <name> may be the name of a Python function, +module, package, or a dotted reference to a class or function within a +module or module in a package. Alternatively, the argument can be the +path to a Python source file. + +Or, at the shell prompt, run "pydoc -k <keyword>" to search for a keyword +in the one-line descriptions of modules. + +Or, at the shell prompt, run "pydoc -p <port>" to start an HTTP server +on a given port on the local machine to generate documentation web pages. + +Or, at the shell prompt, run "pydoc -w <name>" to write out the HTML +documentation for a module to a file named "<name>.html". + +In the Python interpreter, do "from pydoc import help" to provide online +help. Calling help(thing) on a Python object documents the object.""" + +__author__ = "Ka-Ping Yee <ping@lfw.org>" +__version__ = "26 February 2001" + +import sys, imp, os, stat, re, types, inspect +from repr import Repr +from string import expandtabs, find, join, lower, split, strip, rstrip + +# --------------------------------------------------------- common routines + +def synopsis(filename, cache={}): + """Get the one-line summary out of a module file.""" + mtime = os.stat(filename)[stat.ST_MTIME] + lastupdate, result = cache.get(filename, (0, None)) + if lastupdate < mtime: + file = open(filename) + line = file.readline() + while line[:1] == '#' or strip(line) == '': + line = file.readline() + if not line: break + if line[-2:] == '\\\n': + line = line[:-2] + file.readline() + line = strip(line) + if line[:3] == '"""': + line = line[3:] + while strip(line) == '': + line = file.readline() + if not line: break + result = split(line, '"""')[0] + else: result = None + file.close() + cache[filename] = (mtime, result) + return result + +def index(dir): + """Return a list of (module-name, synopsis) pairs for a directory tree.""" + results = [] + for entry in os.listdir(dir): + path = os.path.join(dir, entry) + if ispackage(path): + results.extend(map( + lambda (m, s), pkg=entry: (pkg + '.' + m, s), index(path))) + elif os.path.isfile(path) and entry[-3:] == '.py': + results.append((entry[:-3], synopsis(path))) + return results + +def pathdirs(): + """Convert sys.path into a list of absolute, existing, unique paths.""" + dirs = [] + for dir in sys.path: + dir = os.path.abspath(dir or '.') + if dir not in dirs and os.path.isdir(dir): + dirs.append(dir) + return dirs + +def getdoc(object): + """Get the doc string or comments for an object.""" + result = inspect.getdoc(object) + if not result: + try: result = inspect.getcomments(object) + except: pass + return result and rstrip(result) or '' + +def classname(object, modname): + """Get a class name and qualify it with a module name if necessary.""" + name = object.__name__ + if object.__module__ != modname: + name = object.__module__ + '.' + name + return name + +def isconstant(object): + """Check if an object is of a type that probably means it's a constant.""" + return type(object) in [ + types.FloatType, types.IntType, types.ListType, types.LongType, + types.StringType, types.TupleType, types.TypeType, + hasattr(types, 'UnicodeType') and types.UnicodeType or 0] + +def replace(text, *pairs): + """Do a series of global replacements on a string.""" + for old, new in pairs: + text = join(split(text, old), new) + return text + +def cram(text, maxlen): + """Omit part of a string if needed to make it fit in a maximum length.""" + if len(text) > maxlen: + pre = max(0, (maxlen-3)/2) + post = max(0, maxlen-3-pre) + return text[:pre] + '...' + text[len(text)-post:] + return text + +def cleanid(text): + """Remove the hexadecimal id from a Python object representation.""" + return re.sub(' at 0x[0-9a-f]{5,}>$', '>', text) + +def modulename(path): + """Return the Python module name for a given path, or None.""" + filename = os.path.basename(path) + if lower(filename[-3:]) == '.py': + return filename[:-3] + elif lower(filename[-4:]) == '.pyc': + return filename[:-4] + elif lower(filename[-11:]) == 'module.so': + return filename[:-11] + elif lower(filename[-13:]) == 'module.so.1': + return filename[:-13] + +class DocImportError(Exception): + """Class for errors while trying to import something to document it.""" + def __init__(self, filename, etype, evalue): + self.filename = filename + self.etype = etype + self.evalue = evalue + if type(etype) is types.ClassType: + etype = etype.__name__ + self.args = '%s: %s' % (etype, evalue) + +def importfile(path): + """Import a Python source file or compiled file given its path.""" + magic = imp.get_magic() + file = open(path, 'r') + if file.read(len(magic)) == magic: + kind = imp.PY_COMPILED + else: + kind = imp.PY_SOURCE + file.close() + filename = os.path.basename(path) + name, ext = os.path.splitext(filename) + file = open(path, 'r') + try: + module = imp.load_module(name, file, path, (ext, 'r', kind)) + except: + raise DocImportError(path, sys.exc_type, sys.exc_value) + file.close() + return module + +def ispackage(path): + """Guess whether a path refers to a package directory.""" + if os.path.isdir(path): + init = os.path.join(path, '__init__.py') + initc = os.path.join(path, '__init__.pyc') + if os.path.isfile(init) or os.path.isfile(initc): + return 1 + +# ---------------------------------------------------- formatter base class + +class Doc: + def document(self, object, *args): + """Generate documentation for an object.""" + args = (object,) + args + if inspect.ismodule(object): return apply(self.docmodule, args) + if inspect.isclass(object): return apply(self.docclass, args) + if inspect.ismethod(object): return apply(self.docmethod, args) + if inspect.isbuiltin(object): return apply(self.docbuiltin, args) + if inspect.isfunction(object): return apply(self.docfunction, args) + raise TypeError, "don't know how to document objects of type " + \ + type(object).__name__ + +# -------------------------------------------- HTML documentation generator + +class HTMLRepr(Repr): + """Class for safely making an HTML representation of a Python object.""" + def __init__(self): + Repr.__init__(self) + self.maxlist = self.maxtuple = self.maxdict = 10 + self.maxstring = self.maxother = 50 + + def escape(self, text): + return replace(text, ('&', '&'), ('<', '<'), ('>', '>')) + + def repr(self, object): + result = Repr.repr(self, object) + return result + + def repr1(self, x, level): + methodname = 'repr_' + join(split(type(x).__name__), '_') + if hasattr(self, methodname): + return getattr(self, methodname)(x, level) + else: + return self.escape(cram(cleanid(repr(x)), self.maxother)) + + def repr_string(self, x, level): + text = self.escape(cram(x, self.maxstring)) + return re.sub(r'((\\[\\abfnrtv]|\\x..|\\u....)+)', + r'<font color="#c040c0">\1</font>', repr(text)) + + def repr_instance(self, x, level): + try: + return cram(cleanid(repr(x)), self.maxstring) + except: + return self.escape('<%s instance>' % x.__class__.__name__) + + repr_unicode = repr_string + +class HTMLDoc(Doc): + """Formatter class for HTML documentation.""" + + # ------------------------------------------- HTML formatting utilities + + _repr_instance = HTMLRepr() + repr = _repr_instance.repr + escape = _repr_instance.escape + + def preformat(self, text): + """Format literal preformatted text.""" + text = self.escape(expandtabs(text)) + return replace(text, ('\n\n', '\n \n'), ('\n\n', '\n \n'), + (' ', ' '), ('\n', '<br>\n')) + + def multicolumn(self, list, format, cols=4): + """Format a list of items into a multi-column list.""" + result = '' + rows = (len(list)+cols-1)/cols + + for col in range(cols): + result = result + '<td width="%d%%" valign=top>' % (100/cols) + for i in range(rows*col, rows*col+rows): + if i < len(list): + result = result + format(list[i]) + '<br>' + result = result + '</td>' + return '<table width="100%%"><tr>%s</tr></table>' % result + + def heading(self, title, fgcol, bgcol, extras=''): + """Format a page heading.""" + return """ +<p><table width="100%%" cellspacing=0 cellpadding=0 border=0> +<tr bgcolor="%s"><td colspan=3 valign=bottom><small><small><br></small></small +><font color="%s" face="helvetica, arial"> %s</font></td +><td align=right valign=bottom +><font color="%s" face="helvetica, arial"> %s</font></td></tr></table> + """ % (bgcol, fgcol, title, fgcol, extras) + + def section(self, title, fgcol, bgcol, contents, width=20, + prelude='', marginalia=None, gap=' '): + """Format a section with a heading.""" + if marginalia is None: + marginalia = ' ' * width + result = """ +<p><table width="100%%" cellspacing=0 cellpadding=0 border=0> +<tr bgcolor="%s"><td colspan=3 valign=bottom><small><small><br></small></small +><font color="%s" face="helvetica, arial"> %s</font></td></tr> + """ % (bgcol, fgcol, title) + if prelude: + result = result + """ +<tr><td bgcolor="%s">%s</td> +<td bgcolor="%s" colspan=2>%s</td></tr> + """ % (bgcol, marginalia, bgcol, prelude) + result = result + """ +<tr><td bgcolor="%s">%s</td><td>%s</td> + """ % (bgcol, marginalia, gap) + + result = result + '<td width="100%%">%s</td></tr></table>' % contents + return result + + def bigsection(self, title, *args): + """Format a section with a big heading.""" + title = '<big><strong>%s</strong></big>' % title + return apply(self.section, (title,) + args) + + def footer(self): + return """ +<table width="100%"><tr><td align=right> +<font face="helvetica, arial"><small><small>generated with +<strong>htmldoc</strong> by Ka-Ping Yee</a></small></small></font> +</td></tr></table> + """ + + def namelink(self, name, *dicts): + """Make a link for an identifier, given name-to-URL mappings.""" + for dict in dicts: + if dict.has_key(name): + return '<a href="%s">%s</a>' % (dict[name], name) + return name + + def classlink(self, object, modname, *dicts): + """Make a link for a class.""" + name = object.__name__ + if object.__module__ != modname: + name = object.__module__ + '.' + name + for dict in dicts: + if dict.has_key(object): + return '<a href="%s">%s</a>' % (dict[object], name) + return name + + def modulelink(self, object): + """Make a link for a module.""" + return '<a href="%s.html">%s</a>' % (object.__name__, object.__name__) + + def modpkglink(self, (name, path, ispackage, shadowed)): + """Make a link for a module or package to display in an index.""" + if shadowed: + return '<font color="#909090">%s</font>' % name + if path: + url = '%s.%s.html' % (path, name) + else: + url = '%s.html' % name + if ispackage: + text = '<strong>%s</strong> (package)' % name + else: + text = name + return '<a href="%s">%s</a>' % (url, text) + + def markup(self, text, escape=None, funcs={}, classes={}, methods={}): + """Mark up some plain text, given a context of symbols to look for. + Each context dictionary maps object names to anchor names.""" + escape = escape or self.escape + results = [] + here = 0 + pattern = re.compile(r'\b(((http|ftp)://\S+[\w/])|' + r'(RFC[- ]?(\d+))|' + r'(self\.)?(\w+))\b') + while 1: + match = pattern.search(text, here) + if not match: break + start, end = match.span() + results.append(escape(text[here:start])) + + all, url, scheme, rfc, rfcnum, selfdot, name = match.groups() + if url: + results.append('<a href="%s">%s</a>' % (url, escape(url))) + elif rfc: + url = 'http://www.rfc-editor.org/rfc/rfc%s.txt' % rfcnum + results.append('<a href="%s">%s</a>' % (url, escape(rfc))) + else: + if text[end:end+1] == '(': + results.append(self.namelink(name, methods, funcs, classes)) + elif selfdot: + results.append('self.<strong>%s</strong>' % name) + else: + results.append(self.namelink(name, classes)) + here = end + results.append(escape(text[here:])) + return join(results, '') + + # ---------------------------------------------- type-specific routines + + def doctree(self, tree, modname, classes={}, parent=None): + """Produce HTML for a class tree as given by inspect.getclasstree().""" + result = '' + for entry in tree: + if type(entry) is type(()): + c, bases = entry + result = result + '<dt><font face="helvetica, arial"><small>' + result = result + self.classlink(c, modname, classes) + if bases and bases != (parent,): + parents = [] + for base in bases: + parents.append(self.classlink(base, modname, classes)) + result = result + '(' + join(parents, ', ') + ')' + result = result + '\n</small></font></dt>' + elif type(entry) is type([]): + result = result + \ + '<dd>\n%s</dd>\n' % self.doctree(entry, modname, classes, c) + return '<dl>\n%s</dl>\n' % result + + def docmodule(self, object): + """Produce HTML documentation for a module object.""" + name = object.__name__ + result = '' + head = '<br><big><big><strong> %s</strong></big></big>' % name + try: + file = inspect.getsourcefile(object) + filelink = '<a href="file:%s">%s</a>' % (file, file) + except TypeError: + filelink = '(built-in)' + if hasattr(object, '__version__'): + head = head + ' (version: %s)' % self.escape(object.__version__) + result = result + self.heading( + head, '#ffffff', '#7799ee', '<a href=".">index</a><br>' + filelink) + + second = lambda list: list[1] + modules = map(second, inspect.getmembers(object, inspect.ismodule)) + + classes, cdict = [], {} + for key, value in inspect.getmembers(object, inspect.isclass): + if (inspect.getmodule(value) or object) is object: + classes.append(value) + cdict[key] = cdict[value] = '#' + key + funcs, fdict = [], {} + for key, value in inspect.getmembers(object, inspect.isroutine): + if inspect.isbuiltin(value) or inspect.getmodule(value) is object: + funcs.append(value) + fdict[key] = '#-' + key + if inspect.isfunction(value): fdict[value] = fdict[key] + for c in classes: + for base in c.__bases__: + key, modname = base.__name__, base.__module__ + if modname != name and sys.modules.has_key(modname): + module = sys.modules[modname] + if hasattr(module, key) and getattr(module, key) is base: + if not cdict.has_key(key): + cdict[key] = cdict[base] = modname + '.html#' + key + constants = [] + for key, value in inspect.getmembers(object, isconstant): + if key[:1] != '_': + constants.append((key, value)) + + doc = self.markup(getdoc(object), self.preformat, fdict, cdict) + doc = doc and '<tt>%s</tt>' % doc + result = result + '<p><small>%s</small></p>\n' % doc + + if hasattr(object, '__path__'): + modpkgs = [] + modnames = [] + for file in os.listdir(object.__path__[0]): + if file[:1] != '_': + path = os.path.join(object.__path__[0], file) + modname = modulename(file) + if modname and modname not in modnames: + modpkgs.append((modname, name, 0, 0)) + modnames.append(modname) + elif ispackage(path): + modpkgs.append((file, name, 1, 0)) + modpkgs.sort() + contents = self.multicolumn(modpkgs, self.modpkglink) + result = result + self.bigsection( + 'Package Contents', '#ffffff', '#aa55cc', contents) + + elif modules: + contents = self.multicolumn(modules, self.modulelink) + result = result + self.bigsection( + 'Modules', '#fffff', '#aa55cc', contents) + + if classes: + contents = self.doctree( + inspect.getclasstree(classes, 1), name, cdict) + for item in classes: + contents = contents + self.document(item, fdict, cdict) + result = result + self.bigsection( + 'Classes', '#ffffff', '#ee77aa', contents) + if funcs: + contents = '' + for item in funcs: + contents = contents + self.document(item, fdict, cdict) + result = result + self.bigsection( + 'Functions', '#ffffff', '#eeaa77', contents) + + if constants: + contents = '' + for key, value in constants: + contents = contents + ('<br><strong>%s</strong> = %s' % + (key, self.repr(value))) + result = result + self.bigsection( + 'Constants', '#ffffff', '#55aa55', contents) + + return result + + def docclass(self, object, funcs={}, classes={}): + """Produce HTML documentation for a class object.""" + name = object.__name__ + bases = object.__bases__ + contents = '' + + methods, mdict = [], {} + for key, value in inspect.getmembers(object, inspect.ismethod): + methods.append(value) + mdict[key] = mdict[value] = '#' + name + '-' + key + for item in methods: + contents = contents + self.document( + item, funcs, classes, mdict, name) + + title = '<a name="%s">class <strong>%s</strong></a>' % (name, name) + if bases: + parents = [] + for base in bases: + parents.append(self.classlink(base, object.__module__, classes)) + title = title + '(%s)' % join(parents, ', ') + doc = self.markup(getdoc(object), self.preformat, + funcs, classes, mdict) + if doc: doc = '<small><tt>' + doc + '<br> </tt></small>' + return self.section(title, '#000000', '#ffc8d8', contents, 10, doc) + + def docmethod(self, object, funcs={}, classes={}, methods={}, clname=''): + """Produce HTML documentation for a method object.""" + return self.document( + object.im_func, funcs, classes, methods, clname) + + def formatvalue(self, object): + """Format an argument default value as text.""" + return ('<small><font color="#909090">=%s</font></small>' % + self.repr(object)) + + def docfunction(self, object, funcs={}, classes={}, methods={}, clname=''): + """Produce HTML documentation for a function object.""" + args, varargs, varkw, defaults = inspect.getargspec(object) + argspec = inspect.formatargspec( + args, varargs, varkw, defaults, formatvalue=self.formatvalue) + + if object.__name__ == '<lambda>': + decl = '<em>lambda</em> ' + argspec[1:-1] + else: + anchor = clname + '-' + object.__name__ + decl = '<a name="%s"\n><strong>%s</strong>%s</a>\n' % ( + anchor, object.__name__, argspec) + doc = self.markup(getdoc(object), self.preformat, + funcs, classes, methods) + doc = replace(doc, ('<br>\n', '</tt></small\n><dd><small><tt>')) + doc = doc and '<tt>%s</tt>' % doc + return '<dl><dt>%s<dd><small>%s</small></dl>' % (decl, doc) + + def docbuiltin(self, object, *extras): + """Produce HTML documentation for a built-in function.""" + return '<dl><dt><strong>%s</strong>(...)</dl>' % object.__name__ + + def page(self, object): + """Produce a complete HTML page of documentation for an object.""" + return '''<!doctype html public "-//W3C//DTD HTML 4.0 Transitional//EN"> +<html><title>Python: %s</title> +<body bgcolor="#ffffff"> +%s +</body></html> +''' % (describe(object), self.document(object)) + + def index(self, dir, shadowed=None): + """Generate an HTML index for a directory of modules.""" + modpkgs = [] + if shadowed is None: shadowed = {} + seen = {} + files = os.listdir(dir) + + def found(name, ispackage, + modpkgs=modpkgs, shadowed=shadowed, seen=seen): + if not seen.has_key(name): + modpkgs.append((name, '', ispackage, shadowed.has_key(name))) + seen[name] = 1 + shadowed[name] = 1 + + # Package spam/__init__.py takes precedence over module spam.py. + for file in files: + path = os.path.join(dir, file) + if ispackage(path): found(file, 1) + for file in files: + path = os.path.join(dir, file) + if file[:1] != '_' and os.path.isfile(path): + modname = modulename(file) + if modname: found(modname, 0) + + modpkgs.sort() + contents = self.multicolumn(modpkgs, self.modpkglink) + return self.bigsection(dir, '#ffffff', '#ee77aa', contents) + +# -------------------------------------------- text documentation generator + +class TextRepr(Repr): + """Class for safely making a text representation of a Python object.""" + def __init__(self): + Repr.__init__(self) + self.maxlist = self.maxtuple = self.maxdict = 10 + self.maxstring = self.maxother = 50 + + def repr1(self, x, level): + methodname = 'repr_' + join(split(type(x).__name__), '_') + if hasattr(self, methodname): + return getattr(self, methodname)(x, level) + else: + return cram(cleanid(repr(x)), self.maxother) + + def repr_instance(self, x, level): + try: + return cram(cleanid(repr(x)), self.maxstring) + except: + return '<%s instance>' % x.__class__.__name__ + +class TextDoc(Doc): + """Formatter class for text documentation.""" + + # ------------------------------------------- text formatting utilities + + _repr_instance = TextRepr() + repr = _repr_instance.repr + + def bold(self, text): + """Format a string in bold by overstriking.""" + return join(map(lambda ch: ch + '\b' + ch, text), '') + + def indent(self, text, prefix=' '): + """Indent text by prepending a given prefix to each line.""" + if not text: return '' + lines = split(text, '\n') + lines = map(lambda line, prefix=prefix: prefix + line, lines) + if lines: lines[-1] = rstrip(lines[-1]) + return join(lines, '\n') + + def section(self, title, contents): + """Format a section with a given heading.""" + return self.bold(title) + '\n' + rstrip(self.indent(contents)) + '\n\n' + + # ---------------------------------------------- type-specific routines + + def doctree(self, tree, modname, parent=None, prefix=''): + """Render in text a class tree as returned by inspect.getclasstree().""" + result = '' + for entry in tree: + if type(entry) is type(()): + cl, bases = entry + result = result + prefix + classname(cl, modname) + if bases and bases != (parent,): + parents = map(lambda cl, m=modname: classname(cl, m), bases) + result = result + '(%s)' % join(parents, ', ') + result = result + '\n' + elif type(entry) is type([]): + result = result + self.doctree( + entry, modname, cl, prefix + ' ') + return result + + def docmodule(self, object): + """Produce text documentation for a given module object.""" + result = '' + + name = object.__name__ + lines = split(strip(getdoc(object)), '\n') + if len(lines) == 1: + if lines[0]: name = name + ' - ' + lines[0] + lines = [] + elif len(lines) >= 2 and not rstrip(lines[1]): + if lines[0]: name = name + ' - ' + lines[0] + lines = lines[2:] + result = result + self.section('NAME', name) + try: file = inspect.getfile(object) # XXX or getsourcefile? + except TypeError: file = None + result = result + self.section('FILE', file or '(built-in)') + if lines: + result = result + self.section('DESCRIPTION', join(lines, '\n')) + + classes = [] + for key, value in inspect.getmembers(object, inspect.isclass): + if (inspect.getmodule(value) or object) is object: + classes.append(value) + funcs = [] + for key, value in inspect.getmembers(object, inspect.isroutine): + if inspect.isbuiltin(value) or inspect.getmodule(value) is object: + funcs.append(value) + constants = [] + for key, value in inspect.getmembers(object, isconstant): + if key[:1] != '_': + constants.append((key, value)) + + if hasattr(object, '__path__'): + modpkgs = [] + for file in os.listdir(object.__path__[0]): + if file[:1] != '_': + path = os.path.join(object.__path__[0], file) + modname = modulename(file) + if modname and modname not in modpkgs: + modpkgs.append(modname) + elif ispackage(path): + modpkgs.append(file + ' (package)') + modpkgs.sort() + result = result + self.section( + 'PACKAGE CONTENTS', join(modpkgs, '\n')) + + if classes: + contents = self.doctree( + inspect.getclasstree(classes, 1), object.__name__) + '\n' + for item in classes: + contents = contents + self.document(item) + '\n' + result = result + self.section('CLASSES', contents) + + if funcs: + contents = '' + for item in funcs: + contents = contents + self.document(item) + '\n' + result = result + self.section('FUNCTIONS', contents) + + if constants: + contents = '' + for key, value in constants: + line = key + ' = ' + self.repr(value) + chop = 70 - len(line) + line = self.bold(key) + ' = ' + self.repr(value) + if chop < 0: line = line[:chop] + '...' + contents = contents + line + '\n' + result = result + self.section('CONSTANTS', contents) + + if hasattr(object, '__version__'): + version = str(object.__version__) + if hasattr(object, '__date__'): + version = version + ', ' + str(object.__date__) + result = result + self.section('VERSION', version) + + if hasattr(object, '__author__'): + author = str(object.__author__) + if hasattr(object, '__email__'): + author = author + ' <' + str(object.__email__) + '>' + result = result + self.section('AUTHOR', author) + + return result + + def docclass(self, object): + """Produce text documentation for a given class object.""" + name = object.__name__ + bases = object.__bases__ + + title = 'class ' + self.bold(name) + if bases: + parents = map(lambda c, m=object.__module__: classname(c, m), bases) + title = title + '(%s)' % join(parents, ', ') + + doc = getdoc(object) + contents = doc and doc + '\n' + methods = map(lambda (key, value): value, + inspect.getmembers(object, inspect.ismethod)) + for item in methods: + contents = contents + '\n' + self.document(item) + + if not contents: return title + '\n' + return title + '\n' + self.indent(rstrip(contents), ' | ') + '\n' + + def docmethod(self, object): + """Produce text documentation for a method object.""" + return self.document(object.im_func) + + def formatvalue(self, object): + """Format an argument default value as text.""" + return '=' + self.repr(object) + + def docfunction(self, object): + """Produce text documentation for a function object.""" + try: + args, varargs, varkw, defaults = inspect.getargspec(object) + argspec = inspect.formatargspec( + args, varargs, varkw, defaults, formatvalue=self.formatvalue) + except TypeError: + argspec = '(...)' + + if object.__name__ == '<lambda>': + decl = '<lambda> ' + argspec[1:-1] + else: + decl = self.bold(object.__name__) + argspec + doc = getdoc(object) + if doc: + return decl + '\n' + rstrip(self.indent(doc)) + '\n' + else: + return decl + '\n' + + def docbuiltin(self, object): + """Produce text documentation for a built-in function object.""" + return (self.bold(object.__name__) + '(...)\n' + + rstrip(self.indent(object.__doc__)) + '\n') + +# --------------------------------------------------------- user interfaces + +def pager(text): + """The first time this is called, determine what kind of pager to use.""" + global pager + pager = getpager() + pager(text) + +def getpager(): + """Decide what method to use for paging through text.""" + if type(sys.stdout) is not types.FileType: + return plainpager + if not sys.stdin.isatty() or not sys.stdout.isatty(): + return plainpager + if os.environ.has_key('PAGER'): + return lambda a: pipepager(a, os.environ['PAGER']) + if sys.platform in ['win', 'win32', 'nt']: + return lambda a: tempfilepager(a, 'more') + if hasattr(os, 'system') and os.system('less 2>/dev/null') == 0: + return lambda a: pipepager(a, 'less') + + import tempfile + filename = tempfile.mktemp() + open(filename, 'w').close() + try: + if hasattr(os, 'system') and os.system('more %s' % filename) == 0: + return lambda text: pipepager(text, 'more') + else: + return ttypager + finally: + os.unlink(filename) + +def pipepager(text, cmd): + """Page through text by feeding it to another program.""" + pipe = os.popen(cmd, 'w') + try: + pipe.write(text) + pipe.close() + except IOError: + # Ignore broken pipes caused by quitting the pager program. + pass + +def tempfilepager(text, cmd): + """Page through text by invoking a program on a temporary file.""" + import tempfile + filename = tempfile.mktemp() + file = open(filename, 'w') + file.write(text) + file.close() + try: + os.system(cmd + ' ' + filename) + finally: + os.unlink(filename) + +def plain(text): + """Remove boldface formatting from text.""" + return re.sub('.\b', '', text) + +def ttypager(text): + """Page through text on a text terminal.""" + lines = split(plain(text), '\n') + try: + import tty + fd = sys.stdin.fileno() + old = tty.tcgetattr(fd) + tty.setcbreak(fd) + getchar = lambda: sys.stdin.read(1) + except ImportError: + tty = None + getchar = lambda: sys.stdin.readline()[:-1][:1] + + try: + r = inc = os.environ.get('LINES', 25) - 1 + sys.stdout.write(join(lines[:inc], '\n') + '\n') + while lines[r:]: + sys.stdout.write('-- more --') + sys.stdout.flush() + c = getchar() + + if c in ['q', 'Q']: + sys.stdout.write('\r \r') + break + elif c in ['\r', '\n']: + sys.stdout.write('\r \r' + lines[r] + '\n') + r = r + 1 + continue + if c in ['b', 'B', '\x1b']: + r = r - inc - inc + if r < 0: r = 0 + sys.stdout.write('\n' + join(lines[r:r+inc], '\n') + '\n') + r = r + inc + + finally: + if tty: + tty.tcsetattr(fd, tty.TCSAFLUSH, old) + +def plainpager(text): + """Simply print unformatted text. This is the ultimate fallback.""" + sys.stdout.write(plain(text)) + +def describe(thing): + """Produce a short description of the given kind of thing.""" + if inspect.ismodule(thing): + if thing.__name__ in sys.builtin_module_names: + return 'built-in module ' + thing.__name__ + if hasattr(thing, '__path__'): + return 'package ' + thing.__name__ + else: + return 'module ' + thing.__name__ + if inspect.isbuiltin(thing): + return 'built-in function ' + thing.__name__ + if inspect.isclass(thing): + return 'class ' + thing.__name__ + if inspect.isfunction(thing): + return 'function ' + thing.__name__ + if inspect.ismethod(thing): + return 'method ' + thing.__name__ + return repr(thing) + +def locate(path): + """Locate an object by name (or dotted path), importing as necessary.""" + if not path: # special case: imp.find_module('') strangely succeeds + return None, None + if type(path) is not types.StringType: + return None, path + if hasattr(__builtins__, path): + return None, getattr(__builtins__, path) + parts = split(path, '.') + n = 1 + while n <= len(parts): + path = join(parts[:n], '.') + try: + module = __import__(path) + module = reload(module) + except: + # Did the error occur before or after we found the module? + if sys.modules.has_key(path): + filename = sys.modules[path].__file__ + elif sys.exc_type is SyntaxError: + filename = sys.exc_value.filename + else: + # module not found, so stop looking + break + # error occurred in the imported module, so report it + raise DocImportError(filename, sys.exc_type, sys.exc_value) + try: + x = module + for p in parts[1:]: + x = getattr(x, p) + return join(parts[:-1], '.'), x + except AttributeError: + n = n + 1 + continue + return None, None + +# --------------------------------------- interactive interpreter interface + +text = TextDoc() +html = HTMLDoc() + +def doc(thing): + """Display documentation on an object (for interactive use).""" + if type(thing) is type(""): + try: + path, x = locate(thing) + except DocImportError, value: + print 'problem in %s - %s' % (value.filename, value.args) + return + if x: + thing = x + else: + print 'could not find or import %s' % repr(thing) + return + + desc = describe(thing) + module = inspect.getmodule(thing) + if module and module is not thing: + desc = desc + ' in module ' + module.__name__ + pager('Help on %s:\n\n' % desc + text.document(thing)) + +def writedocs(path, pkgpath=''): + if os.path.isdir(path): + dir = path + for file in os.listdir(dir): + path = os.path.join(dir, file) + if os.path.isdir(path): + writedocs(path, file + '.' + pkgpath) + if os.path.isfile(path): + writedocs(path, pkgpath) + if os.path.isfile(path): + modname = modulename(path) + if modname: + writedoc(pkgpath + modname) + +def writedoc(key): + """Write HTML documentation to a file in the current directory.""" + path, object = locate(key) + if object: + file = open(key + '.html', 'w') + file.write(html.page(object)) + file.close() + print 'wrote', key + '.html' + +class Helper: + def __repr__(self): + return """To get help on a Python object, call help(object). +To get help on a module or package, either import it before calling +help(module) or call help('modulename').""" + + def __call__(self, *args): + if args: + doc(args[0]) + else: + print repr(self) + +help = Helper() + +def man(key): + """Display documentation on an object in a form similar to man(1).""" + path, object = locate(key) + if object: + title = 'Python Library Documentation: ' + describe(object) + if path: title = title + ' in ' + path + pager('\n' + title + '\n\n' + text.document(object)) + found = 1 + else: + print 'could not find or import %s' % repr(key) + +def apropos(key): + """Print all the one-line module summaries that contain a substring.""" + key = lower(key) + for module in sys.builtin_module_names: + desc = __import__(module).__doc__ or '' + desc = split(desc, '\n')[0] + if find(lower(module + ' ' + desc), key) >= 0: + print module, '-', desc or '(no description)' + modules = [] + for dir in pathdirs(): + for module, desc in index(dir): + desc = desc or '' + if module not in modules: + modules.append(module) + if find(lower(module + ' ' + desc), key) >= 0: + desc = desc or '(no description)' + if module[-9:] == '.__init__': + print module[:-9], '(package) -', desc + else: + print module, '-', desc + +# --------------------------------------------------- web browser interface + +def serve(address, callback=None): + import BaseHTTPServer, mimetools + + # Patch up mimetools.Message so it doesn't break if rfc822 is reloaded. + class Message(mimetools.Message): + def __init__(self, fp, seekable=1): + Message = self.__class__ + Message.__bases__[0].__bases__[0].__init__(self, fp, seekable) + self.encodingheader = self.getheader('content-transfer-encoding') + self.typeheader = self.getheader('content-type') + self.parsetype() + self.parseplist() + + class DocHandler(BaseHTTPServer.BaseHTTPRequestHandler): + def send_document(self, title, contents): + self.send_response(200) + self.send_header('Content-Type', 'text/html') + self.end_headers() + self.wfile.write( +'''<!doctype html public "-//W3C//DTD HTML 4.0 Transitional//EN"> +<html><title>Python: %s</title><body bgcolor="#ffffff">''' % title) + self.wfile.write(contents) + self.wfile.write('</body></html>') + + def do_GET(self): + path = self.path + if path[-5:] == '.html': path = path[:-5] + if path[:1] == '/': path = path[1:] + if path and path != '.': + try: + p, x = locate(path) + except DocImportError, value: + self.send_document(path, html.escape( + 'problem with %s - %s' % (value.filename, value.args))) + return + if x: + self.send_document(describe(x), html.document(x)) + else: + self.send_document(path, +'There is no Python module or object named "%s".' % path) + else: + heading = html.heading( + '<br><big><big><strong> ' + 'Python: Index of Modules' + '</strong></big></big>', + '#ffffff', '#7799ee') + builtins = [] + for name in sys.builtin_module_names: + builtins.append('<a href="%s.html">%s</a>' % (name, name)) + indices = ['<p>Built-in modules: ' + join(builtins, ', ')] + seen = {} + for dir in pathdirs(): + indices.append(html.index(dir, seen)) + self.send_document('Index of Modules', heading + join(indices)) + + def log_message(self, *args): pass + + class DocServer(BaseHTTPServer.HTTPServer): + def __init__(self, address, callback): + self.callback = callback + self.base.__init__(self, address, self.handler) + + def server_activate(self): + self.base.server_activate(self) + if self.callback: self.callback() + + DocServer.base = BaseHTTPServer.HTTPServer + DocServer.handler = DocHandler + DocHandler.MessageClass = Message + try: + DocServer(address, callback).serve_forever() + except KeyboardInterrupt: + print 'server stopped' + +# -------------------------------------------------- command-line interface + +if __name__ == '__main__': + import getopt + class BadUsage: pass + + try: + opts, args = getopt.getopt(sys.argv[1:], 'k:p:w') + writing = 0 + + for opt, val in opts: + if opt == '-k': + apropos(lower(val)) + break + if opt == '-p': + try: + port = int(val) + except ValueError: + raise BadUsage + def ready(port=port): + print 'server ready at http://127.0.0.1:%d/' % port + serve(('127.0.0.1', port), ready) + break + if opt == '-w': + if not args: raise BadUsage + writing = 1 + else: + if args: + for arg in args: + try: + if os.path.isfile(arg): + arg = importfile(arg) + if writing: + if os.path.isdir(arg): writedocs(arg) + else: writedoc(arg) + else: man(arg) + except DocImportError, value: + print 'problem in %s - %s' % ( + value.filename, value.args) + else: + if sys.platform in ['mac', 'win', 'win32', 'nt']: + # GUI platforms with threading + import threading + ready = threading.Event() + address = ('127.0.0.1', 12346) + threading.Thread( + target=serve, args=(address, ready.set)).start() + ready.wait() + import webbrowser + webbrowser.open('http://127.0.0.1:12346/') + else: + raise BadUsage + + except (getopt.error, BadUsage): + print """%s <name> ... + Show documentation on something. + <name> may be the name of a Python function, module, or package, + or a dotted reference to a class or function within a module or + module in a package, or the filename of a Python module to import. + +%s -k <keyword> + Search for a keyword in the synopsis lines of all modules. + +%s -p <port> + Start an HTTP server on the given port on the local machine. + +%s -w <module> ... + Write out the HTML documentation for a module to a file. + +%s -w <moduledir> + Write out the HTML documentation for all modules in the tree + under a given directory to files in the current directory. +""" % ((sys.argv[0],) * 5) |