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