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