Make force-loading optional; don't force-load in interactive mode.

Make synopsis() load modules as '__temp__' so they don't clobber anything.
Change "constants" section to "data" section.
Don't show __builtins__ or __doc__ in "data" section.
For Bob Weiner: don't boldface text in Emacs shells or dumb terminals.
Remove Helper.__repr__ (it really belongs in site.py, and should be                 guarded by a check for len(inspect.stack) <= 2).
This commit is contained in:
Ka-Ping Yee 2001-04-13 09:55:49 +00:00
parent 202c99b2e0
commit dec96e92ae
1 changed files with 147 additions and 133 deletions

View File

@ -53,36 +53,6 @@ class or function within a module or module in a package. If the
# --------------------------------------------------------- common routines # --------------------------------------------------------- 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:
info = inspect.getmoduleinfo(filename)
file = open(filename)
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
line = file.readline()
while line[:1] == '#' or not strip(line):
line = file.readline()
if not line: break
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
file.close()
cache[filename] = (mtime, result)
return result
def pathdirs(): def pathdirs():
"""Convert sys.path into a list of absolute, existing, unique paths.""" """Convert sys.path into a list of absolute, existing, unique paths."""
dirs = [] dirs = []
@ -116,12 +86,11 @@ def classname(object, modname):
name = object.__module__ + '.' + name name = object.__module__ + '.' + name
return name return name
def isconstant(object): def isdata(object):
"""Check if an object is of a type that probably means it's a constant.""" """Check if an object is of a type that probably means it's data."""
return type(object) in [ return not (inspect.ismodule(object) or inspect.isclass(object) or
types.FloatType, types.IntType, types.ListType, types.LongType, inspect.isroutine(object) or inspect.isframe(object) or
types.StringType, types.TupleType, types.TypeType, inspect.istraceback(object) or inspect.iscode(object))
hasattr(types, 'UnicodeType') and types.UnicodeType or 0]
def replace(text, *pairs): def replace(text, *pairs):
"""Do a series of global replacements on a string.""" """Do a series of global replacements on a string."""
@ -156,6 +125,46 @@ def allmethods(cl):
methods[key] = getattr(cl, key) methods[key] = getattr(cl, key)
return methods return methods
# ----------------------------------------------------- module manipulation
def ispackage(path):
"""Guess whether a path refers to a package directory."""
if os.path.isdir(path):
for ext in ['.py', '.pyc', '.pyo']:
if os.path.isfile(os.path.join(path, '__init__' + ext)):
return 1
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:
info = inspect.getmoduleinfo(filename)
file = open(filename)
if info and 'b' in info[2]: # binary modules have to be imported
try: module = imp.load_module('__temp__', file, filename, info[1:])
except: return None
result = split(module.__doc__ or '', '\n')[0]
del sys.modules['__temp__']
else: # text modules can be directly examined
line = file.readline()
while line[:1] == '#' or not strip(line):
line = file.readline()
if not line: break
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
file.close()
cache[filename] = (mtime, result)
return result
class ErrorDuringImport(Exception): class ErrorDuringImport(Exception):
"""Errors that occurred while trying to import something to document it.""" """Errors that occurred while trying to import something to document it."""
def __init__(self, filename, (exc, value, tb)): def __init__(self, filename, (exc, value, tb)):
@ -189,12 +198,49 @@ def importfile(path):
file.close() file.close()
return module return module
def ispackage(path): def safeimport(path, forceload=0, cache={}):
"""Guess whether a path refers to a package directory.""" """Import a module; handle errors; return None if the module isn't found.
if os.path.isdir(path):
for ext in ['.py', '.pyc', '.pyo']: If the module *is* found but an exception occurs, it's wrapped in an
if os.path.isfile(os.path.join(path, '__init__' + ext)): ErrorDuringImport exception and reraised. Unlike __import__, if a
return 1 package path is specified, the module at the end of the path is returned,
not the package at the beginning. If the optional 'forceload' argument
is 1, we reload the module from disk (unless it's a dynamic extension)."""
if forceload and 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:
# Python never loads a dynamic extension a second time from the
# same path, even if the file is changed or missing. Deleting
# the entry in sys.modules doesn't help for dynamic extensions,
# so we're not even going to try to keep them up to date.
info = inspect.getmoduleinfo(sys.modules[path].__file__)
if info[3] != imp.C_EXTENSION:
cache[path] = sys.modules[path] # prevent module from clearing
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
else:
# 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
return module
# ---------------------------------------------------- formatter base class # ---------------------------------------------------- formatter base class
@ -221,7 +267,8 @@ class HTMLRepr(Repr):
"""Class for safely making an HTML representation of a Python object.""" """Class for safely making an HTML representation of a Python object."""
def __init__(self): def __init__(self):
Repr.__init__(self) Repr.__init__(self)
self.maxlist = self.maxtuple = self.maxdict = 10 self.maxlist = self.maxtuple = 20
self.maxdict = 10
self.maxstring = self.maxother = 100 self.maxstring = self.maxother = 100
def escape(self, text): def escape(self, text):
@ -344,12 +391,11 @@ def namelink(self, name, *dicts):
def classlink(self, object, modname): def classlink(self, object, modname):
"""Make a link for a class.""" """Make a link for a class."""
name = classname(object, modname) name, module = object.__name__, sys.modules.get(object.__module__)
if sys.modules.has_key(object.__module__) and \ if hasattr(module, name) and getattr(module, name) is object:
getattr(sys.modules[object.__module__], object.__name__) is object:
return '<a href="%s.html#%s">%s</a>' % ( return '<a href="%s.html#%s">%s</a>' % (
object.__module__, object.__name__, name) module.__name__, name, classname(object, modname))
return name return classname(object, modname)
def modulelink(self, object): def modulelink(self, object):
"""Make a link for a module.""" """Make a link for a module."""
@ -475,9 +521,10 @@ def docmodule(self, object, name=None, mod=None):
funcs.append((key, value)) funcs.append((key, value))
fdict[key] = '#-' + key fdict[key] = '#-' + key
if inspect.isfunction(value): fdict[value] = fdict[key] if inspect.isfunction(value): fdict[value] = fdict[key]
constants = [] data = []
for key, value in inspect.getmembers(object, isconstant): for key, value in inspect.getmembers(object, isdata):
constants.append((key, value)) if key not in ['__builtins__', '__doc__']:
data.append((key, value))
doc = self.markup(getdoc(object), self.preformat, fdict, cdict) doc = self.markup(getdoc(object), self.preformat, fdict, cdict)
doc = doc and '<tt>%s</tt>' % doc doc = doc and '<tt>%s</tt>' % doc
@ -518,12 +565,12 @@ def docmodule(self, object, name=None, mod=None):
contents.append(self.document(value, key, name, fdict, cdict)) contents.append(self.document(value, key, name, fdict, cdict))
result = result + self.bigsection( result = result + self.bigsection(
'Functions', '#ffffff', '#eeaa77', join(contents)) 'Functions', '#ffffff', '#eeaa77', join(contents))
if constants: if data:
contents = [] contents = []
for key, value in constants: for key, value in data:
contents.append(self.document(value, key)) contents.append(self.document(value, key))
result = result + self.bigsection( result = result + self.bigsection(
'Constants', '#ffffff', '#55aa55', join(contents, '<br>')) 'Data', '#ffffff', '#55aa55', join(contents, '<br>\n'))
if hasattr(object, '__author__'): if hasattr(object, '__author__'):
contents = self.markup(str(object.__author__), self.preformat) contents = self.markup(str(object.__author__), self.preformat)
result = result + self.bigsection( result = result + self.bigsection(
@ -665,7 +712,8 @@ class TextRepr(Repr):
"""Class for safely making a text representation of a Python object.""" """Class for safely making a text representation of a Python object."""
def __init__(self): def __init__(self):
Repr.__init__(self) Repr.__init__(self)
self.maxlist = self.maxtuple = self.maxdict = 10 self.maxlist = self.maxtuple = 20
self.maxdict = 10
self.maxstring = self.maxother = 100 self.maxstring = self.maxother = 100
def repr1(self, x, level): def repr1(self, x, level):
@ -754,9 +802,10 @@ def docmodule(self, object, name=None, mod=None):
for key, value in inspect.getmembers(object, inspect.isroutine): for key, value in inspect.getmembers(object, inspect.isroutine):
if inspect.isbuiltin(value) or inspect.getmodule(value) is object: if inspect.isbuiltin(value) or inspect.getmodule(value) is object:
funcs.append((key, value)) funcs.append((key, value))
constants = [] data = []
for key, value in inspect.getmembers(object, isconstant): for key, value in inspect.getmembers(object, isdata):
constants.append((key, value)) if key not in ['__builtins__', '__doc__']:
data.append((key, value))
if hasattr(object, '__path__'): if hasattr(object, '__path__'):
modpkgs = [] modpkgs = []
@ -785,11 +834,11 @@ def docmodule(self, object, name=None, mod=None):
contents.append(self.document(value, key, name)) contents.append(self.document(value, key, name))
result = result + self.section('FUNCTIONS', join(contents, '\n')) result = result + self.section('FUNCTIONS', join(contents, '\n'))
if constants: if data:
contents = [] contents = []
for key, value in constants: for key, value in data:
contents.append(self.docother(value, key, name, 70)) contents.append(self.docother(value, key, name, 70))
result = result + self.section('CONSTANTS', join(contents, '\n')) result = result + self.section('DATA', join(contents, '\n'))
if hasattr(object, '__version__'): if hasattr(object, '__version__'):
version = str(object.__version__) version = str(object.__version__)
@ -903,13 +952,15 @@ def getpager():
return plainpager return plainpager
if os.environ.has_key('PAGER'): if os.environ.has_key('PAGER'):
if sys.platform == 'win32': # pipes completely broken in Windows if sys.platform == 'win32': # pipes completely broken in Windows
return lambda a: tempfilepager(a, os.environ['PAGER']) return lambda text: tempfilepager(plain(text), os.environ['PAGER'])
elif os.environ.get('TERM') in ['dumb', 'emacs']:
return lambda text: pipepager(plain(text), os.environ['PAGER'])
else: else:
return lambda a: pipepager(a, os.environ['PAGER']) return lambda text: pipepager(text, os.environ['PAGER'])
if sys.platform == 'win32': if sys.platform == 'win32':
return lambda a: tempfilepager(a, 'more <') return lambda text: tempfilepager(plain(text), 'more <')
if hasattr(os, 'system') and os.system('less 2>/dev/null') == 0: if hasattr(os, 'system') and os.system('less 2>/dev/null') == 0:
return lambda a: pipepager(a, 'less') return lambda text: pipepager(text, 'less')
import tempfile import tempfile
filename = tempfile.mktemp() filename = tempfile.mktemp()
@ -922,6 +973,10 @@ def getpager():
finally: finally:
os.unlink(filename) os.unlink(filename)
def plain(text):
"""Remove boldface formatting from text."""
return re.sub('.\b', '', text)
def pipepager(text, cmd): def pipepager(text, cmd):
"""Page through text by feeding it to another program.""" """Page through text by feeding it to another program."""
pipe = os.popen(cmd, 'w') pipe = os.popen(cmd, 'w')
@ -943,10 +998,6 @@ def tempfilepager(text, cmd):
finally: finally:
os.unlink(filename) os.unlink(filename)
def plain(text):
"""Remove boldface formatting from text."""
return re.sub('.\b', '', text)
def ttypager(text): def ttypager(text):
"""Page through text on a text terminal.""" """Page through text on a text terminal."""
lines = split(plain(text), '\n') lines = split(plain(text), '\n')
@ -1010,49 +1061,12 @@ def describe(thing):
return 'instance of ' + thing.__class__.__name__ return 'instance of ' + thing.__class__.__name__
return type(thing).__name__ return type(thing).__name__
def freshimport(path, cache={}): def locate(path, forceload=0):
"""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:
# Python never loads a dynamic extension a second time from the
# same path, even if the file is changed or missing. Deleting
# the entry in sys.modules doesn't help for dynamic extensions,
# so we're not even going to try to keep them up to date.
info = inspect.getmoduleinfo(sys.modules[path].__file__)
if info[3] != imp.C_EXTENSION:
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
else:
# 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
return module
def locate(path):
"""Locate an object by name or dotted path, importing as necessary.""" """Locate an object by name or dotted path, importing as necessary."""
parts = split(path, '.') parts = split(path, '.')
module, n = None, 0 module, n = None, 0
while n < len(parts): while n < len(parts):
nextmodule = freshimport(join(parts[:n+1], '.')) nextmodule = safeimport(join(parts[:n+1], '.'), forceload)
if nextmodule: module, n = nextmodule, n + 1 if nextmodule: module, n = nextmodule, n + 1
else: break else: break
if module: if module:
@ -1071,12 +1085,12 @@ def locate(path):
text = TextDoc() text = TextDoc()
html = HTMLDoc() html = HTMLDoc()
def doc(thing, title='Python Library Documentation: %s'): def doc(thing, title='Python Library Documentation: %s', forceload=0):
"""Display text documentation, given an object or a path to an object.""" """Display text documentation, given an object or a path to an object."""
suffix, name = '', None suffix, name = '', None
if type(thing) is type(''): if type(thing) is type(''):
try: try:
object = locate(thing) object = locate(thing, forceload)
except ErrorDuringImport, value: except ErrorDuringImport, value:
print value print value
return return
@ -1094,10 +1108,10 @@ def doc(thing, title='Python Library Documentation: %s'):
suffix = ' in module ' + module.__name__ suffix = ' in module ' + module.__name__
pager(title % (desc + suffix) + '\n\n' + text.document(thing, name)) pager(title % (desc + suffix) + '\n\n' + text.document(thing, name))
def writedoc(key): def writedoc(key, forceload=0):
"""Write HTML documentation to a file in the current directory.""" """Write HTML documentation to a file in the current directory."""
try: try:
object = locate(key) object = locate(key, forceload)
except ErrorDuringImport, value: except ErrorDuringImport, value:
print value print value
else: else:
@ -1111,12 +1125,13 @@ def writedoc(key):
else: else:
print 'no Python documentation found for %s' % repr(key) print 'no Python documentation found for %s' % repr(key)
def writedocs(dir, pkgpath='', done={}): def writedocs(dir, pkgpath='', done=None):
"""Write out HTML documentation for all modules in a directory tree.""" """Write out HTML documentation for all modules in a directory tree."""
if done is None: done = {}
for file in os.listdir(dir): for file in os.listdir(dir):
path = os.path.join(dir, file) path = os.path.join(dir, file)
if ispackage(path): if ispackage(path):
writedocs(path, pkgpath + file + '.') writedocs(path, pkgpath + file + '.', done)
elif os.path.isfile(path): elif os.path.isfile(path):
modname = inspect.getmodulename(path) modname = inspect.getmodulename(path)
if modname: if modname:
@ -1251,26 +1266,12 @@ def __init__(self, input, output):
if dir and os.path.isdir(os.path.join(dir, 'lib')): if dir and os.path.isdir(os.path.join(dir, 'lib')):
self.docdir = dir self.docdir = dir
def __repr__(self):
self()
return ''
def __call__(self, request=None): def __call__(self, request=None):
if request is not None: if request is not None:
self.help(request) self.help(request)
else: else:
self.intro() self.intro()
self.output.write('\n') self.interact()
while 1:
self.output.write('help> ')
self.output.flush()
try:
request = self.input.readline()
if not request: break
except KeyboardInterrupt: break
request = strip(replace(request, '"', '', "'", ''))
if lower(request) in ['q', 'quit']: break
self.help(request)
self.output.write(''' self.output.write('''
You're now leaving help and returning to the Python interpreter. You're now leaving help and returning to the Python interpreter.
If you want to ask for help on a particular object directly from the If you want to ask for help on a particular object directly from the
@ -1278,6 +1279,19 @@ def __call__(self, request=None):
has the same effect as typing a particular string at the help> prompt. has the same effect as typing a particular string at the help> prompt.
''') ''')
def interact(self):
self.output.write('\n')
while 1:
self.output.write('help> ')
self.output.flush()
try:
request = self.input.readline()
if not request: break
except KeyboardInterrupt: break
request = strip(replace(request, '"', '', "'", ''))
if lower(request) in ['q', 'quit']: break
self.help(request)
def help(self, request): def help(self, request):
if type(request) is type(''): if type(request) is type(''):
if request == 'help': self.intro() if request == 'help': self.intro()
@ -1361,8 +1375,8 @@ def showtopic(self, topic):
self.output.write('could not read docs from %s\n' % filename) self.output.write('could not read docs from %s\n' % filename)
return return
divpat = re.compile('<div[^>]*navigat.*?</div[^>]*>', re.I | re.S) divpat = re.compile('<div[^>]*navigat.*?</div.*?>', re.I | re.S)
addrpat = re.compile('<address[^>]*>.*?</address[^>]*>', re.I | re.S) addrpat = re.compile('<address.*?>.*?</address.*?>', re.I | re.S)
document = re.sub(addrpat, '', re.sub(divpat, '', file.read())) document = re.sub(addrpat, '', re.sub(divpat, '', file.read()))
file.close() file.close()
@ -1460,7 +1474,7 @@ def run(self, callback, key=None, completer=None):
if key is None: if key is None:
callback(None, modname, '') callback(None, modname, '')
else: else:
desc = split(freshimport(modname).__doc__ or '', '\n')[0] desc = split(__import__(modname).__doc__ or '', '\n')[0]
if find(lower(modname + ' - ' + desc), key) >= 0: if find(lower(modname + ' - ' + desc), key) >= 0:
callback(None, modname, desc) callback(None, modname, desc)
@ -1522,7 +1536,7 @@ def do_GET(self):
if path[:1] == '/': path = path[1:] if path[:1] == '/': path = path[1:]
if path and path != '.': if path and path != '.':
try: try:
obj = locate(path) obj = locate(path, forceload=1)
except ErrorDuringImport, value: except ErrorDuringImport, value:
self.send_document(path, html.escape(str(value))) self.send_document(path, html.escape(str(value)))
return return