From f45cc0eec0049029f08f0227d9950dccf035a4d1 Mon Sep 17 00:00:00 2001 From: Jakub Roztocil Date: Thu, 26 Jul 2012 06:37:03 +0200 Subject: [PATCH] Added docstrings, refactored input. --- README.rst | 2 +- httpie/__main__.py | 3 +- httpie/cli.py | 46 ++++++++++----------- httpie/core.py | 68 ++++++++++++++++++-------------- httpie/{cliparse.py => input.py} | 61 +++++++++++++++++----------- httpie/models.py | 6 +++ httpie/output.py | 6 +-- tests/tests.py | 38 +++++++++--------- 8 files changed, 129 insertions(+), 101 deletions(-) rename httpie/{cliparse.py => input.py} (88%) diff --git a/README.rst b/README.rst index 0f6acedd..83907de4 100644 --- a/README.rst +++ b/README.rst @@ -76,7 +76,7 @@ Raw JSON fields (``field:=value``) This item type is needed when ``Content-Type`` is JSON and a field's value is a ``Boolean``, ``Number``, nested ``Object`` or an ``Array``, because simple data items are always serialized as ``String``. - E.g. ``pies:=[1,2,3]``,or ``'meals=["ham", "spam"]'`` (mind the quotes). + E.g. ``pies:=[1,2,3]``, or ``'meals=["ham", "spam"]'`` (mind the quotes). File fields (``field@/path/to/file``) Only available with ``-f`` / ``--form``. Use ``@`` as the separator, e.g., diff --git a/httpie/__main__.py b/httpie/__main__.py index cbda8afd..bed0ec7b 100644 --- a/httpie/__main__.py +++ b/httpie/__main__.py @@ -1,6 +1,5 @@ #!/usr/bin/env python -""" -The main entry point. Invoke as `http' or `python -m httpie'. +"""The main entry point. Invoke as `http' or `python -m httpie'. """ import sys diff --git a/httpie/cli.py b/httpie/cli.py index e14a3144..edb5b6b6 100644 --- a/httpie/cli.py +++ b/httpie/cli.py @@ -1,22 +1,24 @@ -""" -CLI definition. +"""CLI arguments definition. + +NOTE: the CLI interface may change before reaching v1.0. """ from . import __doc__ from . import __version__ -from . import cliparse from .output import AVAILABLE_STYLES +from .input import (Parser, AuthCredentialsArgType, KeyValueArgType, + PRETTIFY_STDOUT_TTY_ONLY, + SEP_PROXY, SEP_CREDENTIALS, SEP_GROUP_ITEMS, + OUT_REQ_HEAD, OUT_REQ_BODY, OUT_RESP_HEAD, + OUT_RESP_BODY, OUTPUT_OPTIONS) def _(text): - """Normalize white space.""" + """Normalize whitespace.""" return ' '.join(text.strip().split()) -desc = '%s ' -parser = cliparse.Parser( - description=desc % __doc__.strip(), -) +parser = Parser(description='%s ' % __doc__.strip()) parser.add_argument('--version', action='version', version=__version__) @@ -58,7 +60,7 @@ parser.add_argument( prettify = parser.add_mutually_exclusive_group(required=False) prettify.add_argument( '--pretty', dest='prettify', action='store_true', - default=cliparse.PRETTIFY_STDOUT_TTY_ONLY, + default=PRETTIFY_STDOUT_TTY_ONLY, help=_(''' If stdout is a terminal, the response is prettified by default (colorized and indented if it is JSON). @@ -85,35 +87,35 @@ output_options.add_argument('--print', '-p', dest='output_options', If the output is piped to another program or to a file, then only the body is printed by default. '''.format( - request_headers=cliparse.OUT_REQ_HEAD, - request_body=cliparse.OUT_REQ_BODY, - response_headers=cliparse.OUT_RESP_HEAD, - response_body=cliparse.OUT_RESP_BODY, + request_headers=OUT_REQ_HEAD, + request_body=OUT_REQ_BODY, + response_headers=OUT_RESP_HEAD, + response_body=OUT_RESP_BODY, )) ) output_options.add_argument( '--verbose', '-v', dest='output_options', - action='store_const', const=''.join(cliparse.OUTPUT_OPTIONS), + action='store_const', const=''.join(OUTPUT_OPTIONS), help=_(''' Print the whole request as well as the response. Shortcut for --print={0}. - '''.format(''.join(cliparse.OUTPUT_OPTIONS))) + '''.format(''.join(OUTPUT_OPTIONS))) ) output_options.add_argument( '--headers', '-h', dest='output_options', - action='store_const', const=cliparse.OUT_RESP_HEAD, + action='store_const', const=OUT_RESP_HEAD, help=_(''' Print only the response headers. Shortcut for --print={0}. - '''.format(cliparse.OUT_RESP_HEAD)) + '''.format(OUT_RESP_HEAD)) ) output_options.add_argument( '--body', '-b', dest='output_options', - action='store_const', const=cliparse.OUT_RESP_BODY, + action='store_const', const=OUT_RESP_BODY, help=_(''' Print only the response body. Shortcut for --print={0}. - '''.format(cliparse.OUT_RESP_BODY)) + '''.format(OUT_RESP_BODY)) ) parser.add_argument( @@ -149,7 +151,7 @@ parser.add_argument( # ``requests.request`` keyword arguments. parser.add_argument( '--auth', '-a', - type=cliparse.AuthCredentialsArgType(cliparse.SEP_CREDENTIALS), + type=AuthCredentialsArgType(SEP_CREDENTIALS), help=_(''' username:password. If only the username is provided (-a username), @@ -177,7 +179,7 @@ parser.add_argument( ) parser.add_argument( '--proxy', default=[], action='append', - type=cliparse.KeyValueArgType(cliparse.SEP_PROXY), + type=KeyValueArgType(SEP_PROXY), help=_(''' String mapping protocol to the URL of the proxy (e.g. http:foo.bar:3128). @@ -224,7 +226,7 @@ parser.add_argument( parser.add_argument( 'items', nargs='*', metavar='ITEM', - type=cliparse.KeyValueArgType(*cliparse.SEP_GROUP_ITEMS), + type=KeyValueArgType(*SEP_GROUP_ITEMS), help=_(''' A key-value pair whose type is defined by the separator used. It can be an HTTP header (header:value), diff --git a/httpie/core.py b/httpie/core.py index 83ac94d1..abf9350f 100644 --- a/httpie/core.py +++ b/httpie/core.py @@ -1,3 +1,13 @@ +"""This module provides the main functionality of HTTPie. + +Invocation flow: + + 1. Read, validate and process the input (args, `stdin`). + 2. Create a request and send it, get the response. + 3. Process and format the requested parts of the request-response exchange. + 4. Write to `stdout` and exit. + +""" import sys import json @@ -7,8 +17,10 @@ from requests.compat import str from .models import HTTPMessage, Environment from .output import OutputProcessor -from . import cliparse -from . import cli +from .input import (PRETTIFY_STDOUT_TTY_ONLY, + OUT_REQ_BODY, OUT_REQ_HEAD, + OUT_RESP_HEAD, OUT_RESP_BODY) +from .cli import parser TYPE_FORM = 'application/x-www-form-urlencoded; charset=utf-8' @@ -16,6 +28,7 @@ TYPE_JSON = 'application/json; charset=utf-8' def get_response(args, env): + """Send the request and return a `request.Response`.""" auto_json = args.data and not args.form if args.json or auto_json: @@ -69,23 +82,20 @@ def get_response(args, env): sys.exit(1) -def get_output(args, env, response): +def get_output(args, env, request, response): + """Format parts of the `request`-`response` exchange + according to `args` and `env` and return a `unicode`. - do_prettify = ( - args.prettify is True or - (args.prettify == cliparse.PRETTIFY_STDOUT_TTY_ONLY - and env.stdout_isatty) - ) + """ + do_prettify = (args.prettify is True + or (args.prettify == PRETTIFY_STDOUT_TTY_ONLY + and env.stdout_isatty)) - do_output_request = ( - cliparse.OUT_REQ_HEAD in args.output_options - or cliparse.OUT_REQ_BODY in args.output_options - ) + do_output_request = (OUT_REQ_HEAD in args.output_options + or OUT_REQ_BODY in args.output_options) - do_output_response = ( - cliparse.OUT_RESP_HEAD in args.output_options - or cliparse.OUT_RESP_BODY in args.output_options - ) + do_output_response = (OUT_RESP_HEAD in args.output_options + or OUT_RESP_BODY in args.output_options) prettifier = None if do_prettify: @@ -95,10 +105,11 @@ def get_output(args, env, response): buf = [] if do_output_request: - req = HTTPMessage.from_request(response.request).format( + req_msg = HTTPMessage.from_request(request) + req = req_msg.format( prettifier=prettifier, - with_headers=cliparse.OUT_REQ_HEAD in args.output_options, - with_body=cliparse.OUT_REQ_BODY in args.output_options + with_headers=OUT_REQ_HEAD in args.output_options, + with_body=OUT_REQ_BODY in args.output_options ) buf.append(req) buf.append('\n') @@ -106,10 +117,11 @@ def get_output(args, env, response): buf.append('\n') if do_output_response: - resp = HTTPMessage.from_response(response).format( + resp_msg = HTTPMessage.from_response(response) + resp = resp_msg.format( prettifier=prettifier, - with_headers=cliparse.OUT_RESP_HEAD in args.output_options, - with_body=cliparse.OUT_RESP_BODY in args.output_options + with_headers=OUT_RESP_HEAD in args.output_options, + with_body=OUT_RESP_BODY in args.output_options ) buf.append(resp) buf.append('\n') @@ -118,10 +130,7 @@ def get_output(args, env, response): def get_exist_status(code, allow_redirects=False): - """ - Translate HTTP status code to exit status. - - """ + """Translate HTTP status code to exit status.""" if 300 <= code <= 399 and not allow_redirects: # Redirect return 3 @@ -136,13 +145,12 @@ def get_exist_status(code, allow_redirects=False): def main(args=sys.argv[1:], env=Environment()): - """ - Run the main program and write the output to ``env.stdout``. + """Run the main program and write the output to ``env.stdout``. Return exit status. """ - args = cli.parser.parse_args(args=args, env=env) + args = parser.parse_args(args=args, env=env) response = get_response(args, env) status = 0 @@ -155,7 +163,7 @@ def main(args=sys.argv[1:], env=Environment()): response.raw.status, response.raw.reason) env.stderr.write(err.encode('utf8')) - output = get_output(args, env, response) + output = get_output(args, env, response.request, response) output_bytes = output.encode('utf8') f = getattr(env.stdout, 'buffer', env.stdout) f.write(output_bytes) diff --git a/httpie/cliparse.py b/httpie/input.py similarity index 88% rename from httpie/cliparse.py rename to httpie/input.py index 7f830faf..ed31c32d 100644 --- a/httpie/cliparse.py +++ b/httpie/input.py @@ -1,5 +1,4 @@ -""" -CLI argument parsing logic. +"""Parsing and processing of CLI input (args, auth credentials, files, stdin). """ import os @@ -73,6 +72,12 @@ DEFAULT_UA = 'HTTPie/%s' % __version__ class Parser(argparse.ArgumentParser): + """Adds additional logic to `argparse.ArgumentParser`. + + Handles all input (CLI args, file args, stdin), applies defaults, + and performs extra validation. + + """ def __init__(self, *args, **kwargs): kwargs['add_help'] = False @@ -101,14 +106,18 @@ class Parser(argparse.ArgumentParser): return args def _body_from_file(self, args, f): + """Use the content of `f` as the `request.data`. + + There can only be one source of request data. + + """ if args.data: self.error('Request body (from stdin or a file) and request ' 'data (key=value) cannot be mixed.') args.data = f.read() def _guess_method(self, args, env): - """ - Set `args.method` if not specified to either POST or GET + """Set `args.method` if not specified to either POST or GET based on whether the request has data or not. """ @@ -143,9 +152,8 @@ class Parser(argparse.ArgumentParser): args.method = HTTP_POST if has_data else HTTP_GET def _parse_items(self, args): - """ - Parse `args.items` into `args.headers`, - `args.data`, `args.`, and `args.files`. + """Parse `args.items` into `args.headers`, `args.data`, + `args.`, and `args.files`. """ args.headers = CaseInsensitiveDict() @@ -191,6 +199,11 @@ class Parser(argparse.ArgumentParser): args.headers['Content-Type'] = content_type def _process_output_options(self, args, env): + """Apply defaults to output options or validate the provided ones. + + The default output options are stdout-type-sensitive. + + """ if not args.output_options: args.output_options = (OUTPUT_OPTIONS_DEFAULT if env.stdout_isatty else OUTPUT_OPTIONS_DEFAULT_STDOUT_REDIRECTED) @@ -218,8 +231,7 @@ class KeyValue(object): class KeyValueArgType(object): - """ - A key-value pair argument type used with `argparse`. + """A key-value pair argument type used with `argparse`. Parses a key-value arg and constructs a `KeyValue` instance. Used for headers, form data, and other key-value pair types. @@ -232,8 +244,7 @@ class KeyValueArgType(object): self.separators = separators def __call__(self, string): - """ - Parse `string` and return `self.key_value_class()` instance. + """Parse `string` and return `self.key_value_class()` instance. The best of `self.separators` is determined (first found, longest). Back slash escaped characters aren't considered as separators @@ -243,12 +254,14 @@ class KeyValueArgType(object): """ class Escaped(str): - pass + """Represents an escaped character.""" def tokenize(s): - """ - r'foo\=bar\\baz' - => ['foo', Escaped('='), 'bar', Escaped('\'), 'baz'] + """Tokenize `s`. There are only two token types - strings + and escaped characters: + + >>> tokenize(r'foo\=bar\\baz') + ['foo', Escaped('='), 'bar', Escaped('\\'), 'baz'] """ tokens = [''] @@ -305,10 +318,8 @@ class KeyValueArgType(object): class AuthCredentials(KeyValue): - """ - Represents parsed credentials. + """Represents parsed credentials.""" - """ def _getpass(self, prompt): # To allow mocking. return getpass.getpass(prompt) @@ -325,10 +336,16 @@ class AuthCredentials(KeyValue): class AuthCredentialsArgType(KeyValueArgType): + """A key-value arg type that parses credentials.""" key_value_class = AuthCredentials def __call__(self, string): + """Parse credentials from `string`. + + ("username" or "username:password"). + + """ try: return super(AuthCredentialsArgType, self).__call__(string) except argparse.ArgumentTypeError: @@ -342,11 +359,11 @@ class AuthCredentialsArgType(KeyValueArgType): class ParamDict(OrderedDict): + """Multi-value dict for URL parameters and form data.""" #noinspection PyMethodOverriding def __setitem__(self, key, value): - """ - If `key` is assigned more than once, `self[key]` holds a + """ If `key` is assigned more than once, `self[key]` holds a `list` of all the values. This allows having multiple fields with the same name in form @@ -365,12 +382,10 @@ class ParamDict(OrderedDict): def parse_items(items, data=None, headers=None, files=None, params=None): - """ - Parse `KeyValue` `items` into `data`, `headers`, `files`, + """Parse `KeyValue` `items` into `data`, `headers`, `files`, and `params`. """ - if headers is None: headers = CaseInsensitiveDict() if data is None: diff --git a/httpie/models.py b/httpie/models.py index d8e02e5f..49cbd749 100644 --- a/httpie/models.py +++ b/httpie/models.py @@ -4,6 +4,12 @@ from requests.compat import urlparse, is_windows class Environment(object): + """Holds information about the execution context. + + Groups various aspects of the environment in a changeable object + and allows for mocking. + + """ stdin_isatty = sys.stdin.isatty() stdin = sys.stdin diff --git a/httpie/output.py b/httpie/output.py index 08827468..23c630d8 100644 --- a/httpie/output.py +++ b/httpie/output.py @@ -1,5 +1,4 @@ -""" -Colorizing of HTTP messages and content processing. +"""Output processing and formatting. """ import re @@ -22,8 +21,7 @@ AVAILABLE_STYLES = [DEFAULT_STYLE] + list(STYLE_MAP.keys()) class HTTPLexer(lexer.RegexLexer): - """ - Simplified HTTP lexer for Pygments. + """Simplified HTTP lexer for Pygments. It only operates on headers and provides a stronger contrast between their names and values than the original one bundled with Pygments diff --git a/tests/tests.py b/tests/tests.py index 029ed5cd..1edfb70e 100755 --- a/tests/tests.py +++ b/tests/tests.py @@ -37,7 +37,7 @@ from requests.compat import is_py26, is_py3, str TESTS_ROOT = os.path.dirname(__file__) sys.path.insert(0, os.path.realpath(os.path.join(TESTS_ROOT, '..'))) -from httpie import cliparse +from httpie import input from httpie.models import Environment from httpie.core import main, get_output @@ -506,7 +506,7 @@ class VerboseFlagTest(BaseTestCase): class MultipartFormDataFileUploadTest(BaseTestCase): def test_non_existent_file_raises_parse_error(self): - self.assertRaises(cliparse.ParseError, http, + self.assertRaises(input.ParseError, http, '--form', '--traceback', 'POST', @@ -595,7 +595,7 @@ class AuthTest(BaseTestCase): def test_password_prompt(self): - cliparse.AuthCredentials._getpass = lambda self, prompt: 'password' + input.AuthCredentials._getpass = lambda self, prompt: 'password' r = http( '--auth', @@ -681,12 +681,12 @@ class ExitStatusTest(BaseTestCase): class ItemParsingTest(BaseTestCase): def setUp(self): - self.key_value_type = cliparse.KeyValueArgType( - cliparse.SEP_HEADERS, - cliparse.SEP_QUERY, - cliparse.SEP_DATA, - cliparse.SEP_DATA_RAW_JSON, - cliparse.SEP_FILES, + self.key_value_type = input.KeyValueArgType( + input.SEP_HEADERS, + input.SEP_QUERY, + input.SEP_DATA, + input.SEP_DATA_RAW_JSON, + input.SEP_FILES, ) def test_invalid_items(self): @@ -696,7 +696,7 @@ class ItemParsingTest(BaseTestCase): lambda: self.key_value_type(item)) def test_escape(self): - headers, data, files, params = cliparse.parse_items([ + headers, data, files, params = input.parse_items([ # headers self.key_value_type('foo\\:bar:baz'), self.key_value_type('jack\\@jill:hill'), @@ -715,7 +715,7 @@ class ItemParsingTest(BaseTestCase): self.assertIn('bar@baz', files) def test_escape_longsep(self): - headers, data, files, params = cliparse.parse_items([ + headers, data, files, params = input.parse_items([ self.key_value_type('bob\\:==foo'), ]) self.assertDictEqual(params, { @@ -723,7 +723,7 @@ class ItemParsingTest(BaseTestCase): }) def test_valid_items(self): - headers, data, files, params = cliparse.parse_items([ + headers, data, files, params = input.parse_items([ self.key_value_type('string=value'), self.key_value_type('header:value'), self.key_value_type('list:=["a", 1, {}, false]'), @@ -754,7 +754,7 @@ class ItemParsingTest(BaseTestCase): class ArgumentParserTestCase(unittest.TestCase): def setUp(self): - self.parser = cliparse.Parser() + self.parser = input.Parser() def test_guess_when_method_set_and_valid(self): args = argparse.Namespace() @@ -795,7 +795,7 @@ class ArgumentParserTestCase(unittest.TestCase): self.assertEquals(args.url, 'http://example.com/') self.assertEquals( args.items, - [cliparse.KeyValue( + [input.KeyValue( key='data', value='field', sep='=', orig='data=field')]) def test_guess_when_method_set_but_invalid_and_header_field(self): @@ -813,7 +813,7 @@ class ArgumentParserTestCase(unittest.TestCase): self.assertEquals(args.url, 'http://example.com/') self.assertEquals( args.items, - [cliparse.KeyValue( + [input.KeyValue( key='test', value='header', sep=':', orig='test:header')]) def test_guess_when_method_set_but_invalid_and_item_exists(self): @@ -821,16 +821,16 @@ class ArgumentParserTestCase(unittest.TestCase): args.method = 'http://example.com/' args.url = 'new_item=a' args.items = [ - cliparse.KeyValue( + input.KeyValue( key='old_item', value='b', sep='=', orig='old_item=b') ] self.parser._guess_method(args, Environment()) self.assertEquals(args.items, [ - cliparse.KeyValue( + input.KeyValue( key='new_item', value='a', sep='=', orig='new_item=a'), - cliparse.KeyValue(key + input.KeyValue(key ='old_item', value='b', sep='=', orig='old_item=b'), ]) @@ -879,7 +879,7 @@ class UnicodeOutputTestCase(BaseTestCase): args.style = 'default' # colorized output contains escape sequences - output = get_output(args, Environment(), response) + output = get_output(args, Environment(), response.request, response) for key, value in response_dict.items(): self.assertIn(key, output)