Skip to content

bpo-31128: Allow pydoc to bind to arbitrary hostnames #3011

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Sep 14, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Doc/library/pydoc.rst
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ will start a HTTP server on port 1234, allowing you to browse the
documentation at ``http://localhost:1234/`` in your preferred Web browser.
Specifying ``0`` as the port number will select an arbitrary unused port.

:program:`pydoc -n <hostname>` will start the server listening at the given
hostname. By default the hostname is 'localhost' but if you want the server to
be reached from other machines, you may want to change the host name that the
server responds to. During development this is especially useful if you want
to run pydoc from within a container.

:program:`pydoc -b` will start the server and additionally open a web
browser to a module index page. Each served page has a navigation bar at the
top where you can *Get* help on an individual item, *Search* all modules with a
Expand Down Expand Up @@ -98,3 +104,6 @@ Reference Manual pages.
:mod:`pydoc` now uses :func:`inspect.signature` rather than
:func:`inspect.getfullargspec` to extract signature information from
callables.

.. versionchanged:: 3.7
Added the ``-n`` option.
43 changes: 26 additions & 17 deletions Lib/pydoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,15 @@ class or function within a module or module in a package. If the
Run "pydoc -k <keyword>" to search for a keyword in the synopsis lines
of all available modules.

Run "pydoc -n <hostname>" to start an HTTP server with the given
hostname (default: localhost) on the local machine.

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.
open a Web browser to interactively browse documentation. Combine with
the -n and -p options to control the hostname and port used.

Run "pydoc -w <name>" to write out the HTML documentation for a module
to a file named "<name>.html".
Expand Down Expand Up @@ -2162,7 +2165,7 @@ def onerror(modname):

# --------------------------------------- enhanced Web browser interface

def _start_server(urlhandler, port):
def _start_server(urlhandler, hostname, port):
"""Start an HTTP server thread on a specific port.

Start an HTML/text server thread, so HTML or text documents can be
Expand Down Expand Up @@ -2247,8 +2250,8 @@ def log_message(self, *args):

class DocServer(http.server.HTTPServer):

def __init__(self, port, callback):
self.host = 'localhost'
def __init__(self, host, port, callback):
self.host = host
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this class is considered part of the module’s public API, this change would break compatibility. Alternative ways to achieve the same result could be:

  • add host after the existing params, with default value to keep compat
  • allow the existing port to be a (host, port) tuple

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@merwok, I considered that but figured it was a class inside a underscored method so its not guaranteed to remain compatible. I figured it would be better to opt for the more readable signature instead of being backwards compatible. If it's a blocker to make this compatible let me know but if we're implying that this function should be treated as if it is public we may want to remove the prefixed underscore but that will definitely break people.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah! Thanks for noting these classes are inside a private method, that excludes compat considerations. Patch looks fine.

self.address = (self.host, port)
self.callback = callback
self.base.__init__(self, self.address, self.handler)
Expand All @@ -2268,8 +2271,9 @@ def server_activate(self):

class ServerThread(threading.Thread):

def __init__(self, urlhandler, port):
def __init__(self, urlhandler, host, port):
self.urlhandler = urlhandler
self.host = host
self.port = int(port)
threading.Thread.__init__(self)
self.serving = False
Expand All @@ -2282,7 +2286,7 @@ def run(self):
DocServer.handler = DocHandler
DocHandler.MessageClass = email.message.Message
DocHandler.urlhandler = staticmethod(self.urlhandler)
docsvr = DocServer(self.port, self.ready)
docsvr = DocServer(self.host, self.port, self.ready)
self.docserver = docsvr
docsvr.serve_until_quit()
except Exception as e:
Expand All @@ -2300,7 +2304,7 @@ def stop(self):
self.serving = False
self.url = None

thread = ServerThread(urlhandler, port)
thread = ServerThread(urlhandler, hostname, port)
thread.start()
# Wait until thread.serving is True to make sure we are
# really up before returning.
Expand Down Expand Up @@ -2564,14 +2568,14 @@ def get_html_page(url):
raise TypeError('unknown content type %r for url %s' % (content_type, url))


def browse(port=0, *, open_browser=True):
def browse(port=0, *, open_browser=True, hostname='localhost'):
"""Start the enhanced pydoc Web server and open a Web browser.

Use port '0' to start the server on an arbitrary port.
Set open_browser to False to suppress opening a browser.
"""
import webbrowser
serverthread = _start_server(_url_handler, port)
serverthread = _start_server(_url_handler, hostname, port)
if serverthread.error:
print(serverthread.error)
return
Expand Down Expand Up @@ -2618,11 +2622,12 @@ class BadUsage(Exception): pass
sys.path.insert(0, '.')

try:
opts, args = getopt.getopt(sys.argv[1:], 'bk:p:w')
opts, args = getopt.getopt(sys.argv[1:], 'bk:n:p:w')
writing = False
start_server = False
open_browser = False
port = None
port = 0
hostname = 'localhost'
for opt, val in opts:
if opt == '-b':
start_server = True
Expand All @@ -2635,11 +2640,12 @@ class BadUsage(Exception): pass
port = val
if opt == '-w':
writing = True
if opt == '-n':
start_server = True
hostname = val

if start_server:
if port is None:
port = 0
browse(port, open_browser=open_browser)
browse(port, hostname=hostname, open_browser=open_browser)
return

if not args: raise BadUsage
Expand Down Expand Up @@ -2675,14 +2681,17 @@ class BadUsage(Exception): pass
{cmd} -k <keyword>
Search for a keyword in the synopsis lines of all available modules.

{cmd} -n <hostname>
Start an HTTP server with the given hostname (default: localhost).

{cmd} -p <port>
Start an HTTP server on the given port on the local machine. Port
number 0 can be used to get an arbitrary unused port.

{cmd} -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.
to interactively browse documentation. This option can be used in
combination with -n and/or -p.

{cmd} -w <name> ...
Write out the HTML documentation for a module to a file in the current
Expand Down
4 changes: 2 additions & 2 deletions Lib/test/test_pydoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -913,8 +913,8 @@ def my_url_handler(url, content_type):
text = 'the URL sent was: (%s, %s)' % (url, content_type)
return text

serverthread = pydoc._start_server(my_url_handler, port=0)
self.assertIn('localhost', serverthread.docserver.address)
serverthread = pydoc._start_server(my_url_handler, hostname='0.0.0.0', port=0)
self.assertIn('0.0.0.0', serverthread.docserver.address)

starttime = time.time()
timeout = 1 #seconds
Expand Down
1 change: 1 addition & 0 deletions Misc/ACKS
Original file line number Diff line number Diff line change
Expand Up @@ -1173,6 +1173,7 @@ Claude Paroz
Heikki Partanen
Harri Pasanen
Gaël Pasgrimaud
Feanil Patel
Ashish Nitin Patil
Alecsandru Patrascu
Randy Pausch
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Allow the pydoc server to bind to arbitrary hostnames.