Edgewall Software

Ticket #3332: mimeview_refactoring-typerepr-r3507.diff

File mimeview_refactoring-typerepr-r3507.diff, 97.2 kB (added by cboos, 2 years ago)

Further progress on the refactoring. Now covers the Ticket module. For this, I had to introduce a base class for the MimeType?: it's the very general TypeRepr?, which is also subclassed by TypeWrapper?. This enable to use the IContentConverter machinery for converting plain old Python objects to other contents. Still need some more polishing, of course.

  • trac/attachment.py

     
    2626from trac.config import BoolOption, IntOption 
    2727from trac.core import * 
    2828from trac.env import IEnvironmentSetupParticipant 
    29 from trac.mimeview import * 
     29from trac.mimeview.api import Mimeview, MimeType, FileMimeContent 
    3030from trac.util import get_reporter_id, create_unique_file 
    3131from trac.util.datefmt import format_datetime, pretty_timedelta 
    3232from trac.util.markup import Markup, html 
     
    124124    def parent_href(self, req): 
    125125        return req.href(self.parent_type, self.parent_id) 
    126126 
    127     def _get_title(self): 
     127    def _get_title(self): # TODO: should extend to other `parent_type`s 
    128128        return '%s%s: %s' % (self.parent_type == 'ticket' and '#' or '', 
    129129                             self.parent_id, self.filename) 
    130130    title = property(_get_title) 
     
    227227 
    228228    select = classmethod(select) 
    229229 
    230     def open(self): 
     230    def open(self): # deprecate? 
    231231        self.env.log.debug('Trying to open attachment at %s', self.path) 
    232232        try: 
    233233            fd = open(self.path, 'rb') 
     
    505505                                 'author': get_reporter_id(req)} 
    506506 
    507507    def _render_view(self, req, attachment): 
     508        # FIXME: perm_map should extend to other `parent_type`s 
    508509        perm_map = {'ticket': 'TICKET_VIEW', 'wiki': 'WIKI_VIEW'} 
    509510        req.perm.assert_permission(perm_map[attachment.parent_type]) 
    510511 
     
    522523        if req.perm.has_permission(perm_map[attachment.parent_type]): 
    523524            req.hdf['attachment.can_delete'] = 1 
    524525 
    525         fd = attachment.open() 
    526         try: 
    527             mimeview = Mimeview(self.env) 
     526        format = req.args.get('format') 
     527        raw_href = attachment.href(req, format='raw') 
    528528 
    529             # MIME type detection 
    530             str_data = fd.read(1000) 
    531             fd.seek(0) 
    532              
    533             binary = is_binary(str_data) 
    534             mime_type = mimeview.get_mimetype(attachment.filename, str_data) 
     529        mimecontent = FileMimeContent(self.env, attachment.path, raw_href, 
     530                                      'Attachment') 
     531        can_render_as_txt = self.render_unsafe_content and \ 
     532                             not mimecontent.is_binary 
    535533 
    536             # Eventually send the file directly 
    537             format = req.args.get('format') 
    538             if format in ('raw', 'txt'): 
    539                 if not self.render_unsafe_content and not binary: 
    540                     # Force browser to download HTML/SVG/etc pages that may 
    541                     # contain malicious code enabling XSS attacks 
    542                     req.send_header('Content-Disposition', 'attachment;' + 
    543                                     'filename=' + attachment.filename) 
    544                 if not mime_type or (self.render_unsafe_content and \ 
    545                                      not binary and format == 'txt'): 
    546                     mime_type = 'text/plain' 
    547                 if 'charset=' not in mime_type: 
    548                     charset = mimeview.get_charset(str_data, mime_type) 
    549                     mime_type = mime_type + '; charset=' + charset 
    550                 req.send_file(attachment.path, mime_type) 
     534        # Eventually send the file directly 
     535        if format in ('raw', 'txt'): 
     536            if not self.render_unsafe_content and not mimecontent.is_binary: 
     537                # Force browser to download HTML/SVG/etc pages that may 
     538                # contain malicious code enabling XSS attacks 
     539                req.send_header('Content-Disposition', 
     540                                'attachment;filename=%s' % attachment.filename) 
     541            if not mimecontent.type.is_known or \ 
     542                   (format == 'txt' and can_render_as_txt): 
     543                # Force the content to be displayed as text 
     544                type = MimeType('text/plain', mimecontent.encoding) 
     545            else: 
     546                type = mimecontent.type 
     547            req.send_file(attachment.path, type.mimetype_charset) 
    551548 
    552             # add ''Plain Text'' alternate link if needed 
    553             if self.render_unsafe_content and not binary and \ 
    554                mime_type and not mime_type.startswith('text/plain'): 
    555                 plaintext_href = attachment.href(req, format='txt') 
    556                 add_link(req, 'alternate', plaintext_href, 'Plain Text', 
    557                          mime_type) 
     549        mimetype = mimecontent.type.mimetype 
    558550 
    559             # add ''Original Format'' alternate link (always) 
    560             raw_href = attachment.href(req, format='raw') 
    561             add_link(req, 'alternate', raw_href, 'Original Format', mime_type) 
     551        # add ''Plain Text'' alternate link if needed 
     552        if can_render_as_txt and mimecontent.type.is_known and \ 
     553               mimetype != 'text/plain': 
     554            add_link(req, 'alternate', attachment.href(req, format='txt'), 
     555                     'Plain Text', 'text/plain') 
    562556 
    563             self.log.debug("Rendering preview of file %s with mime-type %s" 
    564                            % (attachment.filename, mime_type)) 
     557        # add ''Original Format'' alternate link (always) 
     558        add_link(req, 'alternate', raw_href, 'Original Format', mimetype) 
    565559 
    566             req.hdf['attachment'] = mimeview.preview_to_hdf( 
    567                 req, fd, os.fstat(fd.fileno()).st_size, mime_type, 
    568                 attachment.filename, raw_href, annotations=['lineno']) 
    569         finally: 
    570             fd.close() 
     560        self.log.debug("Rendering preview of file %s with mime-type %s" % \ 
     561                       (attachment.filename, mimetype)) 
    571562 
     563        req.hdf['attachment'] = Mimeview(self.env).preview_to_hdf( 
     564            req, mimecontent, annotations=['lineno']) 
     565 
    572566    def _render_list(self, req, p_type, p_id): 
    573567        self._parent_to_hdf(req, p_type, p_id) 
    574568        req.hdf['attachment'] = { 
  • trac/mimeview/rst.py

     
    2828import re 
    2929 
    3030from trac.core import * 
    31 from trac.mimeview.api import IHTMLPreviewRenderer, content_to_unicode 
     31from trac.mimeview.api import IHTMLPreviewRenderer 
    3232from trac.web.href import Href 
    3333from trac.wiki.formatter import WikiProcessor 
    3434from trac.wiki import WikiSystem 
     
    223223 
    224224        _inliner = rst.states.Inliner() 
    225225        _parser = rst.Parser(inliner=_inliner) 
    226         content = content_to_unicode(self.env, content, mimetype) 
     226        content = unicode(content) 
    227227        content = content.encode('utf-8') 
    228228        html = publish_string(content, writer_name='html', parser=_parser, 
    229229                              settings_overrides={'halt_level': 6}) 
  • trac/mimeview/api.py

     
    1919#         Christian Boos <cboos@neuf.fr> 
    2020 
    2121""" 
    22 The `trac.mimeview` module centralize the intelligence related to 
    23 file metadata, principally concerning the `type` (MIME type) of the content 
    24 and, if relevant, concerning the text encoding (charset) used by the content. 
     22The `trac.mimeview` module centralizes the intelligence related to file 
     23metadata, principally concerning the `type` (MIME type) and the text 
     24encoding (charset) used by the content, if the latter one is relevant. 
    2525 
    2626There are primarily two approaches for getting the MIME type of a given file: 
    2727 * taking advantage of existing conventions for the file name 
    2828 * examining the file content and applying various heuristics 
    2929 
    30 The module also knows how to convert the file content from one type 
    31 to another type. 
     30The module also knows about conversions from one data type to another type, 
     31like conversions to text/html (this is no more a special case). 
    3232 
    33 In some cases, only the `url` pointing to the file's content is actually 
    34 needed, that's why we avoid to read the file's content when it's not needed. 
    35  
    36 The actual `content` to be converted might be a `unicode` object, 
    37 but it can also be the raw byte string (`str`) object, or simply 
    38 an object that can be `read()`. 
     33In order to keep the API simple, we deal with a few classes, 
     34each encapsulating a part of the knowledge related to content. 
     35 * the `MimeType`, for storing the mime type string, the charset, 
     36   but also eventually the name and the commonly used file extension 
     37   for that type 
     38 * the `MimeContent` and `FileMimeContent`, which provide a flexible 
     39   API for handling string and file content, respectively. 
     40   Those classes inherit from the abstract `MimeContentBase`, which 
     41   can be used to make new wrapper classes for any kind of content. 
     42 * the `Conversion` class, which is use to specify conversion from 
     43   one `MimeType` to another 
    3944""" 
    4045 
     46import os 
    4147import re 
    4248from StringIO import StringIO 
    4349 
    4450from trac.config import IntOption, ListOption, Option 
    4551from trac.core import * 
    4652from trac.util import sorted 
    47 from trac.util.text import to_utf8, to_unicode 
     53from trac.util.text import to_utf8, to_unicode, IDENTITY_CHARSET 
    4854from trac.util.markup import escape, Markup, Fragment, html 
    4955 
    5056 
    51 __all__ = ['get_mimetype', 'is_binary', 'detect_unicode', 'Mimeview', 
    52            'content_to_unicode'] 
     57__all__ = ['get_mimetype', 'is_binary', 'detect_unicode', 
     58           'Mimeview', 'Conversion', 'MimeContentBase', 'MimeType',  
     59           'MimeContent', 'FileMimeContent', 
     60           'TypeWrapper', 'ObjectContent', 'IContentConverter', 
     61           'TEXT_PLAIN', 'TEXT_HTML', 
     62           'APPLICATION_RSS_XML', 'APPLICATION_OCTET_STREAM'] 
    5363 
    5464 
    5565# Some common MIME types and their associated keywords and/or file extensions 
    5666 
     67APPLICATION_OCTET_STREAM_STR = 'application/octet-stream' 
     68 
    5769KNOWN_MIME_TYPES = { 
    5870    'application/pdf':        ['pdf'], 
    5971    'application/postscript': ['ps'], 
     
    114126    for e in exts: 
    115127        MIME_MAP[e] = t 
    116128 
    117 # Simple builtin autodetection from the content using a regexp 
    118 MODE_RE = re.compile( 
    119     r"#!(?:[/\w.-_]+/)?(\w+)|"               # look for shebang 
    120     r"-\*-\s*(?:mode:\s*)?([\w+-]+)\s*-\*-|" # look for Emacs' -*- mode -*- 
    121     r"vim:.*?syntax=(\w+)"                   # look for VIM's syntax=<n> 
    122     ) 
    123129 
    124 def get_mimetype(filename, content=None, mime_map=MIME_MAP): 
    125     """Guess the most probable MIME type of a file with the given name. 
     130# -- a few functions for dealing with MIME types / binary / text content 
     131#    in a simple way (get_mimetype, is_binary, detect_unicode) 
    126132 
     133def get_mimetype_from_filename(filename, mime_map=MIME_MAP): 
     134    """Guess the most probable MIME type of file with the given `filename`. 
     135 
    127136    `filename` is either a filename (the lookup will then use the suffix) 
    128137    or some arbitrary keyword. 
    129      
    130     `content` is either a `str` or an `unicode` string. 
     138    `mime_map` maps keywords to MIME types. 
     139 
     140    Return the MIME type as a string, or `None` if not detected. 
    131141    """ 
    132142    suffix = filename.split('.')[-1] 
    133143    if suffix in mime_map: 
     
    141151            mimetype = mimetypes.guess_type(filename)[0] 
    142152        except: 
    143153            pass 
    144         if not mimetype and content: 
    145             match = re.search(MODE_RE, content[:1000]) 
    146             if match: 
    147                 mode = match.group(1) or match.group(3) or \ 
    148                     match.group(2).lower() 
    149                 if mode in mime_map: 
    150                     # 3) mimetype from the content, using the `MODE_RE` 
    151                     return mime_map[mode] 
    152             else: 
    153                 if is_binary(content): 
    154                     # 4) mimetype from the content, using`is_binary` 
    155                     return 'application/octet-stream' 
    156154        return mimetype 
    157155 
     156# Simple builtin autodetection from the content using a regexp 
     157MODE_RE = re.compile( 
     158    r"#!(?:[/\w.-_]+/)?(\w+)|"               # look for shebang 
     159    r"-\*-\s*(?:mode:\s*)?([\w+-]+)\s*-\*-|" # look for Emacs' -*- mode -*- 
     160    r"vim:.*?syntax=(\w+)"                   # look for VIM's syntax=<n> 
     161    ) 
     162 
     163def get_mimetype_from_content(content): 
     164    """Guess the most probable MIME type of file with the given `filename`. 
     165 
     166    `content` is either a `str` or an `unicode` string  
     167 
     168    Return the MIME type as a string, or `None` if not detected. 
     169    """ 
     170    match = re.search(MODE_RE, content[:1000]) 
     171    if match: 
     172        mode = match.group(1) or match.group(3) or \ 
     173            match.group(2).lower() 
     174        if mode in mime_map: 
     175            # 3) mimetype from the content, using the `MODE_RE` 
     176            return mime_map[mode] 
     177    else: 
     178        if is_binary(content): 
     179            # 4) mimetype from the content, using `is_binary` 
     180            return APPLICATION_OCTET_STREAM_STR 
     181 
     182def get_mimetype(filename, content=None, mime_map=MIME_MAP): 
     183    """Auto-detect MIME type either from the `filename` or from the `content`. 
     184    """ 
     185    mimetype = get_mimetype_from_filename(filename, mime_map) 
     186    if not mimetype and content: 
     187        mimetype = get_mimetype_from_content(content) 
     188    return mimetype 
     189 
    158190def is_binary(data): 
    159191    """Detect binary content by checking the first thousand bytes for zeroes. 
    160192 
     
    165197    return '\0' in data[:1000] 
    166198 
    167199def detect_unicode(data): 
    168     """Detect different unicode charsets by looking for BOMs (Byte Order Marks). 
     200    """Detect different unicode charsets by looking for Byte Order Marks. 
    169201 
    170202    Operate obviously only on `str` objects. 
    171203    """ 
     
    178210    else: 
    179211        return None 
    180212 
    181 def content_to_unicode(env, content, mimetype): 
    182     """Retrieve an `unicode` object from a `content` to be previewed""" 
    183     mimeview = Mimeview(env) 
    184     if hasattr(content, 'read'): 
    185         content = content.read(mimeview.max_preview_size) 
    186     return mimeview.to_unicode(content, mimetype) 
    187213 
     214# -- Classes for mimetype, content and conversion 
    188215 
     216class TypeRepr(object): 
     217    """Represent some for of typing.""" 
     218    def match(self, other, regexp=False): 
     219        raise NotImplementedError 
     220 
     221    mimetype = property(lambda x: x._mimetype or APPLICATION_OCTET_STREAM_STR, 
     222                        doc="MIME Type string (without charset information)") 
     223    charset = property(lambda x: x._charset, 
     224                       doc="Eventual charset information") 
     225    is_binary = property(lambda x: x._is_binary()) 
     226 
     227class TypeWrapper(TypeRepr): 
     228    """Typing using Python types.""" 
     229 
     230    def __init__(self, obj): 
     231        self.class_ = isinstance(obj, type) and obj or obj.__class__ 
     232        self._mimetype = self._charset = None 
     233 
     234    def __repr__(self): 
     235        return self.class_.__name__ 
     236 
     237    def _is_binary(self): 
     238        return True 
     239 
     240    def match(self, other, regexp=False): 
     241        other_class = isinstance(other, type) and other or \ 
     242                      isinstance(other, TypeWrapper) and other.class_ or \ 
     243                      other.__class__ 
     244        return other_class == self.class_ 
     245 
     246class MimeType(TypeRepr): 
     247    """Typing of arbitrary content using MIME types. 
     248 
     249    If the MIME type correspond to text content, the object can also 
     250    store a `charset` information. 
     251 
     252    A MIME type has a `name` and has an `extension` 
     253    that can be used for storing the converted data in a file. 
     254 
     255    All the properties of this class are read-only. 
     256    """ 
     257     
     258    def __init__(self, mimetype, charset=None, name=None, extension=None): 
     259        """The `mimetype` string can eventually embed the `charset`.""" 
     260        self._mimetype = mimetype 
     261        # determine charset 
     262        self._charset = charset 
     263        if not self._charset and self._mimetype: 
     264            sep_idx = mimetype.find(';') 
     265            if sep_idx >= 0: 
     266                self._mimetype = mimetype[:sep_idx].strip() 
     267                charset_idx = mimetype.find('charset=', sep_idx) 
     268                if charset_idx >= 0: 
     269                    self._charset = mimetype[charset_idx+8:].strip() 
     270        self._extension = extension 
     271        self._name = name 
     272 
     273    def __repr__(self): 
     274        return 'MIME type: ' + self.mimetype_charset 
     275 
     276    def _get_extension(self): 
     277        if not self._extension: 
     278            self._extension = KNOWN_MIME_TYPES.get(self.mimetype) 
     279            if not self._extension: 
     280                detail = self.mimetype.split('/', 1)[1] 
     281                if detail.startswith('x-'): 
     282                    self._extension = detail[2:] 
     283        return self._extension 
     284 
     285    def _get_mimetype_charset(self): 
     286        """Combine the MIME type and charset information in a single string. 
     287        """ 
     288        if self._mimetype and self._charset: 
     289            return '%s; charset=%s' % (self.mimetype, self.charset) 
     290        else: 
     291            return self.mimetype 
     292 
     293    def _is_binary(self): 
     294        return self._mimetype == APPLICATION_OCTET_STREAM_STR 
     295 
     296    name = property(lambda x: x._name or x._extension) 
     297    extension = property(lambda x: x._get_extension()) 
     298    mimetype_charset = property(lambda x: x._get_mimetype_charset()) 
     299 
     300    def match(self, other, regexp=False): 
     301        """Compare MIME type string only. 
     302 
     303        If `regexp` is set, `self.mimetype` is used as a regexp. 
     304        """ 
     305        if not isinstance(other, MimeType): 
     306            return False 
     307        if regexp: 
     308            return re.match(self.mimetype, other.mimetype) 
     309        else: 
     310            return self.mimetype == other.mimetype 
     311 
     312 
     313TEXT_PLAIN = MimeType('text/plain', 'utf-8', 'Plain Text', 'txt') 
     314TEXT_HTML = MimeType('text/html', 'utf-8', 'HTML', 'html') 
     315 
     316APPLICATION_RSS_XML = MimeType('application/rss+xml', 'utf-8', 
     317                               'RSS Feed', 'xml') 
     318APPLICATION_OCTET_STREAM = MimeType(APPLICATION_OCTET_STREAM_STR, 
     319                                    IDENTITY_CHARSET, 
     320                                    'Undefined (binary)', 'bin')  
     321 
     322 
     323class MimeContentBase(object): 
     324    """An abstract MIME content, with an associated MimeType. 
     325 
     326    Such an object has means to auto-detect both the MIME Type and 
     327    the `encoding` of its content. 
     328 
     329    That `encoding` is more reliable than the `type.charset` information. 
     330    There are additional consistency checks that are performed, and it 
     331    can be `None` if the content is an `unicode` object. 
     332 
     333    The content itself can be accessed in various ways: through 
     334    the iterator protocol, the len() and unicode() operators... 
     335    """ 
     336 
     337    def __init__(self, env, mimetype=None, filename=None, url=None): 
     338        """ 
     339        `mimetype` can be specified as a `MimeType` object, 
     340        or as string, which will then be a hint about the content. 
     341 
     342        If the mimetype is not specified or equal to 
     343        "application/octet-stream", then it will be auto-detected when needed. 
     344 
     345        In case auto-detection fails, APPLICATION_OCTET_STREAM will be the 
     346        corresponding MIME type. 
     347 
     348        The `filename` is simply a suggested basename for that content. 
     349 
     350        The `url` is a link for retrieving the raw content directly 
     351        from the server. This can be useful for converters that can 
     352        provide links to objects, instead of having to expand the 
     353        content inline. 
     354        """ 
     355        self.env = env 
     356        if isinstance(mimetype, basestring): 
     357            mimetype = MimeType(mimetype) 
     358        self._type = mimetype 
     359        self._filename = filename 
     360        self._url = url 
     361        self._binary = None 
     362        self._encoding = False 
     363 
     364    def __repr__(self): 
     365        return '<%s %s "%s">' % (self.__class__.__name__, self._type, 
     366                                 self._filename or self._url) 
     367 
     368    def __unicode__(self): 
     369        """Return the `unicode` object corresponding to the content.""" 
     370        return to_unicode(self.content, self.encoding) 
     371        # Note: this does the right thing if the content is already `unicode` 
     372 
     373    def encode(self, charset): 
     374        """Return a `str`, corresponding to the `charset` encoded content.""" 
     375        if self.encoding == charset: 
     376            return self.content 
     377        else: 
     378            return unicode(self).encode(charset) 
     379 
     380    def _is_binary(self): 
     381        """An heuristic for guessing whether the content is binary or not. 
     382 
     383        This will eventually fetch an `excerpt` of the content. 
     384        """ 
     385        if self._binary is None: 
     386            self._binary = self.type.is_binary 
     387            print `self.type`, self._binary 
     388            if self._binary is None: 
     389                self._binary = is_binary(self.excerpt) or \ 
     390                               (self.type.mimetype in \ 
     391                                Mimeview(self.env).treat_as_binary) 
     392        return self._binary 
     393 
     394    def _get_type(self): 
     395        """Get or determine the MimeType corresponding to this content. 
     396 
     397        An `excerpt` of the content will be examined if needed. 
     398        """ 
     399        if self._type is None: # not set 
     400            mimetype = None 
     401            if self.filename: 
     402                mimemap = Mimeview(self.env).mimemap 
     403                mimetype = get_mimetype_from_filename(self.filename, mimemap) 
     404            if not mimetype: 
     405                mimetype = get_mimetype_from_content(self.excerpt) 
     406            if not mimetype: 
     407                pass # TODO 0.11: go through IMimeTypeDetectors 
     408            self._type = MimeType(mimetype) 
     409        return self._type 
     410 
     411    def _set_type(self, type): 
     412        """Simply replace the existing `type` by the given `MimeType` object. 
     413 
     414        If `None` is given, this will force auto-detection the next time 
     415        `type` will be accessed. 
     416 
     417        Can be used for in-place conversion (e.g. ''any'' to text/plain). 
     418        """ 
     419        self._type = type 
     420 
     421    def _get_encoding(self): 
     422        """Get or determine the current encoding of that `content`. 
     423 
     424        The encoding will be determined using this order: 
     425         * from the charset information present in the mimetype information 
     426         * auto-detection of the charset from the `content` 
     427         * if nothing else worked, use the configured `default_charset` 
     428 
     429        If the `content` happens to be a genuine `unicode` object, then 
     430        this returns `None`. 
     431        If the `content` is binary, then the encoding will be the identity 
     432        charset (ISO Latin 1). 
     433        """ 
     434        if self._encoding is False: 
     435            charset = self.type.charset 
     436            if charset: 
     437                self._encoding = charset 
     438            elif isinstance(self.excerpt, str): 
     439                utf_encoding = detect_unicode(self.excerpt) 
     440                if utf_encoding is not None: 
     441                    self._encoding = utf_encoding 
     442                elif self.is_binary: 
     443                    self._encoding = IDENTITY_CHARSET 
     444            elif isinstance(self.excerpt, unicode): 
     445                self._encoding = None 
     446            if self._encoding is False: 
     447                pass # TODO 0.11: go through ICharsetDetectors here 
     448            if self._encoding is False: 
     449                self._encoding = Mimeview(self.env).default_charset 
     450        return self._encoding 
     451     
     452    def _get_content(self): 
     453        """Retrieve all the content. 
     454 
     455        Default implementation based on iterator. If the iterator itself 
     456        is implemented based on the content... reimplement this one! 
     457        """ 
     458        return "".join(self.__iter__()) 
     459 
     460    def read(self): # TODO: remove in 0.11 
     461        return self.content # (compatibility with IHTMLPreviewRenderer) 
     462 
     463    # Methods that need to be reimplemented by subclasses: 
     464 
     465    def __iter__(self): 
     466        """Iterate on chunks of raw content.""" 
     467        raise NotImplementedError 
     468 
     469    def __len__(self): 
     470        """Length of the raw content, in bytes.""" 
     471        raise NotImplementedError 
     472 
     473    def _get_excerpt(self, len=1000): 
     474        """Extracts the first `len` characters from the content.""" 
     475        raise NotImplementedError 
     476             
     477    type = property(fget=lambda x: x._get_type(), 
     478                    fset=lambda x, y: x._set_type(y)) 
     479    is_binary = property(lambda x: x._is_binary()) 
     480    encoding = property(lambda x: x._get_encoding()) 
     481    excerpt = property(lambda x: x._get_excerpt()) 
     482    content = property(lambda x: x._get_content()) 
     483    filename = property(lambda x: x._filename) 
     484    url = property(lambda x: x._url) 
     485 
     486 
     487class MimeContent(MimeContentBase): 
     488    """MIME-typed content wrapper for a basestring.""" 
     489 
     490    def __init__(self, env, content, mimetype, filename='file', url=None): 
     491        MimeContentBase.__init__(self, env, mimetype, filename, url) 
     492        self._content = content 
     493 
     494    # Reimplemented methods 
     495 
     496    def _get_content(self): 
     497        """Retrieve the wrapped content. 
     498 
     499        Note: therefore this *might* be an `unicode` object. 
     500        Remember that in this case, `encoding` will be `None`. 
     501        """ 
     502        return self._content 
     503 
     504    def __iter__(self): 
     505        """Iterate on chunks of content. 
     506 
     507        If the content `is_binary` property is `False`, those chunks will 
     508        be lines, with the line endings kept. 
     509        """ 
     510        if self.is_binary: 
     511            buf = StringIO(self.content) 
     512            chunk = buf.read(1000) 
     513            while chunk: 
     514                yield chunk 
     515                chunk = buf.read(1000) 
     516        else: 
     517            for line in self.content.splitlines(True): 
     518                yield line 
     519 
     520    def __len__(self): 
     521        """Length of the content, in characters.""" 
     522        return len(self.content) 
     523 
     524    def _get_excerpt(self, len=1000): 
     525        """Extracts the first `len` characters from the content.""" 
     526        return self._content[:len] 
     527 
     528 
     529class FileMimeContent(MimeContentBase): 
     530    """MIME-typed content wrapper for a file.""" 
     531 
     532    def __init__(self, env, path, url=None, kind='File', mimetype=None): 
     533        self._fd = None 
     534        self._path = path 
     535        self._kind = kind 
     536        self._excerpt = None 
     537        MimeContentBase.__init__(self, env, mimetype, os.path.basename(path), 
     538                                 url) 
     539    def __del__(self): 
     540        if self._fd: 
     541            self._fd.close() 
     542 
     543    def _ensure_open(self): 
     544        if not self._fd: 
     545            try: 
     546                self._fd = open(self._path) 
     547            except IOError: 
     548                raise TracError('%s "%s" not found' % (self._kind, 
     549                                                       self._filename)) 
     550    # Reimplemented methods 
     551     
     552    def __iter__(self): 
     553        """Iterate on chunks of raw content.""" 
     554        chunk = self.excerpt 
     555        while chunk: 
     556            yield chunk 
     557            chunk = self._fd.read(1000) 
     558 
     559    def __len__(self): 
     560        """Length of the raw content, in bytes.""" 
     561        if self._fd: 
     562            stat = os.fstat(self._fd.fileno()) 
     563        else: 
     564            stat = os.stat(self._path) 
     565        return stat.st_size 
     566 
     567    def _get_excerpt(self, len=1000): 
     568        """Extracts the `len` first bytes from the content.""" 
     569        if self._excerpt is None: 
     570            self._ensure_open() 
     571            self._excerpt = self._fd.read(1000) 
     572        return self._excerpt 
     573 
     574class ObjectContent(MimeContentBase): 
     575    """Wraps a Python object into a MimeContentBase. 
     576 
     577    Only supports the bare minimum of the MimeContentBase methods. 
     578    """ 
     579 
     580    def __init__(self, env, obj, filename="obj"): 
     581        self._obj = obj 
     582        MimeContentBase.__init__(self, env, TypeWrapper(obj), 
     583                                 filename=filename) 
     584 
     585    def _get_content(self): 
     586        """Retrieve the wrapped content.""" 
     587        return self._obj 
     588 
     589         
     590class NoConversion(TracError): 
     591    def __init__(self, msg, from_, output, key): 
     592        TracError.__init__(self, '%s, from %s to %s' % 
     593                           (msg, repr(from_), output and repr(output) or key)) 
     594 
     595class Conversion(object): 
     596    """A specification for performing a data conversion. 
     597 
     598    Each conversion is identified by a `key` and targets an output `mimetype`. 
     599 
     600    A conversion also specifies a `quality` ranking, which is a number