pydoc.py 60.2 KB
Newer Older
1
#!/usr/bin/env python
2
"""Generate Python documentation in HTML or text for interactive use.
3

Ka-Ping Yee's avatar
Ka-Ping Yee committed
4 5
In the Python interpreter, do "from pydoc import help" to provide online
help.  Calling help(thing) on a Python object documents the object.
6

7
Or, at the shell command line outside of Python:
8

9 10 11 12 13
Run "pydoc <name>" to show documentation on something.  <name> may be
the name of a function, module, package, or a dotted reference to a
class or function within a module or module in a package.  If the
argument contains a path segment delimiter (e.g. slash on Unix,
backslash on Windows) it is treated as the path to a Python source file.
14

15 16
Run "pydoc -k <keyword>" to search for a keyword in the synopsis lines
of all available modules.
17

18 19
Run "pydoc -p <port>" to start an HTTP server on a given port on the
local machine to generate documentation web pages.
Ka-Ping Yee's avatar
Ka-Ping Yee committed
20

21 22 23 24 25
For platforms without a command line, "pydoc -g" starts the HTTP server
and also pops up a little window for controlling it.

Run "pydoc -w <name>" to write out the HTML documentation for a module
to a file named "<name>.html".
Ka-Ping Yee's avatar
Ka-Ping Yee committed
26
"""
27 28

__author__ = "Ka-Ping Yee <ping@lfw.org>"
29
__date__ = "26 February 2001"
Ka-Ping Yee's avatar
Ka-Ping Yee committed
30
__version__ = "$Revision$"
Ka-Ping Yee's avatar
Ka-Ping Yee committed
31 32
__credits__ = """Guido van Rossum, for an excellent programming language.
Tommy Burnette, the original creator of manpy.
33 34 35
Paul Prescod, for all his work on onlinehelp.
Richard Chamberlain, for the first implementation of textdoc.

Ka-Ping Yee's avatar
Ka-Ping Yee committed
36
Mynd you, mse bites Kan be pretty nasti..."""
37

Ka-Ping Yee's avatar
Ka-Ping Yee committed
38 39
# Note: this module is designed to deploy instantly and run under any
# version of Python from 1.5 and up.  That's why it's a single file and
40 41 42 43 44 45 46 47 48
# some 2.0 features (like string methods) are conspicuously absent.

# Known bugs that can't be fixed here:
#   - imp.load_module() cannot be prevented from clobbering existing
#     loaded modules, so calling synopsis() on a binary module file
#     changes the contents of any existing module with the same name.
#   - If the __file__ attribute on a module is a relative path and
#     the current directory is changed with os.chdir(), an incorrect
#     path will be displayed.
Ka-Ping Yee's avatar
Ka-Ping Yee committed
49

50 51
import sys, imp, os, stat, re, types, inspect
from repr import Repr
52
from string import expandtabs, find, join, lower, split, strip, rfind, rstrip
53 54 55 56 57 58 59 60

# --------------------------------------------------------- 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:
61
        info = inspect.getmoduleinfo(filename)
62
        file = open(filename)
63 64 65 66 67
        if info and 'b' in info[2]: # binary modules have to be imported
            try: module = imp.load_module(info[0], file, filename, info[1:])
            except: return None
            result = split(module.__doc__ or '', '\n')[0]
        else: # text modules can be directly examined
68
            line = file.readline()
69
            while line[:1] == '#' or not strip(line):
70 71
                line = file.readline()
                if not line: break
72 73 74 75 76 77 78 79 80 81
            line = strip(line)
            if line[:4] == 'r"""': line = line[1:]
            if line[:3] == '"""':
                line = line[3:]
                if line[-1:] == '\\': line = line[:-1]
                while not strip(line):
                    line = file.readline()
                    if not line: break
                result = strip(split(line, '"""')[0])
            else: result = None
82 83 84 85 86 87 88
        file.close()
        cache[filename] = (mtime, result)
    return result

def pathdirs():
    """Convert sys.path into a list of absolute, existing, unique paths."""
    dirs = []
89
    normdirs = []
90 91
    for dir in sys.path:
        dir = os.path.abspath(dir or '.')
92 93
        normdir = os.path.normcase(dir)
        if normdir not in normdirs and os.path.isdir(dir):
94
            dirs.append(dir)
95
            normdirs.append(normdir)
96 97 98 99
    return dirs

def getdoc(object):
    """Get the doc string or comments for an object."""
100
    result = inspect.getdoc(object) or inspect.getcomments(object)
101
    return result and re.sub('^ *\n', '', rstrip(result)) or ''
102

103 104 105 106 107 108 109 110 111
def splitdoc(doc):
    """Split a doc string into a synopsis line (if any) and the rest."""
    lines = split(strip(doc), '\n')
    if len(lines) == 1:
        return lines[0], ''
    elif len(lines) >= 2 and not rstrip(lines[1]):
        return lines[0], join(lines[2:], '\n')
    return '', join(lines, '\n')

