From e4c68063b9946147509166b512a49294f3891b8c Mon Sep 17 00:00:00 2001 From: Jakub Roztocil Date: Mon, 28 Apr 2014 23:33:30 +0200 Subject: [PATCH] Converted built-in formatters to formatter plugins. Still work in progress and the API should be considered private for now. --- httpie/__init__.py | 2 +- httpie/cli.py | 42 ++++----- httpie/client.py | 8 +- httpie/compat.py | 12 +-- httpie/config.py | 4 +- httpie/core.py | 3 +- httpie/input.py | 5 +- httpie/models.py | 2 +- httpie/output/formatters/__init__.py | 0 .../{processors => formatters}/colors.py | 94 +++++++++---------- .../{processors => formatters}/headers.py | 6 +- .../output/{processors => formatters}/json.py | 9 +- .../output/{processors => formatters}/xml.py | 9 +- httpie/output/processing.py | 50 ++++++++++ httpie/output/processors/__init__.py | 44 --------- httpie/output/processors/base.py | 37 -------- httpie/output/streams.py | 83 ++++++++++------ httpie/plugins/__init__.py | 19 ++-- httpie/plugins/base.py | 67 ++++++++++--- httpie/plugins/builtin.py | 2 +- httpie/plugins/manager.py | 49 +++++++--- httpie/sessions.py | 4 +- tests/test_output.py | 2 +- tests/test_unicode.py | 6 +- tests/utils.py | 1 - 25 files changed, 311 insertions(+), 249 deletions(-) create mode 100644 httpie/output/formatters/__init__.py rename httpie/output/{processors => formatters}/colors.py (89%) rename httpie/output/{processors => formatters}/headers.py (70%) rename httpie/output/{processors => formatters}/json.py (80%) rename httpie/output/{processors => formatters}/xml.py (92%) create mode 100644 httpie/output/processing.py delete mode 100644 httpie/output/processors/__init__.py delete mode 100644 httpie/output/processors/base.py diff --git a/httpie/__init__.py b/httpie/__init__.py index 44842a7e..49714999 100644 --- a/httpie/__init__.py +++ b/httpie/__init__.py @@ -3,7 +3,7 @@ HTTPie - a CLI, cURL-like tool for humans. """ __author__ = 'Jakub Roztocil' -__version__ = '0.9.0dev' +__version__ = '0.9.0-dev' __licence__ = 'BSD' diff --git a/httpie/cli.py b/httpie/cli.py index 133be794..9b7a454e 100644 --- a/httpie/cli.py +++ b/httpie/cli.py @@ -8,18 +8,18 @@ from textwrap import dedent, wrap from argparse import (RawDescriptionHelpFormatter, FileType, OPTIONAL, ZERO_OR_MORE, SUPPRESS) -from . import __doc__ -from . import __version__ -from .plugins.builtin import BuiltinAuthPlugin -from .plugins import plugin_manager -from .sessions import DEFAULT_SESSIONS_DIR -from .output.processors.colors import AVAILABLE_STYLES, DEFAULT_STYLE -from .input import (Parser, AuthCredentialsArgType, KeyValueArgType, - SEP_PROXY, SEP_CREDENTIALS, SEP_GROUP_ALL_ITEMS, - OUT_REQ_HEAD, OUT_REQ_BODY, OUT_RESP_HEAD, - OUT_RESP_BODY, OUTPUT_OPTIONS, OUTPUT_OPTIONS_DEFAULT, - PRETTY_MAP, PRETTY_STDOUT_TTY_ONLY, SessionNameValidator, - readable_file_arg) +from httpie import __doc__, __version__ +from httpie.plugins.builtin import BuiltinAuthPlugin +from httpie.plugins import plugin_manager +from httpie.sessions import DEFAULT_SESSIONS_DIR +from httpie.output.formatters.colors import AVAILABLE_STYLES, DEFAULT_STYLE +from httpie.input import (Parser, AuthCredentialsArgType, KeyValueArgType, + SEP_PROXY, SEP_CREDENTIALS, SEP_GROUP_ALL_ITEMS, + OUT_REQ_HEAD, OUT_REQ_BODY, OUT_RESP_HEAD, + OUT_RESP_BODY, OUTPUT_OPTIONS, + OUTPUT_OPTIONS_DEFAULT, PRETTY_MAP, + PRETTY_STDOUT_TTY_ONLY, SessionNameValidator, + readable_file_arg) class HTTPieHelpFormatter(RawDescriptionHelpFormatter): @@ -60,7 +60,7 @@ parser = Parser( ####################################################################### positional = parser.add_argument_group( - title='Positional arguments', + title='Positional Arguments', description=dedent(""" These arguments come after any flags and in the order they are listed here. Only URL is required. @@ -147,7 +147,7 @@ positional.add_argument( ####################################################################### content_type = parser.add_argument_group( - title='Predefined content types', + title='Predefined Content Types', description=None ) @@ -179,7 +179,7 @@ content_type.add_argument( # Output processing ####################################################################### -output_processing = parser.add_argument_group(title='Output processing') +output_processing = parser.add_argument_group(title='Output Processing') output_processing.add_argument( '--pretty', @@ -208,14 +208,12 @@ output_processing.add_argument( environment variable is set to "xterm-256color" or similar (e.g., via `export TERM=xterm-256color' in your ~/.bashrc). - """ - .format( + """.format( default=DEFAULT_STYLE, available='\n'.join( - '{0: >20}'.format(line.strip()) - for line in - wrap(' '.join(sorted(AVAILABLE_STYLES)), 60) - ), + '{0}{1}'.format(8*' ', line.strip()) + for line in wrap(', '.join(sorted(AVAILABLE_STYLES)), 60) + ).rstrip(), ) ) @@ -223,7 +221,7 @@ output_processing.add_argument( ####################################################################### # Output options ####################################################################### -output_options = parser.add_argument_group(title='Output options') +output_options = parser.add_argument_group(title='Output Options') output_options.add_argument( '--print', '-p', diff --git a/httpie/client.py b/httpie/client.py index a17408e5..6444c17e 100644 --- a/httpie/client.py +++ b/httpie/client.py @@ -4,10 +4,10 @@ from pprint import pformat import requests -from . import sessions -from . import __version__ -from .compat import str -from .plugins import plugin_manager +from httpie import sessions +from httpie import __version__ +from httpie.compat import str +from httpie.plugins import plugin_manager FORM = 'application/x-www-form-urlencoded; charset=utf-8' diff --git a/httpie/compat.py b/httpie/compat.py index 44ccb614..bb222d91 100644 --- a/httpie/compat.py +++ b/httpie/compat.py @@ -1,15 +1,10 @@ """ -Python 2/3 compatibility. +Python 2.6, 2.7, and 3.x compatibility. """ +# Borrow these from requests: #noinspection PyUnresolvedReferences -from requests.compat import ( - is_windows, - bytes, - str, - is_py3, - is_py26, -) +from requests.compat import is_windows, bytes, str, is_py3, is_py26 try: #noinspection PyUnresolvedReferences,PyCompatibility @@ -25,7 +20,6 @@ except ImportError: #noinspection PyCompatibility from urllib2 import urlopen - try: from collections import OrderedDict except ImportError: diff --git a/httpie/config.py b/httpie/config.py index 24d4d56a..e80aa89f 100644 --- a/httpie/config.py +++ b/httpie/config.py @@ -2,8 +2,8 @@ import os import json import errno -from . import __version__ -from .compat import is_windows +from httpie import __version__ +from httpie.compat import is_windows DEFAULT_CONFIG_DIR = os.environ.get( diff --git a/httpie/core.py b/httpie/core.py index 0219df8b..c2de5966 100644 --- a/httpie/core.py +++ b/httpie/core.py @@ -72,10 +72,9 @@ def main(args=sys.argv[1:], env=Environment()): Return exit status code. """ - args = decode_args(args, env.stdin_encoding) + from httpie.cli import parser plugin_manager.load_installed_plugins() - from .cli import parser if env.config.default_options: args = env.config.default_options + args diff --git a/httpie/input.py b/httpie/input.py index 0fa554f7..3cb64168 100644 --- a/httpie/input.py +++ b/httpie/input.py @@ -10,14 +10,13 @@ import getpass from io import BytesIO #noinspection PyCompatibility from argparse import ArgumentParser, ArgumentTypeError, ArgumentError -from .compat import OrderedDict # TODO: Use MultiDict for headers once added to `requests`. # https://github.com/jakubroztocil/httpie/issues/130 from requests.structures import CaseInsensitiveDict -from .compat import urlsplit, str -from .sessions import VALID_SESSION_NAME_PATTERN +from httpie.compat import OrderedDict, urlsplit, str +from httpie.sessions import VALID_SESSION_NAME_PATTERN HTTP_POST = 'POST' diff --git a/httpie/models.py b/httpie/models.py index 5ba2f087..f6b9dff9 100644 --- a/httpie/models.py +++ b/httpie/models.py @@ -1,4 +1,4 @@ -from .compat import urlsplit, str +from httpie.compat import urlsplit, str class HTTPMessage(object): diff --git a/httpie/output/formatters/__init__.py b/httpie/output/formatters/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/httpie/output/processors/colors.py b/httpie/output/formatters/colors.py similarity index 89% rename from httpie/output/processors/colors.py rename to httpie/output/formatters/colors.py index 31e890d8..bcec7ba1 100644 --- a/httpie/output/processors/colors.py +++ b/httpie/output/formatters/colors.py @@ -8,7 +8,7 @@ from pygments.formatters.terminal256 import Terminal256Formatter from pygments.util import ClassNotFound from httpie.compat import is_windows -from .base import BaseProcessor +from httpie.plugins import FormatterPlugin # Colors on Windows via colorama don't look that @@ -18,6 +18,52 @@ AVAILABLE_STYLES.add('solarized') DEFAULT_STYLE = 'solarized' if not is_windows else 'fruity' +class ColorFormatter(FormatterPlugin): + """ + Colorize using Pygments + + This processor that applies syntax highlighting to the headers, + and also to the body if its content type is recognized. + + """ + group_name = 'colors' + + def __init__(self, env, color_scheme=DEFAULT_STYLE, **kwargs): + super(ColorFormatter, self).__init__(**kwargs) + if not env.colors: + self.enabled = False + return + + # Cache to speed things up when we process streamed body by line. + self.lexer_cache = {} + + try: + style_class = pygments.styles.get_style_by_name(color_scheme) + except ClassNotFound: + style_class = Solarized256Style + + if env.is_windows or env.colors == 256: + fmt_class = Terminal256Formatter + else: + fmt_class = TerminalFormatter + self.formatter = fmt_class(style=style_class) + + def format_headers(self, headers): + return pygments.highlight(headers, HTTPLexer(), self.formatter).strip() + + def format_body(self, body, mime): + lexer = self.get_lexer(mime) + if lexer: + body = pygments.highlight(body, lexer, self.formatter) + return body.strip() + + def get_lexer(self, mime): + if mime in self.lexer_cache: + return self.lexer_cache[mime] + self.lexer_cache[mime] = get_lexer(mime) + return self.lexer_cache[mime] + + def get_lexer(mime): mime_types, lexer_names = [mime], [] type_, subtype = mime.split('/') @@ -46,52 +92,6 @@ def get_lexer(mime): return lexer -class PygmentsProcessor(BaseProcessor): - """ - Colorize using Pygments - - This processor that applies syntax highlighting to the headers, - and also to the body if its content type is recognized. - - """ - def __init__(self, *args, **kwargs): - super(PygmentsProcessor, self).__init__(*args, **kwargs) - - if not self.env.colors: - self.enabled = False - return - - # Cache to speed things up when we process streamed body by line. - self.lexers_by_type = {} - - try: - style = pygments.styles.get_style_by_name( - self.kwargs.get('pygments_style', DEFAULT_STYLE)) - except ClassNotFound: - style = Solarized256Style - - if self.env.is_windows or self.env.colors == 256: - fmt_class = Terminal256Formatter - else: - fmt_class = TerminalFormatter - self.formatter = fmt_class(style=style) - - def process_headers(self, headers): - return pygments.highlight(headers, HTTPLexer(), self.formatter).strip() - - def process_body(self, body, mime): - lexer = self.get_lexer(mime) - if lexer: - body = pygments.highlight(body, lexer, self.formatter) - return body.strip() - - def get_lexer(self, mime): - if mime in self.lexers_by_type: - return self.lexers_by_type[mime] - self.lexers_by_type[mime] = get_lexer(mime) - return self.lexers_by_type[mime] - - class HTTPLexer(pygments.lexer.RegexLexer): """Simplified HTTP lexer for Pygments. diff --git a/httpie/output/processors/headers.py b/httpie/output/formatters/headers.py similarity index 70% rename from httpie/output/processors/headers.py rename to httpie/output/formatters/headers.py index 60842a85..146dea3c 100644 --- a/httpie/output/processors/headers.py +++ b/httpie/output/formatters/headers.py @@ -1,9 +1,9 @@ -from .base import BaseProcessor +from httpie.plugins import FormatterPlugin -class HeadersProcessor(BaseProcessor): +class HeadersFormatter(FormatterPlugin): - def process_headers(self, headers): + def format_headers(self, headers): """ Sorts headers by name while retaining relative order of multiple headers with the same name. diff --git a/httpie/output/processors/json.py b/httpie/output/formatters/json.py similarity index 80% rename from httpie/output/processors/json.py rename to httpie/output/formatters/json.py index 2ae05e12..292cc142 100644 --- a/httpie/output/processors/json.py +++ b/httpie/output/formatters/json.py @@ -1,12 +1,15 @@ from __future__ import absolute_import import json -from .base import BaseProcessor, DEFAULT_INDENT +from httpie.plugins import FormatterPlugin -class JSONProcessor(BaseProcessor): +DEFAULT_INDENT = 4 - def process_body(self, body, mime): + +class JSONFormatter(FormatterPlugin): + + def format_body(self, body, mime): if 'json' in mime: try: obj = json.loads(body) diff --git a/httpie/output/processors/xml.py b/httpie/output/formatters/xml.py similarity index 92% rename from httpie/output/processors/xml.py rename to httpie/output/formatters/xml.py index 9f3794f7..dc82e5be 100644 --- a/httpie/output/processors/xml.py +++ b/httpie/output/formatters/xml.py @@ -2,13 +2,16 @@ from __future__ import absolute_import import re from xml.etree import ElementTree -from .base import BaseProcessor, DEFAULT_INDENT +from httpie.plugins import FormatterPlugin DECLARATION_RE = re.compile('<\?xml[^\n]+?\?>', flags=re.I) DOCTYPE_RE = re.compile('', flags=re.I) +DEFAULT_INDENT = 4 + + def indent(elem, indent_text=' ' * DEFAULT_INDENT): """ In-place prettyprint formatter @@ -33,10 +36,10 @@ def indent(elem, indent_text=' ' * DEFAULT_INDENT): return _indent(elem) -class XMLProcessor(BaseProcessor): +class XMLFormatter(FormatterPlugin): # TODO: tests - def process_body(self, body, mime): + def format_body(self, body, mime): if 'xml' in mime: # FIXME: orig NS names get forgotten during the conversion, etc. try: diff --git a/httpie/output/processing.py b/httpie/output/processing.py new file mode 100644 index 00000000..4e6e3d7d --- /dev/null +++ b/httpie/output/processing.py @@ -0,0 +1,50 @@ +import re + +from httpie.plugins import plugin_manager +from httpie.context import Environment + + +MIME_RE = re.compile(r'^[^/]+/[^/]+$') + + +def is_valid_mime(mime): + return mime and MIME_RE.match(mime) + + +class Conversion(object): + + def get_converter(self, mime): + if is_valid_mime(mime): + for converter_class in plugin_manager.get_converters(): + if converter_class.supports(mime): + return converter_class(mime) + + +class Formatting(object): + """A delegate class that invokes the actual processors.""" + + def __init__(self, groups, env=Environment(), **kwargs): + """ + :param groups: names of processor groups to be applied + :param env: Environment + :param kwargs: additional keyword arguments for processors + + """ + available_plugins = plugin_manager.get_formatters_grouped() + self.enabled_plugins = [] + for group in groups: + for cls in available_plugins[group]: + p = cls(env=env, **kwargs) + if p.enabled: + self.enabled_plugins.append(p) + + def format_headers(self, headers): + for p in self.enabled_plugins: + headers = p.format_headers(headers) + return headers + + def format_body(self, content, mime): + if is_valid_mime(mime): + for p in self.enabled_plugins: + content = p.format_body(content, mime) + return content diff --git a/httpie/output/processors/__init__.py b/httpie/output/processors/__init__.py deleted file mode 100644 index 29f80d8a..00000000 --- a/httpie/output/processors/__init__.py +++ /dev/null @@ -1,44 +0,0 @@ -from httpie.context import Environment -from .headers import HeadersProcessor -from .json import JSONProcessor -from .xml import XMLProcessor -from .colors import PygmentsProcessor - - -class ProcessorManager(object): - """A delegate class that invokes the actual processors.""" - - available = { - 'format': [ - HeadersProcessor, - JSONProcessor, - XMLProcessor - ], - 'colors': [ - PygmentsProcessor - ] - } - - def __init__(self, groups, env=Environment(), **kwargs): - """ - :param groups: names of processor groups to be applied - :param env: Environment - :param kwargs: additional keyword arguments for processors - - """ - self.enabled = [] - for group in groups: - for cls in self.available[group]: - p = cls(env, **kwargs) - if p.enabled: - self.enabled.append(p) - - def process_headers(self, headers): - for p in self.enabled: - headers = p.process_headers(headers) - return headers - - def process_body(self, body, mime): - for p in self.enabled: - body = p.process_body(body, mime) - return body diff --git a/httpie/output/processors/base.py b/httpie/output/processors/base.py deleted file mode 100644 index 26e5206c..00000000 --- a/httpie/output/processors/base.py +++ /dev/null @@ -1,37 +0,0 @@ -from httpie.context import Environment - - -# The default number of spaces to indent when pretty printing -DEFAULT_INDENT = 4 - - -class BaseProcessor(object): - """Base output processor class.""" - - def __init__(self, env=Environment(), **kwargs): - """ - :param env: an class:`Environment` instance - :param kwargs: additional keyword argument that some - processor might require. - - """ - self.enabled = True - self.env = env - self.kwargs = kwargs - - def process_headers(self, headers): - """Return processed `headers` - - :param headers: The headers as text. - - """ - return headers - - def process_body(self, content, mime): - """Return processed `content`. - - :param content: The body content as text - :param mime: E.g., 'application/atom+xml'. - - """ - return content diff --git a/httpie/output/streams.py b/httpie/output/streams.py index 95d8138e..8460537d 100644 --- a/httpie/output/streams.py +++ b/httpie/output/streams.py @@ -1,11 +1,12 @@ from itertools import chain from functools import partial +from httpie.compat import str from httpie.context import Environment from httpie.models import HTTPRequest, HTTPResponse from httpie.input import (OUT_REQ_BODY, OUT_REQ_HEAD, OUT_RESP_HEAD, OUT_RESP_BODY) -from httpie.output.processors import ProcessorManager +from httpie.output.processing import Formatting, Conversion BINARY_SUPPRESSED_NOTICE = ( @@ -59,7 +60,6 @@ def build_output_stream(args, env, request, response): exchange each of which yields `bytes` chunks. """ - req_h = OUT_REQ_HEAD in args.output_options req_b = OUT_REQ_BODY in args.output_options resp_h = OUT_RESP_HEAD in args.output_options @@ -111,11 +111,9 @@ def get_stream_type(env, args): Stream = partial( PrettyStream if args.stream else BufferedPrettyStream, env=env, - processor=ProcessorManager( - env=env, - groups=args.prettify, - pygments_style=args.style - ), + conversion=Conversion(), + formatting=Formatting(env=env, groups=args.prettify, + color_scheme=args.style), ) else: Stream = partial(EncodedStream, env=env) @@ -140,23 +138,23 @@ class BaseStream(object): self.with_body = with_body self.on_body_chunk_downloaded = on_body_chunk_downloaded - def _get_headers(self): + def get_headers(self): """Return the headers' bytes.""" return self.msg.headers.encode('utf8') - def _iter_body(self): + def iter_body(self): """Return an iterator over the message body.""" raise NotImplementedError() def __iter__(self): """Return an iterator over `self.msg`.""" if self.with_headers: - yield self._get_headers() + yield self.get_headers() yield b'\r\n\r\n' if self.with_body: try: - for chunk in self._iter_body(): + for chunk in self.iter_body(): yield chunk if self.on_body_chunk_downloaded: self.on_body_chunk_downloaded(chunk) @@ -176,7 +174,7 @@ class RawStream(BaseStream): super(RawStream, self).__init__(**kwargs) self.chunk_size = chunk_size - def _iter_body(self): + def iter_body(self): return self.msg.iter_body(self.chunk_size) @@ -204,7 +202,7 @@ class EncodedStream(BaseStream): # Default to utf8 when unsure. self.output_encoding = output_encoding or 'utf8' - def _iter_body(self): + def iter_body(self): for line, lf in self.msg.iter_lines(self.CHUNK_SIZE): @@ -226,25 +224,44 @@ class PrettyStream(EncodedStream): CHUNK_SIZE = 1 - def __init__(self, processor, **kwargs): + def __init__(self, conversion, formatting, **kwargs): super(PrettyStream, self).__init__(**kwargs) - self.processor = processor + self.formatting = formatting + self.conversion = conversion + self.mime = self.msg.content_type.split(';')[0] - def _get_headers(self): - return self.processor.process_headers( + def get_headers(self): + return self.formatting.format_headers( self.msg.headers).encode(self.output_encoding) - def _iter_body(self): - for line, lf in self.msg.iter_lines(self.CHUNK_SIZE): + def iter_body(self): + first_chunk = True + iter_lines = self.msg.iter_lines(self.CHUNK_SIZE) + for line, lf in iter_lines: if b'\0' in line: + if first_chunk: + converter = self.conversion.get_converter(self.mime) + if converter: + body = bytearray() + # noinspection PyAssignmentToLoopOrWithParameter + for line, lf in chain([(line, lf)], iter_lines): + body.extend(line) + body.extend(lf) + self.mime, body = converter.convert(body) + assert isinstance(body, str) + yield self.process_body(body) + return raise BinarySuppressedError() - yield self._process_body(line) + lf + yield self.process_body(line) + lf + first_chunk = False - def _process_body(self, chunk): - return self.processor.process_body( - body=chunk.decode(self.msg.encoding, 'replace'), - mime=self.msg.content_type.split(';')[0] - ).encode(self.output_encoding, 'replace') + def process_body(self, chunk): + if not isinstance(chunk, str): + # Text when a converter has been used, + # otherwise it will always be bytes. + chunk = chunk.decode(self.msg.encoding, 'replace') + chunk = self.formatting.format_body(content=chunk, mime=self.mime) + return chunk.encode(self.output_encoding, 'replace') class BufferedPrettyStream(PrettyStream): @@ -257,14 +274,20 @@ class BufferedPrettyStream(PrettyStream): CHUNK_SIZE = 1024 * 10 - def _iter_body(self): - + def iter_body(self): # Read the whole body before prettifying it, # but bail out immediately if the body is binary. + converter = None body = bytearray() + for chunk in self.msg.iter_body(self.CHUNK_SIZE): - if b'\0' in chunk: - raise BinarySuppressedError() + if not converter and b'\0' in chunk: + converter = self.conversion.get_converter(self.mime) + if not converter: + raise BinarySuppressedError() body.extend(chunk) - yield self._process_body(body) + if converter: + self.mime, body = converter.convert(body) + + yield self.process_body(body) diff --git a/httpie/plugins/__init__.py b/httpie/plugins/__init__.py index f7b1dffa..d822de18 100644 --- a/httpie/plugins/__init__.py +++ b/httpie/plugins/__init__.py @@ -1,9 +1,16 @@ -from .base import AuthPlugin -from .manager import PluginManager -from .builtin import BasicAuthPlugin, DigestAuthPlugin +from httpie.plugins.base import AuthPlugin, FormatterPlugin, ConverterPlugin +from httpie.plugins.manager import PluginManager +from httpie.plugins.builtin import BasicAuthPlugin, DigestAuthPlugin +from httpie.output.formatters.headers import HeadersFormatter +from httpie.output.formatters.json import JSONFormatter +from httpie.output.formatters.xml import XMLFormatter +from httpie.output.formatters.colors import ColorFormatter plugin_manager = PluginManager() -plugin_manager.register(BasicAuthPlugin) -plugin_manager.register(DigestAuthPlugin) - +plugin_manager.register(BasicAuthPlugin, + DigestAuthPlugin) +plugin_manager.register(HeadersFormatter, + JSONFormatter, + XMLFormatter, + ColorFormatter) diff --git a/httpie/plugins/base.py b/httpie/plugins/base.py index d9dafcee..187c874b 100644 --- a/httpie/plugins/base.py +++ b/httpie/plugins/base.py @@ -1,14 +1,4 @@ -class AuthPlugin(object): - """ - Base auth plugin class. - - See for an example auth plugin. - - """ - - # The value that should be passed to --auth-type - # to use this auth plugin. Eg. "my-auth" - auth_type = None +class BasePlugin(object): # The name of the plugin, eg. "My auth". name = None @@ -20,9 +10,64 @@ class AuthPlugin(object): # This be set automatically once the plugin has been loaded. package_name = None + +class AuthPlugin(BasePlugin): + """ + Base auth plugin class. + + See for an example auth plugin. + + """ + # The value that should be passed to --auth-type + # to use this auth plugin. Eg. "my-auth" + auth_type = None + def get_auth(self, username, password): """ Return a ``requests.auth.AuthBase`` subclass instance. """ raise NotImplementedError() + + +class ConverterPlugin(object): + + def __init__(self, mime): + self.mime = mime + + def convert(self, content_bytes): + raise NotImplementedError + + @classmethod + def supports(cls, mime): + raise NotImplementedError + + +class FormatterPlugin(object): + + def __init__(self, **kwargs): + """ + :param env: an class:`Environment` instance + :param kwargs: additional keyword argument that some + processor might require. + + """ + self.enabled = True + self.kwargs = kwargs + + def format_headers(self, headers): + """Return processed `headers` + + :param headers: The headers as text. + + """ + return headers + + def format_body(self, content, mime): + """Return processed `content`. + + :param mime: E.g., 'application/atom+xml'. + :param content: The body content as text + + """ + return content diff --git a/httpie/plugins/builtin.py b/httpie/plugins/builtin.py index 661055f5..08efe660 100644 --- a/httpie/plugins/builtin.py +++ b/httpie/plugins/builtin.py @@ -2,7 +2,7 @@ from base64 import b64encode import requests.auth -from .base import AuthPlugin +from httpie.plugins.base import AuthPlugin class BuiltinAuthPlugin(AuthPlugin): diff --git a/httpie/plugins/manager.py b/httpie/plugins/manager.py index 61b1a384..82bfcb81 100644 --- a/httpie/plugins/manager.py +++ b/httpie/plugins/manager.py @@ -1,8 +1,12 @@ +from itertools import groupby from pkg_resources import iter_entry_points +from httpie.plugins import AuthPlugin, FormatterPlugin, ConverterPlugin ENTRY_POINT_NAMES = [ - 'httpie.plugins.auth.v1' + 'httpie.plugins.auth.v1', + 'httpie.plugins.formatter.v1', + 'httpie.plugins.converter.v1', ] @@ -14,22 +18,41 @@ class PluginManager(object): def __iter__(self): return iter(self._plugins) - def register(self, plugin): - self._plugins.append(plugin) - - def get_auth_plugins(self): - return list(self._plugins) - - def get_auth_plugin_mapping(self): - return dict((plugin.auth_type, plugin) for plugin in self) - - def get_auth_plugin(self, auth_type): - return self.get_auth_plugin_mapping()[auth_type] + def register(self, *plugins): + for plugin in plugins: + self._plugins.append(plugin) def load_installed_plugins(self): - for entry_point_name in ENTRY_POINT_NAMES: for entry_point in iter_entry_points(entry_point_name): plugin = entry_point.load() plugin.package_name = entry_point.dist.key self.register(entry_point.load()) + + # Auth + def get_auth_plugins(self): + return [plugin for plugin in self if issubclass(plugin, AuthPlugin)] + + def get_auth_plugin_mapping(self): + return dict((plugin.auth_type, plugin) + for plugin in self.get_auth_plugins()) + + def get_auth_plugin(self, auth_type): + return self.get_auth_plugin_mapping()[auth_type] + + # Output processing + def get_formatters(self): + return [plugin for plugin in self + if issubclass(plugin, FormatterPlugin)] + + def get_formatters_grouped(self): + groups = {} + for group_name, group in groupby( + self.get_formatters(), + key=lambda p: getattr(p, 'group_name', 'format')): + groups[group_name] = list(group) + return groups + + def get_converters(self): + return [plugin for plugin in self + if issubclass(plugin, ConverterPlugin)] diff --git a/httpie/sessions.py b/httpie/sessions.py index 795b8577..4c42a085 100644 --- a/httpie/sessions.py +++ b/httpie/sessions.py @@ -7,8 +7,8 @@ import os import requests from requests.cookies import RequestsCookieJar, create_cookie -from .compat import urlsplit -from .config import BaseConfigDict, DEFAULT_CONFIG_DIR +from httpie.compat import urlsplit +from httpie.config import BaseConfigDict, DEFAULT_CONFIG_DIR from httpie.plugins import plugin_manager diff --git a/tests/test_output.py b/tests/test_output.py index 7afb17f8..a5ac1048 100644 --- a/tests/test_output.py +++ b/tests/test_output.py @@ -1,7 +1,7 @@ import pytest from httpie import ExitStatus -from httpie.output.processors.colors import get_lexer +from httpie.output.formatters.colors import get_lexer from utils import TestEnvironment, http, httpbin, HTTP_OK, COLOR, CRLF diff --git a/tests/test_unicode.py b/tests/test_unicode.py index fe8743b1..9ea612f0 100644 --- a/tests/test_unicode.py +++ b/tests/test_unicode.py @@ -44,14 +44,14 @@ class TestUnicode: def test_unicode_raw_json_item(self): r = http('--json', 'POST', httpbin('/post'), u'test:={ "%s" : [ "%s" ] }' % (UNICODE, UNICODE)) + assert HTTP_OK in r assert r.json['json'] == {'test': {UNICODE: [UNICODE]}} def test_unicode_raw_json_item_verbose(self): - r = http('--verbose', - '--json', 'POST', httpbin('/post'), + r = http('--json', 'POST', httpbin('/post'), u'test:={ "%s" : [ "%s" ] }' % (UNICODE, UNICODE)) assert HTTP_OK in r - assert UNICODE in r + assert r.json['json'] == {'test': {UNICODE: [UNICODE]}} def test_unicode_url_query_arg_item(self): r = http(httpbin('/get'), u'test==%s' % UNICODE) diff --git a/tests/utils.py b/tests/utils.py index 38103a59..1a9e5eb6 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -158,7 +158,6 @@ def http(*args, **kwargs): stdout.seek(0) stderr.seek(0) output = stdout.read() - try: output = output.decode('utf8') except UnicodeDecodeError: