home *** CD-ROM | disk | FTP | other *** search
/ SGI Freeware 1999 August / SGI Freeware 1999 August.iso / dist / fw_python.idb / usr / freeware / lib / python1.5 / urllib.py.z / urllib.py
Encoding:
Python Source  |  1999-04-16  |  28.5 KB  |  1,070 lines

  1. # Open an arbitrary URL
  2. #
  3. # See the following document for more info on URLs:
  4. # "Names and Addresses, URIs, URLs, URNs, URCs", at
  5. # http://www.w3.org/pub/WWW/Addressing/Overview.html
  6. #
  7. # See also the HTTP spec (from which the error codes are derived):
  8. # "HTTP - Hypertext Transfer Protocol", at
  9. # http://www.w3.org/pub/WWW/Protocols/
  10. #
  11. # Related standards and specs:
  12. # - RFC1808: the "relative URL" spec. (authoritative status)
  13. # - RFC1738 - the "URL standard". (authoritative status)
  14. # - RFC1630 - the "URI spec". (informational status)
  15. #
  16. # The object returned by URLopener().open(file) will differ per
  17. # protocol.  All you know is that is has methods read(), readline(),
  18. # readlines(), fileno(), close() and info().  The read*(), fileno()
  19. # and close() methods work like those of open files. 
  20. # The info() method returns a mimetools.Message object which can be
  21. # used to query various info about the object, if available.
  22. # (mimetools.Message objects are queried with the getheader() method.)
  23.  
  24. import string
  25. import socket
  26. import os
  27. import sys
  28.  
  29.  
  30. __version__ = '1.10'
  31.  
  32. MAXFTPCACHE = 10        # Trim the ftp cache beyond this size
  33.  
  34. # Helper for non-unix systems
  35. if os.name == 'mac':
  36.     from macurl2path import url2pathname, pathname2url
  37. elif os.name == 'nt':
  38.     from nturl2path import url2pathname, pathname2url 
  39. else:
  40.     def url2pathname(pathname):
  41.         return pathname
  42.     def pathname2url(pathname):
  43.         return pathname
  44.  
  45. _url2pathname = url2pathname
  46. def url2pathname(url):
  47.     return _url2pathname(unquote(url))
  48. _pathname2url = pathname2url
  49. def pathname2url(p):
  50.     return quote(_pathname2url(p))
  51.  
  52. # This really consists of two pieces:
  53. # (1) a class which handles opening of all sorts of URLs
  54. #     (plus assorted utilities etc.)
  55. # (2) a set of functions for parsing URLs
  56. # XXX Should these be separated out into different modules?
  57.  
  58.  
  59. # Shortcut for basic usage
  60. _urlopener = None
  61. def urlopen(url, data=None):
  62.     global _urlopener
  63.     if not _urlopener:
  64.         _urlopener = FancyURLopener()
  65.     if data is None:
  66.         return _urlopener.open(url)
  67.     else:
  68.         return _urlopener.open(url, data)
  69. def urlretrieve(url, filename=None, reporthook=None):
  70.     global _urlopener
  71.     if not _urlopener:
  72.         _urlopener = FancyURLopener()
  73.         return _urlopener.retrieve(url, filename, reporthook)
  74. def urlcleanup():
  75.     if _urlopener:
  76.         _urlopener.cleanup()
  77.  
  78.  
  79. # Class to open URLs.
  80. # This is a class rather than just a subroutine because we may need
  81. # more than one set of global protocol-specific options.
  82. # Note -- this is a base class for those who don't want the
  83. # automatic handling of errors type 302 (relocated) and 401
  84. # (authorization needed).
  85. ftpcache = {}
  86. class URLopener:
  87.  
  88.     __tempfiles = None
  89.  
  90.     # Constructor
  91.     def __init__(self, proxies=None):
  92.         if proxies is None:
  93.             proxies = getproxies()
  94.         assert hasattr(proxies, 'has_key'), "proxies must be a mapping"
  95.         self.proxies = proxies
  96.         server_version = "Python-urllib/%s" % __version__
  97.         self.addheaders = [('User-agent', server_version)]
  98.         self.__tempfiles = []
  99.         self.__unlink = os.unlink # See cleanup()
  100.         self.tempcache = None
  101.         # Undocumented feature: if you assign {} to tempcache,
  102.         # it is used to cache files retrieved with
  103.         # self.retrieve().  This is not enabled by default
  104.         # since it does not work for changing documents (and I
  105.         # haven't got the logic to check expiration headers
  106.         # yet).
  107.         self.ftpcache = ftpcache
  108.         # Undocumented feature: you can use a different
  109.         # ftp cache by assigning to the .ftpcache member;
  110.         # in case you want logically independent URL openers
  111.         # XXX This is not threadsafe.  Bah.
  112.  
  113.     def __del__(self):
  114.         self.close()
  115.  
  116.     def close(self):
  117.         self.cleanup()
  118.  
  119.     def cleanup(self):
  120.         # This code sometimes runs when the rest of this module
  121.         # has already been deleted, so it can't use any globals
  122.         # or import anything.
  123.         if self.__tempfiles:
  124.             for file in self.__tempfiles:
  125.                 try:
  126.                     self.__unlink(file)
  127.                 except:
  128.                     pass
  129.             del self.__tempfiles[:]
  130.         if self.tempcache:
  131.             self.tempcache.clear()
  132.  
  133.     # Add a header to be used by the HTTP interface only
  134.     # e.g. u.addheader('Accept', 'sound/basic')
  135.     def addheader(self, *args):
  136.         self.addheaders.append(args)
  137.  
  138.     # External interface
  139.     # Use URLopener().open(file) instead of open(file, 'r')
  140.     def open(self, fullurl, data=None):
  141.         fullurl = unwrap(fullurl)
  142.         if self.tempcache and self.tempcache.has_key(fullurl):
  143.             filename, headers = self.tempcache[fullurl]
  144.             fp = open(filename, 'rb')
  145.             return addinfourl(fp, headers, fullurl)
  146.         type, url = splittype(fullurl)
  147.         if not type: type = 'file'
  148.         if self.proxies.has_key(type):
  149.             proxy = self.proxies[type]
  150.             type, proxy = splittype(proxy)
  151.             host, selector = splithost(proxy)
  152.             url = (host, fullurl) # Signal special case to open_*()
  153.         name = 'open_' + type
  154.         if '-' in name:
  155.             # replace - with _
  156.             name = string.join(string.split(name, '-'), '_')
  157.         if not hasattr(self, name):
  158.             if data is None:
  159.                 return self.open_unknown(fullurl)
  160.             else:
  161.                 return self.open_unknown(fullurl, data)
  162.         try:
  163.             if data is None:
  164.                 return getattr(self, name)(url)
  165.             else:
  166.                 return getattr(self, name)(url, data)
  167.         except socket.error, msg:
  168.             raise IOError, ('socket error', msg), sys.exc_info()[2]
  169.  
  170.     # Overridable interface to open unknown URL type
  171.     def open_unknown(self, fullurl, data=None):
  172.         type, url = splittype(fullurl)
  173.         raise IOError, ('url error', 'unknown url type', type)
  174.  
  175.     # External interface
  176.     # retrieve(url) returns (filename, None) for a local object
  177.     # or (tempfilename, headers) for a remote object
  178.     def retrieve(self, url, filename=None, reporthook=None):
  179.         url = unwrap(url)
  180.         if self.tempcache and self.tempcache.has_key(url):
  181.             return self.tempcache[url]
  182.         type, url1 = splittype(url)
  183.         if not filename and (not type or type == 'file'):
  184.             try:
  185.                 fp = self.open_local_file(url1)
  186.                 hdrs = fp.info()
  187.                 del fp
  188.                 return url2pathname(splithost(url1)[1]), hdrs
  189.             except IOError, msg:
  190.                 pass
  191.         fp = self.open(url)
  192.         headers = fp.info()
  193.         if not filename:
  194.             import tempfile
  195.             garbage, path = splittype(url)
  196.             garbage, path = splithost(path or "")
  197.             path, garbage = splitquery(path or "")
  198.             path, garbage = splitattr(path or "")
  199.             suffix = os.path.splitext(path)[1]
  200.             filename = tempfile.mktemp(suffix)
  201.             self.__tempfiles.append(filename)
  202.         result = filename, headers
  203.         if self.tempcache is not None:
  204.             self.tempcache[url] = result
  205.         tfp = open(filename, 'wb')
  206.         bs = 1024*8
  207.         size = -1
  208.         blocknum = 1
  209.         if reporthook:
  210.             if headers.has_key("content-length"):
  211.                             size = int(headers["Content-Length"])
  212.             reporthook(0, bs, size)
  213.         block = fp.read(bs)
  214.         if reporthook:
  215.             reporthook(1, bs, size)
  216.         while block:
  217.             tfp.write(block)
  218.             block = fp.read(bs)
  219.             blocknum = blocknum + 1
  220.             if reporthook:
  221.                 reporthook(blocknum, bs, size)
  222.         fp.close()
  223.         tfp.close()
  224.         del fp
  225.         del tfp
  226.         return result
  227.  
  228.     # Each method named open_<type> knows how to open that type of URL
  229.  
  230.     # Use HTTP protocol
  231.     def open_http(self, url, data=None):
  232.         import httplib
  233.         user_passwd = None
  234.         if type(url) is type(""):
  235.             host, selector = splithost(url)
  236.             if host:
  237.                 user_passwd, host = splituser(host)
  238.                 host = unquote(host)
  239.             realhost = host
  240.         else:
  241.             host, selector = url
  242.             urltype, rest = splittype(selector)
  243.             url = rest
  244.             user_passwd = None
  245.             if string.lower(urltype) != 'http':
  246.                 realhost = None
  247.             else:
  248.                 realhost, rest = splithost(rest)
  249.                 if realhost:
  250.                     user_passwd, realhost = \
  251.                              splituser(realhost)
  252.                 if user_passwd:
  253.                     selector = "%s://%s%s" % (urltype,
  254.                                   realhost,
  255.                                   rest)
  256.             #print "proxy via http:", host, selector
  257.         if not host: raise IOError, ('http error', 'no host given')
  258.         if user_passwd:
  259.             import base64
  260.             auth = string.strip(base64.encodestring(user_passwd))
  261.         else:
  262.             auth = None
  263.         h = httplib.HTTP(host)
  264.         if data is not None:
  265.             h.putrequest('POST', selector)
  266.             h.putheader('Content-type',
  267.                     'application/x-www-form-urlencoded')
  268.             h.putheader('Content-length', '%d' % len(data))
  269.         else:
  270.             h.putrequest('GET', selector)
  271.         if auth: h.putheader('Authorization', 'Basic %s' % auth)
  272.         if realhost: h.putheader('Host', realhost)
  273.         for args in self.addheaders: apply(h.putheader, args)
  274.         h.endheaders()
  275.         if data is not None:
  276.             h.send(data + '\r\n')
  277.         errcode, errmsg, headers = h.getreply()
  278.         fp = h.getfile()
  279.         if errcode == 200:
  280.             return addinfourl(fp, headers, "http:" + url)
  281.         else:
  282.             return self.http_error(url,
  283.                            fp, errcode, errmsg, headers)
  284.  
  285.     # Handle http errors.
  286.     # Derived class can override this, or provide specific handlers
  287.     # named http_error_DDD where DDD is the 3-digit error code
  288.     def http_error(self, url, fp, errcode, errmsg, headers):
  289.         # First check if there's a specific handler for this error
  290.         name = 'http_error_%d' % errcode
  291.         if hasattr(self, name):
  292.             method = getattr(self, name)
  293.             result = method(url, fp, errcode, errmsg, headers)
  294.             if result: return result
  295.         return self.http_error_default(
  296.             url, fp, errcode, errmsg, headers)
  297.  
  298.     # Default http error handler: close the connection and raises IOError
  299.     def http_error_default(self, url, fp, errcode, errmsg, headers):
  300.         void = fp.read()
  301.         fp.close()
  302.         raise IOError, ('http error', errcode, errmsg, headers)
  303.  
  304.     # Use Gopher protocol
  305.     def open_gopher(self, url):
  306.         import gopherlib
  307.         host, selector = splithost(url)
  308.         if not host: raise IOError, ('gopher error', 'no host given')
  309.         host = unquote(host)
  310.         type, selector = splitgophertype(selector)
  311.         selector, query = splitquery(selector)
  312.         selector = unquote(selector)
  313.         if query:
  314.             query = unquote(query)
  315.             fp = gopherlib.send_query(selector, query, host)
  316.         else:
  317.             fp = gopherlib.send_selector(selector, host)
  318.         return addinfourl(fp, noheaders(), "gopher:" + url)
  319.  
  320.     # Use local file or FTP depending on form of URL
  321.     def open_file(self, url):
  322.         if url[:2] == '//' and url[2:3] != '/':
  323.             return self.open_ftp(url)
  324.         else:
  325.             return self.open_local_file(url)
  326.  
  327.     # Use local file
  328.     def open_local_file(self, url):
  329.         import mimetypes, mimetools, StringIO
  330.         mtype = mimetypes.guess_type(url)[0]
  331.         headers = mimetools.Message(StringIO.StringIO(
  332.             'Content-Type: %s\n' % (mtype or 'text/plain')))
  333.         host, file = splithost(url)
  334.         if not host:
  335.             return addinfourl(
  336.                 open(url2pathname(file), 'rb'),
  337.                 headers, 'file:'+file)
  338.         host, port = splitport(host)
  339.         if not port and socket.gethostbyname(host) in (
  340.               localhost(), thishost()):
  341.             return addinfourl(
  342.                 open(url2pathname(file), 'rb'),
  343.                 headers, 'file:'+file)
  344.         raise IOError, ('local file error', 'not on local host')
  345.  
  346.     # Use FTP protocol
  347.     def open_ftp(self, url):
  348.         host, path = splithost(url)
  349.         if not host: raise IOError, ('ftp error', 'no host given')
  350.         host, port = splitport(host)
  351.         user, host = splituser(host)
  352.         if user: user, passwd = splitpasswd(user)
  353.         else: passwd = None
  354.         host = unquote(host)
  355.         user = unquote(user or '')
  356.         passwd = unquote(passwd or '')
  357.         host = socket.gethostbyname(host)
  358.         if not port:
  359.             import ftplib
  360.             port = ftplib.FTP_PORT
  361.         else:
  362.             port = int(port)
  363.         path, attrs = splitattr(path)
  364.         path = unquote(path)
  365.         dirs = string.splitfields(path, '/')
  366.         dirs, file = dirs[:-1], dirs[-1]
  367.         if dirs and not dirs[0]: dirs = dirs[1:]
  368.         key = (user, host, port, string.joinfields(dirs, '/'))
  369.         # XXX thread unsafe!
  370.         if len(self.ftpcache) > MAXFTPCACHE:
  371.             # Prune the cache, rather arbitrarily
  372.             for k in self.ftpcache.keys():
  373.                 if k != key:
  374.                     v = self.ftpcache[k]
  375.                     del self.ftpcache[k]
  376.                     v.close()
  377.         try:
  378.             if not self.ftpcache.has_key(key):
  379.                 self.ftpcache[key] = \
  380.                     ftpwrapper(user, passwd,
  381.                            host, port, dirs)
  382.             if not file: type = 'D'
  383.             else: type = 'I'
  384.             for attr in attrs:
  385.                 attr, value = splitvalue(attr)
  386.                 if string.lower(attr) == 'type' and \
  387.                    value in ('a', 'A', 'i', 'I', 'd', 'D'):
  388.                     type = string.upper(value)
  389.                         (fp, retrlen) = self.ftpcache[key].retrfile(file, type)
  390.                         if retrlen is not None and retrlen >= 0:
  391.                             import mimetools, StringIO
  392.                             headers = mimetools.Message(StringIO.StringIO(
  393.                                 'Content-Length: %d\n' % retrlen))
  394.                         else:
  395.                             headers = noheaders()
  396.                         return addinfourl(fp, headers, "ftp:" + url)
  397.         except ftperrors(), msg:
  398.             raise IOError, ('ftp error', msg), sys.exc_info()[2]
  399.  
  400.     # Use "data" URL
  401.     def open_data(self, url, data=None):
  402.         # ignore POSTed data
  403.         #
  404.         # syntax of data URLs:
  405.         # dataurl   := "data:" [ mediatype ] [ ";base64" ] "," data
  406.         # mediatype := [ type "/" subtype ] *( ";" parameter )
  407.         # data      := *urlchar
  408.         # parameter := attribute "=" value
  409.         import StringIO, mimetools, time
  410.         try:
  411.             [type, data] = string.split(url, ',', 1)
  412.         except ValueError:
  413.             raise IOError, ('data error', 'bad data URL')
  414.         if not type:
  415.             type = 'text/plain;charset=US-ASCII'
  416.         semi = string.rfind(type, ';')
  417.         if semi >= 0 and '=' not in type[semi:]:
  418.             encoding = type[semi+1:]
  419.             type = type[:semi]
  420.         else:
  421.             encoding = ''
  422.         msg = []
  423.         msg.append('Date: %s'%time.strftime('%a, %d %b %Y %T GMT',
  424.                             time.gmtime(time.time())))
  425.         msg.append('Content-type: %s' % type)
  426.         if encoding == 'base64':
  427.             import base64
  428.             data = base64.decodestring(data)
  429.         else:
  430.             data = unquote(data)
  431.         msg.append('Content-length: %d' % len(data))
  432.         msg.append('')
  433.         msg.append(data)
  434.         msg = string.join(msg, '\n')
  435.         f = StringIO.StringIO(msg)
  436.         headers = mimetools.Message(f, 0)
  437.         f.fileno = None        # needed for addinfourl
  438.         return addinfourl(f, headers, url)
  439.  
  440.  
  441. # Derived class with handlers for errors we can handle (perhaps)
  442. class FancyURLopener(URLopener):
  443.  
  444.     def __init__(self, *args):
  445.         apply(URLopener.__init__, (self,) + args)
  446.         self.auth_cache = {}
  447.  
  448.     # Default error handling -- don't raise an exception
  449.     def http_error_default(self, url, fp, errcode, errmsg, headers):
  450.         return addinfourl(fp, headers, "http:" + url)
  451.  
  452.     # Error 302 -- relocated (temporarily)
  453.     def http_error_302(self, url, fp, errcode, errmsg, headers):
  454.         # XXX The server can force infinite recursion here!
  455.         if headers.has_key('location'):
  456.             newurl = headers['location']
  457.         elif headers.has_key('uri'):
  458.             newurl = headers['uri']
  459.         else:
  460.             return
  461.         void = fp.read()
  462.         fp.close()
  463.         return self.open(newurl)
  464.  
  465.     # Error 301 -- also relocated (permanently)
  466.     http_error_301 = http_error_302
  467.  
  468.     # Error 401 -- authentication required
  469.     # See this URL for a description of the basic authentication scheme:
  470.     # http://www.ics.uci.edu/pub/ietf/http/draft-ietf-http-v10-spec-00.txt
  471.     def http_error_401(self, url, fp, errcode, errmsg, headers):
  472.         if headers.has_key('www-authenticate'):
  473.             stuff = headers['www-authenticate']
  474.             import re
  475.             match = re.match(
  476.                 '[ \t]*([^ \t]+)[ \t]+realm="([^"]*)"', stuff)
  477.             if match:
  478.                 scheme, realm = match.groups()
  479.                 if string.lower(scheme) == 'basic':
  480.                     return self.retry_http_basic_auth(
  481.                         url, realm)
  482.  
  483.     def retry_http_basic_auth(self, url, realm):
  484.         host, selector = splithost(url)
  485.         i = string.find(host, '@') + 1
  486.         host = host[i:]
  487.         user, passwd = self.get_user_passwd(host, realm, i)
  488.         if not (user or passwd): return None
  489.         host = user + ':' + passwd + '@' + host
  490.         newurl = 'http://' + host + selector
  491.         return self.open(newurl)
  492.  
  493.     def get_user_passwd(self, host, realm, clear_cache = 0):
  494.         key = realm + '@' + string.lower(host)
  495.         if self.auth_cache.has_key(key):
  496.             if clear_cache:
  497.                 del self.auth_cache[key]
  498.             else:
  499.                 return self.auth_cache[key]
  500.         user, passwd = self.prompt_user_passwd(host, realm)
  501.         if user or passwd: self.auth_cache[key] = (user, passwd)
  502.         return user, passwd
  503.  
  504.     def prompt_user_passwd(self, host, realm):
  505.         # Override this in a GUI environment!
  506.         import getpass
  507.         try:
  508.             user = raw_input("Enter username for %s at %s: " %
  509.                      (realm, host))
  510.             passwd = getpass.getpass(
  511.                 "Enter password for %s in %s at %s: " %
  512.                 (user, realm, host))
  513.             return user, passwd
  514.         except KeyboardInterrupt:
  515.             print
  516.             return None, None
  517.  
  518.  
  519. # Utility functions
  520.  
  521. # Return the IP address of the magic hostname 'localhost'
  522. _localhost = None
  523. def localhost():
  524.     global _localhost
  525.     if not _localhost:
  526.         _localhost = socket.gethostbyname('localhost')
  527.     return _localhost
  528.  
  529. # Return the IP address of the current host
  530. _thishost = None
  531. def thishost():
  532.     global _thishost
  533.     if not _thishost:
  534.         _thishost = socket.gethostbyname(socket.gethostname())
  535.     return _thishost
  536.  
  537. # Return the set of errors raised by the FTP class
  538. _ftperrors = None
  539. def ftperrors():
  540.     global _ftperrors
  541.     if not _ftperrors:
  542.         import ftplib
  543.         _ftperrors = ftplib.all_errors
  544.     return _ftperrors
  545.  
  546. # Return an empty mimetools.Message object
  547. _noheaders = None
  548. def noheaders():
  549.     global _noheaders
  550.     if not _noheaders:
  551.         import mimetools
  552.         import StringIO
  553.         _noheaders = mimetools.Message(StringIO.StringIO(), 0)
  554.         _noheaders.fp.close()    # Recycle file descriptor
  555.     return _noheaders
  556.  
  557.  
  558. # Utility classes
  559.  
  560. # Class used by open_ftp() for cache of open FTP connections
  561. class ftpwrapper:
  562.     def __init__(self, user, passwd, host, port, dirs):
  563.         self.user = user
  564.         self.passwd = passwd
  565.         self.host = host
  566.         self.port = port
  567.         self.dirs = dirs
  568.         self.init()
  569.     def init(self):
  570.         import ftplib
  571.         self.busy = 0
  572.         self.ftp = ftplib.FTP()
  573.         self.ftp.connect(self.host, self.port)
  574.         self.ftp.login(self.user, self.passwd)
  575.         for dir in self.dirs:
  576.             self.ftp.cwd(dir)
  577.     def retrfile(self, file, type):
  578.         import ftplib
  579.         self.endtransfer()
  580.         if type in ('d', 'D'): cmd = 'TYPE A'; isdir = 1
  581.         else: cmd = 'TYPE ' + type; isdir = 0
  582.         try:
  583.             self.ftp.voidcmd(cmd)
  584.         except ftplib.all_errors:
  585.             self.init()
  586.             self.ftp.voidcmd(cmd)
  587.         conn = None
  588.         if file and not isdir:
  589.             # Use nlst to see if the file exists at all
  590.             try:
  591.                 self.ftp.nlst(file)
  592.             except ftplib.error_perm, reason:
  593.                 raise IOError, ('ftp error', reason), \
  594.                       sys.exc_info()[2]
  595.             # Restore the transfer mode!
  596.             self.ftp.voidcmd(cmd)
  597.             # Try to retrieve as a file
  598.             try:
  599.                 cmd = 'RETR ' + file
  600.                 conn = self.ftp.ntransfercmd(cmd)
  601.             except ftplib.error_perm, reason:
  602.                 if reason[:3] != '550':
  603.                     raise IOError, ('ftp error', reason), \
  604.                           sys.exc_info()[2]
  605.         if not conn:
  606.             # Set transfer mode to ASCII!
  607.             self.ftp.voidcmd('TYPE A')
  608.             # Try a directory listing
  609.             if file: cmd = 'LIST ' + file
  610.             else: cmd = 'LIST'
  611.             conn = self.ftp.ntransfercmd(cmd)
  612.         self.busy = 1
  613.                 # Pass back both a suitably decorated object and a retrieval length
  614.         return (addclosehook(conn[0].makefile('rb'), self.endtransfer), conn[1])
  615.     def endtransfer(self):
  616.         if not self.busy:
  617.             return
  618.         self.busy = 0
  619.         try:
  620.             self.ftp.voidresp()
  621.         except ftperrors():
  622.             pass
  623.     def close(self):
  624.         self.endtransfer()
  625.         try:
  626.             self.ftp.close()
  627.         except ftperrors():
  628.             pass
  629.  
  630. # Base class for addinfo and addclosehook
  631. class addbase:
  632.     def __init__(self, fp):
  633.         self.fp = fp
  634.         self.read = self.fp.read
  635.         self.readline = self.fp.readline
  636.         self.readlines = self.fp.readlines
  637.         self.fileno = self.fp.fileno
  638.     def __repr__(self):
  639.         return '<%s at %s whose fp = %s>' % (
  640.               self.__class__.__name__, `id(self)`, `self.fp`)
  641.     def close(self):
  642.         self.read = None
  643.         self.readline = None
  644.         self.readlines = None
  645.         self.fileno = None
  646.         if self.fp: self.fp.close()
  647.         self.fp = None
  648.  
  649. # Class to add a close hook to an open file
  650. class addclosehook(addbase):
  651.     def __init__(self, fp, closehook, *hookargs):
  652.         addbase.__init__(self, fp)
  653.         self.closehook = closehook
  654.         self.hookargs = hookargs
  655.     def close(self):
  656.         if self.closehook:
  657.             apply(self.closehook, self.hookargs)
  658.             self.closehook = None
  659.             self.hookargs = None
  660.         addbase.close(self)
  661.  
  662. # class to add an info() method to an open file
  663. class addinfo(addbase):
  664.     def __init__(self, fp, headers):
  665.         addbase.__init__(self, fp)
  666.         self.headers = headers
  667.     def info(self):
  668.         return self.headers
  669.  
  670. # class to add info() and geturl() methods to an open file
  671. class addinfourl(addbase):
  672.     def __init__(self, fp, headers, url):
  673.         addbase.__init__(self, fp)
  674.         self.headers = headers
  675.         self.url = url
  676.     def info(self):
  677.         return self.headers
  678.     def geturl(self):
  679.         return self.url
  680.  
  681.  
  682. # Utility to combine a URL with a base URL to form a new URL
  683.  
  684. def basejoin(base, url):
  685.     type, path = splittype(url)
  686.     if type:
  687.         # if url is complete (i.e., it contains a type), return it
  688.         return url
  689.     host, path = splithost(path)
  690.     type, basepath = splittype(base) # inherit type from base
  691.     if host:
  692.         # if url contains host, just inherit type
  693.         if type: return type + '://' + host + path
  694.         else:
  695.             # no type inherited, so url must have started with //
  696.             # just return it
  697.             return url
  698.     host, basepath = splithost(basepath) # inherit host
  699.     basepath, basetag = splittag(basepath) # remove extraneuous cruft
  700.     basepath, basequery = splitquery(basepath) # idem
  701.     if path[:1] != '/':
  702.         # non-absolute path name
  703.         if path[:1] in ('#', '?'):
  704.             # path is just a tag or query, attach to basepath
  705.             i = len(basepath)
  706.         else:
  707.             # else replace last component
  708.             i = string.rfind(basepath, '/')
  709.         if i < 0:
  710.             # basepath not absolute
  711.             if host:
  712.                 # host present, make absolute
  713.                 basepath = '/'
  714.             else:
  715.                 # else keep non-absolute
  716.                 basepath = ''
  717.         else:
  718.             # remove last file component
  719.             basepath = basepath[:i+1]
  720.         # Interpret ../ (important because of symlinks)
  721.         while basepath and path[:3] == '../':
  722.             path = path[3:]
  723.             i = string.rfind(basepath[:-1], '/')
  724.             if i > 0:
  725.                 basepath = basepath[:i+1]
  726.             elif i == 0:
  727.                 basepath = '/'
  728.                 break
  729.             else:
  730.                 basepath = ''
  731.             
  732.         path = basepath + path
  733.     if type and host: return type + '://' + host + path
  734.     elif type: return type + ':' + path
  735.     elif host: return '//' + host + path # don't know what this means
  736.     else: return path
  737.  
  738.  
  739. # Utilities to parse URLs (most of these return None for missing parts):
  740. # unwrap('<URL:type://host/path>') --> 'type://host/path'
  741. # splittype('type:opaquestring') --> 'type', 'opaquestring'
  742. # splithost('//host[:port]/path') --> 'host[:port]', '/path'
  743. # splituser('user[:passwd]@host[:port]') --> 'user[:passwd]', 'host[:port]'
  744. # splitpasswd('user:passwd') -> 'user', 'passwd'
  745. # splitport('host:port') --> 'host', 'port'
  746. # splitquery('/path?query') --> '/path', 'query'
  747. # splittag('/path#tag') --> '/path', 'tag'
  748. # splitattr('/path;attr1=value1;attr2=value2;...') ->
  749. #   '/path', ['attr1=value1', 'attr2=value2', ...]
  750. # splitvalue('attr=value') --> 'attr', 'value'
  751. # splitgophertype('/Xselector') --> 'X', 'selector'
  752. # unquote('abc%20def') -> 'abc def'
  753. # quote('abc def') -> 'abc%20def')
  754.  
  755. def unwrap(url):
  756.     url = string.strip(url)
  757.     if url[:1] == '<' and url[-1:] == '>':
  758.         url = string.strip(url[1:-1])
  759.     if url[:4] == 'URL:': url = string.strip(url[4:])
  760.     return url
  761.  
  762. _typeprog = None
  763. def splittype(url):
  764.     global _typeprog
  765.     if _typeprog is None:
  766.         import re
  767.         _typeprog = re.compile('^([^/:]+):')
  768.  
  769.     match = _typeprog.match(url)
  770.     if match:
  771.         scheme = match.group(1)
  772.         return scheme, url[len(scheme) + 1:]
  773.     return None, url
  774.  
  775. _hostprog = None
  776. def splithost(url):
  777.     global _hostprog
  778.     if _hostprog is None:
  779.         import re
  780.         _hostprog = re.compile('^//([^/]+)(.*)$')
  781.  
  782.     match = _hostprog.match(url) 
  783.     if match: return match.group(1, 2)
  784.     return None, url
  785.  
  786. _userprog = None
  787. def splituser(host):
  788.     global _userprog
  789.     if _userprog is None:
  790.         import re
  791.         _userprog = re.compile('^([^@]*)@(.*)$')
  792.  
  793.     match = _userprog.match(host)
  794.     if match: return match.group(1, 2)
  795.     return None, host
  796.  
  797. _passwdprog = None
  798. def splitpasswd(user):
  799.     global _passwdprog
  800.     if _passwdprog is None:
  801.         import re
  802.         _passwdprog = re.compile('^([^:]*):(.*)$')
  803.  
  804.     match = _passwdprog.match(user)
  805.     if match: return match.group(1, 2)
  806.     return user, None
  807.  
  808. _portprog = None
  809. def splitport(host):
  810.     global _portprog
  811.     if _portprog is None:
  812.         import re
  813.         _portprog = re.compile('^(.*):([0-9]+)$')
  814.  
  815.     match = _portprog.match(host)
  816.     if match: return match.group(1, 2)
  817.     return host, None
  818.  
  819. # Split host and port, returning numeric port.
  820. # Return given default port if no ':' found; defaults to -1.
  821. # Return numerical port if a valid number are found after ':'.
  822. # Return None if ':' but not a valid number.
  823. _nportprog = None
  824. def splitnport(host, defport=-1):
  825.     global _nportprog
  826.     if _nportprog is None:
  827.         import re
  828.         _nportprog = re.compile('^(.*):(.*)$')
  829.  
  830.     match = _nportprog.match(host)
  831.     if match:
  832.         host, port = match.group(1, 2)
  833.         try:
  834.             if not port: raise string.atoi_error, "no digits"
  835.             nport = string.atoi(port)
  836.         except string.atoi_error:
  837.             nport = None
  838.         return host, nport
  839.     return host, defport
  840.  
  841. _queryprog = None
  842. def splitquery(url):
  843.     global _queryprog
  844.     if _queryprog is None:
  845.         import re
  846.         _queryprog = re.compile('^(.*)\?([^?]*)$')
  847.  
  848.     match = _queryprog.match(url)
  849.     if match: return match.group(1, 2)
  850.     return url, None
  851.  
  852. _tagprog = None
  853. def splittag(url):
  854.     global _tagprog
  855.     if _tagprog is None:
  856.         import re
  857.         _tagprog = re.compile('^(.*)#([^#]*)$')
  858.  
  859.     match = _tagprog.match(url)
  860.     if match: return match.group(1, 2)
  861.     return url, None
  862.  
  863. def splitattr(url):
  864.     words = string.splitfields(url, ';')
  865.     return words[0], words[1:]
  866.  
  867. _valueprog = None
  868. def splitvalue(attr):
  869.     global _valueprog
  870.     if _valueprog is None:
  871.         import re
  872.         _valueprog = re.compile('^([^=]*)=(.*)$')
  873.  
  874.     match = _valueprog.match(attr)
  875.     if match: return match.group(1, 2)
  876.     return attr, None
  877.  
  878. def splitgophertype(selector):
  879.     if selector[:1] == '/' and selector[1:2]:
  880.         return selector[1], selector[2:]
  881.     return None, selector
  882.  
  883. def unquote(s):
  884.     mychr = chr
  885.     myatoi = string.atoi
  886.     list = string.split(s, '%')
  887.     res = [list[0]]
  888.     myappend = res.append
  889.     del list[0]
  890.     for item in list:
  891.         if item[1:2]:
  892.             try:
  893.                 myappend(mychr(myatoi(item[:2], 16))
  894.                      + item[2:])
  895.             except:
  896.                 myappend('%' + item)
  897.         else:
  898.             myappend('%' + item)
  899.     return string.join(res, "")
  900.  
  901. def unquote_plus(s):
  902.     if '+' in s:
  903.         # replace '+' with ' '
  904.         s = string.join(string.split(s, '+'), ' ')
  905.     return unquote(s)
  906.  
  907. always_safe = string.letters + string.digits + '_,.-'
  908. def quote(s, safe = '/'):
  909.     safe = always_safe + safe
  910.     res = list(s)
  911.     for i in range(len(res)):
  912.         c = res[i]
  913.         if c not in safe:
  914.             res[i] = '%%%02x' % ord(c)
  915.     return string.joinfields(res, '')
  916.  
  917. def quote_plus(s, safe = '/'):
  918.     if ' ' in s:
  919.         # replace ' ' with '+'
  920.         l = string.split(s, ' ')
  921.         for i in range(len(l)):
  922.             l[i] = quote(l[i], safe)
  923.         return string.join(l, '+')
  924.     else:
  925.         return quote(s, safe)
  926.  
  927. def urlencode(dict):
  928.      l = []
  929.      for k, v in dict.items():
  930.          k = quote_plus(str(k))
  931.          v = quote_plus(str(v))
  932.          l.append(k + '=' + v)
  933.      return string.join(l, '&')
  934.  
  935.  
  936. # Proxy handling
  937. if os.name == 'mac':
  938.     def getproxies():
  939.         """Return a dictionary of scheme -> proxy server URL mappings.
  940.  
  941.         By convention the mac uses Internet Config to store
  942.         proxies.  An HTTP proxy, for instance, is stored under
  943.         the HttpProxy key.
  944.  
  945.         """
  946.         try:
  947.             import ic
  948.         except ImportError:
  949.             return {}
  950.         
  951.         try:
  952.             config = ic.IC()
  953.         except ic.error:
  954.             return {}
  955.         proxies = {}
  956.         # HTTP:
  957.         if config.has_key('UseHTTPProxy') and config['UseHTTPProxy']:
  958.             try:
  959.                 value = config['HTTPProxyHost']
  960.             except ic.error:
  961.                 pass
  962.             else:
  963.                 proxies['http'] = 'http://%s' % value
  964.         # FTP: XXXX To be done.
  965.         # Gopher: XXXX To be done.
  966.         return proxies
  967.                 
  968. else:
  969.     def getproxies():
  970.         """Return a dictionary of scheme -> proxy server URL mappings.
  971.     
  972.         Scan the environment for variables named <scheme>_proxy;
  973.         this seems to be the standard convention.  If you need a
  974.         different way, you can pass a proxies dictionary to the
  975.         [Fancy]URLopener constructor.
  976.     
  977.         """
  978.         proxies = {}
  979.         for name, value in os.environ.items():
  980.             name = string.lower(name)
  981.             if value and name[-6:] == '_proxy':
  982.                 proxies[name[:-6]] = value
  983.         return proxies
  984.  
  985.  
  986. # Test and time quote() and unquote()
  987. def test1():
  988.     import time
  989.     s = ''
  990.     for i in range(256): s = s + chr(i)
  991.     s = s*4
  992.     t0 = time.time()
  993.     qs = quote(s)
  994.     uqs = unquote(qs)
  995.     t1 = time.time()
  996.     if uqs != s:
  997.         print 'Wrong!'
  998.     print `s`
  999.     print `qs`
  1000.     print `uqs`
  1001.     print round(t1 - t0, 3), 'sec'
  1002.  
  1003.  
  1004. def reporthook(blocknum, blocksize, totalsize):
  1005.     # Report during remote transfers
  1006.     print "Block number: %d, Block size: %d, Total size: %d" % (blocknum, blocksize, totalsize)
  1007.  
  1008. # Test program
  1009. def test(args=[]):
  1010.     if not args:
  1011.         args = [
  1012.             '/etc/passwd',
  1013.             'file:/etc/passwd',
  1014.             'file://localhost/etc/passwd',
  1015.             'ftp://ftp.python.org/etc/passwd',
  1016. ##             'gopher://gopher.micro.umn.edu/1/',
  1017.             'http://www.python.org/index.html',
  1018.             ]
  1019.     try:
  1020.         for url in args:
  1021.             print '-'*10, url, '-'*10
  1022.             fn, h = urlretrieve(url, None, reporthook)
  1023.             print fn, h
  1024.             if h:
  1025.                 print '======'
  1026.                 for k in h.keys(): print k + ':', h[k]
  1027.                 print '======'
  1028.             fp = open(fn, 'rb')
  1029.             data = fp.read()
  1030.             del fp
  1031.             if '\r' in data:
  1032.                 table = string.maketrans("", "")
  1033.                 data = string.translate(data, table, "\r")
  1034.             print data
  1035.             fn, h = None, None
  1036.         print '-'*40
  1037.     finally:
  1038.         urlcleanup()
  1039.  
  1040. def main():
  1041.     import getopt, sys
  1042.     try:
  1043.         opts, args = getopt.getopt(sys.argv[1:], "th")
  1044.     except getopt.error, msg:
  1045.         print msg
  1046.         print "Use -h for help"
  1047.         return
  1048.     t = 0
  1049.     for o, a in opts:
  1050.         if o == '-t':
  1051.             t = t + 1
  1052.         if o == '-h':
  1053.             print "Usage: python urllib.py [-t] [url ...]"
  1054.             print "-t runs self-test;",
  1055.             print "otherwise, contents of urls are printed"
  1056.             return
  1057.     if t:
  1058.         if t > 1:
  1059.             test1()
  1060.         test(args)
  1061.     else:
  1062.         if not args:
  1063.             print "Use -h for help"
  1064.         for url in args:
  1065.             print urlopen(url).read(),
  1066.  
  1067. # Run test program when run as a script
  1068. if __name__ == '__main__':
  1069.     main()
  1070.