112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127
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."""
128 129 130
    while pairs:
        text = join(split(text, pairs[0]), pairs[1])
        pairs = pairs[2:]
131 132 133 134 135 136 137 138 139 140
    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

141
def stripid(text):
142
    """Remove the hexadecimal id from a Python object representation."""
143
    # The behaviour of %p is implementation-dependent; we check two cases.
144 145 146 147
    for pattern in [' at 0x[0-9a-f]{6,}>$', ' at [0-9A-F]{8,}>$']:
        if re.search(pattern, repr(Exception)):
            return re.sub(pattern, '>', text)
    return text
148

149 150 151 152 153 154 155 156 157 158 159 160 161
def allmethods(cl):
    methods = {}
    for key, value in inspect.getmembers(cl, inspect.ismethod):
        methods[key] = 1
    for base in cl.__bases__:
        methods.update(allmethods(base)) # all your base are belong to us
    for key in methods.keys():
        methods[key] = getattr(cl, key)
    return methods

class ErrorDuringImport(Exception):
    """Errors that occurred while trying to import something to document it."""
    def __init__(self, filename, (exc, value, tb)):
162
        self.filename = filename
163
        self.exc = exc
164 165 166 167
        self.value = value
        self.tb = tb

    def __str__(self):
168 169 170 171
        exc = self.exc
        if type(exc) is types.ClassType:
            exc = exc.__name__
        return 'problem in %s - %s: %s' % (self.filename, exc, self.value)
172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187

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:
188
        raise ErrorDuringImport(path, sys.exc_info())
189 190 191 192 193 194
    file.close()
    return module

def ispackage(path):
    """Guess whether a path refers to a package directory."""
    if os.path.isdir(path):
195 196 197
        for ext in ['.py', '.pyc', '.pyo']:
            if os.path.isfile(os.path.join(path, '__init__' + ext)):
                return 1
198 199 200 201

# ---------------------------------------------------- formatter base class

class Doc:
202
    def document(self, object, name=None, *args):
203
        """Generate documentation for an object."""
204
        args = (object, name) + args
205 206
        if inspect.ismodule(object): return apply(self.docmodule, args)
        if inspect.isclass(object): return apply(self.docclass, args)
Ka-Ping Yee's avatar
Ka-Ping Yee committed
207
        if inspect.isroutine(object): return apply(self.docroutine, args)
208 209 210 211 212 213 214 215 216
        return apply(self.docother, args)

    def fail(self, object, name=None, *args):
        """Raise an exception for unimplemented types."""
        message = "don't know how to document object%s of type %s" % (
            name and ' ' + repr(name), type(object).__name__)
        raise TypeError, message

    docmodule = docclass = docroutine = docother = fail
217 218 219 220 221 222 223 224

# -------------------------------------------- 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
225
        self.maxstring = self.maxother = 100
226 227

    def escape(self, text):
228
        return replace(text, '&', '&amp;', '<', '&lt;', '>', '&gt;')
229 230

    def repr(self, object):
231
        return Repr.repr(self, object)
232 233 234 235 236 237

    def repr1(self, x, level):
        methodname = 'repr_' + join(split(type(x).__name__), '_')
        if hasattr(self, methodname):
            return getattr(self, methodname)(x, level)
        else:
238
            return self.escape(cram(stripid(repr(x)), self.maxother))
239 240

    def repr_string(self, x, level):
241 242
        test = cram(x, self.maxstring)
        testrepr = repr(test)
243
        if '\\' in test and '\\' not in replace(testrepr, r'\\', ''):
244 245 246
            # Backslashes are only literal in the string and are never
            # needed to make any special characters, so show a raw string.
            return 'r' + testrepr[0] + self.escape(test) + testrepr[0]
247
        return re.sub(r'((\\[\\abfnrtv\'"]|\\[0-9]..|\\x..|\\u....)+)',
248 249
                      r'<font color="#c040c0">\1</font>',
                      self.escape(testrepr))
250 251 252

    def repr_instance(self, x, level):
        try:
253
            return self.escape(cram(stripid(repr(x)), self.maxstring))
254 255 256 257 258 259 260 261 262 263 264 265 266 267
        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

268 269 270 271
    def page(self, title, contents):
        """Format an HTML page."""
        return '''
<!doctype html public "-//W3C//DTD HTML 4.0 Transitional//EN">
272
<html><head><title>Python: %s</title>
273 274 275
<style type="text/css"><!--
TT { font-family: lucida console, lucida typewriter, courier }
--></style></head><body bgcolor="#f0f0f8">
276 277 278 279 280 281 282 283 284
%s
</body></html>''' % (title, contents)

    def heading(self, title, fgcol, bgcol, extras=''):
        """Format a page heading."""
        return '''
<table width="100%%" cellspacing=0 cellpadding=2 border=0>
<tr bgcolor="%s">
<td valign=bottom><small>&nbsp;<br></small
285
><font color="%s" face="helvetica, arial">&nbsp;<br>%s</font></td
286
><td align=right valign=bottom
287
><font color="%s" face="helvetica, arial">%s</font></td></tr></table>
288 289
    ''' % (bgcol, fgcol, title, fgcol, extras or '&nbsp;')

290
    def section(self, title, fgcol, bgcol, contents, width=10,
291 292 293
                prelude='', marginalia=None, gap='&nbsp;&nbsp;'):
        """Format a section with a heading."""
        if marginalia is None:
294
            marginalia = '<tt>' + '&nbsp;' * width + '</tt>'
295 296 297 298 299 300 301 302
        result = '''
<p><table width="100%%" cellspacing=0 cellpadding=2 border=0>
<tr bgcolor="%s">
<td colspan=3 valign=bottom><small><small>&nbsp;<br></small></small
><font color="%s" face="helvetica, arial">%s</font></td></tr>
    ''' % (bgcol, fgcol, title)
        if prelude:
            result = result + '''
303 304 305 306 307
<tr bgcolor="%s"><td rowspan=2>%s</td>
<td colspan=2>%s</td></tr>
<tr><td>%s</td>''' % (bgcol, marginalia, prelude, gap)
        else:
            result = result + '''
308
<tr><td bgcolor="%s">%s</td><td>%s</td>''' % (bgcol, marginalia, gap)
309

310
        return result + '\n<td width="100%%">%s</td></tr></table>' % contents
311 312 313 314 315 316

    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)

317 318 319
    def preformat(self, text):
        """Format literal preformatted text."""
        text = self.escape(expandtabs(text))
320 321
        return replace(text, '\n\n', '\n \n', '\n\n', '\n \n',
                             ' ', '&nbsp;', '\n', '<br>\n')
322 323 324 325 326 327 328 329 330

    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):
331
                    result = result + format(list[i]) + '<br>\n'
332 333 334
            result = result + '</td>'
        return '<table width="100%%"><tr>%s</tr></table>' % result

335 336
    def small(self, text): return '<small>%s</small>' % text
    def grey(self, text): return '<font color="#909090">%s</font>' % text
337 338 339 340 341 342 343 344 345 346

    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."""
347
        name = classname(object, modname)
348 349 350 351 352 353 354 355 356 357 358 359
        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:
360
            return self.grey(name)
361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376
        if path:
            url = '%s.%s.html' % (path, name)
        else:
            url = '%s.html' % name
        if ispackage:
            text = '<strong>%s</strong>&nbsp;(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
377 378
        pattern = re.compile(r'\b((http|ftp)://\S+[\w/]|'
                                r'RFC[- ]?(\d+)|'
379
                                r'PEP[- ]?(\d+)|'
380 381 382 383 384 385 386
                                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]))

387
            all, scheme, rfc, pep, selfdot, name = match.groups()
388 389
            if scheme:
                results.append('<a href="%s">%s</a>' % (all, escape(all)))
390
            elif rfc:
391 392 393 394
                url = 'http://www.rfc-editor.org/rfc/rfc%d.txt' % int(rfc)
                results.append('<a href="%s">%s</a>' % (url, escape(all)))
            elif pep:
                url = 'http://www.python.org/peps/pep-%04d.html' % int(pep)
395 396 397 398 399
                results.append('<a href="%s">%s</a>' % (url, escape(all)))
            elif text[end:end+1] == '(':
                results.append(self.namelink(name, methods, funcs, classes))
            elif selfdot:
                results.append('self.<strong>%s</strong>' % name)
400
            else:
401
                results.append(self.namelink(name, classes))
402 403 404 405 406 407
            here = end
        results.append(escape(text[here:]))
        return join(results, '')

    # ---------------------------------------------- type-specific routines

408
    def formattree(self, tree, modname, classes={}, parent=None):
