Improve support for 'type/subtype+suffix' mime types in the colors output formatter.

E.g.:
* application/ld+json
* application/hal+json

Closes #189, #206
This commit is contained in:
Jakub Roztocil 2014-04-28 10:08:03 +02:00
parent 76ab8b84be
commit faec00fd99
3 changed files with 109 additions and 90 deletions

View File

@ -1,11 +1,11 @@
import pygments import pygments.lexer
from pygments import token, lexer import pygments.token
from pygments.styles import get_style_by_name, STYLE_MAP import pygments.styles
from pygments.lexers import get_lexer_for_mimetype, get_lexer_by_name import pygments.lexers
import pygments.style
from pygments.formatters.terminal import TerminalFormatter from pygments.formatters.terminal import TerminalFormatter
from pygments.formatters.terminal256 import Terminal256Formatter from pygments.formatters.terminal256 import Terminal256Formatter
from pygments.util import ClassNotFound from pygments.util import ClassNotFound
from pygments.style import Style
from httpie.compat import is_windows from httpie.compat import is_windows
from .base import BaseProcessor from .base import BaseProcessor
@ -13,11 +13,39 @@ from .base import BaseProcessor
# Colors on Windows via colorama don't look that # Colors on Windows via colorama don't look that
# great and fruity seems to give the best result there. # great and fruity seems to give the best result there.
AVAILABLE_STYLES = set(STYLE_MAP.keys()) AVAILABLE_STYLES = set(pygments.styles.STYLE_MAP.keys())
AVAILABLE_STYLES.add('solarized') AVAILABLE_STYLES.add('solarized')
DEFAULT_STYLE = 'solarized' if not is_windows else 'fruity' DEFAULT_STYLE = 'solarized' if not is_windows else 'fruity'
def get_lexer(mime):
mime_types, lexer_names = [mime], []
type_, subtype = mime.split('/')
if '+' not in subtype:
lexer_names.append(subtype)
else:
subtype_name, subtype_suffix = subtype.split('+')
lexer_names.extend([subtype_name, subtype_suffix])
mime_types.extend([
'%s/%s' % (type_, subtype_name),
'%s/%s' % (type_, subtype_suffix)
])
lexer = None
for mime_type in mime_types:
try:
lexer = pygments.lexers.get_lexer_for_mimetype(mime_type)
break
except ClassNotFound:
pass
else:
for name in lexer_names:
try:
lexer = pygments.lexers.get_lexer_by_name(name)
except ClassNotFound:
pass
return lexer
class PygmentsProcessor(BaseProcessor): class PygmentsProcessor(BaseProcessor):
""" """
Colorize using Pygments Colorize using Pygments
@ -37,7 +65,7 @@ class PygmentsProcessor(BaseProcessor):
self.lexers_by_type = {} self.lexers_by_type = {}
try: try:
style = get_style_by_name( style = pygments.styles.get_style_by_name(
self.kwargs.get('pygments_style', DEFAULT_STYLE)) self.kwargs.get('pygments_style', DEFAULT_STYLE))
except ClassNotFound: except ClassNotFound:
style = Solarized256Style style = Solarized256Style
@ -58,23 +86,13 @@ class PygmentsProcessor(BaseProcessor):
return body.strip() return body.strip()
def get_lexer(self, mime): def get_lexer(self, mime):
lexer = self.lexers_by_type.get(mime) if mime in self.lexers_by_type:
if not lexer: return self.lexers_by_type[mime]
try: self.lexers_by_type[mime] = get_lexer(mime)
lexer = get_lexer_for_mimetype(mime) return self.lexers_by_type[mime]
except ClassNotFound:
if '+' in mime:
# 'application/atom+xml' => 'xml'
subtype = mime.split('+')[-1]
try:
lexer = get_lexer_by_name(subtype)
except ClassNotFound:
pass
self.lexers_by_type[mime] = lexer
return lexer
class HTTPLexer(lexer.RegexLexer): class HTTPLexer(pygments.lexer.RegexLexer):
"""Simplified HTTP lexer for Pygments. """Simplified HTTP lexer for Pygments.
It only operates on headers and provides a stronger contrast between It only operates on headers and provides a stronger contrast between
@ -90,39 +108,39 @@ class HTTPLexer(lexer.RegexLexer):
'root': [ 'root': [
# Request-Line # Request-Line
(r'([A-Z]+)( +)([^ ]+)( +)(HTTP)(/)(\d+\.\d+)', (r'([A-Z]+)( +)([^ ]+)( +)(HTTP)(/)(\d+\.\d+)',
lexer.bygroups( pygments.lexer.bygroups(
token.Name.Function, pygments.token.Name.Function,
token.Text, pygments.token.Text,
token.Name.Namespace, pygments.token.Name.Namespace,
token.Text, pygments.token.Text,
token.Keyword.Reserved, pygments.token.Keyword.Reserved,
token.Operator, pygments.token.Operator,
token.Number pygments.token.Number
)), )),
# Response Status-Line # Response Status-Line
(r'(HTTP)(/)(\d+\.\d+)( +)(\d{3})( +)(.+)', (r'(HTTP)(/)(\d+\.\d+)( +)(\d{3})( +)(.+)',
lexer.bygroups( pygments.lexer.bygroups(
token.Keyword.Reserved, # 'HTTP' pygments.token.Keyword.Reserved, # 'HTTP'
token.Operator, # '/' pygments.token.Operator, # '/'
token.Number, # Version pygments.token.Number, # Version
token.Text, pygments.token.Text,
token.Number, # Status code pygments.token.Number, # Status code
token.Text, pygments.token.Text,
token.Name.Exception, # Reason pygments.token.Name.Exception, # Reason
)), )),
# Header # Header
(r'(.*?)( *)(:)( *)(.+)', lexer.bygroups( (r'(.*?)( *)(:)( *)(.+)', pygments.lexer.bygroups(
token.Name.Attribute, # Name pygments.token.Name.Attribute, # Name
token.Text, pygments.token.Text,
token.Operator, # Colon pygments.token.Operator, # Colon
token.Text, pygments.token.Text,
token.String # Value pygments.token.String # Value
)) ))
] ]
} }
class Solarized256Style(Style): class Solarized256Style(pygments.style.Style):
""" """
solarized256 solarized256
------------ ------------
@ -152,43 +170,43 @@ class Solarized256Style(Style):
background_color = BASE03 background_color = BASE03
styles = { styles = {
token.Keyword: GREEN, pygments.token.Keyword: GREEN,
token.Keyword.Constant: ORANGE, pygments.token.Keyword.Constant: ORANGE,
token.Keyword.Declaration: BLUE, pygments.token.Keyword.Declaration: BLUE,
token.Keyword.Namespace: ORANGE, pygments.token.Keyword.Namespace: ORANGE,
token.Keyword.Reserved: BLUE, pygments.token.Keyword.Reserved: BLUE,
token.Keyword.Type: RED, pygments.token.Keyword.Type: RED,
token.Name.Attribute: BASE1, pygments.token.Name.Attribute: BASE1,
token.Name.Builtin: BLUE, pygments.token.Name.Builtin: BLUE,
token.Name.Builtin.Pseudo: BLUE, pygments.token.Name.Builtin.Pseudo: BLUE,
token.Name.Class: BLUE, pygments.token.Name.Class: BLUE,
token.Name.Constant: ORANGE, pygments.token.Name.Constant: ORANGE,
token.Name.Decorator: BLUE, pygments.token.Name.Decorator: BLUE,
token.Name.Entity: ORANGE, pygments.token.Name.Entity: ORANGE,
token.Name.Exception: YELLOW, pygments.token.Name.Exception: YELLOW,
token.Name.Function: BLUE, pygments.token.Name.Function: BLUE,
token.Name.Tag: BLUE, pygments.token.Name.Tag: BLUE,
token.Name.Variable: BLUE, pygments.token.Name.Variable: BLUE,
token.String: CYAN, pygments.token.String: CYAN,
token.String.Backtick: BASE01, pygments.token.String.Backtick: BASE01,
token.String.Char: CYAN, pygments.token.String.Char: CYAN,
token.String.Doc: CYAN, pygments.token.String.Doc: CYAN,
token.String.Escape: RED, pygments.token.String.Escape: RED,
token.String.Heredoc: CYAN, pygments.token.String.Heredoc: CYAN,
token.String.Regex: RED, pygments.token.String.Regex: RED,
token.Number: CYAN, pygments.token.Number: CYAN,
token.Operator: BASE1, pygments.token.Operator: BASE1,
token.Operator.Word: GREEN, pygments.token.Operator.Word: GREEN,
token.Comment: BASE01, pygments.token.Comment: BASE01,
token.Comment.Preproc: GREEN, pygments.token.Comment.Preproc: GREEN,
token.Comment.Special: GREEN, pygments.token.Comment.Special: GREEN,
token.Generic.Deleted: CYAN, pygments.token.Generic.Deleted: CYAN,
token.Generic.Emph: 'italic', pygments.token.Generic.Emph: 'italic',
token.Generic.Error: RED, pygments.token.Generic.Error: RED,
token.Generic.Heading: ORANGE, pygments.token.Generic.Heading: ORANGE,
token.Generic.Inserted: GREEN, pygments.token.Generic.Inserted: GREEN,
token.Generic.Strong: 'bold', pygments.token.Generic.Strong: 'bold',
token.Generic.Subheading: ORANGE, pygments.token.Generic.Subheading: ORANGE,
token.Token: BASE1, pygments.token.Token: BASE1,
token.Token.Other: ORANGE, pygments.token.Token.Other: ORANGE,
} }

View File

@ -11,7 +11,6 @@ def has_docutils():
try: try:
#noinspection PyUnresolvedReferences #noinspection PyUnresolvedReferences
import docutils import docutils
return True return True
except ImportError: except ImportError:
return False return False

View File

@ -29,15 +29,17 @@ class TestVerboseFlag:
class TestColors: class TestColors:
@pytest.mark.parametrize('mime', [ @pytest.mark.parametrize('mime', [
'text/html', 'application/json',
'foo/html', 'application/json+foo',
'text/html+foo', 'application/foo+json',
'text/foo+html' 'foo/json',
'foo/json+bar',
'foo/bar+json',
]) ])
def test_get_lexer(self, mime): def test_get_lexer(self, mime):
lexer = get_lexer(mime) lexer = get_lexer(mime)
assert lexer is not None assert lexer is not None
assert lexer.name == 'HTML' assert lexer.name == 'JSON'
def test_get_lexer_not_found(self): def test_get_lexer_not_found(self):
assert get_lexer('xxx/yyy') is None assert get_lexer('xxx/yyy') is None