2012-07-26 07:37:03 +03:00
|
|
|
"""Output processing and formatting.
|
2012-07-20 22:54:41 +03:00
|
|
|
|
|
|
|
"""
|
2012-07-14 17:27:11 +03:00
|
|
|
import re
|
2012-02-25 15:39:38 +03:00
|
|
|
import json
|
2012-07-26 01:26:23 +03:00
|
|
|
|
2012-02-25 15:39:38 +03:00
|
|
|
import pygments
|
2012-07-20 22:54:41 +03:00
|
|
|
from pygments import token, lexer
|
2012-04-28 15:18:59 +03:00
|
|
|
from pygments.styles import get_style_by_name, STYLE_MAP
|
2012-08-01 18:35:32 +03:00
|
|
|
from pygments.lexers import get_lexer_for_mimetype, get_lexer_by_name
|
2012-04-28 15:13:40 +03:00
|
|
|
from pygments.formatters.terminal import TerminalFormatter
|
2012-07-20 22:54:41 +03:00
|
|
|
from pygments.formatters.terminal256 import Terminal256Formatter
|
|
|
|
from pygments.util import ClassNotFound
|
2012-07-28 14:24:44 +03:00
|
|
|
from requests.compat import is_windows
|
2012-07-26 01:26:23 +03:00
|
|
|
|
2012-07-30 13:11:16 +03:00
|
|
|
from .solarized import Solarized256Style
|
2012-07-28 06:45:44 +03:00
|
|
|
from .models import Environment
|
2012-02-25 15:39:38 +03:00
|
|
|
|
2012-07-20 22:54:41 +03:00
|
|
|
|
|
|
|
DEFAULT_STYLE = 'solarized'
|
|
|
|
AVAILABLE_STYLES = [DEFAULT_STYLE] + list(STYLE_MAP.keys())
|
2012-07-28 06:45:44 +03:00
|
|
|
BINARY_SUPPRESSED_NOTICE = (
|
|
|
|
'+-----------------------------------------+\n'
|
|
|
|
'| NOTE: binary data not shown in terminal |\n'
|
|
|
|
'+-----------------------------------------+'
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2012-08-02 00:21:52 +03:00
|
|
|
def formatted_stream(msg, prettifier=None, with_headers=True, with_body=True,
|
|
|
|
env=Environment()):
|
|
|
|
"""Return an iterator yielding `bytes` representing `msg`
|
|
|
|
(a `models.HTTPMessage` subclass).
|
2012-07-28 06:45:44 +03:00
|
|
|
|
2012-08-02 00:21:52 +03:00
|
|
|
The body can be binary so we always yield `bytes`.
|
2012-07-28 06:45:44 +03:00
|
|
|
|
2012-07-28 07:09:25 +03:00
|
|
|
If `prettifier` is set or the output is a terminal then a binary
|
|
|
|
body is not included in the output and is replaced with notice.
|
2012-07-28 06:45:44 +03:00
|
|
|
|
2012-07-28 07:09:25 +03:00
|
|
|
Generally, when the `stdout` is redirected, the output matches the actual
|
2012-07-30 15:23:22 +03:00
|
|
|
message as much as possible (formatting and character encoding-wise).
|
|
|
|
When `--pretty` is set (or implied), or when the output is a terminal,
|
|
|
|
then we prefer readability over precision.
|
2012-07-28 06:45:44 +03:00
|
|
|
|
|
|
|
"""
|
2012-07-30 11:58:16 +03:00
|
|
|
# Output encoding.
|
|
|
|
if env.stdout_isatty:
|
|
|
|
# Use encoding suitable for the terminal. Unsupported characters
|
|
|
|
# will be replaced in the output.
|
|
|
|
errors = 'replace'
|
|
|
|
output_encoding = getattr(env.stdout, 'encoding', None)
|
|
|
|
else:
|
|
|
|
# Preserve the message encoding.
|
|
|
|
errors = 'strict'
|
|
|
|
output_encoding = msg.encoding
|
|
|
|
if not output_encoding:
|
|
|
|
# Default to utf8
|
|
|
|
output_encoding = 'utf8'
|
|
|
|
|
|
|
|
if prettifier:
|
|
|
|
env.init_colors()
|
|
|
|
|
2012-07-28 06:45:44 +03:00
|
|
|
if with_headers:
|
2012-07-29 04:52:24 +03:00
|
|
|
headers = '\n'.join([msg.line, msg.headers])
|
2012-07-28 06:45:44 +03:00
|
|
|
|
|
|
|
if prettifier:
|
2012-07-29 04:52:24 +03:00
|
|
|
headers = prettifier.process_headers(headers)
|
2012-07-28 06:45:44 +03:00
|
|
|
|
2012-08-02 00:21:52 +03:00
|
|
|
yield headers.encode(output_encoding, errors).strip()
|
2012-07-28 06:45:44 +03:00
|
|
|
|
2012-08-02 00:21:52 +03:00
|
|
|
if with_body:
|
2012-07-28 06:45:44 +03:00
|
|
|
|
2012-08-02 00:21:52 +03:00
|
|
|
prefix = b'\n\n' if with_headers else None
|
2012-07-28 06:45:44 +03:00
|
|
|
|
2012-07-30 11:58:16 +03:00
|
|
|
if not (env.stdout_isatty or prettifier):
|
|
|
|
# Verbatim body even if it's binary.
|
2012-08-02 00:21:52 +03:00
|
|
|
for body_chunk in msg:
|
|
|
|
if prefix:
|
|
|
|
yield prefix
|
|
|
|
prefix = None
|
|
|
|
yield body_chunk
|
|
|
|
elif msg.body:
|
2012-07-28 06:45:44 +03:00
|
|
|
try:
|
2012-08-02 00:21:52 +03:00
|
|
|
body = msg.body.decode(msg.encoding)
|
2012-07-28 06:45:44 +03:00
|
|
|
except UnicodeDecodeError:
|
2012-07-30 11:58:16 +03:00
|
|
|
# Suppress binary data.
|
|
|
|
body = BINARY_SUPPRESSED_NOTICE.encode(output_encoding)
|
2012-07-28 06:45:44 +03:00
|
|
|
if not with_headers:
|
2012-08-02 00:21:52 +03:00
|
|
|
yield b'\n'
|
2012-07-28 06:45:44 +03:00
|
|
|
else:
|
2012-07-30 11:58:16 +03:00
|
|
|
if prettifier and msg.content_type:
|
|
|
|
body = prettifier.process_body(
|
|
|
|
body, msg.content_type).strip()
|
2012-07-28 06:45:44 +03:00
|
|
|
|
2012-07-30 11:58:16 +03:00
|
|
|
body = body.encode(output_encoding, errors)
|
2012-08-02 00:21:52 +03:00
|
|
|
if prefix:
|
|
|
|
yield prefix
|
|
|
|
yield body
|
2012-07-17 04:48:10 +03:00
|
|
|
|
2012-02-25 15:39:38 +03:00
|
|
|
|
2012-07-20 22:54:41 +03:00
|
|
|
class HTTPLexer(lexer.RegexLexer):
|
2012-07-26 07:37:03 +03:00
|
|
|
"""Simplified HTTP lexer for Pygments.
|
2012-07-20 22:54:41 +03:00
|
|
|
|
|
|
|
It only operates on headers and provides a stronger contrast between
|
|
|
|
their names and values than the original one bundled with Pygments
|
|
|
|
(`pygments.lexers.text import HttpLexer`), especially when
|
|
|
|
Solarized color scheme is used.
|
|
|
|
|
|
|
|
"""
|
|
|
|
name = 'HTTP'
|
|
|
|
aliases = ['http']
|
|
|
|
filenames = ['*.http']
|
|
|
|
tokens = {
|
|
|
|
'root': [
|
2012-02-25 15:39:38 +03:00
|
|
|
|
2012-07-20 22:54:41 +03:00
|
|
|
# Request-Line
|
|
|
|
(r'([A-Z]+)( +)([^ ]+)( +)(HTTP)(/)(\d+\.\d+)',
|
|
|
|
lexer.bygroups(
|
|
|
|
token.Name.Function,
|
|
|
|
token.Text,
|
|
|
|
token.Name.Namespace,
|
|
|
|
token.Text,
|
|
|
|
token.Keyword.Reserved,
|
|
|
|
token.Operator,
|
|
|
|
token.Number
|
|
|
|
)),
|
|
|
|
|
|
|
|
# Response Status-Line
|
|
|
|
(r'(HTTP)(/)(\d+\.\d+)( +)(\d{3})( +)(.+)',
|
|
|
|
lexer.bygroups(
|
|
|
|
token.Keyword.Reserved, # 'HTTP'
|
|
|
|
token.Operator, # '/'
|
|
|
|
token.Number, # Version
|
|
|
|
token.Text,
|
|
|
|
token.Number, # Status code
|
|
|
|
token.Text,
|
|
|
|
token.Name.Exception, # Reason
|
|
|
|
)),
|
|
|
|
|
|
|
|
# Header
|
|
|
|
(r'(.*?)( *)(:)( *)(.+)', lexer.bygroups(
|
|
|
|
token.Name.Attribute, # Name
|
|
|
|
token.Text,
|
|
|
|
token.Operator, # Colon
|
|
|
|
token.Text,
|
|
|
|
token.String # Value
|
|
|
|
))
|
|
|
|
]}
|
2012-07-14 17:27:11 +03:00
|
|
|
|
2012-04-26 14:05:59 +03:00
|
|
|
|
2012-07-21 03:59:43 +03:00
|
|
|
class BaseProcessor(object):
|
|
|
|
|
|
|
|
enabled = True
|
|
|
|
|
|
|
|
def __init__(self, env, **kwargs):
|
|
|
|
self.env = env
|
|
|
|
self.kwargs = kwargs
|
|
|
|
|
|
|
|
def process_headers(self, headers):
|
|
|
|
return headers
|
|
|
|
|
2012-08-01 18:35:32 +03:00
|
|
|
def process_body(self, content, content_type, subtype):
|
|
|
|
"""Return processed `content`.
|
|
|
|
|
|
|
|
:param content: `str`
|
|
|
|
:param content_type: full content type, e.g., 'application/atom+xml'
|
|
|
|
:param subtype: e.g., 'xml'
|
|
|
|
|
|
|
|
"""
|
2012-07-21 03:59:43 +03:00
|
|
|
return content
|
|
|
|
|
|
|
|
|
|
|
|
class JSONProcessor(BaseProcessor):
|
|
|
|
|
2012-08-01 18:35:32 +03:00
|
|
|
def process_body(self, content, content_type, subtype):
|
|
|
|
if subtype == 'json':
|
2012-07-21 03:59:43 +03:00
|
|
|
try:
|
2012-08-01 18:35:32 +03:00
|
|
|
# Indent the JSON data, sort keys by name, and
|
|
|
|
# avoid unicode escapes to improve readability.
|
|
|
|
content = json.dumps(json.loads(content),
|
|
|
|
sort_keys=True,
|
|
|
|
ensure_ascii=False,
|
|
|
|
indent=4)
|
2012-07-21 03:59:43 +03:00
|
|
|
except ValueError:
|
2012-08-01 18:35:32 +03:00
|
|
|
# Invalid JSON but we don't care.
|
2012-07-21 03:59:43 +03:00
|
|
|
pass
|
|
|
|
return content
|
|
|
|
|
|
|
|
|
|
|
|
class PygmentsProcessor(BaseProcessor):
|
|
|
|
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
super(PygmentsProcessor, self).__init__(*args, **kwargs)
|
|
|
|
|
|
|
|
if not self.env.colors:
|
|
|
|
self.enabled = False
|
|
|
|
return
|
2012-04-28 15:13:40 +03:00
|
|
|
|
2012-07-20 22:54:41 +03:00
|
|
|
try:
|
2012-07-21 03:59:43 +03:00
|
|
|
style = get_style_by_name(
|
|
|
|
self.kwargs.get('pygments_style', DEFAULT_STYLE))
|
2012-07-20 22:54:41 +03:00
|
|
|
except ClassNotFound:
|
2012-07-30 13:11:16 +03:00
|
|
|
style = Solarized256Style
|
2012-04-28 15:13:40 +03:00
|
|
|
|
2012-07-21 03:59:43 +03:00
|
|
|
if is_windows or self.env.colors == 256:
|
|
|
|
fmt_class = Terminal256Formatter
|
|
|
|
else:
|
|
|
|
fmt_class = TerminalFormatter
|
|
|
|
self.formatter = fmt_class(style=style)
|
2012-04-28 15:13:40 +03:00
|
|
|
|
2012-07-21 03:59:43 +03:00
|
|
|
def process_headers(self, headers):
|
|
|
|
return pygments.highlight(
|
|
|
|
headers, HTTPLexer(), self.formatter)
|
2012-07-20 22:54:41 +03:00
|
|
|
|
2012-08-01 18:35:32 +03:00
|
|
|
def process_body(self, content, content_type, subtype):
|
2012-07-21 03:59:43 +03:00
|
|
|
try:
|
2012-08-01 18:35:32 +03:00
|
|
|
try:
|
|
|
|
lexer = get_lexer_for_mimetype(content_type)
|
|
|
|
except ClassNotFound:
|
|
|
|
lexer = get_lexer_by_name(subtype)
|
2012-07-21 03:59:43 +03:00
|
|
|
except ClassNotFound:
|
|
|
|
pass
|
|
|
|
else:
|
|
|
|
content = pygments.highlight(content, lexer, self.formatter)
|
|
|
|
return content
|
|
|
|
|
|
|
|
|
|
|
|
class OutputProcessor(object):
|
|
|
|
|
|
|
|
installed_processors = [
|
|
|
|
JSONProcessor,
|
|
|
|
PygmentsProcessor
|
|
|
|
]
|
|
|
|
|
|
|
|
def __init__(self, env, **kwargs):
|
|
|
|
processors = [
|
|
|
|
cls(env, **kwargs)
|
|
|
|
for cls in self.installed_processors
|
|
|
|
]
|
|
|
|
self.processors = [p for p in processors if p.enabled]
|
|
|
|
|
|
|
|
def process_headers(self, headers):
|
|
|
|
for processor in self.processors:
|
2012-08-01 18:35:32 +03:00
|
|
|
headers = processor.process_headers(headers)
|
2012-07-21 03:59:43 +03:00
|
|
|
return headers
|
|
|
|
|
|
|
|
def process_body(self, content, content_type):
|
2012-08-01 18:35:32 +03:00
|
|
|
# e.g., 'application/atom+xml'
|
2012-04-28 15:13:40 +03:00
|
|
|
content_type = content_type.split(';')[0]
|
2012-08-01 18:35:32 +03:00
|
|
|
# e.g., 'xml'
|
|
|
|
subtype = content_type.split('/')[-1].split('+')[-1]
|
2012-07-14 17:27:11 +03:00
|
|
|
|
2012-07-21 03:59:43 +03:00
|
|
|
for processor in self.processors:
|
2012-08-01 18:35:32 +03:00
|
|
|
content = processor.process_body(content, content_type, subtype)
|
2012-04-26 14:05:59 +03:00
|
|
|
|
2012-07-21 03:59:43 +03:00
|
|
|
return content
|