409 410 411 412 413 414 415 416 417 418 419 420 421 422
        """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([]):
423 424
                result = result + '<dd>\n%s</dd>\n' % self.formattree(
                    entry, modname, classes, c)
425 426
        return '<dl>\n%s</dl>\n' % result

427
    def docmodule(self, object, name=None, mod=None):
428
        """Produce HTML documentation for a module object."""
429
        name = object.__name__ # ignore the passed-in name
Ka-Ping Yee's avatar
Ka-Ping Yee committed
430 431 432 433 434 435 436 437
        parts = split(name, '.')
        links = []
        for i in range(len(parts)-1):
            links.append(
                '<a href="%s.html"><font color="#ffffff">%s</font></a>' %
                (join(parts[:i+1], '.'), parts[i]))
        linkedname = join(links + parts[-1:], '.')
        head = '<big><big><strong>%s</strong></big></big>' % linkedname
438
        try:
439
            path = inspect.getabsfile(object)
440
            filelink = '<a href="file:%s">%s</a>' % (path, path)
441 442
        except TypeError:
            filelink = '(built-in)'
443
        info = []
444
        if hasattr(object, '__version__'):
445
            version = str(object.__version__)
446 447
            if version[:11] == '$' + 'Revision: ' and version[-1:] == '$':
                version = strip(version[11:-1])
448
            info.append('version %s' % self.escape(version))
449 450 451 452
        if hasattr(object, '__date__'):
            info.append(self.escape(str(object.__date__)))
        if info:
            head = head + ' (%s)' % join(info, ', ')
Ka-Ping Yee's avatar
Ka-Ping Yee committed
453
        result = self.heading(
454 455
            head, '#ffffff', '#7799ee', '<a href=".">index</a><br>' + filelink)

456 457
        modules = inspect.getmembers(object, inspect.ismodule)

458 459
        classes, cdict = [], {}
        for key, value in inspect.getmembers(object, inspect.isclass):
460
            if (inspect.getmodule(value) or object) is object:
461
                classes.append((key, value))
462
                cdict[key] = cdict[value] = '#' + key
463 464 465 466 467 468 469 470
        for key, value in classes:
            for base in value.__bases__:
                key, modname = base.__name__, base.__module__
                module = sys.modules.get(modname)
                if modname != name and module and hasattr(module, key):
                    if getattr(module, key) is base:
                        if not cdict.has_key(key):
                            cdict[key] = cdict[base] = modname + '.html#' + key
471 472
        funcs, fdict = [], {}
        for key, value in inspect.getmembers(object, inspect.isroutine):
473
            if inspect.isbuiltin(value) or inspect.getmodule(value) is object:
474
                funcs.append((key, value))
475 476 477 478
                fdict[key] = '#-' + key
                if inspect.isfunction(value): fdict[value] = fdict[key]
        constants = []
        for key, value in inspect.getmembers(object, isconstant):
479
            constants.append((key, value))
480 481 482

        doc = self.markup(getdoc(object), self.preformat, fdict, cdict)
        doc = doc and '<tt>%s</tt>' % doc
483
        result = result + '<p>%s</p>\n' % self.small(doc)
484 485 486 487 488

        if hasattr(object, '__path__'):
            modpkgs = []
            modnames = []
            for file in os.listdir(object.__path__[0]):
489 490 491 492 493 494 495
                path = os.path.join(object.__path__[0], file)
                modname = inspect.getmodulename(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))
496 497 498 499 500
            modpkgs.sort()
            contents = self.multicolumn(modpkgs, self.modpkglink)
            result = result + self.bigsection(
                'Package Contents', '#ffffff', '#aa55cc', contents)
        elif modules:
501 502
            contents = self.multicolumn(
                modules, lambda (key, value), s=self: s.modulelink(value))
503 504 505 506
            result = result + self.bigsection(
                'Modules', '#fffff', '#aa55cc', contents)

        if classes:
507 508 509 510
            classlist = map(lambda (key, value): value, classes)
            contents = [self.formattree(
                inspect.getclasstree(classlist, 1), name, cdict)]
            for key, value in classes:
511
                contents.append(self.document(value, key, name, fdict, cdict))
512
            result = result + self.bigsection(
513
                'Classes', '#ffffff', '#ee77aa', join(contents))
514
        if funcs:
515 516
            contents = []
            for key, value in funcs:
517
                contents.append(self.document(value, key, name, fdict, cdict))
518
            result = result + self.bigsection(
519
                'Functions', '#ffffff', '#eeaa77', join(contents))
520
        if constants:
521
            contents = []
522
            for key, value in constants:
523
                contents.append(self.document(value, key))
524
            result = result + self.bigsection(
525
                'Constants', '#ffffff', '#55aa55', join(contents, '<br>'))
526 527 528 529 530 531 532 533 534
        if hasattr(object, '__author__'):
            contents = self.markup(str(object.__author__), self.preformat)
            result = result + self.bigsection(
                'Author', '#ffffff', '#7799ee', contents)
        if hasattr(object, '__credits__'):
            contents = self.markup(str(object.__credits__), self.preformat)
            result = result + self.bigsection(
                'Credits', '#ffffff', '#7799ee', contents)

535 536
        return result

537
    def docclass(self, object, name=None, mod=None, funcs={}, classes={}):
538
        """Produce HTML documentation for a class object."""
539 540
        realname = object.__name__
        name = name or realname
541 542 543
        bases = object.__bases__
        contents = ''

544
        methods, mdict = allmethods(object).items(), {}
545
        methods.sort()
546 547
        for key, value in methods:
            mdict[key] = mdict[value] = '#' + name + '-' + key
548
        for key, value in methods:
549
            contents = contents + self.document(
550
                value, key, mod, funcs, classes, mdict, object)
551 552 553 554 555 556 557

        if name == realname:
            title = '<a name="%s">class <strong>%s</strong></a>' % (
                name, realname)
        else:
            title = '<strong>%s</strong> = <a name="%s">class %s</a>' % (
                name, name, realname)
558 559 560
        if bases:
            parents = []
            for base in bases:
561 562
                parents.append(
                    self.classlink(base, object.__module__, classes))
563
            title = title + '(%s)' % join(parents, ', ')
564 565
        doc = self.markup(
            getdoc(object), self.preformat, funcs, classes, mdict)
566 567 568
        doc = self.small(doc and '<tt>%s<br>&nbsp;</tt>' % doc or
                                 self.small('&nbsp;'))
        return self.section(title, '#000000', '#ffc8d8', contents, 5, doc)
569 570 571

    def formatvalue(self, object):
        """Format an argument default value as text."""
572
        return self.small(self.grey('=' + self.repr(object)))
573

574
    def docroutine(self, object, name=None, mod=None,
575
                   funcs={}, classes={}, methods={}, cl=None):
Ka-Ping Yee's avatar
Ka-Ping Yee committed
576
        """Produce HTML documentation for a function or method object."""
577 578
        realname = object.__name__
        name = name or realname
579
        anchor = (cl and cl.__name__ or '') + '-' + name
580
        note = ''
581
        skipdocs = 0
582
        if inspect.ismethod(object):
583
            if cl:
584 585 586 587 588 589
                imclass = object.im_class
                if imclass is not cl:
                    url = '%s.html#%s-%s' % (
                        imclass.__module__, imclass.__name__, name)
                    note = ' from <a href="%s">%s</a>' % (
                        url, classname(imclass, mod))
590 591 592
                    skipdocs = 1
            else:
                note = (object.im_self and
593
                        ' method of %s instance' + object.im_self.__class__ or
594
                        ' unbound %s method' % object.im_class.__name__)
595 596 597 598 599
            object = object.im_func

        if name == realname:
            title = '<a name="%s"><strong>%s</strong></a>' % (anchor, realname)
        else:
600 601
            if (cl and cl.__dict__.has_key(realname) and
                cl.__dict__[realname] is object):
602
                reallink = '<a href="#%s">%s</a>' % (
603 604 605 606 607 608
                    cl.__name__ + '-' + realname, realname)
                skipdocs = 1
            else:
                reallink = realname
            title = '<a name="%s"><strong>%s</strong></a> = %s' % (
                anchor, name, reallink)
Ka-Ping Yee's avatar
Ka-Ping Yee committed
609
        if inspect.isbuiltin(object):
610
            argspec = '(...)'
611
        else:
Ka-Ping Yee's avatar
Ka-Ping Yee committed
612 613 614
            args, varargs, varkw, defaults = inspect.getargspec(object)
            argspec = inspect.formatargspec(
                args, varargs, varkw, defaults, formatvalue=self.formatvalue)
615 616 617
            if realname == '<lambda>':
                decl = '<em>lambda</em>'
                argspec = argspec[1:-1] # remove parentheses
Ka-Ping Yee's avatar
Ka-Ping Yee committed
618

619 620
        decl = title + argspec + (note and self.small(self.grey(
            '<font face="helvetica, arial">%s</font>' % note)))
621

622
        if skipdocs:
623
            return '<dl><dt>%s</dl>\n' % decl
624 625 626
        else:
            doc = self.markup(
                getdoc(object), self.preformat, funcs, classes, methods)
627 628
            doc = doc and '<dd>' + self.small('<tt>%s</tt>' % doc)
            return '<dl><dt>%s%s</dl>\n' % (decl, doc)
629

630
    def docother(self, object, name=None, mod=None):
631
        """Produce HTML documentation for a data object."""
632 633
        lhs = name and '<strong>%s</strong> = ' % name or ''
        return lhs + self.repr(object)
634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654

    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)
655 656
            if os.path.isfile(path):
                modname = inspect.getmodulename(file)
657 658 659 660 661 662 663 664 665 666 667 668 669
                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
670
        self.maxstring = self.maxother = 100
671 672 673 674 675 676

    def repr1(self, x, level):
        methodname = 'repr_' + join(split(type(x).__name__), '_')
        if hasattr(self, methodname):
            return getattr(self, methodname)(x, level)
        else:
677
            return cram(stripid(repr(x)), self.maxother)
678

679 680 681
    def repr_string(self, x, level):
        test = cram(x, self.maxstring)
        testrepr = repr(test)
682
        if '\\' in test and '\\' not in replace(testrepr, r'\\', ''):
683 684 685 686 687
            # Backslashes are only literal in the string and are never
            # needed to make any special characters, so show a raw string.
            return 'r' + testrepr[0] + test + testrepr[0]
        return testrepr

688 689
    def repr_instance(self, x, level):
        try:
690
            return cram(stripid(repr(x)), self.maxstring)
691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719
        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

720
    def formattree(self, tree, modname, parent=None, prefix=''):
721 722 723 724
        """Render in text a class tree as returned by inspect.getclasstree()."""
        result = ''
        for entry in tree:
            if type(entry) is type(()):
725 726
                c, bases = entry
                result = result + prefix + classname(c, modname)
727
                if bases and bases != (parent,):
728
                    parents = map(lambda c, m=modname: classname(c, m), bases)
729 730 731
                    result = result + '(%s)' % join(parents, ', ')
                result = result + '\n'
            elif type(entry) is type([]):
732 733
                result = result + self.formattree(
                    entry, modname, c, prefix + '    ')
734 735
        return result

736
    def docmodule(self, object, name=None, mod=None):
737
        """Produce text documentation for a given module object."""
738
        name = object.__name__ # ignore the passed-in name
739 740
        synop, desc = splitdoc(getdoc(object))
        result = self.section('NAME', name + (synop and ' - ' + synop))
741 742 743 744 745

        try:
            file = inspect.getabsfile(object)
        except TypeError:
            file = '(built-in)'
746
        result = result + self.section('FILE', file)
747 748
        if desc:
            result = result + self.section('DESCRIPTION', desc)
749 750 751 752

        classes = []
        for key, value in inspect.getmembers(object, inspect.isclass):
            if (inspect.getmodule(value) or object) is object:
753
                classes.append((key, value))
754 755 756
        funcs = []
        for key, value in inspect.getmembers(object, inspect.isroutine):
            if inspect.isbuiltin(value) or inspect.getmodule(value) is object:
757
                funcs.append((key, value))
758 759
        constants = []
        for key, value in inspect.getmembers(object, isconstant):
760
            constants.append((key, value))
761 762 763 764

        if hasattr(object, '__path__'):
            modpkgs = []
            for file in os.listdir(object.__path__[0]):
765 766 767 768 769 770
                path = os.path.join(object.__path__[0], file)
                modname = inspect.getmodulename(file)
                if modname and modname not in modpkgs:
                    modpkgs.append(modname)
                elif ispackage(path):
                    modpkgs.append(file + ' (package)')
771 772 773 774 775
            modpkgs.sort()
            result = result + self.section(
                'PACKAGE CONTENTS', join(modpkgs, '\n'))

        if classes:
776 777 778 779
            classlist = map(lambda (key, value): value, classes)
            contents = [self.formattree(
                inspect.getclasstree(classlist, 1), name)]
            for key, value in classes:
780
                contents.append(self.document(value, key, name))
781
            result = result + self.section('CLASSES', join(contents, '\n'))
782 783

        if funcs:
784 785
            contents = []
            for key, value in funcs:
786
                contents.append(self.document(value, key, name))
787
            result = result + self.section('FUNCTIONS', join(contents, '\n'))
788 789

        if constants:
790
            contents = []
791
            for key, value in constants:
792
                contents.append(self.docother(value, key, name, 70))
793
            result = result + self.section('CONSTANTS', join(contents, '\n'))
794 795 796

        if hasattr(object, '__version__'):
            version = str(object.__version__)
797 798
            if version[:11] == '$' + 'Revision: ' and version[-1:] == '$':
                version = strip(version[11:-1])
799
            result = result + self.section('VERSION', version)
800 801
        if hasattr(object, '__date__'):
            result = result + self.section('DATE', str(object.__date__))
802
        if hasattr(object, '__author__'):
803 804 805
            result = result + self.section('AUTHOR', str(object.__author__))
        if hasattr(object, '__credits__'):
            result = result + self.section('CREDITS', str(object.__credits__))
806 807
        return result

808
    def docclass(self, object, name=None, mod=None):
809
        """Produce text documentation for a given class object."""
810 811
        realname = object.__name__
        name = name or realname
812 813
        bases = object.__bases__

814 815 816 817
        if name == realname:
            title = 'class ' + self.bold(realname)
        else:
            title = self.bold(name) + ' = class ' + realname
818
        if bases:
819 820
            def makename(c, m=object.__module__): return classname(c, m)
            parents = map(makename, bases)
821 822 823 824
            title = title + '(%s)' % join(parents, ', ')

        doc = getdoc(object)
        contents = doc and doc + '\n'
825 826 827
        methods = allmethods(object).items()
        methods.sort()
        for key, value in methods:
828
            contents = contents + '\n' + self.document(value, key, mod, object)
829 830 831 832 833 834 835 836

        if not contents: return title + '\n'
        return title + '\n' + self.indent(rstrip(contents), ' |  ') + '\n'

    def formatvalue(self, object):
        """Format an argument default value as text."""
        return '=' + self.repr(object)

837
    def docroutine(self, object, name=None, mod=None, cl=None):
Ka-Ping Yee's avatar
Ka-Ping Yee committed
838
        """Produce text documentation for a function or method object."""
839 840 841
        realname = object.__name__
        name = name or realname
        note = ''
842
        skipdocs = 0
843
        if inspect.ismethod(object):
844
            imclass = object.im_class
845
            if cl:
846 847
                if imclass is not cl:
                    note = ' from ' + classname(imclass, mod)
848 849
                    skipdocs = 1
            else:
850 851 852
                note = (object.im_self and
                        ' method of %s instance' + object.im_self.__class__ or
                        ' unbound %s method' % classname(imclass, mod))
853 854 855 856 857
            object = object.im_func

        if name == realname:
            title = self.bold(realname)
        else:
858 859 860
            if (cl and cl.__dict__.has_key(realname) and
                cl.__dict__[realname] is object):
                skipdocs = 1
861
            title = self.bold(name) + ' = ' + realname
Ka-Ping Yee's avatar
Ka-Ping Yee committed
862
        if inspect.isbuiltin(object):
863
            argspec = '(...)'
Ka-Ping Yee's avatar
Ka-Ping Yee committed
864
        else:
865 866 867
            args, varargs, varkw, defaults = inspect.getargspec(object)
            argspec = inspect.formatargspec(
                args, varargs, varkw, defaults, formatvalue=self.formatvalue)
868 869 870 871 872
            if realname == '<lambda>':
                title = 'lambda'
                argspec = argspec[1:-1] # remove parentheses
        decl = title + argspec + note

873
        if skipdocs:
874
            return decl + '\n'
875 876 877
        else:
            doc = getdoc(object) or ''
            return decl + '\n' + (doc and rstrip(self.indent(doc)) + '\n')
878

879
    def docother(self, object, name=None, mod=None, maxlen=None):
880 881 882
        """Produce text documentation for a data object."""
        repr = self.repr(object)
        if maxlen:
883
            line = (name and name + ' = ' or '') + repr
884 885
            chop = maxlen - len(line)
            if chop < 0: repr = repr[:chop] + '...'
886
        line = (name and self.bold(name) + ' = ' or '') + repr
887 888
        return line

889 890 891 892 893 894 895 896 897 898 899 900 901 902 903
# --------------------------------------------------------- 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'):
904 905 906 907
        if sys.platform == 'win32': # pipes completely broken in Windows
            return lambda a: tempfilepager(a, os.environ['PAGER'])
        else:
            return lambda a: pipepager(a, os.environ['PAGER'])
908 909
    if sys.platform == 'win32':
        return lambda a: tempfilepager(a, 'more <')
910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941
    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:
942
        os.system(cmd + ' ' + filename)
943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958
    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)
Ka-Ping Yee's avatar
Ka-Ping Yee committed
959
    except (ImportError, AttributeError):
960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992
        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):
993
    """Produce a short description of the given thing."""
994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008
    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__
1009 1010 1011 1012
    if type(thing) is types.InstanceType:
        return 'instance of ' + thing.__class__.__name__
    return type(thing).__name__

1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035
def freshimport(path, cache={}):
    """Import a module freshly from disk, making sure it's up to date."""
    if sys.modules.has_key(path):
        # This is the only way to be sure.  Checking the mtime of the file
        # isn't good enough (e.g. what if the module contains a class that
        # inherits from another module that has changed?).
        if path not in sys.builtin_module_names:
            del sys.modules[path]
    try:
        module = __import__(path)
    except:
        # Did the error occur before or after the module was found?
        (exc, value, tb) = info = sys.exc_info()
        if sys.modules.has_key(path):
            # An error occured while executing the imported module.
            raise ErrorDuringImport(sys.modules[path].__file__, info)
        elif exc is SyntaxError:
            # A SyntaxError occurred before we could execute the module.
            raise ErrorDuringImport(value.filename, info)
        elif exc is ImportError and \
             split(lower(str(value)))[:2] == ['no', 'module']:
            # The module was not found.
            return None
