Index: Lib/pydoc.py |
=================================================================== |
--- Lib/pydoc.py (revision 86529) |
+++ Lib/pydoc.py (working copy) |
@@ -15,11 +15,16 @@ |
Run "pydoc -k <keyword>" to search for a keyword in the synopsis lines |
of all available modules. |
-Run "pydoc -p <port>" to start an HTTP server on a given port on the |
-local machine to generate documentation web pages. |
+Run "pydoc -p <port>" to start an HTTP server on the given port on the |
+local machine. Port number 0 can be used to get an arbitrary unused port. |
+Run "pydoc -b" to start an HTTP server on an arbitrary unused port and |
+open a web browser to interactively browse documentation. The -p option |
+can be used with the -b option to explicitly specify the server port. |
+ |
For platforms without a command line, "pydoc -g" starts the HTTP server |
and also pops up a little window for controlling it. |
+(The "-g" option is deprecated.) |
Run "pydoc -w <name>" to write out the HTML documentation for a module |
to a file named "<name>.html". |
@@ -51,7 +56,7 @@ |
# the current directory is changed with os.chdir(), an incorrect |
# path will be displayed. |
-import sys, imp, os, re, inspect, builtins, pkgutil |
+import sys, imp, os, re, inspect, builtins, pkgutil, time, warnings |
from reprlib import Repr |
from traceback import extract_tb as _extract_tb |
from collections import deque |
@@ -512,6 +517,10 @@ |
else: |
text = name |
return '<a href="%s">%s</a>' % (url, text) |
+ |
+ def filelink(self, url, path): |
+ """Make a link to source file.""" |
+ return '<a href="file:%s">%s</a>' % (url, path) |
def markup(self, text, escape=None, funcs={}, classes={}, methods={}): |
"""Mark up some plain text, given a context of symbols to look for. |
@@ -591,7 +600,7 @@ |
if sys.platform == 'win32': |
import nturl2path |
url = nturl2path.pathname2url(path) |
- filelink = '<a href="file:%s">%s</a>' % (url, path) |
+ filelink = self.filelink(url, path) |
except TypeError: |
filelink = '(built-in)' |
info = [] |
@@ -1843,6 +1852,35 @@ |
'Related help topics: ' + ', '.join(xrefs.split()) + '\n') |
self.output.write('\n%s\n' % buffer.getvalue()) |
+ def _gettopic(self, topic, more_xrefs=''): |
+ """ Returns unbuffered tuple of (topic, xrefs). |
+ |
+ If an error occurs, topic is the error message, and xrefs is ''. |
+ This function duplicates the showtopic method but returns it's |
+ result directly so it can be formatted for display in an html page. |
+ """ |
+ try: |
+ import pydoc_data.topics |
+ except ImportError: |
+ return(''' |
+Sorry, topic and keyword documentation is not available because the |
+module "pydoc_data.topics" could not be found. |
+''' , '') |
+ return |
+ target = self.topics.get(topic, self.keywords.get(topic)) |
+ if not target: |
+ return 'no documentation found for %s' % repr(topic), '' |
+ if type(target) is type(''): |
+ return self._gettopic(target, more_xrefs) |
+ label, xrefs = target |
+ try: |
+ doc = pydoc_data.topics.topics[label] |
+ except KeyError: |
+ return 'no documentation found for %s' % repr(topic), '' |
+ if more_xrefs: |
+ xrefs = (xrefs or '') + ' ' + more_xrefs |
+ return doc, xrefs |
+ |
def showsymbol(self, symbol): |
target = self.symbols[symbol] |
topic, _, xrefs = target.partition(' ') |
@@ -1924,6 +1962,14 @@ |
for importer, modname, ispkg in pkgutil.walk_packages(onerror=onerror): |
if self.quit: |
break |
+ |
+ # XXX Skipping this file is a get-around for bug that causes python |
+ # to crash with a segfault. http://bugs.python.org/issue9319 |
+ # |
+ # TODO: Remove this once the bug is fixed. |
+ if modname == "test.badsyntax_pep3120": |
+ continue |
+ |
if key is None: |
callback(None, modname, '') |
else: |
@@ -1979,6 +2025,9 @@ |
def serve(port, callback=None, completer=None): |
import http.server, email.message, select |
+ msg = """The pydoc.serve() function is deprecated.""" |
+ warnings.warn(msg, DeprecationWarning, stacklevel=2) |
+ |
class DocHandler(http.server.BaseHTTPRequestHandler): |
def send_document(self, title, contents): |
try: |
@@ -2058,6 +2107,11 @@ |
def gui(): |
"""Graphical interface (starts web server and pops up a control window).""" |
+ |
+ msg = """The pydoc.gui() function and "pydoc -g" option are depreciated, |
+ use "pydoc.browse() function and "pydoc -b" option instead.""" |
+ warnings.warn(msg, DeprecationWarning, stacklevel=2) |
+ |
class GUI: |
def __init__(self, window, port=7464): |
self.window = window |
@@ -2236,7 +2290,386 @@ |
root.destroy() |
except KeyboardInterrupt: |
pass |
+ |
+ |
+# --------------------------------------- enhanced web browser interface |
+def _startserver(urlhandler, port): |
+ """ Start a HTTP server thread on a specific port. |
+ Use address http://localhost:<port #>/ |
+ |
+ Start an HTML/text server thread, so HTML or text documents can be |
+ browsed dynamically and interactively with a web browser. |
+ |
+ Example use |
+ =========== |
+ |
+ >>> import time |
+ >>> import pydoc |
+ |
+ Define a URL handler. To determine what the client is asking |
+ for check the URL and content_type. |
+ |
+ Then get or generate some text or HTML code and return it. |
+ |
+ >>> def my_url_handler(url, content_type): |
+ ... text = 'the URL sent was: (%s, %s)' % (url, content_type) |
+ ... return text |
+ |
+ Start server thread on port 0. |
+ If you use port 0, the server will pick a random port number. |
+ You can then use serverthread.port to get the port number. |
+ |
+ >>> port = 0 |
+ >>> serverthread = pydoc._startserver(my_url_handler, port) |
+ |
+ Check that the server is really started. If it is, open browser |
+ and get first page. Use serverthread.url as the starting page. |
+ |
+ >>> if serverthread.serving: |
+ ... import webbrowser |
+ |
+ #... webbrowser.open(serverthread.url) |
+ #True |
+ |
+ Let the server do it's thing. We just need to monitor it's status. |
+ Use time.sleep so the loop doesn't hog the CPU. |
+ |
+ >>> starttime = time.time() |
+ >>> timeout = 1 #seconds |
+ |
+ This is a short timeout for testing purposes. |
+ |
+ >>> while serverthread.serving: |
+ ... time.sleep(.01) |
+ ... if serverthread.serving and time.time() - starttime > timeout: |
+ ... serverthread.stop() |
+ ... break |
+ |
+ Print any errors that may have occurred. |
+ |
+ >>> print(serverthread.error) |
+ None |
+ |
+ """ |
+ import http.server |
+ import email.message |
+ import select |
+ import threading |
+ |
+ class DocHandler(http.server.BaseHTTPRequestHandler): |
+ """ Handle server requests from browser. """ |
+ def do_GET(self): |
+ """ Process a request from a HTML browser. |
+ The URL received is in self.path. |
+ Get an HTML page from self.urlhandler and send it. |
+ """ |
+ if self.path.endswith('.css'): |
+ content_type = 'text/css' |
+ else: |
+ content_type = 'text/html' |
+ self.send_response(200) |
+ self.send_header('Content-Type', content_type) |
+ self.end_headers() |
+ self.wfile.write(bytes(self.urlhandler(self.path, content_type), |
+ 'UTF-8')) |
+ |
+ def log_message(self, *args): |
+ # Don't log messages. |
+ pass |
+ |
+ class DocServer(http.server.HTTPServer): |
+ def __init__(self, port, callback): |
+ self.host = (sys.platform == 'mac') and '127.0.0.1' or 'localhost' |
+ self.address = ('', port) |
+ self.callback = callback |
+ self.base.__init__(self, self.address, self.handler) |
+ self.quit = False |
+ |
+ def serve_until_quit(self): |
+ while not self.quit: |
+ rd, wr, ex = select.select([self.socket.fileno()], [], [], 1) |
+ if rd: |
+ self.handle_request() |
+ |
+ def server_activate(self): |
+ self.base.server_activate(self) |
+ if self.callback: |
+ self.callback(self) |
+ |
+ class ServerThread(threading.Thread): |
+ """ Use to start the server as a thread in an application. """ |
+ def __init__(self, urlhandler, port): |
+ self.urlhandler = urlhandler |
+ self.port = int(port) |
+ threading.Thread.__init__(self) |
+ self.serving = False |
+ self.error = None |
+ |
+ def run(self): |
+ """ Start the server. """ |
+ try: |
+ DocServer.base = http.server.HTTPServer |
+ DocServer.handler = DocHandler |
+ DocHandler.MessageClass = email.message.Message |
+ DocHandler.urlhandler = staticmethod(self.urlhandler) |
+ docsvr = DocServer(self.port, self.ready) |
+ self.docserver = docsvr |
+ docsvr.serve_until_quit() |
+ except Exception as e: |
+ self.error = e |
+ |
+ def ready(self, server): |
+ self.serving = True |
+ self.host = server.host |
+ self.port = server.server_port |
+ self.url = 'http://%s:%d/' % (self.host, self.port) |
+ |
+ def stop(self): |
+ """ Stop the server and this thread nicely """ |
+ self.docserver.quit = True |
+ self.serving = False |
+ self.url = None |
+ |
+ thread = ServerThread(urlhandler, port) |
+ thread.start() |
+ # Wait until thread.serving is True to make sure we are |
+ # really up before returning. |
+ while not thread.error and not thread.serving: |
+ time.sleep(.01) |
+ return thread |
+ |
+def browse(port=0, *, open_browser=True): |
+ """ Start the enhanced pydoc web server and open a web browser. |
+ """ |
+ import webbrowser |
+ |
+ class _HTMLDoc(HTMLDoc): |
+ def filelink(self, url, path): |
+ return '<a href="getfile?key=%s">%s</a>' % (url, path) |
+ |
+ html = _HTMLDoc() |
+ |
+ def html_navbar(): return \ |
+ """ |
+ <table><tr> |
+ <td width="50%%"> Python %s </td> |
+ <td><form action="get"> |
+ <input type=text name=key size=15> |
+ <input type=submit value="Get"> |
+ </form></td> |
+ <td><form action="search"> |
+ <input type=text name=key size=15> |
+ <input type=submit value="Search"> |
+ </form></td> |
+ <td> |
+ <a href="index.html">Index of Modules</a> |
+ : <a href="topics.html">Topics</a> |
+ : <a href="keywords.html">Keywords</a> |
+ </td> |
+ </tr></table> |
+ """ % sys.version |
+ |
+ def html_index(): |
+ """ Index of modules web page. """ |
+ def bltinlink(name): |
+ return '<a href="%s.html">%s</a>' % (name, name) |
+ heading = html.heading( |
+ '<big><big><strong>Index of Modules</strong></big></big>', |
+ '#ffffff', '#7799ee') |
+ names = list(filter(lambda x: x != '__main__', |
+ sys.builtin_module_names)) |
+ contents = html.multicolumn(names, bltinlink) |
+ indices = ['<p>' + html.bigsection( |
+ 'Built-in Modules', '#ffffff', '#ee77aa', contents)] |
+ seen = {} |
+ for dir in sys.path: |
+ indices.append(html.index(dir, seen)) |
+ contents = heading + ''.join(indices) + \ |
+ '''<p align=right><font color="#909090" face="helvetica, arial"><strong> |
+ pydoc</strong> by Ka-Ping Yee <ping@lfw.org></font>''' |
+ return html.page('Index of Modules' ,contents) |
+ |
+ def html_search(key): |
+ """ Search results page. """ |
+ # scan for modules |
+ search_result = [] |
+ def callback(path, modname, desc): |
+ if modname[-9:] == '.__init__': |
+ modname = modname[:-9] + ' (package)' |
+ search_result.append((modname, desc and '- ' + desc)) |
+ try: |
+ import warnings |
+ except ImportError: |
+ pass |
+ else: |
+ warnings.filterwarnings('ignore') # ignore problems during import |
+ ModuleScanner().run(callback, key) |
+ # format page |
+ def bltinlink(name): |
+ return '<a href="%s.html">%s</a>' % (name, name) |
+ results = [] |
+ heading = html.heading( |
+ '<big><big><strong>Search Results</strong></big></big>', |
+ '#ffffff', '#7799ee') |
+ for name, desc in search_result: |
+ results.append(bltinlink(name) + desc) |
+ contents = heading + html.bigsection( |
+ 'key = %s' % key, '#ffffff', '#ee77aa', '<br>'.join(results)) |
+ return html.page('Search Results', contents) |
+ |
+ def html_getfile(path): |
+ """ Get and display a source file listing safely. """ |
+ path = os.sep + path.replace('%20', ' ') |
+ try: |
+ f = open(path, 'r') |
+ lines = html.escape(f.read()) |
+ finally: |
+ f.close() |
+ body = '<pre>%s</pre>' % lines |
+ heading = html.heading( |
+ '<big><big><strong>File Listing</strong></big></big>', |
+ '#ffffff', '#7799ee') |
+ contents = heading + html.bigsection( |
+ 'File: %s' % path, '#ffffff', '#ee77aa', body) |
+ return html.page('getfile: %s' % path, contents) |
+ |
+ def html_topics(): |
+ """ Index of topic texts available. """ |
+ def bltinlink(name): |
+ return '<a href="%s.html">%s</a>' % (name, name) |
+ heading = html.heading( |
+ '<big><big><strong>INDEX</strong></big></big>', |
+ '#ffffff', '#7799ee') |
+ names = sorted(Helper.topics.keys()) |
+ def bltinlink(name): |
+ return '<a href="%s.html">%s</a>' % (name, name) |
+ contents = html.multicolumn(names, bltinlink) |
+ contents = heading + html.bigsection( |
+ 'Topics', '#ffffff', '#ee77aa', contents) |
+ return html.page('Topics', contents) |
+ |
+ def html_keywords(): |
+ """ Index of keywords. """ |
+ heading = html.heading( |
+ '<big><big><strong>INDEX</strong></big></big>', |
+ '#ffffff', '#7799ee') |
+ names = sorted(Helper.keywords.keys()) |
+ def bltinlink(name): |
+ return '<a href="%s.html">%s</a>' % (name, name) |
+ contents = html.multicolumn(names, bltinlink) |
+ contents = heading + html.bigsection( |
+ 'Keywords', '#ffffff', '#ee77aa', contents) |
+ return html.page('Keywords', contents) |
+ |
+ def html_topicpage(topic): |
+ """ Topic or keyword help page. """ |
+ import io |
+ buf = io.StringIO() |
+ htmlhelp = Helper(buf, buf) |
+ contents, xrefs = htmlhelp._gettopic(topic) |
+ if topic in htmlhelp.keywords: |
+ title = 'KEYWORD' |
+ else: |
+ title = 'TOPIC' |
+ heading = html.heading( |
+ '<big><big><strong>%s</strong></big></big>' % title, |
+ '#ffffff', '#7799ee') |
+ contents = '<pre>%s</pre>' % contents |
+ contents = html.bigsection(topic , '#ffffff','#ee77aa', contents) |
+ xrefs = sorted(xrefs.split()) |
+ def bltinlink(name): |
+ return '<a href="%s.html">%s</a>' % (name, name) |
+ xrefs = html.multicolumn(xrefs, bltinlink) |
+ xrefs = html.section('Related help topics: ', '#ffffff', '#ee77aa', xrefs) |
+ return html.page('%s: %s' % (title, topic), heading + contents + xrefs) |
+ |
+ def html_error(url): |
+ heading = html.heading( |
+ '<big><big><strong>Error</strong></big></big>', |
+ '#ffffff', '#ee0000') |
+ return heading + url |
+ |
+ def get_html_page(url): |
+ """ Function url handler uses to get the html page to get |
+ depending on the url. |
+ """ |
+ if url[-5:] == '.html': url = url[:-5] |
+ if url[:1] == '/': url = url[1:] |
+ if url.startswith("get?key="): |
+ url = url[8:] |
+ title = url |
+ contents = '' |
+ if url in ("", ".", "index"): |
+ contents = html_index() |
+ elif url == "topics": |
+ contents = html_topics() |
+ elif url == "keywords": |
+ contents = html_keywords() |
+ elif url.startswith("search?key="): |
+ contents = html_search(url[11:]) |
+ elif url.startswith("getfile?key="): |
+ url = url[12:] |
+ try: |
+ contents = html_getfile(url) |
+ except IOError as value: |
+ contents = html_error('could not read file %s' % repr(url)) |
+ else: |
+ obj = None |
+ try: |
+ obj = locate(url, forceload=1) |
+ except ErrorDuringImport as value: |
+ contents = html.escape(str(value)) |
+ if obj: |
+ title = describe(obj) |
+ contents = html.document(obj, url) |
+ elif url in Helper.keywords or url in Helper.topics: |
+ contents = html_topicpage(url) |
+ else: |
+ contents = html_error('no Python documentation found for %s' |
+ % repr(url)) |
+ return html.page(title, html_navbar() + contents) |
+ |
+ def url_handler(url, content_type): |
+ """ html server html and style sheet requests. """ |
+ if url.startswith('/'): |
+ url = url[1:] |
+ if content_type == 'text/css': |
+ fp = open(os.path.join(path_here, url)) |
+ css = ''.join(fp.readlines()) |
+ fp.close() |
+ return css |
+ elif content_type == 'text/html': |
+ return get_html_page(url) |
+ return 'Error: unknown content type ' + content_type |
+ |
+ serverthread = _startserver(url_handler, port) |
+ if serverthread.error: |
+ print(serverthread.error) |
+ return |
+ if serverthread.serving: |
+ server_help_msg = """Python Version: %s |
+Server ready at: %s |
+Server commands: [b]rowser, [q]uit'""" % (sys.version, serverthread.url) |
+ if open_browser: |
+ webbrowser.open(serverthread.url) |
+ try: |
+ print(server_help_msg) |
+ while serverthread.serving: |
+ cmd = input('server>') |
+ cmd = cmd.lower() |
+ if cmd == 'q': |
+ break |
+ elif cmd == 'b': |
+ webbrowser.open(serverthread.url) |
+ else: |
+ print(server_help_msg) |
+ finally: |
+ if serverthread.serving: |
+ serverthread.stop() |
+ print('Server stopped') |
+ |
+ |
# -------------------------------------------------- command-line interface |
def ispath(x): |
@@ -2256,29 +2689,32 @@ |
sys.path.insert(0, '.') |
try: |
- opts, args = getopt.getopt(sys.argv[1:], 'gk:p:w') |
- writing = 0 |
- |
+ opts, args = getopt.getopt(sys.argv[1:], 'bgk:p:w') |
+ writing = False |
+ start_server = False |
+ open_browser = False |
+ port = None |
for opt, val in opts: |
if opt == '-g': |
gui() |
return |
+ if opt == '-b': |
+ start_server = True |
+ open_browser = True |
if opt == '-k': |
apropos(val) |
return |
if opt == '-p': |
- try: |
- port = int(val) |
- except ValueError: |
- raise BadUsage |
- def ready(server): |
- print('pydoc server ready at %s' % server.url) |
- def stopped(): |
- print('pydoc server stopped') |
- serve(port, ready, stopped) |
- return |
+ start_server = True |
+ port = val |
if opt == '-w': |
- writing = 1 |
+ writing = True |
+ |
+ if start_server == True: |
+ if port == None: |
+ port = 0 |
+ browse(port, open_browser=open_browser) |
+ return |
if not args: raise BadUsage |
for arg in args: |
@@ -2314,15 +2750,24 @@ |
Search for a keyword in the synopsis lines of all available modules. |
%s -p <port> |
- Start an HTTP server on the given port on the local machine. |
+ Start an HTTP server on the given port on the local machine. Port |
+ number 0 can be used to get an arbitrary unused port. |
+%s -b |
+ Start an HTTP server on an arbitrary unused port and open a web browser |
+ to interactively browse documentation. The -p option can be used with |
+ the -b option to explicitly specify the server port. |
+ |
%s -g |
Pop up a graphical interface for finding and serving documentation. |
+ (The "-g" option is deprecated.) |
%s -w <name> ... |
Write out the HTML documentation for a module to a file in the current |
directory. If <name> contains a '%s', it is treated as a filename; if |
it names a directory, documentation is written for all the contents. |
-""" % (cmd, os.sep, cmd, cmd, cmd, cmd, os.sep)) |
+""" % (cmd, os.sep, cmd, cmd, cmd, cmd, cmd, os.sep)) |
-if __name__ == '__main__': cli() |
+if __name__ == '__main__': |
+ cli() |
+ |