Ticket #3332: mimeview_refactoring-typerepr-r3507.diff
| File mimeview_refactoring-typerepr-r3507.diff, 97.2 kB (added by cboos, 2 years ago) |
|---|
-
trac/attachment.py
26 26 from trac.config import BoolOption, IntOption 27 27 from trac.core import * 28 28 from trac.env import IEnvironmentSetupParticipant 29 from trac.mimeview import *29 from trac.mimeview.api import Mimeview, MimeType, FileMimeContent 30 30 from trac.util import get_reporter_id, create_unique_file 31 31 from trac.util.datefmt import format_datetime, pretty_timedelta 32 32 from trac.util.markup import Markup, html … … 124 124 def parent_href(self, req): 125 125 return req.href(self.parent_type, self.parent_id) 126 126 127 def _get_title(self): 127 def _get_title(self): # TODO: should extend to other `parent_type`s 128 128 return '%s%s: %s' % (self.parent_type == 'ticket' and '#' or '', 129 129 self.parent_id, self.filename) 130 130 title = property(_get_title) … … 227 227 228 228 select = classmethod(select) 229 229 230 def open(self): 230 def open(self): # deprecate? 231 231 self.env.log.debug('Trying to open attachment at %s', self.path) 232 232 try: 233 233 fd = open(self.path, 'rb') … … 505 505 'author': get_reporter_id(req)} 506 506 507 507 def _render_view(self, req, attachment): 508 # FIXME: perm_map should extend to other `parent_type`s 508 509 perm_map = {'ticket': 'TICKET_VIEW', 'wiki': 'WIKI_VIEW'} 509 510 req.perm.assert_permission(perm_map[attachment.parent_type]) 510 511 … … 522 523 if req.perm.has_permission(perm_map[attachment.parent_type]): 523 524 req.hdf['attachment.can_delete'] = 1 524 525 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') 528 528 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 535 533 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) 551 548 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 558 550 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') 562 556 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) 565 559 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)) 571 562 563 req.hdf['attachment'] = Mimeview(self.env).preview_to_hdf( 564 req, mimecontent, annotations=['lineno']) 565 572 566 def _render_list(self, req, p_type, p_id): 573 567 self._parent_to_hdf(req, p_type, p_id) 574 568 req.hdf['attachment'] = { -
trac/mimeview/rst.py
28 28 import re 29 29 30 30 from trac.core import * 31 from trac.mimeview.api import IHTMLPreviewRenderer , content_to_unicode31 from trac.mimeview.api import IHTMLPreviewRenderer 32 32 from trac.web.href import Href 33 33 from trac.wiki.formatter import WikiProcessor 34 34 from trac.wiki import WikiSystem … … 223 223 224 224 _inliner = rst.states.Inliner() 225 225 _parser = rst.Parser(inliner=_inliner) 226 content = content_to_unicode(self.env, content, mimetype)226 content = unicode(content) 227 227 content = content.encode('utf-8') 228 228 html = publish_string(content, writer_name='html', parser=_parser, 229 229 settings_overrides={'halt_level': 6}) -
trac/mimeview/api.py
19 19 # Christian Boos <cboos@neuf.fr> 20 20 21 21 """ 22 The `trac.mimeview` module centralize the intelligence related to23 file metadata, principally concerning the `type` (MIME type) of the content24 and, if relevant, concerning the text encoding (charset) used by the content.22 The `trac.mimeview` module centralizes the intelligence related to file 23 metadata, principally concerning the `type` (MIME type) and the text 24 encoding (charset) used by the content, if the latter one is relevant. 25 25 26 26 There are primarily two approaches for getting the MIME type of a given file: 27 27 * taking advantage of existing conventions for the file name 28 28 * examining the file content and applying various heuristics 29 29 30 The module also knows how to convert the file content from one type31 to another type.30 The module also knows about conversions from one data type to another type, 31 like conversions to text/html (this is no more a special case). 32 32 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()`. 33 In order to keep the API simple, we deal with a few classes, 34 each 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 39 44 """ 40 45 46 import os 41 47 import re 42 48 from StringIO import StringIO 43 49 44 50 from trac.config import IntOption, ListOption, Option 45 51 from trac.core import * 46 52 from trac.util import sorted 47 from trac.util.text import to_utf8, to_unicode 53 from trac.util.text import to_utf8, to_unicode, IDENTITY_CHARSET 48 54 from trac.util.markup import escape, Markup, Fragment, html 49 55 50 56 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'] 53 63 54 64 55 65 # Some common MIME types and their associated keywords and/or file extensions 56 66 67 APPLICATION_OCTET_STREAM_STR = 'application/octet-stream' 68 57 69 KNOWN_MIME_TYPES = { 58 70 'application/pdf': ['pdf'], 59 71 'application/postscript': ['ps'], … … 114 126 for e in exts: 115 127 MIME_MAP[e] = t 116 128 117 # Simple builtin autodetection from the content using a regexp118 MODE_RE = re.compile(119 r"#!(?:[/\w.-_]+/)?(\w+)|" # look for shebang120 r"-\*-\s*(?:mode:\s*)?([\w+-]+)\s*-\*-|" # look for Emacs' -*- mode -*-121 r"vim:.*?syntax=(\w+)" # look for VIM's syntax=<n>122 )123 129 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) 126 132 133 def get_mimetype_from_filename(filename, mime_map=MIME_MAP): 134 """Guess the most probable MIME type of file with the given `filename`. 135 127 136 `filename` is either a filename (the lookup will then use the suffix) 128 137 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. 131 141 """ 132 142 suffix = filename.split('.')[-1] 133 143 if suffix in mime_map: … … 141 151 mimetype = mimetypes.guess_type(filename)[0] 142 152 except: 143 153 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'156 154 return mimetype 157 155 156 # Simple builtin autodetection from the content using a regexp 157 MODE_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 163 def 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 182 def 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 158 190 def is_binary(data): 159 191 """Detect binary content by checking the first thousand bytes for zeroes. 160 192 … … 165 197 return '\0' in data[:1000] 166 198 167 199 def detect_unicode(data): 168 """Detect different unicode charsets by looking for B OMs (Byte Order Marks).200 """Detect different unicode charsets by looking for Byte Order Marks. 169 201 170 202 Operate obviously only on `str` objects. 171 203 """ … … 178 210 else: 179 211 return None 180 212 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)187 213 214 # -- Classes for mimetype, content and conversion 188 215 216 class 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 227 class 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 246 class 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 313 TEXT_PLAIN = MimeType('text/plain', 'utf-8', 'Plain Text', 'txt') 314 TEXT_HTML = MimeType('text/html', 'utf-8', 'HTML', 'html') 315 316 APPLICATION_RSS_XML = MimeType('application/rss+xml', 'utf-8', 317 'RSS Feed', 'xml') 318 APPLICATION_OCTET_STREAM = MimeType(APPLICATION_OCTET_STREAM_STR, 319 IDENTITY_CHARSET, 320 'Undefined (binary)', 'bin') 321 322 323 class 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 487 class 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 529 class 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 574 class 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 590 class 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 595 class 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