1036
        else:
1037 1038 1039 1040 1041
            # Some other error occurred during the importing process.
            raise ErrorDuringImport(path, sys.exc_info())
    for part in split(path, '.')[1:]:
        try: module = getattr(module, part)
        except AttributeError: return None
1042
    return module
1043 1044

def locate(path):
1045
    """Locate an object by name or dotted path, importing as necessary."""
1046
    parts = split(path, '.')
1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061
    module, n = None, 0
    while n < len(parts):
        nextmodule = freshimport(join(parts[:n+1], '.'))
        if nextmodule: module, n = nextmodule, n + 1
        else: break
    if module:
        object = module
        for part in parts[n:]:
            try: object = getattr(object, part)
            except AttributeError: return None
        return object
    else:
        import __builtin__
        if hasattr(__builtin__, path):
            return getattr(__builtin__, path)
1062 1063 1064 1065 1066 1067

# --------------------------------------- interactive interpreter interface

text = TextDoc()
html = HTMLDoc()

1068
def doc(thing, title='Python Library Documentation: %s'):
1069 1070 1071
    """Display text documentation, given an object or a path to an object."""
    suffix, name = '', None
    if type(thing) is type(''):
1072
        try:
1073 1074
            object = locate(thing)
        except ErrorDuringImport, value:
