Edgewall Software

Ticket #3332: mimeview_api_refactoring-r3612.diff

File mimeview_api_refactoring-r3612.diff, 117.3 kB (added by cboos, 2 years ago)

First stable snapshot of the refactoring

  • 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, TEXT_PLAIN 
    3030from trac.util import get_reporter_id, create_unique_file 
    3131from trac.util.datefmt import format_datetime, pretty_timedelta 
    3232from trac.util.html 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) 
     
    245245    select = classmethod(select) 
    246246    delete_all = classmethod(delete_all) 
    247247 
    248     def open(self): 
     248    def open(self): # deprecate? 
    249249        self.env.log.debug('Trying to open attachment at %s', self.path) 
    250250        try: 
    251251            fd = open(self.path, 'rb') 
     
    523523                                 'author': get_reporter_id(req)} 
    524524 
    525525    def _render_view(self, req, attachment): 
     526        # FIXME: perm_map should extend to other `parent_type`s 
    526527        perm_map = {'ticket': 'TICKET_VIEW', 'wiki': 'WIKI_VIEW'} 
    527528        req.perm.assert_permission(perm_map[attachment.parent_type]) 
    528529 
     
    533534        req.hdf['attachment'] = attachment_to_hdf(self.env, req, None, 
    534535                                                  attachment) 
    535536        # Override the 'oneliner' 
    536         req.hdf['attachment.description'] = wiki_to_html(attachment.description, 
    537                                                         self.env, req) 
     537        req.hdf['attachment.description'] = wiki_to_html( 
     538            attachment.description, self.env, req) 
    538539 
    539540        perm_map = {'ticket': 'TICKET_ADMIN', 'wiki': 'WIKI_DELETE'} 
    540541        if req.perm.has_permission(perm_map[attachment.parent_type]): 
    541542            req.hdf['attachment.can_delete'] = 1 
    542543 
    543         fd = attachment.open() 
    544         try: 
    545             mimeview = Mimeview(self.env) 
     544        content = FileMimeContent(self.env, attachment.path, 
     545                                  url=attachment.href(req, format='raw'), 
     546                                  kind='Attachment') 
    546547 
    547             # MIME type detection 
    548             str_data = fd.read(1000) 
    549             fd.seek(0) 
    550              
    551             binary = is_binary(str_data) 
    552             mime_type = mimeview.get_mimetype(attachment.filename, str_data) 
     548        # Eventually send the file directly 
     549        format = req.args.get('format') 
     550        if format in ('raw', 'txt'): 
     551            disposition = 'inline' 
     552            if self.render_unsafe_content: 
     553                if content.type.is_unknown or \ 
     554                       (format == 'txt' and not content.is_binary): 
     555                    # Force the content to be displayed as text 
     556                    content.type = TEXT_PLAIN ### SHOULD BE ENOUGH 
     557                    content.type = MimeType('text/plain', content.encoding) 
     558            elif not content.is_binary: 
     559                # Force browser to download HTML/SVG/etc pages that may 
     560                # contain malicious code enabling XSS aattacks 
     561                disposition = 'attachment' 
     562            content.send(req, disposition) 
    553563 
    554             # Eventually send the file directly 
    555             format = req.args.get('format') 
    556             if format in ('raw', 'txt'): 
    557                 if not self.render_unsafe_content and not binary: 
    558                     # Force browser to download HTML/SVG/etc pages that may 
    559                     # contain malicious code enabling XSS attacks 
    560                     req.send_header('Content-Disposition', 'attachment;' + 
    561                                     'filename=' + attachment.filename) 
    562                 if not mime_type or (self.render_unsafe_content and \ 
    563                                      not binary and format == 'txt'): 
    564                     mime_type = 'text/plain' 
    565                 if 'charset=' not in mime_type: 
    566                     charset = mimeview.get_charset(str_data, mime_type) 
    567                     mime_type = mime_type + '; charset=' + charset 
    568                 req.send_file(attachment.path, mime_type) 
     564        # add ''Plain Text'' alternate link if needed 
     565        if self.render_unsafe_content and not \ 
     566               (content.is_binary or 
     567                content.type.is_unknown or  
     568                TEXT_PLAIN.match(content.type)): 
     569            add_link(req, 'alternate', attachment.href(req, format='txt'), 
     570                     TEXT_PLAIN.name, TEXT_PLAIN.mimetype) 
    569571 
    570             # add ''Plain Text'' alternate link if needed 
    571             if self.render_unsafe_content and not binary and \ 
    572                mime_type and not mime_type.startswith('text/plain'): 
    573                 plaintext_href = attachment.href(req, format='txt') 
    574                 add_link(req, 'alternate', plaintext_href, 'Plain Text', 
    575                          mime_type) 
     572        # add ''Original Format'' alternate link (always) 
     573        add_link(req, 'alternate', content.url, 
     574                 'Original Format', content.type.mimetype) 
    576575 
    577             # add ''Original Format'' alternate link (always) 
    578             raw_href = attachment.href(req, format='raw') 
    579             add_link(req, 'alternate', raw_href, 'Original Format', mime_type) 
     576        req.hdf['attachment'] = Mimeview(self.env).preview_to_hdf( 
     577            req, content, annotations=['lineno']) 
    580578 
    581             self.log.debug("Rendering preview of file %s with mime-type %s" 
    582                            % (attachment.filename, mime_type)) 
    583  
    584             req.hdf['attachment'] = mimeview.preview_to_hdf( 
    585                 req, fd, os.fstat(fd.fileno()).st_size, mime_type, 
    586                 attachment.filename, raw_href, annotations=['lineno']) 
    587         finally: 
    588             fd.close() 
    589  
    590579    def _render_list(self, req, p_type, p_id): 
    591580        self._parent_to_hdf(req, p_type, p_id) 
    592581        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.util.html import Element 
    3333from trac.web.href import Href 
    3434from trac.wiki.formatter import WikiProcessor 
     
    180180 
    181181        _inliner = rst.states.Inliner() 
    182182        _parser = rst.Parser(inliner=_inliner) 
    183         content = content_to_unicode(self.env, content, mimetype) 
     183        content = unicode(content) 
    184184        parts = publish_parts(content, writer_name='html', parser=_parser, 
    185185                              settings_overrides={'halt_level': 6,  
    186186                                                  'file_insertion_enabled': 0,  
  • 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 about typed content. 
    2523 
    26 There are primarily two approaches for getting the MIME type of a given file: 
    27  * taking advantage of existing conventions for the file name 
    28  * examining the file content and applying various heuristics 
     24Originally, this was about file metadata, principally  its MIME type and 
     25eventually the text encoding (charset) used by that content. 
    2926 
    30 The module also knows how to convert the file content from one type 
    31 to another type. 
     27Now, this has evolved into managing any kind of typed content, 
     28and deals also with converting a content from one type to another. 
     29A common situation which is now handled part of the general case 
     30is the conversion of any kind of content to a text/html representation. 
    3231 
    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. 
     32In order to keep the API of conversion interface IContentConverter simple, 
     33we introduced a few classes, each encapsulating a part of the knowledge 
     34related to the content and the conversion: 
    3535 
    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()`. 
     36 * the `ContentType` is used to describe the type of a content. 
     37   This is an abstract superclass for: 
     38 
     39   - `MimeType`, used for storing the mime type string, the charset, 
     40     and eventually the name and the commonly used file extension 
     41     for that type 
     42 
     43   - `ObjectType`, used when wrapping an arbitrary Python object 
     44 
     45 * the `AbstractContent`, which wraps access to the actual content, 
     46   and provides a few generic methods, among which `convert()` is 
     47   the most important. 
     48 
     49   - the `ObjectContent`, when the content is an arbitrary Python object 
     50 
     51   - the `MimeContent` abstract class, which offers an uniform API to 
     52     access the data over a wide range of containers: 
     53      * the `StringMimeContent`, for handling string content 
     54        (like `str`, `unicode` or `Markup` objects) 
     55      * the `StructuredMimeContent`, for handling structured content 
     56        (like `Element` objects) 
     57      * the `LineIteratorMimeContent`, for handling line-oriented string 
     58        content (like string iterators or string arrays) 
     59      * the `FileMimeContent`, for handling file content 
     60      * etc. (as an example, the repository layer defines a `NodeMimeContent`) 
     61 
     62     That API provides different ways to get access to the data: 
     63      * chunks(), an iterator for reading chunks of raw content 
     64      * lines(), an iterator for reading lines from text content 
     65      * markup(), when it makes sense to interpret the content as markup 
     66      * `__unicode()__`, for converting the content to an `unicode` object 
     67      * encode(), for converting the content to a `str` object. Note that 
     68        we don't use `__str__` for that on purpose, as we need to be able 
     69        to specify the charset. 
     70      * size(), for getting a hint about the content size without actually 
     71        reading it. 
     72 
     73 * the `Conversion` class, which is used to specify how a given 
     74   `IContentConverter` component will perform a content conversion. 
    3975""" 
    4076 
     77import os 
    4178import re 
    4279from StringIO import StringIO 
    4380 
    4481from trac.config import IntOption, ListOption, Option 
    4582from trac.core import * 
    4683from trac.util import sorted 
    47 from trac.util.text import to_utf8, to_unicode 
     84from trac.util.datefmt import http_date 
     85from trac.util.text import to_utf8, to_unicode, IDENTITY_CHARSET 
    4886from trac.util.html import escape, Markup, Fragment, html 
    4987 
    5088 
    51 __all__ = ['get_mimetype', 'is_binary', 'detect_unicode', 'Mimeview', 
    52            'content_to_unicode'] 
     89__all__ = ['get_mimetype', 'is_binary', 'detect_unicode', 
     90           'IContentConverter', 'IHTMLPreviewAnnotator', 
     91           'IHTMLPreviewRenderer', # deprecated 
     92           'ObjectType', 'MimeType', 
     93           'ObjectContent', 
     94           'MimeContent', 'FileMimeContent', 'StringMimeContent', 
     95           'StructuredMimeContent', 'LineIteratorMimeContent', 
     96           'Mimeview', 'Conversion', 
     97           'TEXT_PLAIN', 'TEXT_HTML', 'TEXT_CSV', 'TEXT_TSV', 
     98           'APPLICATION_RSS_XML', 'APPLICATION_OCTET_STREAM'] 
    5399 
    54100 
    55101# Some common MIME types and their associated keywords and/or file extensions 
    56102 
     103APPLICATION_OCTET_STREAM_STR = 'application/octet-stream' 
     104 
    57105KNOWN_MIME_TYPES = { 
    58106    'application/pdf':        ['pdf'], 
    59107    'application/postscript': ['ps'], 
     
    114162    for e in exts: 
    115163        MIME_MAP[e] = t 
    116164 
    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     ) 
    123165 
    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. 
     166# -- get_mimetype, is_binary, detect_unicode: a few functions for dealing 
     167#    with MIME types, binary and text content in a simple way 
    126168 
     169def get_mimetype_from_filename(filename, mime_map=MIME_MAP): 
     170    """Guess the most probable MIME type of file with the given `filename`. 
     171 
    127172    `filename` is either a filename (the lookup will then use the suffix) 
    128173    or some arbitrary keyword. 
    129      
    130     `content` is either a `str` or an `unicode` string. 
     174    `mime_map` maps keywords to MIME types. 
     175 
     176    Return the MIME type as a string, or `None` if nothing was detected. 
    131177    """ 
    132178    suffix = filename.split('.')[-1] 
    133179    if suffix in mime_map: 
     
    141187            mimetype = mimetypes.guess_type(filename)[0] 
    142188        except: 
    143189            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' 
    156190        return mimetype 
    157191 
     192# Simple builtin autodetection from the content using a regexp 
     193MODE_RE = re.compile( 
     194    # look for /usr/bin/prog1 or /usr/bin/env prog2 
     195    r"#!(?:[/\w.-_]+/)?(?P<prog1>\w+)(?:\s+(?P<prog2>\w+))?|" 
     196    # look for Emacs' -*- mode -*- 
     197    r"-\*-\s*(?:mode:\s*)?(?P<emacsmode>[\w+-]+)\s*-\*-|" 
     198    # look for VIM's syntax=<n> 
     199    r"vim:.*?syntax=(?P<vimsyntax>\w+)" 
     200    ) 
     201 
     202def get_mimetype_from_content(content, mime_map=MIME_MAP): 
     203    """Guess the most probable MIME type of file with the given `filename`. 
     204 
     205    `content` is either a `str` or an `unicode` string  
     206 
     207    Return the MIME type as a string, or `None` if not detected. 
     208    """ 
     209    match = re.search(MODE_RE, content[:1000]) 
     210    if match: 
     211        mode = match.group('prog1') 
     212        if mode and mode == 'env': 
     213            mode = match.group('prog2') 
     214        if not mode: 
     215            mode = match.group('vimsyntax') or \ 
     216                   match.group('emacsmode').lower() 
     217        if mode in mime_map: 
     218            # 3) mimetype from the content, using the `MODE_RE` 
     219            return mime_map[mode] 
     220    else: 
     221        if is_binary(content): 
     222            # 4) mimetype from the content, using `is_binary` 
     223            return APPLICATION_OCTET_STREAM_STR 
     224 
     225def get_mimetype(filename, content=None, mime_map=MIME_MAP): 
     226    """Auto-detect MIME type either from the `filename` or from the `content`. 
     227    """ 
     228    mimetype = get_mimetype_from_filename(filename, mime_map) 
     229    if not mimetype and content: 
     230        mimetype = get_mimetype_from_content(content, mime_map) 
     231    return mimetype 
     232 
    158233def is_binary(data): 
    159234    """Detect binary content by checking the first thousand bytes for zeroes. 
    160235 
     
    165240    return '\0' in data[:1000] 
    166241 
    167242def detect_unicode(data): 
    168     """Detect different unicode charsets by looking for BOMs (Byte Order Marks). 
     243    """Detect different unicode charsets by looking for Byte Order Marks. 
    169244 
    170245    Operate obviously only on `str` objects. 
    171246    """ 
     
    178253    else: 
    179254        return None 
    180255 
    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) 
    187256 
     257# -- ContentType and subclasses  
    188258 
    189 class IHTMLPreviewRenderer(Interface): 
    190     """Extension point interface for components that add HTML renderers of 
    191     specific content types to the `Mimeview` component. 
     259class ContentType(object): 
     260    """Abstract representation of the "type" of some content.""" 
    192261 
    193     (Deprecated) 
     262    def match(self, other): 
     263        """Compare this instance with another `ContentType` instance. 
     264 
     265        Return True if `other` can be say to ''match'' our type. 
     266        """ 
     267        raise NotImplementedError 
     268 
     269    is_binary = property(lambda x: x._is_binary()) 
     270 
     271    mimetype = property(lambda x: x._get_mimetype(), 
     272                        doc="MIME Type string (without charset information)") 
     273 
     274    name = 'unknown' 
     275 
     276 
     277class ObjectType(ContentType): 
     278    """Represent the "type" of a content by using a Python class. 
     279 
     280    The corresponding content is then expected to be an `ObjectContent`, 
     281    wrapping a Python instance of that class. 
    194282    """ 
    195283 
    196     # implementing classes should set this property to True if they 
    197     # support text content where Trac should expand tabs into spaces 
    198     expand_tabs = False 
     284    def __init__(self, obj): 
     285        self._class = isinstance(obj, type) and obj or obj.__class__ 
     286        self._mimetype = self._charset = None 
     287        self.name = self._class.__name__ 
    199288 
    200     def get_quality_ratio(mimetype): 
    201         """Return the level of support this renderer provides for the `content` 
    202         of the specified MIME type. The return value must be a number between 
    203         0 and 9, where 0 means no support and 9 means "perfect" support. 
     289    def __repr__(self): 
     290        return "<ObjectType %s>" % self.name 
     291 
     292    # Reimplemented methods 
     293     
     294    def _is_binary(self): return True 
     295    def _get_mimetype(self): return None 
     296    # or return 'application/x-python-'+self.name ? 
     297 
     298    def match(self, other, regexp=False): 
     299        other_class = isinstance(other, type) and other or \ 
     300                      isinstance(other, ObjectType) and other._class or \ 
     301                      other.__class__ 
     302        return other_class == self._class 
     303 
     304 
     305class MimeType(ContentType): 
     306    """Represent the "type" of a content by using a MIME type string. 
     307 
     308    If the type is not binary (i.e. is some kind of text), the `charset` 
     309    information can also be given, when creating the instance. 
     310 
     311    A MIME type has a `name` and has an `extension` that can be used 
     312    for storing the converted data in a file. 
     313 
     314    All the properties of this class are read-only. 
     315    """ 
     316     
     317    def __init__(self, mimetype, charset=None, name=None, extension=None, 
     318                 env=None): 
     319        """Create a MimeType based on a `mimetype` string. 
     320 
     321        That string can eventually contain a `charset=...`, which will be 
     322        retained as the charset for this instance, unless there's an 
     323        explicit `charset` parameter given. 
     324 
     325        Another possibility is to give a pattern object for the `mimetype` 
     326        argument, in which case this instance can be used to do pattern 
     327        matching when using `match()`. 
     328 
     329        When optional `env` parameter is given, additional knowledge is 
     330        used for determining whether this type is binary or not. 
    204331        """ 
     332        self.env = env 
     333        self._mimetype = mimetype 
     334        # determine charset 
     335        self._charset = charset 
     336        if not self._charset and isinstance(self._mimetype, basestring): 
     337            sep_idx = mimetype.find(';') 
     338            if sep_idx >= 0: 
     339                self._mimetype = mimetype[:sep_idx].strip() 
     340                charset_idx = mimetype.find('charset=', sep_idx) 
     341                if charset_idx >= 0: 
     342                    self._charset = mimetype[charset_idx+8:].strip() 
     343        if extension and not extension.startswith('.'): 
     344            extension = '.' + extension 
     345        self._extension = extension 
     346        self._name = name 
    205347 
    206     def render(req, mimetype, content, filename=None, url=None): 
    207         """Render an XHTML preview of the raw `content`. 
     348    def __repr__(self): 
     349        return '<MimeType "%s">' % self.mimetype_charset 
    208350 
    209         The `content` might be: 
    210          * a `str` object 
    211          * an `unicode` string 
    212          * any object with a `read` method, returning one of the above 
     351    def _get_extension(self): 
     352        if self._extension is None: 
     353            self._extension = KNOWN_MIME_TYPES.get(self.mimetype) ### use env 
     354            if not self._extension: 
     355                detail = self.mimetype.split('/', 1)[1] 
     356                if detail.startswith('x-'): 
     357                    detail = detail[2:] 
     358                self._extension = '.' + detail 
     359        return self._extension 
    213360 
    214         It is assumed that the content will correspond to the given `mimetype`. 
     361    def _get_mimetype_charset(self): 
     362        """Combine in a single string the MIME type and charset information.""" 
     363        if self._mimetype and self._charset: 
     364            return '%s; charset=%s' % (self.mimetype, self.charset) 
     365        else: 
     366            return self.mimetype ### or default to utf-8 if not binary?  
     367     
     368    charset = property(lambda x: x._charset, 
     369                       doc="Charset information if available") 
    215370 
    216         Besides the `content` value, the same content may eventually 
    217         be available through the `filename` or `url` parameters. 
    218         This is useful for renderers that embed objects, using <object> or 
    219         <img> instead of including the content inline. 
     371    name = property(lambda x: x._name or x._get_extension()) 
     372    extension = property(lambda x: x._get_extension()) 
     373    mimetype_charset = property(lambda x: x._get_mimetype_charset(), 
     374                                doc="MIME Type plus charset information") 
     375 
     376    is_unknown = property(lambda x: x._mimetype is None) 
     377     
     378    # Reimplemented methods 
     379     
     380    def _is_binary(self): 
     381        return (self._mimetype == APPLICATION_OCTET_STREAM_STR or  
     382                (self.env and self.mimetype in 
     383                 Mimeview(self.env).treat_as_binary) or  
     384                self.is_unknown) 
     385 
     386    def _get_mimetype(self): 
     387        return self._mimetype or APPLICATION_OCTET_STREAM_STR 
     388 
     389    def match(self, other): 
     390        if isinstance(other, MimeType): 
     391            other = other.mimetype 
     392        if not isinstance(other, basestring): 
     393            return False 
     394        if hasattr(self.mimetype, 'match'): 
     395            return self.mimetype.match(other) 
     396        else: 
     397            return self.mimetype == other 
     398 
     399 
     400# A few commonly used MIME types: 
     401 
     402TEXT_PLAIN = MimeType('text/plain', 'utf-8', 'Plain Text', 'txt') 
     403TEXT_HTML = MimeType('text/html', 'utf-8', 'HTML', 'html') 
     404TEXT_CSV = MimeType('text/csv', 'utf-8', 'Comma-delimited Text', 'csv') 
     405TEXT_TSV = MimeType('text/tab-separated-values', 'utf-8', 
     406                    'Tab-delimited Text', 'tsv') 
     407 
     408APPLICATION_RSS_XML = MimeType('application/rss+xml', 'utf-8', 
     409                               'RSS Feed', 'xml') 
     410APPLICATION_OCTET_STREAM = MimeType(APPLICATION_OCTET_STREAM_STR, 
     411                                    IDENTITY_CHARSET, 
     412                                    'Undefined (binary)', 'bin')  
     413 
     414 
     415# -- AbstractContent and subclasses 
     416 
     417class AbstractContent(object): 
     418    """An abstract content, with an associated content type. 
     419 
     420    There can also be a `filename` eventually associated to this content. 
     421    """ 
     422 
     423    def __init__(self, env, content, type, filename=None): 
     424        self.env = env 
     425        self.content = content 
     426        self._type = type 
     427        self.filename = filename 
     428 
     429    def _is_binary(self): raise NotImplementedError 
     430 
     431    def _get_type(self): return self._type 
     432    def _set_type(self, type): self._type = type 
     433 
     434    # Properties 
     435     
     436    type = property(fget=lambda x: x._get_type(), 
     437                    fset=lambda x, y: x._set_type(y), doc= 
     438                    """The corresponding `ContentType` for this content.""") 
     439 
     440    is_binary = property(lambda x: x._is_binary(), doc= 
     441                         "True if content should be considered to be binary") 
     442     
     443    # Methods 
     444     
     445    def basename(self): 
     446        """Return a possible file basename for this content.""" 
     447        return self.filename and os.path.splitext(self.filename)[0] or '' 
     448 
     449    def convert(self, req, format=None, typespec=None, context=None): 
     450        """Convert this content to a `MimeContent`. 
     451 
     452        The converter can be selected by either: 
    220453         
    221         Can return the generated XHTML text as a single string or as an 
    222         iterable that yields strings. In the latter case, the list will 
    223         be considered to correspond to lines of text in the original content. 
     454         - specifying the desired output `format`, which must match the 
     455           `format` of one of the `Conversion` object returned by 
     456           `Mimeview.get_conversions(self.type)`. 
     457           There's usually only one such converter. 
     458            
     459         - giving a `typespec`, which is a `MimeType` object. 
     460           All matching converters are tried in turn, until one succeeds. 
     461           This is quite flexible, as one can use a regexp match ### FIXME 
     462 
     463        Either succeeds or raise `NoConversion` error. 
    224464        """ 
     465        if not typespec and not format: 
     466            self.env.log.error("Convert called but no conversion specified.") 
     467            return self 
     468        if isinstance(format, MimeType): # or signal a programming error? 
     469            typespec = format 
     470            format = None 
    225471 
     472        # Get all available conversions for our `type`, and keep those 
     473        # which are matching either the `format` or the `typespec`. 
     474        mimeview = Mimeview(self.env) 
     475        candidates = [] 
     476        for conversion, converter in mimeview.get_converters(self.type): 
     477            if conversion.format == format or \ 
     478                   (typespec and typespec.match(conversion.output)): 
     479                candidates.append((conversion, converter)) 
     480        if not candidates: 
     481            raise NoConversion('No available MIME conversions', 
     482                               self.type, typespec, format) 
     483 
     484        tab_expanded = None # we don't want to expand tabs more than once. 
     485 
     486        # First candidate which converts successfully wins. 
     487        self.env.log.debug('Converting %s' % repr(self)) 
     488        if not context: 
     489            class ToplevelContext(object): 
     490                def __init__(self, req): 
     491                    self.req = req 
     492            context = ToplevelContext(req) 
     493        content = self 
     494        for conversion, converter in candidates: 
     495            self.env.log.debug('Trying converter %s using %s' % 
     496                               (repr(converter), repr(conversion))) 
     497            if conversion.expand_tabs and not tab_expanded: 
     498                expanded = unicode(content).expandtabs(mimeview.tab_width) 
     499                content = StringMimeContent(self.env, expanded, content.type, 
     500                                            filename=content.filename) 
     501                self.env.log.debug('(tab expansion performed)') 
     502            try: 
     503                result = converter.convert_content(context, conversion, content) 
     504                if result is not None: 
     505                    # Check if the conversion is really valid, by trying to 
     506                    # access an excerpt of the result. This might trigger 
     507                    # errors with legacy renderers returning an iterator... 
     508                    check_result = result.excerpt  
     509                    return result 
     510            except Exception, e: 
     511                self.env.log.warning('MIME conversion using %s failed (%s)' 
     512                                      % (repr(converter), e), exc_info=True) 
     513        raise NoConversion('No MIME conversions succeeded', 
     514                           content.type, typespec, format) 
     515 
     516 
     517class ObjectContent(AbstractContent): 
     518    """Content is a Python a object, and its type is an `ObjectType`. 
     519 
     520    Only supports the bare minimum of the `AbstractContent` methods. 
     521    `filename` in this case will be the suggested base name to be used 
     522    when creating a filename for a converted content. 
     523    """ 
     524 
     525    def __init__(self, env, obj, filename=None): 
     526        AbstractContent.__init__(self, env, obj, ObjectType(obj), 
     527                                 filename=filename) 
     528 
     529    # Reimplemented property readers 
     530 
     531    def _is_binary(self): 
     532        return True 
     533 
     534    def basename(self): 
     535        return AbstractContent.basename(self) or self.type.name 
     536 
     537 
     538c