From 681b652bf91ed35ee8c9a2f0b384c2ec47ff2f7c Mon Sep 17 00:00:00 2001 From: Jakub Roztocil Date: Mon, 16 Jul 2012 23:41:27 +0200 Subject: [PATCH] Allow stdin data with password prompt; added tests Closes #70 --- README.rst | 24 ++++++++++------ httpie/cli.py | 4 +-- httpie/cliparse.py | 69 ++++++++++++++++++++++++++++++++++++---------- tests/tests.py | 10 +++++++ 4 files changed, 82 insertions(+), 25 deletions(-) diff --git a/README.rst b/README.rst index 34e7b89c..b8d01d1e 100644 --- a/README.rst +++ b/README.rst @@ -118,6 +118,7 @@ Flags ^^^^^ Most of the flags mirror the arguments understood by ``requests.request``. See ``http -h`` for more details:: + $ http --help usage: http [-h] [--version] [--json | --form] [--traceback] [--pretty | --ugly] [--print OUTPUT_OPTIONS | --verbose | --headers | --body] @@ -147,13 +148,15 @@ Most of the flags mirror the arguments understood by ``requests.request``. See ` optional arguments: -h, --help show this help message and exit --version show program's version number and exit - --json, -j (default) Data items are serialized as a JSON object. - The Content-Type and Accept headers are set to - application/json (if not set via the command line). - --form, -f Data items are serialized as form fields. The Content- - Type is set to application/x-www-form-urlencoded (if - not specifid). The presence of any file fields results - into a multipart/form-data request. + --json, -j (default) Data items from the command line are + serialized as a JSON object. The Content-Type and + Accept headers are set to application/json (if not + specified). + --form, -f Data items from the command line are serialized as + form fields. The Content-Type is set to application/x + -www-form-urlencoded (if not specified). The presence + of any file fields results into a multipart/form-data + request. --traceback Print exception traceback should one occur. --pretty If stdout is a terminal, the response is prettified by default (colorized and indented if it is JSON). This @@ -180,8 +183,8 @@ Most of the flags mirror the arguments understood by ``requests.request``. See ` make sure that the $TERM environment variable is set to "xterm-256color" or similar (e.g., via `export TERM =xterm-256color' in your ~/.bashrc). - --auth AUTH, -a AUTH username:password. If the password is omitted, HTTPie - will prompt for it. + --auth AUTH, -a AUTH username:password. If the password is omitted (-a + username), HTTPie will prompt for it. --auth-type {basic,digest} The authentication mechanism to be used. Defaults to "basic". @@ -223,7 +226,10 @@ Changelog --------- * `0.2.3dev `_ + * --auth now prompts for a password if none is provided. * Added support for request payloads from a file path with automatic ``Content-Type`` (``http URL @/path``). + * Fixed missing query string when displaing the request headers via ``--verbose``. + * Fixed Content-Type for requests with no data. * `0.2.2 `_ (2012-06-24) * The ``METHOD`` positional argument can now be omitted (defaults to ``GET``, or to ``POST`` with data). * Fixed --verbose --form. diff --git a/httpie/cli.py b/httpie/cli.py index 6f4358dc..4c894494 100644 --- a/httpie/cli.py +++ b/httpie/cli.py @@ -123,10 +123,10 @@ parser.add_argument( # ``requests.request`` keyword arguments. parser.add_argument( - '--auth', '-a', type=cliparse.AuthCredentials(cliparse.SEP_COMMON), + '--auth', '-a', type=cliparse.AuthCredentialsType(cliparse.SEP_COMMON), help=_(''' username:password. - If the password is omitted, HTTPie will prompt for it. + If the password is omitted (-a username), HTTPie will prompt for it. '''), ) diff --git a/httpie/cliparse.py b/httpie/cliparse.py index e2635936..29ac989b 100644 --- a/httpie/cliparse.py +++ b/httpie/cliparse.py @@ -8,9 +8,7 @@ import re import json import argparse import mimetypes - -from collections import namedtuple -from getpass import getpass +import getpass try: from collections import OrderedDict @@ -54,13 +52,22 @@ class Parser(argparse.ArgumentParser): def parse_args(self, args=None, namespace=None, stdin=sys.stdin, stdin_isatty=sys.stdin.isatty()): + args = super(Parser, self).parse_args(args, namespace) + self._validate_output_options(args) self._validate_auth_options(args) self._guess_method(args, stdin_isatty) self._parse_items(args) + if not stdin_isatty: self._body_from_file(args, stdin) + + if args.auth and not args.auth.has_password(): + # stdin has already been read (if not a tty) so + # it's save to prompt now. + args.auth.prompt_password() + return args def _body_from_file(self, args, f): @@ -161,12 +168,24 @@ class ParseError(Exception): pass -KeyValue = namedtuple('KeyValue', ['key', 'value', 'sep', 'orig']) +class KeyValue(object): + """Base key-value pair parsed from CLI.""" + + def __init__(self, key, value, sep, orig): + self.key = key + self.value = value + self.sep = sep + self.orig = orig + + def __eq__(self, other): + return self.__dict__ == other.__dict__ class KeyValueType(object): """A type used with `argparse`.""" + key_value_class = KeyValue + def __init__(self, *separators): self.separators = separators self.escapes = ['\\\\' + sep for sep in separators] @@ -202,21 +221,43 @@ class KeyValueType(object): for sepstr in self.separators: key = key.replace('\\' + sepstr, sepstr) value = value.replace('\\' + sepstr, sepstr) - return KeyValue(key=key, value=value, sep=sep, orig=string) + return self.key_value_class(key=key, value=value, sep=sep, orig=string) -class AuthCredentials(KeyValueType): +class AuthCredentials(KeyValue): + """ + Represents parsed credentials. + + """ + def _getpass(self, prompt): + return getpass.getpass(prompt) + + def has_password(self): + return self.value is not None + + def prompt_password(self): + try: + self.value = self._getpass("Password for user '%s': " % self.key) + except (EOFError, KeyboardInterrupt): + sys.stderr.write('\n') + sys.exit(0) + + +class AuthCredentialsType(KeyValueType): + + key_value_class = AuthCredentials + def __call__(self, string): try: - return super(AuthCredentials, self).__call__(string) + return super(AuthCredentialsType, self).__call__(string) except argparse.ArgumentTypeError: - try: - password = getpass("Password for user '%s': " % string) - except (EOFError, KeyboardInterrupt): - sys.stderr.write('\n') - sys.exit(0) - return KeyValue(key=string, value=password, sep=SEP_COMMON, - orig=string) + # No password provided, will prompt for it later. + return self.key_value_class( + key=string, + value=None, + sep=SEP_COMMON, + orig=string + ) def parse_items(items, data=None, headers=None, files=None): diff --git a/tests/tests.py b/tests/tests.py index 0259086a..4f6d4fac 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -286,6 +286,16 @@ class AuthTest(BaseTestCase): self.assertIn('"authenticated": true', r) self.assertIn('"user": "user"', r) + def test_password_prompt(self): + cliparse.AuthCredentials._getpass = lambda self, prompt: 'password' + + r = http('--auth', 'user', + 'GET', 'httpbin.org/basic-auth/user/password') + + self.assertIn('HTTP/1.1 200', r) + self.assertIn('"authenticated": true', r) + self.assertIn('"user": "user"', r) + ################################################################# # CLI argument parsing related tests.