1075
            print value
1076
            return
1077
        if not object:
1078
            print 'no Python documentation found for %s' % repr(thing)
1079
            return
1080 1081 1082 1083
        parts = split(thing, '.')
        if len(parts) > 1: suffix = ' in ' + join(parts[:-1], '.')
        name = parts[-1]
        thing = object
1084 1085 1086

    desc = describe(thing)
    module = inspect.getmodule(thing)
1087 1088
    if not suffix and module and module is not thing:
        suffix = ' in module ' + module.__name__
1089
    pager(title % (desc + suffix) + '\n\n' + text.document(thing, name))
1090 1091 1092

def writedoc(key):
    """Write HTML documentation to a file in the current directory."""
1093
    try:
1094 1095
        object = locate(key)
    except ErrorDuringImport, value:
1096 1097 1098
        print value
    else:
        if object:
1099
            page = html.page(describe(object),
1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114
                             html.document(object, object.__name__))
            file = open(key + '.html', 'w')
            file.write(page)
            file.close()
            print 'wrote', key + '.html'
        else:
            print 'no Python documentation found for %s' % repr(key)

def writedocs(dir, pkgpath='', done={}):
    """Write out HTML documentation for all modules in a directory tree."""
    for file in os.listdir(dir):
        path = os.path.join(dir, file)
        if ispackage(path):
            writedocs(path, pkgpath + file + '.')
        elif os.path.isfile(path):
