Package cssutils :: Module util |
[hide private]
[frames] | no frames]

# Source Code for Module cssutils.util

      1  """base classes and helper functions for css and stylesheets packages
      2  """
      3  __all__ = []
      4  __docformat__ = 'restructuredtext'
      5  __version__ = '$Id: util.py 1429 2008-08-11 19:01:52Z cthedot $'
      6
      7  import codecs
      8  from itertools import ifilter
      9  import types
     10  import urllib2
     11  import xml.dom
     12
     13  from helper import normalize
     14  import tokenize2
     15  import cssutils
     16  import encutils
     17
     18 -class Base(object):
     19      """
     20      Base class for most CSS and StyleSheets classes
     21
     22      **Superceded by Base2 which is used for new seq handling class.**
     23      See cssutils.util.Base2
     24
     25      Contains helper methods for inheriting classes helping parsing
     26
     27      ``_normalize`` is static as used by Preferences.
     28      """
     29      __tokenizer2 = tokenize2.Tokenizer()
     30
     31      _log = cssutils.log
     32      _prods = tokenize2.CSSProductions
     33
     34      # for more on shorthand properties see
     35      # http://www.dustindiaz.com/css-shorthand/
     36      # format: shorthand: [(propname, mandatorycheck?)*]
     37      _SHORTHANDPROPERTIES = {
     38              u'background': [],
     39              u'background-position': [],
     40              u'border': [],
     41              u'border-left': [],
     42              u'border-right': [],
     43              u'border-top': [],
     44              u'border-bottom': [],
     45              #u'border-color': [], # list or single but same values
     46              #u'border-style': [], # list or single but same values
     47              #u'border-width': [], # list or single but same values
     48              u'cue': [],
     49              u'font': [],
     50              u'list-style': [],
     51              #u'margin': [], # list or single but same values
     52              u'outline': [],
     53              #u'padding': [], # list or single but same values
     54              u'pause': []
     55              }
     56
     57      @staticmethod
     58 -    def _normalize(x):
     59          """
     60          normalizes x, namely:
     61
     62          - remove any \ before non unicode sequences (0-9a-zA-Z) so for
     63            x=="c\olor\" return "color" (unicode escape sequences should have
     64            been resolved by the tokenizer already)
     65          - lowercase
     66          """
     67          return normalize(x)
     68
     69 -    def _checkReadonly(self):
     70          "raises xml.dom.NoModificationAllowedErr if rule/... is readonly"
     71          if hasattr(self, '_readonly') and self._readonly:
     72              raise xml.dom.NoModificationAllowedErr(
     73                  u'%s is readonly.' % self.__class__)
     74              return True
     75          return False
     76
     77 -    def _splitNamespacesOff(self, text_namespaces_tuple):
     78          """
     79          returns tuple (text, dict-of-namespaces) or if no namespaces are
     80          in cssText returns (cssText, {})
     81
     82          used in Selector, SelectorList, CSSStyleRule, CSSMediaRule and
     83          CSSStyleSheet
     84          """
     85          if isinstance(text_namespaces_tuple, tuple):
     86              return text_namespaces_tuple[0], _SimpleNamespaces(self._log,
     87                                                      text_namespaces_tuple[1])
     88          else:
     89              return text_namespaces_tuple, _SimpleNamespaces(log=self._log)
     90
     91 -    def _tokenize2(self, textortokens):
     92          """
     93          returns tokens of textortokens which may already be tokens in which
     94          case simply returns input
     95          """
     96          if not textortokens:
     97              return None
     98          elif isinstance(textortokens, basestring):
     99              # needs to be tokenized
    100              return self.__tokenizer2.tokenize(
    101                   textortokens)
    102          elif types.GeneratorType == type(textortokens):
    103              # already tokenized
    104              return textortokens
    105          elif isinstance(textortokens, tuple):
    106              # a single token (like a comment)
    107              return [textortokens]
    108          else:
    109              # already tokenized but return generator
    110              return (x for x in textortokens)
    111
    112 -    def _nexttoken(self, tokenizer, default=None):
    113          "returns next token in generator tokenizer or the default value"
    114          try:
    115              return tokenizer.next()
    116          except (StopIteration, AttributeError):
    117              return default
    118
    119 -    def _type(self, token):
    120          "returns type of Tokenizer token"
    121          if token:
    122              return token[0]
    123          else:
    124              return None
    125
    126 -    def _tokenvalue(self, token, normalize=False):
    127          "returns value of Tokenizer token"
    128          if token and normalize:
    129              return Base._normalize(token[1])
    130          elif token:
    131              return token[1]
    132          else:
    133              return None
    134
    135 -    def _stringtokenvalue(self, token):
    136          """
    137          for STRING returns the actual content without surrounding "" or ''
    138          and without respective escapes, e.g.::
    139
    140               "with \" char" => with " char
    141          """
    142          if token:
    143              value = token[1]
    144              return value.replace('\\'+value[0], value[0])[1:-1]
    145          else:
    146              return None
    147
    148 -    def _uritokenvalue(self, token):
    149          """
    150          for URI returns the actual content without surrounding url()
    151          or url(""), url('') and without respective escapes, e.g.::
    152
    153               url("\"") => "
    154          """
    155          if token:
    156              value = token[1][4:-1].strip()
    157              if (value[0] in '\'"') and (value[0] == value[-1]):
    158                  # a string "..." or '...'
    159                  value = value.replace('\\'+value[0], value[0])[1:-1]
    160              return value
    161          else:
    162              return None
    163
    164 -    def _tokensupto2(self,
    165                       tokenizer,
    166                       starttoken=None,
    167                       blockstartonly=False, # {
    168                       blockendonly=False, # }
    169                       mediaendonly=False,
    170                       importmediaqueryendonly=False, # ; or STRING
    171                       mediaqueryendonly=False, # { or STRING
    172                       semicolon=False, # ;
    173                       propertynameendonly=False, # :
    174                       propertyvalueendonly=False, # ! ; }
    175                       propertypriorityendonly=False, # ; }
    176                       selectorattendonly=False, # ]
    177                       funcendonly=False, # )
    178                       listseponly=False, # ,
    179                       separateEnd=False # returns (resulttokens, endtoken)
    180                       ):
    181          """
    182          returns tokens upto end of atrule and end index
    183          end is defined by parameters, might be ; } ) or other
    184
    185          default looks for ending "}" and ";"
    186          """
    187          ends = u';}'
    188          endtypes = ()
    189          brace = bracket = parant = 0 # {}, [], ()
    190
    191          if blockstartonly: # {
    192              ends = u'{'
    193              brace = -1 # set to 0 with first {
    194          elif blockendonly: # }
    195              ends = u'}'
    196              brace = 1
    197          elif mediaendonly: # }
    198              ends = u'}'
    199              brace = 1 # rules } and mediarules }
    200          elif importmediaqueryendonly:
    201              # end of mediaquery which may be ; or STRING
    202              ends = u';'
    203              endtypes = ('STRING',)
    204          elif mediaqueryendonly:
    205              # end of mediaquery which may be { or STRING
    206              # special case, see below
    207              ends = u'{'
    208              brace = -1 # set to 0 with first {
    209              endtypes = ('STRING',)
    210          elif semicolon:
    211              ends = u';'
    212          elif propertynameendonly: # : and ; in case of an error
    213              ends = u':;'
    214          elif propertyvalueendonly: # ; or !important
    215              ends = u';!'
    216          elif propertypriorityendonly: # ;
    217              ends = u';'
    218          elif selectorattendonly: # ]
    219              ends = u']'
    220              if starttoken and self._tokenvalue(starttoken) == u'[':
    221                  bracket = 1
    222          elif funcendonly: # )
    223              ends = u')'
    224              parant = 1
    225          elif listseponly: # ,
    226              ends = u','
    227
    228          resulttokens = []
    229          if starttoken:
    230              resulttokens.append(starttoken)
    231          if tokenizer:
    232              for token in tokenizer:
    233                  typ, val, line, col = token
    234                  if 'EOF' == typ:
    235                      resulttokens.append(token)
    236                      break
    237                  if u'{' == val:
    238                      brace += 1
    239                  elif u'}' == val:
    240                      brace -= 1
    241                  elif u'[' == val:
    242                      bracket += 1
    243                  elif u']' == val:
    244                      bracket -= 1
    245                  # function( or single (
    246                  elif u'(' == val or \
    247                       Base._prods.FUNCTION == typ:
    248                      parant += 1
    249                  elif u')' == val:
    250                      parant -= 1
    251
    252                  resulttokens.append(token)
    253
    254                  if (brace == bracket == parant == 0) and (
    255                      val in ends or typ in endtypes):
    256                      break
    257                  elif mediaqueryendonly and brace == -1 and (
    258                       bracket == parant == 0) and typ in endtypes:
    259                       # mediaqueryendonly with STRING
    260                      break
    261
    262          if separateEnd:
    263              # TODO: use this method as generator, then this makes sense
    264              if resulttokens:
    265                  return resulttokens[:-1], resulttokens[-1]
    266              else:
    267                  return resulttokens, None
    268          else:
    269              return resulttokens
    270
    271 -    def _valuestr(self, t):
    272          """
    273          returns string value of t (t may be a string, a list of token tuples
    274          or a single tuple in format (type, value, line, col).
    275          Mainly used to get a string value of t for error messages.
    276          """
    277          if not t:
    278              return u''
    279          elif isinstance(t, basestring):
    280              return t
    281          else:
    282              return u''.join([x[1] for x in t])
    283
    284 -    def _adddefaultproductions(self, productions, new=None):
    285          """
    286          adds default productions if not already present, used by
    287          _parse only
    288
    289          each production should return the next expected token
    290          normaly a name like "uri" or "EOF"
    291          some have no expectation like S or COMMENT, so simply return
    292          the current value of self.__expected
    293          """
    294          def ATKEYWORD(expected, seq, token, tokenizer=None):
    295              "default impl for unexpected @rule"
    296              if expected != 'EOF':
    297                  # TODO: parentStyleSheet=self
    298                  rule = cssutils.css.CSSUnknownRule()
    299                  rule.cssText = self._tokensupto2(tokenizer, token)
    300                  if rule.wellformed:
    301                      seq.append(rule)
    302                  return expected
    303              else:
    304                  new['wellformed'] = False
    305                  self._log.error(u'Expected EOF.', token=token)
    306                  return expected
    307
    308          def COMMENT(expected, seq, token, tokenizer=None):
    309              "default implementation for COMMENT token adds CSSCommentRule"
    310              seq.append(cssutils.css.CSSComment([token]))
    311              return expected
    312
    313          def S(expected, seq, token, tokenizer=None):
    314              "default implementation for S token, does nothing"
    315              return expected
    316
    317          def EOF(expected=None, seq=None, token=None, tokenizer=None):
    318              "default implementation for EOF token"
    319              return 'EOF'
    320
    321          p = {'ATKEYWORD': ATKEYWORD,
    322               'COMMENT': COMMENT,
    323               'S': S,
    324               'EOF': EOF # only available if fullsheet
    325               }
    326          p.update(productions)
    327          return p
    328
    329 -    def _parse(self, expected, seq, tokenizer, productions, default=None,
    330                 new=None):
    331          """
    332          puts parsed tokens in seq by calling a production with
    333              (seq, tokenizer, token)
    334
    335          expected
    336              a name what token or value is expected next, e.g. 'uri'
    337          seq
    338              to add rules etc to
    339          tokenizer
    340              call tokenizer.next() to get next token
    341          productions
    342              callbacks {tokentype: callback}
    343          default
    344              default callback if tokentype not in productions
    345          new
    346              used to init default productions
    347
    348          returns (wellformed, expected) which the last prod might have set
    349          """
    350          wellformed = True
    351          if tokenizer:
    352              prods = self._adddefaultproductions(productions, new)
    353              for token in tokenizer:
    354                  p = prods.get(token[0], default)
    355                  if p:
    356                      expected = p(expected, seq, token, tokenizer)
    357                  else:
    358                      wellformed = False
    359                      self._log.error(u'Unexpected token (%s, %s, %s, %s)' % token)
    360          return wellformed, expected
    361
    362
    363 -class Base2(Base):
    364      """
    365      Base class for new seq handling, used by Selector for now only
    366      """
    367 -    def __init__(self):
    368          self._seq = Seq()
    369
    370 -    def _setSeq(self, newseq):
    371          """
    372          sets newseq and makes it readonly
    373          """
    374          newseq._readonly = True
    375          self._seq = newseq
    376
    377      seq = property(lambda self: self._seq, doc="seq for most classes")
    378
    379 -    def _tempSeq(self, readonly=False):
    380          "get a writeable Seq() which is added later"
    381          return Seq(readonly=readonly)
    382
    383 -    def _adddefaultproductions(self, productions, new=None):
    384          """
    385          adds default productions if not already present, used by
    386          _parse only
    387
    388          each production should return the next expected token
    389          normaly a name like "uri" or "EOF"
    390          some have no expectation like S or COMMENT, so simply return
    391          the current value of self.__expected
    392          """
    393          def ATKEYWORD(expected, seq, token, tokenizer=None):
    394              "default impl for unexpected @rule"
    395              if expected != 'EOF':
    396                  # TODO: parentStyleSheet=self
    397                  rule = cssutils.css.CSSUnknownRule()
    398                  rule.cssText = self._tokensupto2(tokenizer, token)
    399                  if rule.wellformed:
    400                      seq.append(rule, cssutils.css.CSSRule.UNKNOWN_RULE,
    401                                 line=token[2], col=token[3])
    402                  return expected
    403              else:
    404                  new['wellformed'] = False
    405                  self._log.error(u'Expected EOF.', token=token)
    406                  return expected
    407
    408          def COMMENT(expected, seq, token, tokenizer=None):
    409              "default impl, adds CSSCommentRule if not token == EOF"
    410              if expected == 'EOF':
    411                  new['wellformed'] = False
    412                  self._log.error(u'Expected EOF but found comment.', token=token)
    413              seq.append(cssutils.css.CSSComment([token]), 'COMMENT')
    414              return expected
    415
    416          def S(expected, seq, token, tokenizer=None):
    417              "default impl, does nothing if not token == EOF"
    418              if expected == 'EOF':
    419                  new['wellformed'] = False
    420                  self._log.error(u'Expected EOF but found whitespace.', token=token)
    421              return expected
    422
    423          def EOF(expected=None, seq=None, token=None, tokenizer=None):
    424              "default implementation for EOF token"
    425              return 'EOF'
    426
    427          defaultproductions = {'ATKEYWORD': ATKEYWORD,
    428               'COMMENT': COMMENT,
    429               'S': S,
    430               'EOF': EOF # only available if fullsheet
    431               }
    432          defaultproductions.update(productions)
    433          return defaultproductions
    434
    435
    436 -class Seq(object):
    437      """
    438      property seq of Base2 inheriting classes, holds a list of Item objects.
    439
    440      used only by Selector for now
    441
    442      is normally readonly, only writable during parsing
    443      """
    444 -    def __init__(self, readonly=True):
    445          """
    446          only way to write to a Seq is to initialize it with new items
    447          each itemtuple has (value, type, line) where line is optional
    448          """
    449          self._seq = []
    450          self._readonly = readonly
    451
    452 -    def __delitem__(self, i):
    453          del self._seq[i]
    454
    455 -    def __getitem__(self, i):
    456          return self._seq[i]
    457
    458 -    def __setitem__(self, i, (val, typ, line, col)):
    459          self._seq[i] = Item(val, typ, line, col)
    460
    461 -    def __iter__(self):
    462          return iter(self._seq)
    463
    464 -    def __len__(self):
    465          return len(self._seq)
    466
    467 -    def append(self, val, typ, line=None, col=None):
    468          "if not readonly add new Item()"
    469          if self._readonly:
    470              raise AttributeError('Seq is readonly.')
    471          else:
    472              self._seq.append(Item(val, typ, line, col))
    473
    474 -    def appendItem(self, item):
    475          "if not readonly add item which must be an Item"
    476          if self._readonly:
    477              raise AttributeError('Seq is readonly.')
    478          else:
    479              self._seq.append(item)
    480
    481 -    def replace(self, index=-1, val=None, typ=None, line=None, col=None):
    482          """
    483          if not readonly replace Item at index with new Item or
    484          simply replace value or type
    485          """
    486          if self._readonly:
    487              raise AttributeError('Seq is readonly.')
    488          else:
    489              self._seq[index] = Item(val, typ, line, col)
    490
    491 -    def __repr__(self):
    492          "returns a repr same as a list of tuples of (value, type)"
    493          return u'cssutils.%s.%s([\n    %s])' % (self.__module__,
    494                                            self.__class__.__name__,
    495              u',\n    '.join([u'(%r, %r)' % (item.type, item.value)
    496                            for item in self._seq]
    497              ))
    498 -    def __str__(self):
    499          return "<cssutils.%s.%s object length=%r at 0x%x>" % (
    500                  self.__module__, self.__class__.__name__, len(self), id(self))
    501
    502 -class Item(object):
    503      """
    504      an item in the seq list of classes (successor to tuple items in old seq)
    505
    506      each item has attributes:
    507
    508      type
    509          a sematic type like "element", "attribute"
    510      value
    511          the actual value which may be a string, number etc or an instance
    512          of e.g. a CSSComment
    513      *line*
    514          **NOT IMPLEMENTED YET, may contain the line in the source later**
    515      """
    516 -    def __init__(self, value, type, line=None, col=None):
    517          self.__value = value
    518          self.__type = type
    519          self.__line = line
    520          self.__col = col
    521
    522      type = property(lambda self: self.__type)
    523      value = property(lambda self: self.__value)
    524      line = property(lambda self: self.__line)
    525      col = property(lambda self: self.__col)
    526
    527 -    def __repr__(self):
    528          return "%s.%s(value=%r, type=%r, line=%r, col=%r)" % (
    529                  self.__module__, self.__class__.__name__,
    530                  self.__value, self.__type, self.__line, self.__col)
    531
    532
    533 -class ListSeq(object):
    534      """
    535      (EXPERIMENTAL)
    536      A base class used for list classes like css.SelectorList or
    537      stylesheets.MediaList
    538
    539      adds list like behaviour running on inhering class' property ``seq``
    540
    541      - item in x => bool
    542      - len(x) => integer
    543      - get, set and del x[i]
    544      - for item in x
    545      - append(item)
    546
    547      some methods must be overwritten in inheriting class
    548      """
    549 -    def __init__(self):
    550          self.seq = [] # does not need to use ``Seq`` as simple list only
    551
    552 -    def __contains__(self, item):
    553          return item in self.seq
    554
    555 -    def __delitem__(self, index):
    556          del self.seq[index]
    557
    558 -    def __getitem__(self, index):
    559          return self.seq[index]
    560
    561 -    def __iter__(self):
    562          def gen():
    563              for x in self.seq:
    564                  yield x
    565          return gen()
    566
    567 -    def __len__(self):
    568          return len(self.seq)
    569
    570 -    def __setitem__(self, index, item):
    571          "must be overwritten"
    572          raise NotImplementedError
    573
    574 -    def append(self, item):
    575          "must be overwritten"
    576          raise NotImplementedError
    577
    578
    579 -class _Namespaces(object):
    580      """
    581      A dictionary like wrapper for @namespace rules used in a CSSStyleSheet.
    582      Works on effective namespaces, so e.g. if::
    583
    584          @namespace p1 "uri";
    585          @namespace p2 "uri";
    586
    587      only the second rule is effective and kept.
    588
    589      namespaces
    590          a dictionary {prefix: namespaceURI} containing the effective namespaces
    591          only. These are the latest set in the CSSStyleSheet.
    592      parentStyleSheet
    593          the parent CSSStyleSheet
    594      """
    595 -    def __init__(self, parentStyleSheet, log=None, *args):
    596          "no initial values are set, only the relevant sheet is"
    597          self.parentStyleSheet = parentStyleSheet
    598          self._log = log
    599
    600 -    def __contains__(self, prefix):
    601          return prefix in self.namespaces
    602
    603 -    def __delitem__(self, prefix):
    604          """deletes CSSNamespaceRule(s) with rule.prefix == prefix
    605
    606          prefix '' and None are handled the same
    607          """
    608          if not prefix:
    609              prefix = u''
    610          delrule = self.__findrule(prefix)
    611          for i, rule in enumerate(ifilter(lambda r: r.type == r.NAMESPACE_RULE,
    612                              self.parentStyleSheet.cssRules)):
    613              if rule == delrule:
    614                  self.parentStyleSheet.deleteRule(i)
    615                  return
    616
    617          self._log.error('Prefix %r not found.' % prefix,
    618                          error=xml.dom.NamespaceErr)
    619
    620 -    def __getitem__(self, prefix):
    621          try:
    622              return self.namespaces[prefix]
    623          except KeyError, e:
    624              self._log.error('Prefix %r not found.' % prefix,
    625                              error=xml.dom.NamespaceErr)
    626
    627 -    def __iter__(self):
    628          return self.namespaces.__iter__()
    629
    630 -    def __len__(self):
    631          return len(self.namespaces)
    632
    633 -    def __setitem__(self, prefix, namespaceURI):
    634          "replaces prefix or sets new rule, may raise NoModificationAllowedErr"
    635          if not prefix:
    636              prefix = u'' # None or ''
    637          rule = self.__findrule(prefix)
    638          if not rule:
    639              self.parentStyleSheet.insertRule(cssutils.css.CSSNamespaceRule(
    640                                                      prefix=prefix,
    641                                                      namespaceURI=namespaceURI),
    642                                    inOrder=True)
    643          else:
    644              if prefix in self.namespaces:
    645                  rule.namespaceURI = namespaceURI # raises NoModificationAllowedErr
    646              if namespaceURI in self.namespaces.values():
    647                  rule.prefix = prefix
    648
    649 -    def __findrule(self, prefix):
    650          # returns namespace rule where prefix == key
    651          for rule in ifilter(lambda r: r.type == r.NAMESPACE_RULE,
    652                              reversed(self.parentStyleSheet.cssRules)):
    653              if rule.prefix == prefix:
    654                  return rule
    655
    656 -    def __getNamespaces(self):
    657          namespaces = {}
    658          for rule in ifilter(lambda r: r.type == r.NAMESPACE_RULE,
    659                              reversed(self.parentStyleSheet.cssRules)):
    660              if rule.namespaceURI not in namespaces.values():
    661                  namespaces[rule.prefix] = rule.namespaceURI
    662          return namespaces
    663
    664      namespaces = property(__getNamespaces,
    665          doc=u'Holds only effective @namespace rules in self.parentStyleSheets'
    666               '@namespace rules.')
    667
    668 -    def get(self, prefix, default):
    669          return self.namespaces.get(prefix, default)
    670
    671 -    def items(self):
    672          return self.namespaces.items()
    673
    674 -    def keys(self):
    675          return self.namespaces.keys()
    676
    677 -    def values(self):
    678          return self.namespaces.values()
    679
    680 -    def prefixForNamespaceURI(self, namespaceURI):
    681          """
    682          returns effective prefix for given namespaceURI or raises IndexError
    683          if this cannot be found"""
    684          for prefix, uri in self.namespaces.items():
    685              if uri == namespaceURI:
    686                  return prefix
    687          raise IndexError(u'NamespaceURI %r not found.' % namespaceURI)
    688
    689 -    def __str__(self):
    690          return u"<cssutils.util.%s object parentStyleSheet=%r at 0x%x>" % (
    691                  self.__class__.__name__, str(self.parentStyleSheet), id(self))
    692
    693
    694 -class _SimpleNamespaces(_Namespaces):
    695      """
    696      namespaces used in objects like Selector as long as they are not connected
    697      to a CSSStyleSheet
    698      """
    699 -    def __init__(self, log=None, *args):
    700          """init"""
    701          super(_SimpleNamespaces, self).__init__(parentStyleSheet=None, log=log)
    702          self.__namespaces = dict(*args)
    703
    704 -    def __setitem__(self, prefix, namespaceURI):
    705          self.__namespaces[prefix] = namespaceURI
    706
    707      namespaces = property(lambda self: self.__namespaces,
    708                            doc=u'Dict Wrapper for self.sheets @namespace rules.')
    709
    710 -    def __str__(self):
    711          return u"<cssutils.util.%s object namespaces=%r at 0x%x>" % (
    712                  self.__class__.__name__, self.namespaces, id(self))
    713
    714 -    def __repr__(self):
    715          return u"cssutils.util.%s(%r)" % (self.__class__.__name__,
    716              self.namespaces)
    717
    718
    719 -def _defaultFetcher(url):
    720      """Retrieve data from ``url``. cssutils default implementation of fetch
    721      URL function.
    722
    723      Returns ``(encoding, string)`` or ``None``
    724      """
    725      try:
    726          res = urllib2.urlopen(url)
    727      except OSError, e:
    728          # e.g if file URL and not found
    729          cssutils.log.warn(e, error=OSError)
    730      except (OSError, ValueError), e:
    731          # invalid url, e.g. "1"
    732          cssutils.log.warn(u'ValueError, %s' % e.message, error=ValueError)
    733      except urllib2.HTTPError, e:
    734          # http error, e.g. 404, e can be raised
    735          cssutils.log.warn(u'HTTPError opening url=%r: %s %s' %
    736                            (url, e.code, e.msg), error=e)
    737      except urllib2.URLError, e:
    738          # URLError like mailto: or other IO errors, e can be raised
    739          cssutils.log.warn(u'URLError, %s' % e.reason, error=e)
    740      else:
    741          if res:
    742              mimeType, encoding = encutils.getHTTPInfo(res)
    743              if mimeType != u'text/css':
    744                  cssutils.log.error(u'Expected "text/css" mime type for url=%s but found: %r' %
    745                                    (url, mimeType), error=ValueError)
    746              return encoding, res.read()
    747
    748 -def _readUrl(url, fetcher=None, overrideEncoding=None, parentEncoding=None):
    749      """
    750      Read cssText from url and decode it using all relevant methods (HTTP
    751      header, BOM, @charset). Returns
    752
    753      - encoding used to decode text (which is needed to set encoding of
    754        stylesheet properly)
    755      - type of encoding (how it was retrieved, see list below)
    756      - decodedCssText
    757
    758      ``fetcher``
    759          see cssutils.registerFetchUrl for details
    760      ``overrideEncoding``
    761          If given this encoding is used and all other encoding information is
    762          ignored (HTTP, BOM etc)
    763      ``parentEncoding``
    764          Encoding of parent stylesheet (while e.g. reading @import references sheets)
    765          or document if available.
    766
    767      Priority or encoding information
    768      --------------------------------
    769      **cssutils only**: 0. overrideEncoding
    770
    771      1. An HTTP "charset" parameter in a "Content-Type" field (or similar parameters in other protocols)
    772      2. BOM and/or @charset (see below)
    773      3. <link charset=""> or other metadata from the linking mechanism (if any)
    774      4. charset of referring style sheet or document (if any)
    775      5. Assume UTF-8
    776
    777      """
    778      enctype = None
    779
    780      if not fetcher:
    781          fetcher = _defaultFetcher
    782      r = fetcher(url)
    783      if r and len(r) == 2 and r[1] is not None:
    784          httpEncoding, content = r
    785
    786          if overrideEncoding:
    787              enctype = 0 # 0. override encoding
    788              encoding = overrideEncoding
    789          elif httpEncoding:
    790              enctype = 1 # 1. HTTP
    791              encoding = httpEncoding
    792          else:
    793              # check content
    794              contentEncoding, explicit = cssutils.codec.detectencoding_str(content)
    795              if explicit:
    796                  enctype = 2 # 2. BOM/@charset: explicitly
    797                  encoding = contentEncoding
    798              elif parentEncoding:
    799                  enctype = 4 # 4. parent stylesheet or document
    800                  # may also be None in which case 5. is used in next step anyway
    801                  encoding = parentEncoding
    802              else:
    803                  enctype = 5 # 5. assume UTF-8
    804                  encoding = 'utf-8'
    805
    806          try:
    807              # encoding may still be wrong if encoding *is lying*!
    808              if content is not None:
    809                  decodedCssText = codecs.lookup("css")[1](content, encoding=encoding)[0]
    810              else:
    811                  decodedCssText = None
    812          except UnicodeDecodeError, e:
    813              cssutils.log.warn(e, neverraise=True)
    814              decodedCssText = None
    815
    816          return encoding, enctype, decodedCssText
    817      else:
    818          return None, None, None
    819