1115
            modname = inspect.getmodulename(path)
1116 1117 1118 1119 1120
            if modname:
                modname = pkgpath + modname
                if not done.has_key(modname):
                    done[modname] = 1
                    writedoc(modname)
1121 1122 1123

class Helper:
    def __repr__(self):
1124 1125 1126
        return '''Welcome to Python %s!

To get help on a Python object, call help(object).
1127
To get help on a module or package, either import it before calling
1128
help(module) or call help('modulename').''' % sys.version[:3]
1129 1130 1131 1132 1133 1134 1135 1136 1137

    def __call__(self, *args):
        if args:
            doc(args[0])
        else:
            print repr(self)

help = Helper()

Ka-Ping Yee's avatar
Ka-Ping Yee committed
1138 1139
class Scanner:
    """A generic tree iterator."""
1140
    def __init__(self, roots, children, descendp):
Ka-Ping Yee's avatar
Ka-Ping Yee committed
1141 1142 1143
        self.roots = roots[:]
        self.state = []
        self.children = children
1144
        self.descendp = descendp
Ka-Ping Yee's avatar
Ka-Ping Yee committed
1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156

    def next(self):
        if not self.state:
            if not self.roots:
                return None
            root = self.roots.pop(0)
            self.state = [(root, self.children(root))]
        node, children = self.state[-1]
        if not children:
            self.state.pop()
            return self.next()
        child = children.pop(0)
1157
        if self.descendp(child):
Ka-Ping Yee's avatar
Ka-Ping Yee committed
1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170
            self.state.append((child, self.children(child)))
        return child

class ModuleScanner(Scanner):
    """An interruptible scanner that searches module synopses."""
    def __init__(self):
        roots = map(lambda dir: (dir, ''), pathdirs())
        Scanner.__init__(self, roots, self.submodules, self.ispackage)

    def submodules(self, (dir, package)):
        children = []
        for file in os.listdir(dir):
            path = os.path.join(dir, file)
Tim Peters's avatar
Tim Peters committed
1171
            if ispackage(path):
Ka-Ping Yee's avatar
Ka-Ping Yee committed
1172 1173 1174
                children.append((path, package + (package and '.') + file))
            else:
                children.append((path, package))
1175
        children.sort() # so that spam.py comes before spam.pyc or spam.pyo
Ka-Ping Yee's avatar
Ka-Ping Yee committed
1176 1177 1178 1179 1180
        return children

    def ispackage(self, (dir, package)):
        return ispackage(dir)

1181 1182
    def run(self, callback, key=None, completer=None):
        if key: key = lower(key)
Ka-Ping Yee's avatar
Ka-Ping Yee committed
1183 1184 1185 1186
        self.quit = 0
        seen = {}

        for modname in sys.builtin_module_names:
1187 1188
            if modname != '__main__':
                seen[modname] = 1
1189 1190 1191 1192 1193 1194
                if key is None:
                    callback(None, modname, '')
                else:
                    desc = split(freshimport(modname).__doc__ or '', '\n')[0]
                    if find(lower(modname + ' - ' + desc), key) >= 0:
                        callback(None, modname, desc)
Ka-Ping Yee's avatar
Ka-Ping Yee committed
1195 1196 1197 1198 1199

        while not self.quit:
            node = self.next()
            if not node: break
            path, package = node
1200
            modname = inspect.getmodulename(path)
Ka-Ping Yee's avatar
Ka-Ping Yee committed
1201 1202 1203
            if os.path.isfile(path) and modname:
                modname = package + (package and '.') + modname
                if not seen.has_key(modname):
1204
                    seen[modname] = 1 # if we see spam.py, skip spam.pyc
1205 1206 1207
                    if key is None:
                        callback(path, modname, '')
                    else:
1208 1209 1210
                        desc = synopsis(path) or ''
                        if find(lower(modname + ' - ' + desc), key) >= 0:
                            callback(path, modname, desc)
Ka-Ping Yee's avatar
Ka-Ping Yee committed
1211
        if completer: completer()
1212 1213 1214

def apropos(key):
    """Print all the one-line module summaries that contain a substring."""
Ka-Ping Yee's avatar
Ka-Ping Yee committed
1215 1216 1217
    def callback(path, modname, desc):
        if modname[-9:] == '.__init__':
            modname = modname[:-9] + ' (package)'
1218 1219 1220 1221
        print modname, desc and '- ' + desc
    try: import warnings
    except ImportError: pass
    else: warnings.filterwarnings('ignore') # ignore problems during import
1222
    ModuleScanner().run(callback, key)
1223 1224 1225

# --------------------------------------------------- web browser interface

1226
def serve(port, callback=None, completer=None):
1227
    import BaseHTTPServer, mimetools, select
1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240

    # 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):
Ka-Ping Yee's avatar
Ka-Ping Yee committed
1241 1242 1243 1244
            try:
                self.send_response(200)
                self.send_header('Content-Type', 'text/html')
                self.end_headers()
1245
                self.wfile.write(html.page(title, contents))
Ka-Ping Yee's avatar
Ka-Ping Yee committed
1246
            except IOError: pass
1247 1248 1249 1250 1251 1252 1253

        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:
1254 1255
                    obj = locate(path)
                except ErrorDuringImport, value:
1256
                    self.send_document(path, html.escape(str(value)))
1257
                    return
1258 1259
                if obj:
                    self.send_document(describe(obj), html.document(obj, path))
1260 1261
                else:
                    self.send_document(path,
1262
'no Python documentation found for %s' % repr(path))
1263 1264
            else:
                heading = html.heading(
Ka-Ping Yee's avatar
Ka-Ping Yee committed
1265 1266
'<big><big><strong>Python: Index of Modules</strong></big></big>',
'#ffffff', '#7799ee')
1267 1268
                def bltinlink(name):
                    return '<a href="%s.html">%s</a>' % (name, name)
1269 1270
                names = filter(lambda x: x != '__main__',
                               sys.builtin_module_names)
1271 1272 1273 1274
                contents = html.multicolumn(names, bltinlink)
                indices = ['<p>' + html.bigsection(
                    'Built-in Modules', '#ffffff', '#ee77aa', contents)]

1275 1276 1277
                seen = {}
                for dir in pathdirs():
                    indices.append(html.index(dir, seen))
1278
                contents = heading + join(indices) + '''<p align=right>
Ka-Ping Yee's avatar
Ka-Ping Yee committed
1279
<small><small><font color="#909090" face="helvetica, arial"><strong>
1280
pydoc</strong> by Ka-Ping Yee &lt;ping@lfw.org&gt;</font></small></small>'''
Ka-Ping Yee's avatar
Ka-Ping Yee committed
1281
                self.send_document('Index of Modules', contents)
1282 1283 1284

        def log_message(self, *args): pass

1285
    class DocServer(BaseHTTPServer.HTTPServer):
Ka-Ping Yee's avatar
Ka-Ping Yee committed
1286
        def __init__(self, port, callback):
1287
            host = (sys.platform == 'mac') and '127.0.0.1' or 'localhost'
1288
            self.address = ('', port)
1289
            self.url = 'http://%s:%d/' % (host, port)
1290
            self.callback = callback
Ka-Ping Yee's avatar
Ka-Ping Yee committed
1291 1292 1293 1294 1295 1296 1297 1298
            self.base.__init__(self, self.address, self.handler)

        def serve_until_quit(self):
            import select
            self.quit = 0
            while not self.quit:
                rd, wr, ex = select.select([self.socket.fileno()], [], [], 1)
                if rd: self.handle_request()
1299 1300 1301

        def server_activate(self):
            self.base.server_activate(self)
Ka-Ping Yee's avatar
Ka-Ping Yee committed
1302
            if self.callback: self.callback(self)
1303 1304 1305 1306 1307

    DocServer.base = BaseHTTPServer.HTTPServer
    DocServer.handler = DocHandler
    DocHandler.MessageClass = Message
    try:
1308 1309 1310 1311 1312
        try:
            DocServer(port, callback).serve_until_quit()
        except (KeyboardInterrupt, select.error):
            pass
    finally:
1313
        if completer: completer()
Ka-Ping Yee's avatar
Ka-Ping Yee committed
1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340

# ----------------------------------------------------- graphical interface

def gui():
    """Graphical interface (starts web server and pops up a control window)."""
    class GUI:
        def __init__(self, window, port=7464):
            self.window = window
            self.server = None
            self.scanner = None

            import Tkinter
            self.server_frm = Tkinter.Frame(window)
            self.title_lbl = Tkinter.Label(self.server_frm,
                text='Starting server...\n ')
            self.open_btn = Tkinter.Button(self.server_frm,
                text='open browser', command=self.open, state='disabled')
            self.quit_btn = Tkinter.Button(self.server_frm,
                text='quit serving', command=self.quit, state='disabled')

            self.search_frm = Tkinter.Frame(window)
            self.search_lbl = Tkinter.Label(self.search_frm, text='Search for')
            self.search_ent = Tkinter.Entry(self.search_frm)
            self.search_ent.bind('<Return>', self.search)
            self.stop_btn = Tkinter.Button(self.search_frm,
                text='stop', pady=0, command=self.stop, state='disabled')
            if sys.platform == 'win32':
1341
                # Trying to hide and show this button crashes under Windows.
Ka-Ping Yee's avatar
Ka-Ping Yee committed
1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355
                self.stop_btn.pack(side='right')

            self.window.title('pydoc')
            self.window.protocol('WM_DELETE_WINDOW', self.quit)
            self.title_lbl.pack(side='top', fill='x')
            self.open_btn.pack(side='left', fill='x', expand=1)
            self.quit_btn.pack(side='right', fill='x', expand=1)
            self.server_frm.pack(side='top', fill='x')

            self.search_lbl.pack(side='left')
            self.search_ent.pack(side='right', fill='x', expand=1)
            self.search_frm.pack(side='top', fill='x')
            self.search_ent.focus_set()

1356
            font = ('helvetica', sys.platform == 'win32' and 8 or 10)
1357
            self.result_lst = Tkinter.Listbox(window, font=font, height=6)
Ka-Ping Yee's avatar
Ka-Ping Yee committed
1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384
            self.result_lst.bind('<Button-1>', self.select)
            self.result_lst.bind('<Double-Button-1>', self.goto)
            self.result_scr = Tkinter.Scrollbar(window,
                orient='vertical', command=self.result_lst.yview)
            self.result_lst.config(yscrollcommand=self.result_scr.set)

            self.result_frm = Tkinter.Frame(window)
            self.goto_btn = Tkinter.Button(self.result_frm,
                text='go to selected', command=self.goto)
            self.hide_btn = Tkinter.Button(self.result_frm,
                text='hide results', command=self.hide)
            self.goto_btn.pack(side='left', fill='x', expand=1)
            self.hide_btn.pack(side='right', fill='x', expand=1)

            self.window.update()
            self.minwidth = self.window.winfo_width()
            self.minheight = self.window.winfo_height()
            self.bigminheight = (self.server_frm.winfo_reqheight() +
                                 self.search_frm.winfo_reqheight() +
                                 self.result_lst.winfo_reqheight() +
                                 self.result_frm.winfo_reqheight())
            self.bigwidth, self.bigheight = self.minwidth, self.bigminheight
            self.expanded = 0
            self.window.wm_geometry('%dx%d' % (self.minwidth, self.minheight))
            self.window.wm_minsize(self.minwidth, self.minheight)

            import threading
1385 1386
            threading.Thread(
                target=serve, args=(port, self.ready, self.quit)).start()
Ka-Ping Yee's avatar
Ka-Ping Yee committed
1387 1388 1389 1390 1391 1392 1393 1394

        def ready(self, server):
            self.server = server
            self.title_lbl.config(
                text='Python documentation server at\n' + server.url)
            self.open_btn.config(state='normal')
            self.quit_btn.config(state='normal')

1395 1396 1397 1398 1399 1400
        def open(self, event=None, url=None):
            url = url or self.server.url
            try:
                import webbrowser
                webbrowser.open(url)
            except ImportError: # pre-webbrowser.py compatibility
1401
                if sys.platform == 'win32':
1402 1403
                    os.system('start "%s"' % url)
                elif sys.platform == 'mac':
1404
                    try: import ic
1405
                    except ImportError: pass
1406
                    else: ic.launchurl(url)
1407 1408 1409
                else:
                    rc = os.system('netscape -remote "openURL(%s)" &' % url)
                    if rc: os.system('netscape "%s" &' % url)
Ka-Ping Yee's avatar
Ka-Ping Yee committed
1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459

        def quit(self, event=None):
            if self.server:
                self.server.quit = 1
            self.window.quit()

        def search(self, event=None):
            key = self.search_ent.get()
            self.stop_btn.pack(side='right')
            self.stop_btn.config(state='normal')
            self.search_lbl.config(text='Searching for "%s"...' % key)
            self.search_ent.forget()
            self.search_lbl.pack(side='left')
            self.result_lst.delete(0, 'end')
            self.goto_btn.config(state='disabled')
            self.expand()

            import threading
            if self.scanner:
                self.scanner.quit = 1
            self.scanner = ModuleScanner()
            threading.Thread(target=self.scanner.run,
                             args=(key, self.update, self.done)).start()

        def update(self, path, modname, desc):
            if modname[-9:] == '.__init__':
                modname = modname[:-9] + ' (package)'
            self.result_lst.insert('end',
                modname + ' - ' + (desc or '(no description)'))

        def stop(self, event=None):
            if self.scanner:
                self.scanner.quit = 1
                self.scanner = None

        def done(self):
            self.scanner = None
            self.search_lbl.config(text='Search for')
            self.search_lbl.pack(side='left')
            self.search_ent.pack(side='right', fill='x', expand=1)
            if sys.platform != 'win32': self.stop_btn.forget()
            self.stop_btn.config(state='disabled')

        def select(self, event=None):
            self.goto_btn.config(state='normal')

        def goto(self, event=None):
            selection = self.result_lst.curselection()
            if selection:
                modname = split(self.result_lst.get(selection[0]))[0]
1460
                self.open(url=self.server.url + modname + '.html')
Ka-Ping Yee's avatar
Ka-Ping Yee committed
1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489

        def collapse(self):
            if not self.expanded: return
            self.result_frm.forget()
            self.result_scr.forget()
            self.result_lst.forget()
            self.bigwidth = self.window.winfo_width()
            self.bigheight = self.window.winfo_height()
            self.window.wm_geometry('%dx%d' % (self.minwidth, self.minheight))
            self.window.wm_minsize(self.minwidth, self.minheight)
            self.expanded = 0

        def expand(self):
            if self.expanded: return
            self.result_frm.pack(side='bottom', fill='x')
            self.result_scr.pack(side='right', fill='y')
            self.result_lst.pack(side='top', fill='both', expand=1)
            self.window.wm_geometry('%dx%d' % (self.bigwidth, self.bigheight))
            self.window.wm_minsize(self.minwidth, self.bigminheight)
            self.expanded = 1

        def hide(self, event=None):
            self.stop()
            self.collapse()

    import Tkinter
    try:
        gui = GUI(Tkinter.Tk())
        Tkinter.mainloop()
1490
    except KeyboardInterrupt:
Ka-Ping Yee's avatar
Ka-Ping Yee committed
1491
        pass
1492 1493 1494

# -------------------------------------------------- command-line interface

1495 1496 1497
def ispath(x):
    return type(x) is types.StringType and find(x, os.sep) >= 0

1498
def cli():
Ka-Ping Yee's avatar
Ka-Ping Yee committed
1499
    """Command-line interface (looks at sys.argv to decide what to do)."""
1500 1501 1502
    import getopt
    class BadUsage: pass

1503
    # Scripts don't get the current directory in their path by default.
1504 1505 1506
    scriptdir = os.path.dirname(sys.argv[0])
    if scriptdir in sys.path:
        sys.path.remove(scriptdir)
1507
    sys.path.insert(0, '.')
Ka-Ping Yee's avatar
Ka-Ping Yee committed
1508

1509
    try:
Ka-Ping Yee's avatar
Ka-Ping Yee committed
1510
        opts, args = getopt.getopt(sys.argv[1:], 'gk:p:w')
1511 1512 1513
        writing = 0

        for opt, val in opts:
Ka-Ping Yee's avatar
Ka-Ping Yee committed
1514 1515 1516
            if opt == '-g':
                gui()
                return
1517
            if opt == '-k':
Ka-Ping Yee's avatar
Ka-Ping Yee committed
1518 1519
                apropos(val)
                return
1520 1521 1522 1523 1524
            if opt == '-p':
                try:
                    port = int(val)
                except ValueError:
                    raise BadUsage
Ka-Ping Yee's avatar
Ka-Ping Yee committed
1525
                def ready(server):
1526 1527 1528 1529
                    print 'pydoc server ready at %s' % server.url
                def stopped():
                    print 'pydoc server stopped'
                serve(port, ready, stopped)
Ka-Ping Yee's avatar
Ka-Ping Yee committed
1530
                return
1531 1532
            if opt == '-w':
                writing = 1
Ka-Ping Yee's avatar
Ka-Ping Yee committed
1533 1534 1535 1536

        if not args: raise BadUsage
        for arg in args:
            try:
1537
                if ispath(arg) and os.path.isfile(arg):
Ka-Ping Yee's avatar
Ka-Ping Yee committed
1538
                    arg = importfile(arg)
1539 1540 1541 1542 1543 1544
                if writing:
                    if ispath(arg) and os.path.isdir(arg):
                        writedocs(arg)
                    else:
                        writedoc(arg)
                else:
1545
                    doc(arg)
1546
            except ErrorDuringImport, value:
1547
                print value
1548 1549

    except (getopt.error, BadUsage):
Ka-Ping Yee's avatar
Ka-Ping Yee committed
1550 1551 1552 1553 1554 1555 1556 1557
        cmd = sys.argv[0]
        print """pydoc - the Python documentation tool

%s <name> ...
    Show text documentation on something.  <name> may be the name of a
    function, module, or package, or a dotted reference to a class or
    function within a module or module in a package.  If <name> contains
    a '%s', it is used as the path to a Python source file to document.
1558 1559

%s -k <keyword>
Ka-Ping Yee's avatar
Ka-Ping Yee committed
1560
    Search for a keyword in the synopsis lines of all available modules.
1561 1562 1563 1564

%s -p <port>
    Start an HTTP server on the given port on the local machine.

Ka-Ping Yee's avatar
Ka-Ping Yee committed
1565
%s -g
1566
    Pop up a graphical interface for finding and serving documentation.
Ka-Ping Yee's avatar
Ka-Ping Yee committed
1567 1568 1569

%s -w <name> ...
    Write out the HTML documentation for a module to a file in the current
1570 1571
    directory.  If <name> contains a '%s', it is treated as a filename; if
    it names a directory, documentation is written for all the contents.
Ka-Ping Yee's avatar
Ka-Ping Yee committed
1572 1573 1574
""" % (cmd, os.sep, cmd, cmd, cmd, cmd, os.sep)

if __name__ == '__main__': cli()