Compare commits

..

3 Commits
1.0.1 ... 0.9.2

Author SHA1 Message Date
80a2c8266c 0.9.2 2015-02-24 07:49:24 +01:00
bc742b62cf Use absolute links to LICENCE, etc. 2015-02-24 07:47:25 +01:00
43ce0b6fa1 Don't depend on requests.compat
#314
2015-02-24 07:39:26 +01:00
62 changed files with 1576 additions and 3244 deletions

View File

@ -1,17 +0,0 @@
# http://editorconfig.org
root = true
[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.yml]
indent_size = 2
[Makefile]
indent_style = tab
indent_size = 8

18
.gitignore vendored
View File

@ -1,13 +1,11 @@
.DS_Store dist
.idea/ httpie.egg-info
__pycache__/ build
dist/
build/
*.egg-info
.cache/
.tox/
.coverage
*.pyc *.pyc
*.egg *.egg
.tox
README.html
.coverage
htmlcov htmlcov
.pytest_cache/ .idea
.DS_Store

View File

@ -1,96 +1,20 @@
# <https://travis-ci.org/jakubroztocil/httpie> # https://travis-ci.org/jakubroztocil/httpie
sudo: false sudo: false
language: python
os: os:
- linux - linux
- osx
env: env:
global: global:
- NEWEST_PYTHON=3.7 - NEWEST_PYTHON=3.4
language: python
python: python:
# <https://docs.travis-ci.com/user/languages/python/> - 2.6
- 2.7 - 2.7
# Python 3.4 fails installing packages
# <https://travis-ci.org/jakubroztocil/httpie/jobs/403263566#L636>
# - 3.4
- 3.5
- 3.6
# - 3.7 # is done in the matrix below as described in travis-ci/travis-ci#9069
- pypy - pypy
- 3.3
# pypy3 currently fails because of a Flask issue - 3.4
# - pypy3 - pypy3
cache: pip
matrix:
include:
# Add manually defined OS X builds
# <https://docs.travis-ci.com/user/multi-os/#Python-example-(unsupported-languages)>
- os: osx
language: generic
env:
# Stock OSX Python
- TOXENV=py27-osx-builtin
- BREW_PYTHON_PACKAGE=
- os: osx
language: generic
env:
# Latest Python 2.7 from Homebrew
- TOXENV=py27
- BREW_PYTHON_PACKAGE=python@2
- os: osx
language: generic
env:
# Latest Python 3.x from Homebrew
- TOXENV=py37 # <= needs to be kept up-to-date to reflect latest minor version
- BREW_PYTHON_PACKAGE=python@3
# Travis Python 3.7 must run sudo on
- os: linux
python: 3.7
env: TOXENV=py37
sudo: true # Required for Python 3.7
dist: xenial # Required for Python 3.7
# Add a codestyle-only build
- os: linux
python: 3.6
env: CODESTYLE_ONLY=true
install:
- |
if [[ $TRAVIS_OS_NAME == 'osx' ]]; then
if [[ -n "$BREW_PYTHON_PACKAGE" ]]; then
brew update
if ! brew list --versions "$BREW_PYTHON_PACKAGE" >/dev/null; then
brew install "$BREW_PYTHON_PACKAGE"
elif ! brew outdated "$BREW_PYTHON_PACKAGE"; then
brew upgrade "$BREW_PYTHON_PACKAGE"
fi
fi
sudo pip2 install tox
fi
script: script:
- | - make
if [[ $TRAVIS_OS_NAME == 'linux' ]]; then
if [[ $CODESTYLE_ONLY ]]; then
make pycodestyle
else
make test
fi
else
PATH="/usr/local/bin:$PATH" tox -e "$TOXENV"
fi
after_success: after_success:
- | - if [[ $TRAVIS_PYTHON_VERSION == $NEWEST_PYTHON ]]; then pip install python-coveralls && coveralls; fi
if [[ $TRAVIS_PYTHON_VERSION == $NEWEST_PYTHON && $TRAVIS_OS_NAME == 'linux' ]]; then
make coveralls
fi
notifications:
webhooks:
# options: [always|never|change] default: always
on_success: always
on_failure: always
on_start: always
urls:
# https://gitter.im/jkbrzt/httpie
- https://webhooks.gitter.im/e/c42fcd359a110d02830b

View File

@ -8,8 +8,6 @@ HTTPie authors
Patches and ideas Patches and ideas
----------------- -----------------
`Complete list of contributors on GitHub <https://github.com/jakubroztocil/httpie/graphs/contributors>`_
* `Cláudia T. Delgado <https://github.com/claudiatd>`_ (logo) * `Cláudia T. Delgado <https://github.com/claudiatd>`_ (logo)
* `Hank Gay <https://github.com/gthank>`_ * `Hank Gay <https://github.com/gthank>`_
* `Jake Basile <https://github.com/jakebasile>`_ * `Jake Basile <https://github.com/jakebasile>`_
@ -34,7 +32,3 @@ Patches and ideas
* `Nathan LaFreniere <https://github.com/nlf>`_ * `Nathan LaFreniere <https://github.com/nlf>`_
* `Matthias Lehmann <https://github.com/matleh>`_ * `Matthias Lehmann <https://github.com/matleh>`_
* `Dennis Brakhane <https://github.com/brakhane>`_ * `Dennis Brakhane <https://github.com/brakhane>`_
* `Matt Layman <https://github.com/mblayman>`_
* `Edward Yang <https://github.com/honorabrutroll>`_

View File

@ -6,101 +6,9 @@ This document records all notable changes to `HTTPie <http://httpie.org>`_.
This project adheres to `Semantic Versioning <http://semver.org/>`_. This project adheres to `Semantic Versioning <http://semver.org/>`_.
`1.0.1`_ (2018-11-14) `1.0.0-dev`_ (Unreleased)
------------------------- -------------------------
* Removed external URL calls from tests.
`1.0.0`_ (2018-11-02)
-------------------------
* Added ``--style=auto`` which follows the terminal ANSI color styles.
* Added support for selecting TLS 1.3 via ``--ssl=tls1.3``
(available once implemented in upstream libraries).
* Added ``true``/``false`` as valid values for ``--verify``
(in addition to ``yes``/``no``) and the boolean value is case-insensitive.
* Changed the default ``--style`` from ``solarized`` to ``auto`` (on Windows it stays ``fruity``).
* Fixed default headers being incorrectly case-sensitive.
* Removed Python 2.6 support.
`0.9.9`_ (2016-12-08)
---------------------
* Fixed README.
`0.9.8`_ (2016-12-08)
---------------------
* Extended auth plugin API.
* Added exit status code ``7`` for plugin errors.
* Added support for ``curses``-less Python installations.
* Fixed ``REQUEST_ITEM`` arg incorrectly being reported as required.
* Improved ``CTRL-C`` interrupt handling.
* Added the standard exit status code ``130`` for keyboard interrupts.
`0.9.6`_ (2016-08-13)
---------------------
* Added Python 3 as a dependency for Homebrew installations
to ensure some of the newer HTTP features work out of the box
for macOS users (starting with HTTPie 0.9.4.).
* Added the ability to unset a request header with ``Header:``, and send an
empty value with ``Header;``.
* Added ``--default-scheme <URL_SCHEME>`` to enable things like
``$ alias https='http --default-scheme=https``.
* Added ``-I`` as a shortcut for ``--ignore-stdin``.
* Added fish shell completion (located in ``extras/httpie-completion.fish``
in the Github repo).
* Updated ``requests`` to 2.10.0 so that SOCKS support can be added via
``pip install requests[socks]``.
* Changed the default JSON ``Accept`` header from ``application/json``
to ``application/json, */*``.
* Changed the pre-processing of request HTTP headers so that any leading
and trailing whitespace is removed.
`0.9.4`_ (2016-07-01)
---------------------
* Added ``Content-Type`` of files uploaded in ``multipart/form-data`` requests
* Added ``--ssl=<PROTOCOL>`` to specify the desired SSL/TLS protocol version
to use for HTTPS requests.
* Added JSON detection with ``--json, -j`` to work around incorrect
``Content-Type``
* Added ``--all`` to show intermediate responses such as redirects (with ``--follow``)
* Added ``--history-print, -P WHAT`` to specify formatting of intermediate responses
* Added ``--max-redirects=N`` (default 30)
* Added ``-A`` as short name for ``--auth-type``
* Added ``-F`` as short name for ``--follow``
* Removed the ``implicit_content_type`` config option
(use ``"default_options": ["--form"]`` instead)
* Redirected ``stdout`` doesn't trigger an error anymore when ``--output FILE``
is set
* Changed the default ``--style`` back to ``solarized`` for better support
of light and dark terminals
* Improved ``--debug`` output
* Fixed ``--session`` when used with ``--download``
* Fixed ``--download`` to trim too long filenames before saving the file
* Fixed the handling of ``Content-Type`` with multiple ``+subtype`` parts
* Removed the XML formatter as the implementation suffered from multiple issues
`0.9.3`_ (2016-01-01)
---------------------
* Changed the default color ``--style`` from ``solarized`` to ``monokai``
* Added basic Bash autocomplete support (need to be installed manually)
* Added request details to connection error messages
* Fixed ``'requests.packages.urllib3' has no attribute 'disable_warnings'``
errors that occurred in some installations
* Fixed colors and formatting on Windows
* Fixed ``--auth`` prompt on Windows
`0.9.2`_ (2015-02-24) `0.9.2`_ (2015-02-24)
@ -115,8 +23,8 @@ This project adheres to `Semantic Versioning <http://semver.org/>`_.
--------------------- ---------------------
* Added support for Requests transport adapter plugins * Added support for Requests transport adapter plugins
(see `httpie-unixsocket <https://github.com/httpie/httpie-unixsocket>`_ (see `httpie-unixsocket <https://github.com/msabramo/httpie-unixsocket>`_
and `httpie-http2 <https://github.com/httpie/httpie-http2>`_) and `httpie-http2 <https://github.com/jakubroztocil/httpie-http2>`_)
`0.9.0`_ (2015-01-31) `0.9.0`_ (2015-01-31)
@ -341,10 +249,4 @@ This project adheres to `Semantic Versioning <http://semver.org/>`_.
.. _0.9.0: https://github.com/jakubroztocil/httpie/compare/0.8.0...0.9.0 .. _0.9.0: https://github.com/jakubroztocil/httpie/compare/0.8.0...0.9.0
.. _0.9.1: https://github.com/jakubroztocil/httpie/compare/0.9.0...0.9.1 .. _0.9.1: https://github.com/jakubroztocil/httpie/compare/0.9.0...0.9.1
.. _0.9.2: https://github.com/jakubroztocil/httpie/compare/0.9.1...0.9.2 .. _0.9.2: https://github.com/jakubroztocil/httpie/compare/0.9.1...0.9.2
.. _0.9.3: https://github.com/jakubroztocil/httpie/compare/0.9.2...0.9.3 .. _1.0.0-dev: https://github.com/jakubroztocil/httpie/compare/0.9.2...master
.. _0.9.4: https://github.com/jakubroztocil/httpie/compare/0.9.3...0.9.4
.. _0.9.6: https://github.com/jakubroztocil/httpie/compare/0.9.4...0.9.6
.. _0.9.8: https://github.com/jakubroztocil/httpie/compare/0.9.6...0.9.8
.. _0.9.9: https://github.com/jakubroztocil/httpie/compare/0.9.8...0.9.9
.. _1.0.0: https://github.com/jakubroztocil/httpie/compare/0.9.9...1.0.0
.. _1.0.1: https://github.com/jakubroztocil/httpie/compare/1.0.0...1.0.1

View File

@ -1,13 +1,12 @@
######################
Contributing to HTTPie Contributing to HTTPie
###################### ######################
Bug reports and code and documentation patches are welcome. You can Bug reports and code and documentation patches are greatly appretiated. You can
help this project also by using the development version of HTTPie also help by using the development version of HTTPie and reporting any bugs you
and by reporting any bugs you might encounter. might encounter.
1. Reporting bugs Bug Reports
================= ===========
**It's important that you provide the full command argument list **It's important that you provide the full command argument list
as well as the output of the failing command.** as well as the output of the failing command.**
@ -16,24 +15,21 @@ to your bug report, e.g.:
.. code-block:: bash .. code-block:: bash
$ http --debug [COMPLETE ARGUMENT LIST THAT TRIGGERS THE ERROR] $ http --debug [arguments that trigger the error]
[COMPLETE OUTPUT] [complete output]
2. Contributing Code and Docs Contributing Code and Documentation
============================= ===================================
Before working on a new feature or a bug, please browse `existing issues`_ Before working on a new feature or a bug, please browse `existing issues`_
to see whether it has been previously discussed. If the change in question to see whether it has been previously discussed. If the change in question
is a bigger one, it's always good to discuss before you start working on is a bigger one, it's always good to discuss before your starting working on
it. it.
Creating Development Environment Development Environment
-------------------------------- -----------------------
Go to https://github.com/jakubroztocil/httpie and fork the project repository.
.. code-block:: bash .. code-block:: bash
@ -47,71 +43,53 @@ Go to https://github.com/jakubroztocil/httpie and fork the project repository.
# Install dev. requirements and also HTTPie (in editable mode # Install dev. requirements and also HTTPie (in editable mode
# so that the `http' command will point to your working copy): # so that the `http' command will point to your working copy):
make init make
Making Changes Making Changes
-------------- --------------
Please make sure your changes conform to `Style Guide for Python Code`_ (PEP8) Please make sure your changes conform to `Style Guide for Python Code`_ (PEP8).
and that ``make pycodestyle`` passes.
Testing Tests
-------
Before opening a pull requests, please make sure the `test suite`_ passes
in all of the `supported Python environments`_. You should also add tests
for any new features and bug fixes.
HTTPie uses `pytest`_ and `Tox`_ for testing.
Running all tests:
******************
.. code-block:: bash
# Run all tests on the current Python interpreter with coverage
make test
# Run all tests in all of the supported and available Pythons via Tox
make test-tox
# Run all tests for code as well as packaging, etc.
make test-all
# Test PEP8 compliance
make pycodestyle
Running specific tests:
***********************
.. code-block:: bash
# Run specific tests on the current Python
py.test tests/test_uploads.py
py.test tests/test_uploads.py::TestMultipartFormDataFileUpload
py.test tests/test_uploads.py::TestMultipartFormDataFileUpload::test_upload_ok
# Run specific tests on the on all Pythons via Tox
# (change to `tox -e py37' to limit Python version)
tox -- tests/test_uploads.py --verbose
tox -- tests/test_uploads.py::TestMultipartFormDataFileUpload --verbose
tox -- tests/test_uploads.py::TestMultipartFormDataFileUpload::test_upload_ok --verbose
----- -----
See `Makefile`_ for additional development utilities. Before opening a pull requests, please make sure the `test suite`_ passes
Don't forget to add yourself to `AUTHORS`_! in all of the `supported Python environments`_. You should also **add tests
for any new features and bug fixes**.
HTTPie uses `pytest`_ and `Tox`_.
.. code-block:: bash
### Running all tests:
# Current Python
make test
# Current Python with coverage
make test-cover
# All the supported and available Pythons via Tox
make test-tox
### Running specific tests:
# Current Python
pytest tests/test_uploads.py
# All Pythons
tox -- tests/test_uploads.py --verbose
Don't forget to add yourself to `AUTHORS.rst`_.
.. _Tox: http://tox.testrun.org .. _Tox: http://tox.testrun.org
.. _supported Python environments: https://github.com/jakubroztocil/httpie/blob/master/tox.ini .. _supported Python environments: https://github.com/jakubroztocil/httpie/blob/master/tox.ini
.. _existing issues: https://github.com/jakubroztocil/httpie/issues?state=open .. _existing issues: https://github.com/jakubroztocil/httpie/issues?state=open
.. _AUTHORS: https://github.com/jakubroztocil/httpie/blob/master/AUTHORS.rst .. _AUTHORS.rst: https://github.com/jakubroztocil/httpie/blob/master/AUTHORS.rst
.. _Makefile: https://github.com/jakubroztocil/httpie/blob/master/Makefile
.. _pytest: http://pytest.org/ .. _pytest: http://pytest.org/
.. _Style Guide for Python Code: http://python.org/dev/peps/pep-0008/ .. _Style Guide for Python Code: http://python.org/dev/peps/pep-0008/
.. _test suite: https://github.com/jakubroztocil/httpie/tree/master/tests .. _test suite: https://github.com/jakubroztocil/httpie/tree/master/tests

View File

@ -1,4 +1,4 @@
Copyright © 2012-2017 Jakub Roztocil <jakub@roztocil.co> Copyright © 2012 Jakub Roztocil <jakub@roztocil.co>
Redistribution and use in source and binary forms, with or without Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met: modification, are permitted provided that the following conditions are met:

104
Makefile
View File

@ -1,57 +1,38 @@
###############################################################################
# See ./CONTRIBUTING.rst
###############################################################################
VERSION=$(shell grep __version__ httpie/__init__.py) VERSION=$(shell grep __version__ httpie/__init__.py)
REQUIREMENTS="requirements-dev.txt" REQUIREMENTS="requirements-dev.txt"
TAG="\n\n\033[0;32m\#\#\# " TAG="\n\n\033[0;32m\#\#\# "
END=" \#\#\# \033[0m\n" END=" \#\#\# \033[0m\n"
all: test all: test
uninstall-httpie:
@echo $(TAG)Removing existing installation of HTTPie$(END)
- pip uninstall --yes httpie >/dev/null
! which http
@echo
uninstall-all: uninstall-httpie
- pip uninstall --yes -r $(REQUIREMENTS)
init: uninstall-httpie init: uninstall-httpie
@echo $(TAG)Installing dev requirements$(END) @echo $(TAG)Installing dev requirements$(END)
pip install --upgrade -r $(REQUIREMENTS) pip install --upgrade -r $(REQUIREMENTS)
@echo $(TAG)Installing HTTPie$(END) @echo $(TAG)Installing HTTPie$(END)
pip install --upgrade --editable . pip install --upgrade --editable .
@echo @echo
clean:
@echo $(TAG)Cleaning up$(END)
rm -rf .tox *.egg dist build .coverage .cache .pytest_cache httpie.egg-info
find . -name '__pycache__' -delete -print -o -name '*.pyc' -delete -print
@echo
###############################################################################
# Testing
###############################################################################
test: init test: init
@echo $(TAG)Running tests on the current Python interpreter with coverage $(END) @echo $(TAG)Running tests in on current Python with coverage $(END)
py.test --cov ./httpie --cov ./tests --doctest-modules --verbose ./httpie ./tests py.test --cov ./httpie --cov ./tests --doctest-modules --verbose ./httpie ./tests
@echo @echo
# test-all is meant to test everything — even this Makefile
test-all: uninstall-all clean init test test-tox test-dist pycodestyle
@echo
test-dist: test-sdist test-bdist-wheel
@echo
test-tox: init test-tox: init
@echo $(TAG)Running tests on all Pythons via Tox$(END) @echo $(TAG)Running tests on all Pythons via Tox$(END)
tox tox
@echo @echo
test-dist: test-sdist test-bdist-wheel
@echo
test-sdist: clean uninstall-httpie test-sdist: clean uninstall-httpie
@echo $(TAG)Testing sdist build an installation$(END) @echo $(TAG)Testing sdist build an installation$(END)
@ -60,7 +41,6 @@ test-sdist: clean uninstall-httpie
which http which http
@echo @echo
test-bdist-wheel: clean uninstall-httpie test-bdist-wheel: clean uninstall-httpie
@echo $(TAG)Testing wheel build an installation$(END) @echo $(TAG)Testing wheel build an installation$(END)
python setup.py bdist_wheel python setup.py bdist_wheel
@ -68,66 +48,20 @@ test-bdist-wheel: clean uninstall-httpie
which http which http
@echo @echo
# This tests everything, even this Makefile.
test-all: uninstall-all clean init test test-tox test-dist
pycodestyle: publish: test-all
which pycodestyle || pip install pycodestyle
pycodestyle
@echo
coveralls:
which coveralls || pip install python-coveralls
coveralls
@echo
###############################################################################
# Publishing to PyPi
###############################################################################
publish: test-all publish-no-test
publish-no-test:
@echo $(TAG)Testing wheel build an installation$(END) @echo $(TAG)Testing wheel build an installation$(END)
@echo "$(VERSION)" @echo "$(VERSION)"
@echo "$(VERSION)" | grep -q "dev" && echo '!!!Not publishing dev version!!!' && exit 1 || echo ok @echo "$(VERSION)" | grep -q "dev" && echo "!!!Not publishing dev version!!!" && exit 1
python setup.py register python setup.py register
python setup.py sdist upload python setup.py sdist upload
python setup.py bdist_wheel upload python setup.py bdist_wheel upload
@echo @echo
clean:
@echo $(TAG)Cleaning up$(END)
############################################################################### rm -rf .tox *.egg dist build .coverage
# Uninstalling find . -name '__pycache__' -delete -print -o -name '*.pyc' -delete -print
###############################################################################
uninstall-httpie:
@echo $(TAG)Uninstalling httpie$(END)
- pip uninstall --yes httpie &2>/dev/null
@echo "Verifying…"
cd .. && ! python -m httpie --version &2>/dev/null
@echo "Done"
@echo @echo
uninstall-all: uninstall-httpie
@echo $(TAG)Uninstalling httpie requirements$(END)
- pip uninstall --yes pygments requests
@echo $(TAG)Uninstalling development requirements$(END)
- pip uninstall --yes -r $(REQUIREMENTS)
###############################################################################
# Utils
###############################################################################
homebrew-formula-vars:
extras/get-homebrew-formula-vars.py

File diff suppressed because it is too large Load Diff

17
appveyor.yml Normal file
View File

@ -0,0 +1,17 @@
# https://ci.appveyor.com/project/jakubroztocil/httpie
build: false
environment:
matrix:
- PYTHON: "C:/Python27"
- PYTHON: "C:/Python34"
init:
- "ECHO %PYTHON%"
- ps: "ls C:/Python*"
install:
- ps: (new-object net.webclient).DownloadFile('https://raw.github.com/pypa/pip/master/contrib/get-pip.py', 'C:/get-pip.py')
- "%PYTHON%/python.exe C:/get-pip.py"
- "%PYTHON%/Scripts/pip.exe install -e ."
test_script:
- "%PYTHON%/Scripts/pip.exe --version"
- "%PYTHON%/Scripts/http.exe --debug"
- "%PYTHON%/python.exe setup.py test"

View File

@ -1,62 +0,0 @@
#!/usr/bin/env python3
"""
Generate URLs and file hashes to be included in the Homebrew formula
after a new release of HTTPie has been published on PyPi.
https://github.com/Homebrew/homebrew-core/blob/master/Formula/httpie.rb
"""
import hashlib
import requests
PACKAGES = [
'httpie',
'pygments',
'requests',
'certifi',
'urllib3',
'idna',
'chardet',
'PySocks',
]
def get_package_meta(package_name):
api_url = 'https://pypi.python.org/pypi/{}/json'.format(package_name)
resp = requests.get(api_url).json()
hasher = hashlib.sha256()
for release in resp['urls']:
download_url = release['url']
if download_url.endswith('.tar.gz'):
hasher.update(requests.get(download_url).content)
return {
'name': package_name,
'url': download_url,
'sha256': hasher.hexdigest(),
}
else:
raise RuntimeError(
'{}: download not found: {}'.format(package_name, resp))
def main():
package_meta_map = {
package_name: get_package_meta(package_name)
for package_name in PACKAGES
}
httpie_meta = package_meta_map.pop('httpie')
print()
print(' url "{url}"'.format(url=httpie_meta['url']))
print(' sha256 "{sha256}"'.format(sha256=httpie_meta['sha256']))
print()
for dep_meta in package_meta_map.values():
print(' resource "{name}" do'.format(name=dep_meta['name']))
print(' url "{url}"'.format(url=dep_meta['url']))
print(' sha256 "{sha256}"'.format(sha256=dep_meta['sha256']))
print(' end')
print('')
if __name__ == '__main__':
main()

View File

@ -1,23 +0,0 @@
#!/usr/bin/env bash
_http_complete() {
local cur_word=${COMP_WORDS[COMP_CWORD]}
local prev_word=${COMP_WORDS[COMP_CWORD - 1]}
if [[ "$cur_word" == -* ]]; then
_http_complete_options "$cur_word"
fi
}
complete -o default -F _http_complete http
_http_complete_options() {
local cur_word=$1
local options="-j --json -f --form --pretty -s --style -p --print
-v --verbose -h --headers -b --body -S --stream -o --output -d --download
-c --continue --session --session-read-only -a --auth --auth-type --proxy
--follow --verify --cert --cert-key --timeout --check-status --ignore-stdin
--help --version --traceback --debug"
COMPREPLY=( $( compgen -W "$options" -- "$cur_word" ) )
}

View File

@ -1,59 +0,0 @@
function __fish_httpie_auth_types
echo "basic"\t"Basic HTTP auth"
echo "digest"\t"Digest HTTP auth"
end
function __fish_httpie_styles
echo "autumn"
echo "borland"
echo "bw"
echo "colorful"
echo "default"
echo "emacs"
echo "friendly"
echo "fruity"
echo "igor"
echo "manni"
echo "monokai"
echo "murphy"
echo "native"
echo "paraiso-dark"
echo "paraiso-light"
echo "pastie"
echo "perldoc"
echo "rrt"
echo "solarized"
echo "tango"
echo "trac"
echo "vim"
echo "vs"
echo "xcode"
end
complete -x -c http -s s -l style -d 'Output coloring style (default is "monokai")' -A -a '(__fish_httpie_styles)'
complete -c http -s f -l form -d 'Data items from the command line are serialized as form fields'
complete -c http -s j -l json -d '(default) Data items from the command line are serialized as a JSON object'
complete -x -c http -l pretty -d 'Controls output processing' -a "all colors format none" -A
complete -x -c http -s p -l print -d 'String specifying what the output should contain'
complete -c http -s v -l verbose -d 'Print the whole request as well as the response'
complete -c http -s h -l headers -d 'Print only the response headers'
complete -c http -s b -l body -d 'Print only the response body'
complete -c http -s S -l stream -d 'Always stream the output by line'
complete -c http -s o -l output -d 'Save output to FILE'
complete -c http -s d -l download -d 'Do not print the response body to stdout'
complete -c http -s c -l continue -d 'Resume an interrupted download'
complete -x -c http -l session -d 'Create, or reuse and update a session'
complete -x -c http -s a -l auth -d 'If only the username is provided (-a username), HTTPie will prompt for the password'
complete -x -c http -l auth-type -d 'The authentication mechanism to be used' -a '(__fish_httpie_auth_types)' -A
complete -x -c http -l proxy -d 'String mapping protocol to the URL of the proxy'
complete -c http -l follow -d 'Allow full redirects'
complete -x -c http -l verify -d 'SSL cert verification'
complete -c http -l cert -d 'SSL cert'
complete -c http -l cert-key -d 'Private SSL cert key'
complete -x -c http -l timeout -d 'Connection timeout in seconds'
complete -c http -l check-status -d 'Error with non-200 HTTP status code'
complete -c http -l ignore-stdin -d 'Do not attempt to read stdin'
complete -c http -l help -d 'Show help'
complete -c http -l version -d 'Show version'
complete -c http -l traceback -d 'Prints exception traceback should one occur'
complete -c http -l debug -d 'Show debugging information'

View File

@ -1,69 +0,0 @@
# The latest Homebrew formula as submitted to Homebrew/homebrew-core.
# Only useful for testing until it gets accepted by homebrew maintainers.
# (It will need to be updated from the repo version before next release.)
#
# https://github.com/Homebrew/homebrew-core/blob/master/Formula/httpie.rb
#
class Httpie < Formula
include Language::Python::Virtualenv
desc "User-friendly cURL replacement (command-line HTTP client)"
homepage "https://httpie.org/"
url "https://files.pythonhosted.org/packages/44/ee/7177b743400d7f82a69bf30cb3c24ea4bb1f4aea68878bc540f732bf4940/httpie-1.0.0.tar.gz"
sha256 "1650342d2eca2622092196bf106ab8f68ea2dbb2ed265d37191185618e159a25"
head "https://github.com/jakubroztocil/httpie.git"
bottle do
cellar :any_skip_relocation
sha256 "7e9db255e324dd63b66106ca62ed7e4e81f6634c624dec3ff49c293aba1072a6" => :mojave
sha256 "437504a11416284b17d3a801c267d0fd5e15416f38cff3abf7ed99b096b4828a" => :high_sierra
sha256 "10b25fc787076719b1f1f9c242c5e9d872ebd1c7a6d83e6f1af983a17cd8ca55" => :sierra
sha256 "1bd35480d1ef401bdad9c322e7c1624aefc9b5056530ab990e327d0bc397e4fb" => :el_capitan
end
depends_on "python" ["3.6.5_1"]
resource "pygments" do
url "https://files.pythonhosted.org/packages/71/2a/2e4e77803a8bd6408a2903340ac498cb0a2181811af7c9ec92cb70b0308a/Pygments-2.2.0.tar.gz"
sha256 "dbae1046def0efb574852fab9e90209b23f556367b5a320c0bcb871c77c3e8cc"
end
resource "requests" do
url "https://files.pythonhosted.org/packages/97/10/92d25b93e9c266c94b76a5548f020f3f1dd0eb40649cb1993532c0af8f4c/requests-2.20.0.tar.gz"
sha256 "99dcfdaaeb17caf6e526f32b6a7b780461512ab3f1d992187801694cba42770c"
end
resource "certifi" do
url "https://files.pythonhosted.org/packages/41/b6/4f0cefba47656583217acd6cd797bc2db1fede0d53090fdc28ad2c8e0716/certifi-2018.10.15.tar.gz"
sha256 "6d58c986d22b038c8c0df30d639f23a3e6d172a05c3583e766f4c0b785c0986a"
end
resource "urllib3" do
url "https://files.pythonhosted.org/packages/a5/74/05ffd00b4b5c08306939c485869f5dc40cbc27357195b0a98b18e4c48893/urllib3-1.24.tar.gz"
sha256 "41c3db2fc01e5b907288010dec72f9d0a74e37d6994e6eb56849f59fea2265ae"
end
resource "idna" do
url "https://files.pythonhosted.org/packages/65/c4/80f97e9c9628f3cac9b98bfca0402ede54e0563b56482e3e6e45c43c4935/idna-2.7.tar.gz"
sha256 "684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16"
end
resource "chardet" do
url "https://files.pythonhosted.org/packages/fc/bb/a5768c230f9ddb03acc9ef3f0d4a3cf93462473795d18e9535498c8f929d/chardet-3.0.4.tar.gz"
sha256 "84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"
end
resource "PySocks" do
url "https://files.pythonhosted.org/packages/53/12/6bf1d764f128636cef7408e8156b7235b150ea31650d0260969215bb8e7d/PySocks-1.6.8.tar.gz"
sha256 "3fe52c55890a248676fd69dc9e3c4e811718b777834bcaab7a8125cf9deac672"
end
def install
virtualenv_install_with_resources
end
test do
raw_url = "https://raw.githubusercontent.com/Homebrew/homebrew-core/master/Formula/httpie.rb"
assert_match "PYTHONPATH", shell_output("#{bin}/http --ignore-stdin #{raw_url}")
end
end

Binary file not shown.

Before

Width:  |  Height:  |  Size: 681 KiB

After

Width:  |  Height:  |  Size: 182 KiB

View File

@ -2,31 +2,18 @@
HTTPie - a CLI, cURL-like tool for humans. HTTPie - a CLI, cURL-like tool for humans.
""" """
__version__ = '1.0.1'
__author__ = 'Jakub Roztocil' __author__ = 'Jakub Roztocil'
__version__ = '0.9.2'
__licence__ = 'BSD' __licence__ = 'BSD'
class ExitStatus: class ExitStatus:
"""Program exit code constants.""" """Exit status code constants."""
SUCCESS = 0 OK = 0
ERROR = 1 ERROR = 1
PLUGIN_ERROR = 7
# 128+2 SIGINT <http://www.tldp.org/LDP/abs/html/exitcodes.html>
ERROR_CTRL_C = 130
ERROR_TIMEOUT = 2 ERROR_TIMEOUT = 2
ERROR_TOO_MANY_REDIRECTS = 6
# Used only when requested with --check-status: # Used only when requested with --check-status:
ERROR_HTTP_3XX = 3 ERROR_HTTP_3XX = 3
ERROR_HTTP_4XX = 4 ERROR_HTTP_4XX = 4
ERROR_HTTP_5XX = 5 ERROR_HTTP_5XX = 5
EXIT_STATUS_LABELS = {
value: key
for key, value in ExitStatus.__dict__.items()
if key.isupper()
}

View File

@ -3,16 +3,8 @@
""" """
import sys import sys
from .core import main
def main():
try:
from .core import main
sys.exit(main())
except KeyboardInterrupt:
from . import ExitStatus
sys.exit(ExitStatus.ERROR_CTRL_C)
if __name__ == '__main__': if __name__ == '__main__':
main() sys.exit(main())

View File

@ -3,29 +3,23 @@
NOTE: the CLI interface may change before reaching v1.0. NOTE: the CLI interface may change before reaching v1.0.
""" """
# noinspection PyCompatibility
from argparse import (
RawDescriptionHelpFormatter, FileType,
OPTIONAL, ZERO_OR_MORE, SUPPRESS
)
from textwrap import dedent, wrap from textwrap import dedent, wrap
#noinspection PyCompatibility
from argparse import (RawDescriptionHelpFormatter, FileType,
OPTIONAL, ZERO_OR_MORE, SUPPRESS)
from httpie import __doc__, __version__ from httpie import __doc__, __version__
from httpie.input import ( from httpie.plugins.builtin import BuiltinAuthPlugin
HTTPieArgumentParser, KeyValueArgType, from httpie.plugins import plugin_manager
SEP_PROXY, SEP_GROUP_ALL_ITEMS, 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_REQ_HEAD, OUT_REQ_BODY, OUT_RESP_HEAD,
OUT_RESP_BODY, OUTPUT_OPTIONS, OUT_RESP_BODY, OUTPUT_OPTIONS,
OUTPUT_OPTIONS_DEFAULT, PRETTY_MAP, OUTPUT_OPTIONS_DEFAULT, PRETTY_MAP,
PRETTY_STDOUT_TTY_ONLY, SessionNameValidator, PRETTY_STDOUT_TTY_ONLY, SessionNameValidator,
readable_file_arg, SSL_VERSION_ARG_MAPPING readable_file_arg)
)
from httpie.output.formatters.colors import (
AVAILABLE_STYLES, DEFAULT_STYLE, AUTO_STYLE
)
from httpie.plugins import plugin_manager
from httpie.plugins.builtin import BuiltinAuthPlugin
from httpie.sessions import DEFAULT_SESSIONS_DIR
class HTTPieHelpFormatter(RawDescriptionHelpFormatter): class HTTPieHelpFormatter(RawDescriptionHelpFormatter):
@ -46,9 +40,7 @@ class HTTPieHelpFormatter(RawDescriptionHelpFormatter):
text = dedent(text).strip() + '\n\n' text = dedent(text).strip() + '\n\n'
return text.splitlines() return text.splitlines()
parser = Parser(
parser = HTTPieArgumentParser(
prog='http',
formatter_class=HTTPieHelpFormatter, formatter_class=HTTPieHelpFormatter,
description='%s <http://httpie.org>' % __doc__.strip(), description='%s <http://httpie.org>' % __doc__.strip(),
epilog=dedent(""" epilog=dedent("""
@ -59,7 +51,7 @@ parser = HTTPieArgumentParser(
https://github.com/jakubroztocil/httpie/issues https://github.com/jakubroztocil/httpie/issues
"""), """)
) )
@ -96,7 +88,6 @@ positional.add_argument(
metavar='URL', metavar='URL',
help=""" help="""
The scheme defaults to 'http://' if the URL does not include one. The scheme defaults to 'http://' if the URL does not include one.
(You can override this with: --default-scheme=https)
You can also use a shorthand for localhost You can also use a shorthand for localhost
@ -109,7 +100,6 @@ positional.add_argument(
'items', 'items',
metavar='REQUEST_ITEM', metavar='REQUEST_ITEM',
nargs=ZERO_OR_MORE, nargs=ZERO_OR_MORE,
default=None,
type=KeyValueArgType(*SEP_GROUP_ALL_ITEMS), type=KeyValueArgType(*SEP_GROUP_ALL_ITEMS),
help=r""" help=r"""
Optional key-value pairs to be included in the request. The separator used Optional key-value pairs to be included in the request. The separator used
@ -212,21 +202,18 @@ output_processing.add_argument(
help=""" help="""
Output coloring style (default is "{default}"). One of: Output coloring style (default is "{default}"). One of:
{available_styles} {available}
The "{auto_style}" style follows your terminal's ANSI color styles. For this option to work properly, please make sure that the $TERM
environment variable is set to "xterm-256color" or similar
For non-{auto_style} styles to work properly, please make sure that the
$TERM environment variable is set to "xterm-256color" or similar
(e.g., via `export TERM=xterm-256color' in your ~/.bashrc). (e.g., via `export TERM=xterm-256color' in your ~/.bashrc).
""".format( """.format(
default=DEFAULT_STYLE, default=DEFAULT_STYLE,
available_styles='\n'.join( available='\n'.join(
'{0}{1}'.format(8 * ' ', line.strip()) '{0}{1}'.format(8*' ', line.strip())
for line in wrap(', '.join(sorted(AVAILABLE_STYLES)), 60) for line in wrap(', '.join(sorted(AVAILABLE_STYLES)), 60)
).rstrip(), ).rstrip(),
auto_style=AUTO_STYLE,
) )
) )
@ -262,6 +249,17 @@ output_options.add_argument(
default=OUTPUT_OPTIONS_DEFAULT, default=OUTPUT_OPTIONS_DEFAULT,
) )
) )
output_options.add_argument(
'--verbose', '-v',
dest='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(OUTPUT_OPTIONS))
)
output_options.add_argument( output_options.add_argument(
'--headers', '-h', '--headers', '-h',
dest='output_options', dest='output_options',
@ -285,42 +283,6 @@ output_options.add_argument(
.format(OUT_RESP_BODY) .format(OUT_RESP_BODY)
) )
output_options.add_argument(
'--verbose', '-v',
dest='verbose',
action='store_true',
help="""
Verbose output. Print the whole request as well as the response. Also print
any intermediary requests/responses (such as redirects).
It's a shortcut for: --all --print={0}
"""
.format(''.join(OUTPUT_OPTIONS))
)
output_options.add_argument(
'--all',
default=False,
action='store_true',
help="""
By default, only the final request/response is shown. Use this flag to show
any intermediary requests/responses as well. Intermediary requests include
followed redirects (with --follow), the first unauthorized request when
Digest auth is used (--auth=digest), etc.
"""
)
output_options.add_argument(
'--history-print', '-P',
dest='output_options_history',
metavar='WHAT',
help="""
The same as --print, -p but applies only to intermediary requests/responses
(such as redirects) when their inclusion is enabled with --all. If this
options is not specified, then they are formatted the same way as the final
response.
"""
)
output_options.add_argument( output_options.add_argument(
'--stream', '-S', '--stream', '-S',
action='store_true', action='store_true',
@ -345,9 +307,8 @@ output_options.add_argument(
dest='output_file', dest='output_file',
metavar='FILE', metavar='FILE',
help=""" help="""
Save output to FILE instead of stdout. If --download is also set, then only Save output to FILE. If --download is set, then only the response body is
the response body is saved to FILE. Other parts of the HTTP exchange are saved to the file. Other parts of the HTTP exchange are printed to stderr.
printed to stderr.
""" """
@ -416,6 +377,7 @@ sessions.add_argument(
""" """
) )
####################################################################### #######################################################################
# Authentication # Authentication
####################################################################### #######################################################################
@ -424,8 +386,8 @@ sessions.add_argument(
auth = parser.add_argument_group(title='Authentication') auth = parser.add_argument_group(title='Authentication')
auth.add_argument( auth.add_argument(
'--auth', '-a', '--auth', '-a',
default=None,
metavar='USER[:PASS]', metavar='USER[:PASS]',
type=AuthCredentialsArgType(SEP_CREDENTIALS),
help=""" help="""
If only the username is provided (-a username), HTTPie will prompt If only the username is provided (-a username), HTTPie will prompt
for the password. for the password.
@ -433,22 +395,11 @@ auth.add_argument(
""", """,
) )
class _AuthTypeLazyChoices(object):
# Needed for plugin testing
def __contains__(self, item):
return item in plugin_manager.get_auth_plugin_mapping()
def __iter__(self):
return iter(sorted(plugin_manager.get_auth_plugin_mapping().keys()))
_auth_plugins = plugin_manager.get_auth_plugins() _auth_plugins = plugin_manager.get_auth_plugins()
auth.add_argument( auth.add_argument(
'--auth-type', '-A', '--auth-type',
choices=_AuthTypeLazyChoices(), choices=[plugin.auth_type for plugin in _auth_plugins],
default=None, default=_auth_plugins[0].auth_type,
help=""" help="""
The authentication mechanism to be used. Defaults to "{default}". The authentication mechanism to be used. Defaults to "{default}".
@ -493,21 +444,45 @@ network.add_argument(
""" """
) )
network.add_argument( network.add_argument(
'--follow', '-F', '--follow',
default=False, default=False,
action='store_true', action='store_true',
help=""" help="""
Follow 30x Location redirects. Set this flag if full redirects are allowed (e.g. re-POST-ing of data at
new Location).
"""
)
network.add_argument(
'--verify',
default='yes',
help="""
Set to "no" to skip checking the host's SSL certificate. You can also pass
the path to a CA_BUNDLE file for private certs. You can also set the
REQUESTS_CA_BUNDLE environment variable. Defaults to "yes".
""" """
) )
network.add_argument( network.add_argument(
'--max-redirects', '--cert',
type=int, default=None,
default=30, type=readable_file_arg,
help=""" help="""
By default, requests have a limit of 30 redirects (works with --follow). You can specify a local cert to use as client side SSL certificate.
This file may either contain both private key and certificate or you may
specify --cert-key separately.
"""
)
network.add_argument(
'--cert-key',
default=None,
type=readable_file_arg,
help="""
The private key to use with SSL. Only needed if --cert is given and the
certificate file does not contain the private key.
""" """
) )
@ -541,57 +516,6 @@ network.add_argument(
) )
#######################################################################
# SSL
#######################################################################
ssl = parser.add_argument_group(title='SSL')
ssl.add_argument(
'--verify',
default='yes',
help="""
Set to "no" (or "false") to skip checking the host's SSL certificate.
Defaults to "yes" ("true"). You can also pass the path to a CA_BUNDLE file
for private certs. (Or you can set the REQUESTS_CA_BUNDLE environment
variable instead.)
"""
)
ssl.add_argument(
'--ssl', # TODO: Maybe something more general, such as --secure-protocol?
dest='ssl_version',
choices=list(sorted(SSL_VERSION_ARG_MAPPING.keys())),
help="""
The desired protocol version to use. This will default to
SSL v2.3 which will negotiate the highest protocol that both
the server and your installation of OpenSSL support. Available protocols
may vary depending on OpenSSL installation (only the supported ones
are shown here).
"""
)
ssl.add_argument(
'--cert',
default=None,
type=readable_file_arg,
help="""
You can specify a local cert to use as client side SSL certificate.
This file may either contain both private key and certificate or you may
specify --cert-key separately.
"""
)
ssl.add_argument(
'--cert-key',
default=None,
type=readable_file_arg,
help="""
The private key to use with SSL. Only needed if --cert is given and the
certificate file does not contain the private key.
"""
)
####################################################################### #######################################################################
# Troubleshooting # Troubleshooting
####################################################################### #######################################################################
@ -599,7 +523,7 @@ ssl.add_argument(
troubleshooting = parser.add_argument_group(title='Troubleshooting') troubleshooting = parser.add_argument_group(title='Troubleshooting')
troubleshooting.add_argument( troubleshooting.add_argument(
'--ignore-stdin', '-I', '--ignore-stdin',
action='store_true', action='store_true',
default=False, default=False,
help=""" help="""
@ -630,15 +554,7 @@ troubleshooting.add_argument(
action='store_true', action='store_true',
default=False, default=False,
help=""" help="""
Prints the exception traceback should one occur. Prints exception traceback should one occur.
"""
)
troubleshooting.add_argument(
'--default-scheme',
default="http",
help="""
The default scheme to use if not specified in the URL.
""" """
) )
@ -647,8 +563,8 @@ troubleshooting.add_argument(
action='store_true', action='store_true',
default=False, default=False,
help=""" help="""
Prints the exception traceback should one occur, as well as other Prints exception traceback should one occur, and also other information
information useful for debugging HTTPie itself and for reporting bugs. that is useful for debugging HTTPie itself and for reporting bugs.
""" """
) )

View File

@ -1,54 +1,28 @@
import json import json
import sys import sys
from pprint import pformat
import requests import requests
from requests.adapters import HTTPAdapter from requests.packages import urllib3
from requests.structures import CaseInsensitiveDict
from httpie import sessions from httpie import sessions
from httpie import __version__ from httpie import __version__
from httpie.compat import str from httpie.compat import str
from httpie.input import SSL_VERSION_ARG_MAPPING
from httpie.plugins import plugin_manager from httpie.plugins import plugin_manager
from httpie.utils import repr_dict_nice
try:
# https://urllib3.readthedocs.io/en/latest/security.html
# noinspection PyPackageRequirements
import urllib3
urllib3.disable_warnings()
except (ImportError, AttributeError):
# In some rare cases, the user may have an old version of the requests
# or urllib3, and there is no method called "disable_warnings." In these
# cases, we don't need to call the method.
# They may get some noisy output but execution shouldn't die. Move on.
pass
FORM_CONTENT_TYPE = 'application/x-www-form-urlencoded; charset=utf-8' # https://urllib3.readthedocs.org/en/latest/security.html
JSON_CONTENT_TYPE = 'application/json' urllib3.disable_warnings()
JSON_ACCEPT = '{0}, */*'.format(JSON_CONTENT_TYPE)
FORM = 'application/x-www-form-urlencoded; charset=utf-8'
JSON = 'application/json'
DEFAULT_UA = 'HTTPie/%s' % __version__ DEFAULT_UA = 'HTTPie/%s' % __version__
class HTTPieHTTPAdapter(HTTPAdapter): def get_requests_session():
def __init__(self, ssl_version=None, **kwargs):
self._ssl_version = ssl_version
super(HTTPieHTTPAdapter, self).__init__(**kwargs)
def init_poolmanager(self, *args, **kwargs):
kwargs['ssl_version'] = self._ssl_version
super(HTTPieHTTPAdapter, self).init_poolmanager(*args, **kwargs)
def get_requests_session(ssl_version):
requests_session = requests.Session() requests_session = requests.Session()
requests_session.mount( for cls in plugin_manager.get_trasnsport_plugins():
'https://',
HTTPieHTTPAdapter(ssl_version=ssl_version)
)
for cls in plugin_manager.get_transport_plugins():
transport_plugin = cls() transport_plugin = cls()
requests_session.mount(prefix=transport_plugin.prefix, requests_session.mount(prefix=transport_plugin.prefix,
adapter=transport_plugin.get_adapter()) adapter=transport_plugin.get_adapter())
@ -58,12 +32,7 @@ def get_requests_session(ssl_version):
def get_response(args, config_dir): def get_response(args, config_dir):
"""Send the request and return a `request.Response`.""" """Send the request and return a `request.Response`."""
ssl_version = None requests_session = get_requests_session()
if args.ssl_version:
ssl_version = SSL_VERSION_ARG_MAPPING[args.ssl_version]
requests_session = get_requests_session(ssl_version)
requests_session.max_redirects = args.max_redirects
if not args.session and not args.session_read_only: if not args.session and not args.session_read_only:
kwargs = get_requests_kwargs(args) kwargs = get_requests_kwargs(args)
@ -83,44 +52,35 @@ def get_response(args, config_dir):
def dump_request(kwargs): def dump_request(kwargs):
sys.stderr.write('\n>>> requests.request(**%s)\n\n' sys.stderr.write('\n>>> requests.request(%s)\n\n'
% repr_dict_nice(kwargs)) % pformat(kwargs))
def finalize_headers(headers): def encode_headers(headers):
final_headers = {} # This allows for unicode headers which is non-standard but practical.
for name, value in headers.items():
if value is not None:
# >leading or trailing LWS MAY be removed without
# >changing the semantics of the field value"
# -https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html
# Also, requests raises `InvalidHeader` for leading spaces.
value = value.strip()
if isinstance(value, str):
# See: https://github.com/jakubroztocil/httpie/issues/212 # See: https://github.com/jakubroztocil/httpie/issues/212
value = value.encode('utf8') return dict(
(name, value.encode('utf8') if isinstance(value, str) else value)
final_headers[name] = value for name, value in headers.items()
return final_headers )
def get_default_headers(args): def get_default_headers(args):
default_headers = CaseInsensitiveDict({ default_headers = {
'User-Agent': DEFAULT_UA 'User-Agent': DEFAULT_UA
}) }
auto_json = args.data and not args.form auto_json = args.data and not args.form
# FIXME: Accept is set to JSON with `http url @./file.txt`.
if args.json or auto_json: if args.json or auto_json:
default_headers['Accept'] = JSON_ACCEPT default_headers['Accept'] = 'application/json'
if args.json or (auto_json and args.data): if args.json or (auto_json and args.data):
default_headers['Content-Type'] = JSON_CONTENT_TYPE default_headers['Content-Type'] = JSON
elif args.form and not args.files: elif args.form and not args.files:
# If sending files, `requests` will set # If sending files, `requests` will set
# the `Content-Type` for us. # the `Content-Type` for us.
default_headers['Content-Type'] = FORM_CONTENT_TYPE default_headers['Content-Type'] = FORM
return default_headers return default_headers
@ -132,7 +92,7 @@ def get_requests_kwargs(args, base_headers=None):
# Serialize JSON data, if needed. # Serialize JSON data, if needed.
data = args.data data = args.data
auto_json = data and not args.form auto_json = data and not args.form
if (args.json or auto_json) and isinstance(data, dict): if args.json or auto_json and isinstance(data, dict):
if data: if data:
data = json.dumps(data) data = json.dumps(data)
else: else:
@ -145,7 +105,12 @@ def get_requests_kwargs(args, base_headers=None):
if base_headers: if base_headers:
headers.update(base_headers) headers.update(base_headers)
headers.update(args.headers) headers.update(args.headers)
headers = finalize_headers(headers) headers = encode_headers(headers)
credentials = None
if args.auth:
auth_plugin = plugin_manager.get_auth_plugin(args.auth_type)()
credentials = auth_plugin.get_auth(args.auth.key, args.auth.value)
cert = None cert = None
if args.cert: if args.cert:
@ -161,14 +126,12 @@ def get_requests_kwargs(args, base_headers=None):
'data': data, 'data': data,
'verify': { 'verify': {
'yes': True, 'yes': True,
'true': True, 'no': False
'no': False, }.get(args.verify, args.verify),
'false': False,
}.get(args.verify.lower(), args.verify),
'cert': cert, 'cert': cert,
'timeout': args.timeout, 'timeout': args.timeout,
'auth': args.auth, 'auth': credentials,
'proxies': {p.key: p.value for p in args.proxy}, 'proxies': dict((p.key, p.value) for p in args.proxy),
'files': args.files, 'files': args.files,
'allow_redirects': args.follow, 'allow_redirects': args.follow,
'params': args.params, 'params': args.params,

View File

@ -1,26 +1,25 @@
""" """
Python 2.7, and 3.x compatibility. Python 2.6, 2.7, and 3.x compatibility.
""" """
import sys import sys
is_py2 = sys.version_info[0] == 2 # Taken from `requests.compat`
is_py27 = sys.version_info[:2] == (2, 7) _ver = sys.version_info
is_py3 = sys.version_info[0] == 3 is_py2 = (_ver[0] == 2)
is_pypy = 'pypy' in sys.version.lower() is_py26 = (is_py2 and _ver[1] == 6)
is_py27 = (is_py2 and _ver[1] == 7)
is_py3 = (_ver[0] == 3)
is_pypy = ('pypy' in _ver)
is_windows = 'win32' in str(sys.platform).lower() is_windows = 'win32' in str(sys.platform).lower()
if is_py2: if is_py2:
# noinspection PyShadowingBuiltins
bytes = str bytes = str
# noinspection PyUnresolvedReferences,PyShadowingBuiltins
str = unicode str = unicode
elif is_py3: elif is_py3:
# noinspection PyShadowingBuiltins
str = str str = str
# noinspection PyShadowingBuiltins
bytes = bytes bytes = bytes
@ -35,5 +34,142 @@ try: # pragma: no cover
# noinspection PyCompatibility # noinspection PyCompatibility
from urllib.request import urlopen from urllib.request import urlopen
except ImportError: # pragma: no cover except ImportError: # pragma: no cover
# noinspection PyCompatibility,PyUnresolvedReferences # noinspection PyCompatibility
from urllib2 import urlopen from urllib2 import urlopen
try: # pragma: no cover
from collections import OrderedDict
except ImportError: # pragma: no cover
# Python 2.6 OrderedDict class, needed for headers, parameters, etc .###
# <https://pypi.python.org/pypi/ordereddict/1.1>
# noinspection PyCompatibility
from UserDict import DictMixin
# noinspection PyShadowingBuiltins
class OrderedDict(dict, DictMixin):
# Copyright (c) 2009 Raymond Hettinger
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.
# noinspection PyMissingConstructor
def __init__(self, *args, **kwds):
if len(args) > 1:
raise TypeError('expected at most 1 arguments, got %d'
% len(args))
try:
self.__end
except AttributeError:
self.clear()
self.update(*args, **kwds)
def clear(self):
self.__end = end = []
# noinspection PyUnusedLocal
end += [None, end, end] # sentinel node for doubly linked list
self.__map = {} # key --> [key, prev, next]
dict.clear(self)
def __setitem__(self, key, value):
if key not in self:
end = self.__end
curr = end[1]
curr[2] = end[1] = self.__map[key] = [key, curr, end]
dict.__setitem__(self, key, value)
def __delitem__(self, key):
dict.__delitem__(self, key)
key, prev, next = self.__map.pop(key)
prev[2] = next
next[1] = prev
def __iter__(self):
end = self.__end
curr = end[2]
while curr is not end:
yield curr[0]
curr = curr[2]
def __reversed__(self):
end = self.__end
curr = end[1]
while curr is not end:
yield curr[0]
curr = curr[1]
def popitem(self, last=True):
if not self:
raise KeyError('dictionary is empty')
if last:
key = reversed(self).next()
else:
key = iter(self).next()
value = self.pop(key)
return key, value
def __reduce__(self):
items = [[k, self[k]] for k in self]
tmp = self.__map, self.__end
del self.__map, self.__end
inst_dict = vars(self).copy()
self.__map, self.__end = tmp
if inst_dict:
return self.__class__, (items,), inst_dict
return self.__class__, (items,)
def keys(self):
return list(self)
setdefault = DictMixin.setdefault
update = DictMixin.update
pop = DictMixin.pop
values = DictMixin.values
items = DictMixin.items
iterkeys = DictMixin.iterkeys
itervalues = DictMixin.itervalues
iteritems = DictMixin.iteritems
def __repr__(self):
if not self:
return '%s()' % (self.__class__.__name__,)
return '%s(%r)' % (self.__class__.__name__, self.items())
def copy(self):
return self.__class__(self)
# noinspection PyMethodOverriding
@classmethod
def fromkeys(cls, iterable, value=None):
d = cls()
for key in iterable:
d[key] = value
return d
def __eq__(self, other):
if isinstance(other, OrderedDict):
if len(self) != len(other):
return False
for p, q in zip(self.items(), other.items()):
if p != q:
return False
return True
return dict.__eq__(self, other)
def __ne__(self, other):
return not self == other

View File

@ -6,11 +6,11 @@ from httpie import __version__
from httpie.compat import is_windows from httpie.compat import is_windows
DEFAULT_CONFIG_DIR = str(os.environ.get( DEFAULT_CONFIG_DIR = os.environ.get(
'HTTPIE_CONFIG_DIR', 'HTTPIE_CONFIG_DIR',
os.path.expanduser('~/.httpie') if not is_windows else os.path.expanduser('~/.httpie') if not is_windows else
os.path.expandvars(r'%APPDATA%\\httpie') os.path.expandvars(r'%APPDATA%\\httpie')
)) )
class BaseConfigDict(dict): class BaseConfigDict(dict):
@ -80,10 +80,11 @@ class BaseConfigDict(dict):
class Config(BaseConfigDict): class Config(BaseConfigDict):
name = 'config' name = 'config'
helpurl = 'https://httpie.org/doc#config' helpurl = 'https://github.com/jakubroztocil/httpie#config'
about = 'HTTPie configuration file' about = 'HTTPie configuration file'
DEFAULTS = { DEFAULTS = {
'implicit_content_type': 'json',
'default_options': [] 'default_options': []
} }
@ -92,21 +93,5 @@ class Config(BaseConfigDict):
self.update(self.DEFAULTS) self.update(self.DEFAULTS)
self.directory = directory self.directory = directory
def load(self):
super(Config, self).load()
self._migrate_implicit_content_type()
def _get_path(self): def _get_path(self):
return os.path.join(self.directory, self.name + '.json') return os.path.join(self.directory, self.name + '.json')
def _migrate_implicit_content_type(self):
"""Migrate the removed implicit_content_type config option"""
try:
implicit_content_type = self.pop('implicit_content_type')
except KeyError:
self.save()
else:
if implicit_content_type == 'form':
self['default_options'].insert(0, '--form')
self.save()
self.load()

View File

@ -1,14 +1,8 @@
import sys import sys
try:
import curses
except ImportError:
curses = None # Compiled w/o curses
from httpie.compat import is_windows from httpie.compat import is_windows
from httpie.config import DEFAULT_CONFIG_DIR, Config from httpie.config import DEFAULT_CONFIG_DIR, Config
from httpie.utils import repr_dict_nice
class Environment(object): class Environment(object):
""" """
@ -32,12 +26,17 @@ class Environment(object):
stderr_isatty = stderr.isatty() stderr_isatty = stderr.isatty()
colors = 256 colors = 256
if not is_windows: if not is_windows:
if curses: import curses
try: try:
curses.setupterm() curses.setupterm()
try:
colors = curses.tigetnum('colors') colors = curses.tigetnum('colors')
except TypeError:
# pypy3 (2.4.0)
colors = curses.tigetnum(b'colors')
except curses.error: except curses.error:
pass pass
del curses
else: else:
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
import colorama.initialise import colorama.initialise
@ -83,17 +82,3 @@ class Environment(object):
else: else:
self._config.load() self._config.load()
return self._config return self._config
def __str__(self):
defaults = dict(type(self).__dict__)
actual = dict(defaults)
actual.update(self.__dict__)
actual['config'] = self.config
return repr_dict_nice(dict(
(key, value)
for key, value in actual.items()
if not key.startswith('_'))
)
def __repr__(self):
return '<{0} {1}>'.format(type(self).__name__, str(self))

View File

@ -12,7 +12,6 @@ Invocation flow:
""" """
import sys import sys
import errno import errno
import platform
import requests import requests
from requests import __version__ as requests_version from requests import __version__ as requests_version
@ -21,13 +20,12 @@ from pygments import __version__ as pygments_version
from httpie import __version__ as httpie_version, ExitStatus from httpie import __version__ as httpie_version, ExitStatus
from httpie.compat import str, bytes, is_py3 from httpie.compat import str, bytes, is_py3
from httpie.client import get_response from httpie.client import get_response
from httpie.downloads import Downloader from httpie.downloads import Download
from httpie.context import Environment from httpie.context import Environment
from httpie.plugins import plugin_manager from httpie.plugins import plugin_manager
from httpie.output.streams import ( from httpie.output.streams import (
build_output_stream, build_output_stream,
write_stream, write, write_with_colors_win_py3
write_stream_with_colors_win_py3
) )
@ -43,25 +41,22 @@ def get_exit_status(http_status, follow=False):
# Server Error # Server Error
return ExitStatus.ERROR_HTTP_5XX return ExitStatus.ERROR_HTTP_5XX
else: else:
return ExitStatus.SUCCESS return ExitStatus.OK
def print_debug_info(env): def print_debug_info(env):
env.stderr.writelines([ env.stderr.writelines([
'HTTPie %s\n' % httpie_version, 'HTTPie %s\n' % httpie_version,
'HTTPie data: %s\n' % env.config.directory,
'Requests %s\n' % requests_version, 'Requests %s\n' % requests_version,
'Pygments %s\n' % pygments_version, 'Pygments %s\n' % pygments_version,
'Python %s\n%s\n' % (sys.version, sys.executable), 'Python %s %s\n' % (sys.version, sys.platform)
'%s %s' % (platform.system(), platform.release()),
]) ])
env.stderr.write('\n\n')
env.stderr.write(repr(env))
env.stderr.write('\n')
def decode_args(args, stdin_encoding): def decode_args(args, stdin_encoding):
""" """
Convert all bytes args to str Convert all bytes ags to str
by decoding them using stdin encoding. by decoding them using stdin encoding.
""" """
@ -72,109 +67,8 @@ def decode_args(args, stdin_encoding):
] ]
def program(args, env, log_error): def main(args=sys.argv[1:], env=Environment()):
""" """Run the main program and write the output to ``env.stdout``.
The main program without error handling
:param args: parsed args (argparse.Namespace)
:type env: Environment
:param log_error: error log function
:return: status code
"""
exit_status = ExitStatus.SUCCESS
downloader = None
show_traceback = args.debug or args.traceback
try:
if args.download:
args.follow = True # --download implies --follow.
downloader = Downloader(
output_file=args.output_file,
progress_file=env.stderr,
resume=args.download_resume
)
downloader.pre_request(args.headers)
final_response = get_response(args, config_dir=env.config.directory)
if args.all:
responses = final_response.history + [final_response]
else:
responses = [final_response]
for response in responses:
if args.check_status or downloader:
exit_status = get_exit_status(
http_status=response.status_code,
follow=args.follow
)
if not env.stdout_isatty and exit_status != ExitStatus.SUCCESS:
log_error(
'HTTP %s %s', response.raw.status, response.raw.reason,
level='warning'
)
write_stream_kwargs = {
'stream': build_output_stream(
args=args,
env=env,
request=response.request,
response=response,
output_options=(
args.output_options
if response is final_response
else args.output_options_history
)
),
# NOTE: `env.stdout` will in fact be `stderr` with `--download`
'outfile': env.stdout,
'flush': env.stdout_isatty or args.stream
}
try:
if env.is_windows and is_py3 and 'colors' in args.prettify:
write_stream_with_colors_win_py3(**write_stream_kwargs)
else:
write_stream(**write_stream_kwargs)
except IOError as e:
if not show_traceback and e.errno == errno.EPIPE:
# Ignore broken pipes unless --traceback.
env.stderr.write('\n')
else:
raise
if downloader and exit_status == ExitStatus.SUCCESS:
# Last response body download.
download_stream, download_to = downloader.start(final_response)
write_stream(
stream=download_stream,
outfile=download_to,
flush=False,
)
downloader.finish()
if downloader.interrupted:
exit_status = ExitStatus.ERROR
log_error('Incomplete download: size=%d; downloaded=%d' % (
downloader.status.total_size,
downloader.status.downloaded
))
return exit_status
finally:
if downloader and not downloader.finished:
downloader.failed()
if (not isinstance(args, list) and args.output_file
and args.output_file_specified):
args.output_file.close()
def main(args=sys.argv[1:], env=Environment(), custom_log_error=None):
"""
The main function.
Pre-process args, handle some special types of invocations,
and run the main program with error handling.
Return exit status code. Return exit status code.
@ -182,79 +76,118 @@ def main(args=sys.argv[1:], env=Environment(), custom_log_error=None):
args = decode_args(args, env.stdin_encoding) args = decode_args(args, env.stdin_encoding)
plugin_manager.load_installed_plugins() plugin_manager.load_installed_plugins()
def log_error(msg, *args, **kwargs):
msg = msg % args
level = kwargs.get('level', 'error')
assert level in ['error', 'warning']
env.stderr.write('\nhttp: %s: %s\n' % (level, msg))
from httpie.cli import parser from httpie.cli import parser
if env.config.default_options: if env.config.default_options:
args = env.config.default_options + args args = env.config.default_options + args
if custom_log_error: def error(msg, *args, **kwargs):
log_error = custom_log_error msg = msg % args
level = kwargs.get('level', 'error')
env.stderr.write('\nhttp: %s: %s\n' % (level, msg))
include_debug_info = '--debug' in args debug = '--debug' in args
include_traceback = include_debug_info or '--traceback' in args traceback = debug or '--traceback' in args
exit_status = ExitStatus.OK
if include_debug_info: if debug:
print_debug_info(env) print_debug_info(env)
if args == ['--debug']: if args == ['--debug']:
return ExitStatus.SUCCESS return exit_status
exit_status = ExitStatus.SUCCESS download = None
try: try:
parsed_args = parser.parse_args(args=args, env=env) args = parser.parse_args(args=args, env=env)
except KeyboardInterrupt:
env.stderr.write('\n') if args.download:
if include_traceback: args.follow = True # --download implies --follow.
raise download = Download(
exit_status = ExitStatus.ERROR_CTRL_C output_file=args.output_file,
except SystemExit as e: progress_file=env.stderr,
if e.code != ExitStatus.SUCCESS: resume=args.download_resume
env.stderr.write('\n')
if include_traceback:
raise
exit_status = ExitStatus.ERROR
else:
try:
exit_status = program(
args=parsed_args,
env=env,
log_error=log_error,
) )
download.pre_request(args.headers)
response = get_response(args, config_dir=env.config.directory)
if args.check_status or download:
exit_status = get_exit_status(
http_status=response.status_code,
follow=args.follow
)
if not env.stdout_isatty and exit_status != ExitStatus.OK:
error('HTTP %s %s',
response.raw.status,
response.raw.reason,
level='warning')
write_kwargs = {
'stream': build_output_stream(
args, env, response.request, response),
# This will in fact be `stderr` with `--download`
'outfile': env.stdout,
'flush': env.stdout_isatty or args.stream
}
try:
if env.is_windows and is_py3 and 'colors' in args.prettify:
write_with_colors_win_py3(**write_kwargs)
else:
write(**write_kwargs)
if download and exit_status == ExitStatus.OK:
# Response body download.
download_stream, download_to = download.start(response)
write(
stream=download_stream,
outfile=download_to,
flush=False,
)
download.finish()
if download.interrupted:
exit_status = ExitStatus.ERROR
error('Incomplete download: size=%d; downloaded=%d' % (
download.status.total_size,
download.status.downloaded
))
except IOError as e:
if not traceback and e.errno == errno.EPIPE:
# Ignore broken pipes unless --traceback.
env.stderr.write('\n')
else:
raise
except KeyboardInterrupt: except KeyboardInterrupt:
env.stderr.write('\n') if traceback:
if include_traceback:
raise raise
exit_status = ExitStatus.ERROR_CTRL_C env.stderr.write('\n')
exit_status = ExitStatus.ERROR
except SystemExit as e: except SystemExit as e:
if e.code != ExitStatus.SUCCESS: if e.code != ExitStatus.OK:
env.stderr.write('\n') if traceback:
if include_traceback:
raise raise
env.stderr.write('\n')
exit_status = ExitStatus.ERROR exit_status = ExitStatus.ERROR
except requests.Timeout: except requests.Timeout:
exit_status = ExitStatus.ERROR_TIMEOUT exit_status = ExitStatus.ERROR_TIMEOUT
log_error('Request timed out (%ss).', parsed_args.timeout) error('Request timed out (%ss).', args.timeout)
except requests.TooManyRedirects:
exit_status = ExitStatus.ERROR_TOO_MANY_REDIRECTS
log_error('Too many redirects (--max-redirects=%s).',
parsed_args.max_redirects)
except Exception as e: except Exception as e:
# TODO: Further distinction between expected and unexpected errors. # TODO: Better distinction between expected and unexpected errors.
msg = str(e) # Network errors vs. bugs, etc.
if hasattr(e, 'request'): if traceback:
request = e.request
if hasattr(request, 'url'):
msg += ' while doing %s request to URL: %s' % (
request.method, request.url)
log_error('%s: %s', type(e).__name__, msg)
if include_traceback:
raise raise
error('%s: %s', type(e).__name__, str(e))
exit_status = ExitStatus.ERROR exit_status = ExitStatus.ERROR
finally:
if download and not download.finished:
download.failed()
return exit_status return exit_status

View File

@ -7,7 +7,6 @@ from __future__ import division
import os import os
import re import re
import sys import sys
import errno
import mimetypes import mimetypes
import threading import threading
from time import sleep, time from time import sleep, time
@ -54,8 +53,8 @@ def parse_content_range(content_range, resumed_from):
raise ContentRangeError('Missing Content-Range') raise ContentRangeError('Missing Content-Range')
pattern = ( pattern = (
r'^bytes (?P<first_byte_pos>\d+)-(?P<last_byte_pos>\d+)' '^bytes (?P<first_byte_pos>\d+)-(?P<last_byte_pos>\d+)'
r'/(\*|(?P<instance_length>\d+))$' '/(\*|(?P<instance_length>\d+))$'
) )
match = re.match(pattern, content_range) match = re.match(pattern, content_range)
@ -136,51 +135,16 @@ def filename_from_url(url, content_type):
return fn return fn
def trim_filename(filename, max_len):
if len(filename) > max_len:
trim_by = len(filename) - max_len
name, ext = os.path.splitext(filename)
if trim_by >= len(name):
filename = filename[:-trim_by]
else:
filename = name[:-trim_by] + ext
return filename
def get_filename_max_length(directory):
max_len = 255
try:
pathconf = os.pathconf
except AttributeError:
pass # non-posix
else:
try:
max_len = pathconf(directory, 'PC_NAME_MAX')
except OSError as e:
if e.errno != errno.EINVAL:
raise
return max_len
def trim_filename_if_needed(filename, directory='.', extra=0):
max_len = get_filename_max_length(directory) - extra
if len(filename) > max_len:
filename = trim_filename(filename, max_len)
return filename
def get_unique_filename(filename, exists=os.path.exists): def get_unique_filename(filename, exists=os.path.exists):
attempt = 0 attempt = 0
while True: while True:
suffix = '-' + str(attempt) if attempt > 0 else '' suffix = '-' + str(attempt) if attempt > 0 else ''
try_filename = trim_filename_if_needed(filename, extra=len(suffix)) if not exists(filename + suffix):
try_filename += suffix return filename + suffix
if not exists(try_filename):
return try_filename
attempt += 1 attempt += 1
class Downloader(object): class Download(object):
def __init__(self, output_file=None, def __init__(self, output_file=None,
resume=False, progress_file=sys.stderr): resume=False, progress_file=sys.stderr):
@ -214,8 +178,8 @@ class Downloader(object):
:type request_headers: dict :type request_headers: dict
""" """
# Ask the server not to encode the content so that we can resume, etc. # Disable content encoding so that we can resume, etc.
request_headers['Accept-Encoding'] = 'identity' request_headers['Accept-Encoding'] = None
if self._resume: if self._resume:
bytes_have = os.path.getsize(self._output_file.name) bytes_have = os.path.getsize(self._output_file.name)
if bytes_have: if bytes_have:
@ -237,8 +201,6 @@ class Downloader(object):
""" """
assert not self.status.time_started assert not self.status.time_started
# FIXME: some servers still might sent Content-Encoding: gzip
# <https://github.com/jakubroztocil/httpie/issues/423>
try: try:
total_size = int(response.headers['Content-Length']) total_size = int(response.headers['Content-Length'])
except (KeyError, ValueError, TypeError): except (KeyError, ValueError, TypeError):
@ -337,6 +299,7 @@ class Status(object):
def started(self, resumed_from=0, total_size=None): def started(self, resumed_from=0, total_size=None):
assert self.time_started is None assert self.time_started is None
if total_size is not None:
self.total_size = total_size self.total_size = total_size
self.downloaded = self.resumed_from = resumed_from self.downloaded = self.resumed_from = resumed_from
self.time_started = time() self.time_started = time()
@ -447,8 +410,8 @@ class ProgressReporterThread(threading.Thread):
else 0) else 0)
def sum_up(self): def sum_up(self):
actually_downloaded = ( actually_downloaded = (self.status.downloaded
self.status.downloaded - self.status.resumed_from) - self.status.resumed_from)
time_taken = self.status.time_finished - self.status.time_started time_taken = self.status.time_finished - self.status.time_started
self.output.write(CLEAR_LINE) self.output.write(CLEAR_LINE)

View File

@ -2,23 +2,21 @@
""" """
import os import os
import ssl
import sys import sys
import re import re
import errno import errno
import mimetypes import mimetypes
import getpass import getpass
from io import BytesIO from io import BytesIO
from collections import namedtuple, Iterable, OrderedDict from collections import namedtuple, Iterable
# noinspection PyCompatibility # noinspection PyCompatibility
from argparse import ArgumentParser, ArgumentTypeError, ArgumentError from argparse import ArgumentParser, ArgumentTypeError, ArgumentError
# TODO: Use MultiDict for headers once added to `requests`. # TODO: Use MultiDict for headers once added to `requests`.
# https://github.com/jakubroztocil/httpie/issues/130 # https://github.com/jakubroztocil/httpie/issues/130
from httpie.plugins import plugin_manager
from requests.structures import CaseInsensitiveDict from requests.structures import CaseInsensitiveDict
from httpie.compat import urlsplit, str, is_pypy, is_py27 from httpie.compat import OrderedDict, urlsplit, str, is_pypy, is_py27
from httpie.sessions import VALID_SESSION_NAME_PATTERN from httpie.sessions import VALID_SESSION_NAME_PATTERN
from httpie.utils import load_json_preserve_order from httpie.utils import load_json_preserve_order
@ -29,11 +27,12 @@ URL_SCHEME_RE = re.compile(r'^[a-z][a-z0-9.+-]*://', re.IGNORECASE)
HTTP_POST = 'POST' HTTP_POST = 'POST'
HTTP_GET = 'GET' HTTP_GET = 'GET'
HTTP = 'http://'
HTTPS = 'https://'
# Various separators used in args # Various separators used in args
SEP_HEADERS = ':' SEP_HEADERS = ':'
SEP_HEADERS_EMPTY = ';'
SEP_CREDENTIALS = ':' SEP_CREDENTIALS = ':'
SEP_PROXY = ':' SEP_PROXY = ':'
SEP_DATA = '=' SEP_DATA = '='
@ -67,7 +66,6 @@ SEP_GROUP_RAW_JSON_ITEMS = frozenset([
# Separators allowed in ITEM arguments # Separators allowed in ITEM arguments
SEP_GROUP_ALL_ITEMS = frozenset([ SEP_GROUP_ALL_ITEMS = frozenset([
SEP_HEADERS, SEP_HEADERS,
SEP_HEADERS_EMPTY,
SEP_QUERY, SEP_QUERY,
SEP_DATA, SEP_DATA,
SEP_DATA_RAW_JSON, SEP_DATA_RAW_JSON,
@ -105,22 +103,7 @@ OUTPUT_OPTIONS_DEFAULT = OUT_RESP_HEAD + OUT_RESP_BODY
OUTPUT_OPTIONS_DEFAULT_STDOUT_REDIRECTED = OUT_RESP_BODY OUTPUT_OPTIONS_DEFAULT_STDOUT_REDIRECTED = OUT_RESP_BODY
SSL_VERSION_ARG_MAPPING = { class Parser(ArgumentParser):
'ssl2.3': 'PROTOCOL_SSLv23',
'ssl3': 'PROTOCOL_SSLv3',
'tls1': 'PROTOCOL_TLSv1',
'tls1.1': 'PROTOCOL_TLSv1_1',
'tls1.2': 'PROTOCOL_TLSv1_2',
'tls1.3': 'PROTOCOL_TLSv1_3',
}
SSL_VERSION_ARG_MAPPING = {
cli_arg: getattr(ssl, ssl_constant)
for cli_arg, ssl_constant in SSL_VERSION_ARG_MAPPING.items()
if hasattr(ssl, ssl_constant)
}
class HTTPieArgumentParser(ArgumentParser):
"""Adds additional logic to `argparse.ArgumentParser`. """Adds additional logic to `argparse.ArgumentParser`.
Handles all input (CLI args, file args, stdin), applies defaults, Handles all input (CLI args, file args, stdin), applies defaults,
@ -130,13 +113,13 @@ class HTTPieArgumentParser(ArgumentParser):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
kwargs['add_help'] = False kwargs['add_help'] = False
super(HTTPieArgumentParser, self).__init__(*args, **kwargs) super(Parser, self).__init__(*args, **kwargs)
# noinspection PyMethodOverriding # noinspection PyMethodOverriding
def parse_args(self, env, args=None, namespace=None): def parse_args(self, env, args=None, namespace=None):
self.env = env self.env = env
self.args, no_options = super(HTTPieArgumentParser, self)\ self.args, no_options = super(Parser, self)\
.parse_known_args(args, namespace) .parse_known_args(args, namespace)
if self.args.debug: if self.args.debug:
@ -144,6 +127,7 @@ class HTTPieArgumentParser(ArgumentParser):
# Arguments processing and environment setup. # Arguments processing and environment setup.
self._apply_no_options(no_options) self._apply_no_options(no_options)
self._apply_config()
self._validate_download_options() self._validate_download_options()
self._setup_standard_streams() self._setup_standard_streams()
self._process_output_options() self._process_output_options()
@ -153,7 +137,7 @@ class HTTPieArgumentParser(ArgumentParser):
if not self.args.ignore_stdin and not env.stdin_isatty: if not self.args.ignore_stdin and not env.stdin_isatty:
self._body_from_file(self.env.stdin) self._body_from_file(self.env.stdin)
if not URL_SCHEME_RE.match(self.args.url): if not URL_SCHEME_RE.match(self.args.url):
scheme = self.args.default_scheme + "://" scheme = HTTP
# See if we're using curl style shorthand for localhost (:3000/foo) # See if we're using curl style shorthand for localhost (:3000/foo)
shorthand = re.match(r'^:(?!:)(\d*)(/?.*)$', self.args.url) shorthand = re.match(r'^:(?!:)(\d*)(/?.*)$', self.args.url)
@ -180,17 +164,19 @@ class HTTPieArgumentParser(ArgumentParser):
}.get(file, file) }.get(file, file)
if not hasattr(file, 'buffer') and isinstance(message, str): if not hasattr(file, 'buffer') and isinstance(message, str):
message = message.encode(self.env.stdout_encoding) message = message.encode(self.env.stdout_encoding)
super(HTTPieArgumentParser, self)._print_message(message, file) super(Parser, self)._print_message(message, file)
def _setup_standard_streams(self): def _setup_standard_streams(self):
""" """
Modify `env.stdout` and `env.stdout_isatty` based on args, if needed. Modify `env.stdout` and `env.stdout_isatty` based on args, if needed.
""" """
self.args.output_file_specified = bool(self.args.output_file) if not self.env.stdout_isatty and self.args.output_file:
self.error('Cannot use --output, -o with redirected output.')
if self.args.download: if self.args.download:
# FIXME: Come up with a cleaner solution. # FIXME: Come up with a cleaner solution.
if not self.args.output_file and not self.env.stdout_isatty: if not self.env.stdout_isatty:
# Use stdout as the download output file. # Use stdout as the download output file.
self.args.output_file = self.env.stdout self.args.output_file = self.env.stdout
# With `--download`, we write everything that would normally go to # With `--download`, we write everything that would normally go to
@ -215,15 +201,28 @@ class HTTPieArgumentParser(ArgumentParser):
self.env.stdout = self.args.output_file self.env.stdout = self.args.output_file
self.env.stdout_isatty = False self.env.stdout_isatty = False
def _apply_config(self):
if (not self.args.json
and self.env.config.implicit_content_type == 'form'):
self.args.form = True
def _process_auth(self): def _process_auth(self):
# TODO: refactor """
self.args.auth_plugin = None If only a username provided via --auth, then ask for a password.
default_auth_plugin = plugin_manager.get_auth_plugins()[0] Or, take credentials from the URL, if provided.
auth_type_set = self.args.auth_type is not None
"""
url = urlsplit(self.args.url) url = urlsplit(self.args.url)
if self.args.auth is None and not auth_type_set: if self.args.auth:
if url.username is not None: if not self.args.auth.has_password():
# Stdin already read (if not a tty) so it's save to prompt.
if self.args.ignore_stdin:
self.error('Unable to prompt for passwords because'
' --ignore-stdin is set.')
self.args.auth.prompt_password(url.netloc)
elif url.username is not None:
# Handle http://username:password@hostname/ # Handle http://username:password@hostname/
username = url.username username = url.username
password = url.password or '' password = url.password or ''
@ -234,41 +233,6 @@ class HTTPieArgumentParser(ArgumentParser):
orig=SEP_CREDENTIALS.join([username, password]) orig=SEP_CREDENTIALS.join([username, password])
) )
if self.args.auth is not None or auth_type_set:
if not self.args.auth_type:
self.args.auth_type = default_auth_plugin.auth_type
plugin = plugin_manager.get_auth_plugin(self.args.auth_type)()
if plugin.auth_require and self.args.auth is None:
self.error('--auth required')
plugin.raw_auth = self.args.auth
self.args.auth_plugin = plugin
already_parsed = isinstance(self.args.auth, AuthCredentials)
if self.args.auth is None or not plugin.auth_parse:
self.args.auth = plugin.get_auth()
else:
if already_parsed:
# from the URL
credentials = self.args.auth
else:
credentials = parse_auth(self.args.auth)
if (not credentials.has_password()
and plugin.prompt_password):
if self.args.ignore_stdin:
# Non-tty stdin read by now
self.error(
'Unable to prompt for passwords because'
' --ignore-stdin is set.'
)
credentials.prompt_password(url.netloc)
self.args.auth = plugin.get_auth(
username=credentials.key,
password=credentials.value,
)
def _apply_no_options(self, no_options): def _apply_no_options(self, no_options):
"""For every `--no-OPTION` in `no_options`, set `args.OPTION` to """For every `--no-OPTION` in `no_options`, set `args.OPTION` to
its default value. This allows for un-setting of options, e.g., its default value. This allows for un-setting of options, e.g.,
@ -303,8 +267,7 @@ class HTTPieArgumentParser(ArgumentParser):
""" """
if self.args.data: if self.args.data:
self.error('Request body (from stdin or a file) and request ' self.error('Request body (from stdin or a file) and request '
'data (key=value) cannot be mixed. Pass ' 'data (key=value) cannot be mixed.')
'--ignore-stdin to let key/value take priority.')
self.args.data = getattr(fd, 'buffer', fd).read() self.args.data = getattr(fd, 'buffer', fd).read()
def _guess_method(self): def _guess_method(self):
@ -340,10 +303,8 @@ class HTTPieArgumentParser(ArgumentParser):
# Infer the method # Infer the method
has_data = ( has_data = (
(not self.args.ignore_stdin and not self.env.stdin_isatty) (not self.args.ignore_stdin and not self.env.stdin_isatty)
or any( or any(item.sep in SEP_GROUP_DATA_ITEMS
item.sep in SEP_GROUP_DATA_ITEMS for item in self.args.items)
for item in self.args.items
)
) )
self.args.method = HTTP_POST if has_data else HTTP_GET self.args.method = HTTP_POST if has_data else HTTP_GET
@ -375,14 +336,17 @@ class HTTPieArgumentParser(ArgumentParser):
'Invalid file fields (perhaps you meant --form?): %s' 'Invalid file fields (perhaps you meant --form?): %s'
% ','.join(file_fields)) % ','.join(file_fields))
fn, fd, ct = self.args.files[''] fn, fd = self.args.files['']
self.args.files = {} self.args.files = {}
self._body_from_file(fd) self._body_from_file(fd)
if 'Content-Type' not in self.args.headers: if 'Content-Type' not in self.args.headers:
content_type = get_content_type(fn) mime, encoding = mimetypes.guess_type(fn, strict=False)
if content_type: if mime:
content_type = mime
if encoding:
content_type = '%s; charset=%s' % (mime, encoding)
self.args.headers['Content-Type'] = content_type self.args.headers['Content-Type'] = content_type
def _process_output_options(self): def _process_output_options(self):
@ -391,32 +355,18 @@ class HTTPieArgumentParser(ArgumentParser):
The default output options are stdout-type-sensitive. The default output options are stdout-type-sensitive.
""" """
def check_options(value, option): if not self.args.output_options:
unknown = set(value) - OUTPUT_OPTIONS
if unknown:
self.error('Unknown output options: {0}={1}'.format(
option,
','.join(unknown)
))
if self.args.verbose:
self.args.all = True
if self.args.output_options is None:
if self.args.verbose:
self.args.output_options = ''.join(OUTPUT_OPTIONS)
else:
self.args.output_options = ( self.args.output_options = (
OUTPUT_OPTIONS_DEFAULT OUTPUT_OPTIONS_DEFAULT
if self.env.stdout_isatty if self.env.stdout_isatty
else OUTPUT_OPTIONS_DEFAULT_STDOUT_REDIRECTED else OUTPUT_OPTIONS_DEFAULT_STDOUT_REDIRECTED
) )
if self.args.output_options_history is None: unknown_output_options = set(self.args.output_options) - OUTPUT_OPTIONS
self.args.output_options_history = self.args.output_options if unknown_output_options:
self.error(
check_options(self.args.output_options, '--print') 'Unknown output options: %s' % ','.join(unknown_output_options)
check_options(self.args.output_options_history, '--history-print') )
if self.args.download and OUT_RESP_BODY in self.args.output_options: if self.args.download and OUT_RESP_BODY in self.args.output_options:
# Response body is always downloaded with --download and it goes # Response body is always downloaded with --download and it goes
@ -428,8 +378,7 @@ class HTTPieArgumentParser(ArgumentParser):
if self.args.prettify == PRETTY_STDOUT_TTY_ONLY: if self.args.prettify == PRETTY_STDOUT_TTY_ONLY:
self.args.prettify = PRETTY_MAP[ self.args.prettify = PRETTY_MAP[
'all' if self.env.stdout_isatty else 'none'] 'all' if self.env.stdout_isatty else 'none']
elif (self.args.prettify and self.env.is_windows elif self.args.prettify and self.env.is_windows:
and self.args.output_file):
self.error('Only terminal output can be colorized on Windows.') self.error('Only terminal output can be colorized on Windows.')
else: else:
# noinspection PyTypeChecker # noinspection PyTypeChecker
@ -507,7 +456,7 @@ class KeyValueArgType(object):
"""Represents an escaped character.""" """Represents an escaped character."""
def tokenize(string): def tokenize(string):
r"""Tokenize `string`. There are only two token types - strings """Tokenize `string`. There are only two token types - strings
and escaped characters: and escaped characters:
tokenize(r'foo\=bar\\baz') tokenize(r'foo\=bar\\baz')
@ -572,7 +521,7 @@ class AuthCredentials(KeyValue):
def _getpass(self, prompt): def _getpass(self, prompt):
# To allow mocking. # To allow mocking.
return getpass.getpass(str(prompt)) return getpass.getpass(prompt)
def has_password(self): def has_password(self):
return self.value is not None return self.value is not None
@ -609,9 +558,6 @@ class AuthCredentialsArgType(KeyValueArgType):
) )
parse_auth = AuthCredentialsArgType(SEP_CREDENTIALS)
class RequestItemsDict(OrderedDict): class RequestItemsDict(OrderedDict):
"""Multi-value dict for URL parameters and form data.""" """Multi-value dict for URL parameters and form data."""
@ -626,7 +572,7 @@ class RequestItemsDict(OrderedDict):
else: else:
super(RequestItemsDict, self).__init__(*args, **kwargs) super(RequestItemsDict, self).__init__(*args, **kwargs)
# noinspection PyMethodOverriding #noinspection PyMethodOverriding
def __setitem__(self, key, value): 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. `list` of all the values.
@ -662,21 +608,6 @@ RequestItems = namedtuple('RequestItems',
['headers', 'data', 'files', 'params']) ['headers', 'data', 'files', 'params'])
def get_content_type(filename):
"""
Return the content type for ``filename`` in format appropriate
for Content-Type headers, or ``None`` if the file type is unknown
to ``mimetypes``.
"""
mime, encoding = mimetypes.guess_type(filename, strict=False)
if mime:
content_type = mime
if encoding:
content_type = '%s; charset=%s' % (mime, encoding)
return content_type
def parse_items(items, def parse_items(items,
headers_class=CaseInsensitiveDict, headers_class=CaseInsensitiveDict,
data_class=OrderedDict, data_class=OrderedDict,
@ -690,20 +621,11 @@ def parse_items(items,
data = [] data = []
files = [] files = []
params = [] params = []
for item in items: for item in items:
value = item.value value = item.value
if item.sep == SEP_HEADERS: if item.sep == SEP_HEADERS:
if value == '':
# No value => unset the header
value = None
target = headers
elif item.sep == SEP_HEADERS_EMPTY:
if item.value:
raise ParseError(
'Invalid item "%s" '
'(to specify an empty header use `Header;`)'
% item.orig
)
target = headers target = headers
elif item.sep == SEP_QUERY: elif item.sep == SEP_QUERY:
target = params target = params
@ -711,8 +633,7 @@ def parse_items(items,
try: try:
with open(os.path.expanduser(value), 'rb') as f: with open(os.path.expanduser(value), 'rb') as f:
value = (os.path.basename(value), value = (os.path.basename(value),
BytesIO(f.read()), BytesIO(f.read()))
get_content_type(value))
except IOError as e: except IOError as e:
raise ParseError('"%s": %s' % (item.orig, e)) raise ParseError('"%s": %s' % (item.orig, e))
target = files target = files

View File

@ -48,7 +48,7 @@ class HTTPResponse(HTTPMessage):
def iter_lines(self, chunk_size): def iter_lines(self, chunk_size):
return ((line, b'\n') for line in self._orig.iter_lines(chunk_size)) return ((line, b'\n') for line in self._orig.iter_lines(chunk_size))
# noinspection PyProtectedMember #noinspection PyProtectedMember
@property @property
def headers(self): def headers(self):
original = self._orig.raw._original_response original = self._orig.raw._original_response

View File

@ -1,6 +1,3 @@
from __future__ import absolute_import
import json
import pygments.lexer import pygments.lexer
import pygments.token import pygments.token
import pygments.styles import pygments.styles
@ -8,26 +5,16 @@ import pygments.lexers
import pygments.style 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.lexers.special import TextLexer
from pygments.lexers.text import HttpLexer as PygmentsHttpLexer
from pygments.util import ClassNotFound from pygments.util import ClassNotFound
from httpie.compat import is_windows
from httpie.plugins import FormatterPlugin from httpie.plugins import FormatterPlugin
AUTO_STYLE = 'auto' # Follows terminal ANSI color styles # Colors on Windows via colorama don't look that
DEFAULT_STYLE = AUTO_STYLE # great and fruity seems to give the best result there.
SOLARIZED_STYLE = 'solarized' # Bundled here AVAILABLE_STYLES = set(pygments.styles.STYLE_MAP.keys())
if is_windows: AVAILABLE_STYLES.add('solarized')
# Colors on Windows via colorama don't look that DEFAULT_STYLE = 'solarized'
# great and fruity seems to give the best result there.
DEFAULT_STYLE = 'fruity'
AVAILABLE_STYLES = set(pygments.styles.get_all_styles())
AVAILABLE_STYLES.add(SOLARIZED_STYLE)
AVAILABLE_STYLES.add(AUTO_STYLE)
class ColorFormatter(FormatterPlugin): class ColorFormatter(FormatterPlugin):
@ -40,81 +27,58 @@ class ColorFormatter(FormatterPlugin):
""" """
group_name = 'colors' group_name = 'colors'
def __init__(self, env, explicit_json=False, def __init__(self, env, color_scheme=DEFAULT_STYLE, **kwargs):
color_scheme=DEFAULT_STYLE, **kwargs):
super(ColorFormatter, self).__init__(**kwargs) super(ColorFormatter, self).__init__(**kwargs)
if not env.colors: if not env.colors:
self.enabled = False self.enabled = False
return return
use_auto_style = color_scheme == AUTO_STYLE # Cache to speed things up when we process streamed body by line.
has_256_colors = env.colors == 256 self.lexer_cache = {}
if use_auto_style or not has_256_colors:
http_lexer = PygmentsHttpLexer()
formatter = TerminalFormatter()
else:
http_lexer = SimplifiedHTTPLexer()
formatter = Terminal256Formatter(
style=self.get_style_class(color_scheme)
)
self.explicit_json = explicit_json # --json try:
self.formatter = formatter style_class = pygments.styles.get_style_by_name(color_scheme)
self.http_lexer = http_lexer except ClassNotFound:
style_class = Solarized256Style
if env.colors == 256:
fmt_class = Terminal256Formatter
else:
fmt_class = TerminalFormatter
self.formatter = fmt_class(style=style_class)
def format_headers(self, headers): def format_headers(self, headers):
return pygments.highlight( return pygments.highlight(headers, HTTPLexer(), self.formatter).strip()
code=headers,
lexer=self.http_lexer,
formatter=self.formatter,
).strip()
def format_body(self, body, mime): def format_body(self, body, mime):
lexer = self.get_lexer_for_body(mime, body) lexer = self.get_lexer(mime)
if lexer: if lexer:
body = pygments.highlight( body = pygments.highlight(body, lexer, self.formatter)
code=body,
lexer=lexer,
formatter=self.formatter,
)
return body.strip() return body.strip()
def get_lexer_for_body(self, mime, body): def get_lexer(self, mime):
return get_lexer( if mime in self.lexer_cache:
mime=mime, return self.lexer_cache[mime]
explicit_json=self.explicit_json, self.lexer_cache[mime] = get_lexer(mime)
body=body, return self.lexer_cache[mime]
)
def get_style_class(self, color_scheme):
try:
return pygments.styles.get_style_by_name(color_scheme)
except ClassNotFound:
return Solarized256Style
def get_lexer(mime, explicit_json=False, body=''): def get_lexer(mime):
# Build candidate mime type and lexer names.
mime_types, lexer_names = [mime], [] mime_types, lexer_names = [mime], []
type_, subtype = mime.split('/', 1) type_, subtype = mime.split('/')
if '+' not in subtype: if '+' not in subtype:
lexer_names.append(subtype) lexer_names.append(subtype)
else: else:
subtype_name, subtype_suffix = subtype.split('+', 1) subtype_name, subtype_suffix = subtype.split('+')
lexer_names.extend([subtype_name, subtype_suffix]) lexer_names.extend([subtype_name, subtype_suffix])
mime_types.extend([ mime_types.extend([
'%s/%s' % (type_, subtype_name), '%s/%s' % (type_, subtype_name),
'%s/%s' % (type_, subtype_suffix) '%s/%s' % (type_, subtype_suffix)
]) ])
# as a last resort, if no lexer feels responsible, and
# As a last resort, if no lexer feels responsible, and
# the subtype contains 'json', take the JSON lexer # the subtype contains 'json', take the JSON lexer
if 'json' in subtype: if 'json' in subtype:
lexer_names.append('json') lexer_names.append('json')
# Try to resolve the right lexer.
lexer = None lexer = None
for mime_type in mime_types: for mime_type in mime_types:
try: try:
@ -128,20 +92,10 @@ def get_lexer(mime, explicit_json=False, body=''):
lexer = pygments.lexers.get_lexer_by_name(name) lexer = pygments.lexers.get_lexer_by_name(name)
except ClassNotFound: except ClassNotFound:
pass pass
if explicit_json and body and (not lexer or isinstance(lexer, TextLexer)):
# JSON response with an incorrect Content-Type?
try:
json.loads(body) # FIXME: the body also gets parsed in json.py
except ValueError:
pass # Nope
else:
lexer = pygments.lexers.get_lexer_by_name('json')
return lexer return lexer
class SimplifiedHTTPLexer(pygments.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
@ -189,6 +143,8 @@ class SimplifiedHTTPLexer(pygments.lexer.RegexLexer):
} }
# TODO: As Solarized is not the default theme any longer, it should be removed
# or bundled directly with Pygments so that we don't need to support it.
class Solarized256Style(pygments.style.Style): class Solarized256Style(pygments.style.Style):
""" """
solarized256 solarized256

View File

@ -10,24 +10,17 @@ DEFAULT_INDENT = 4
class JSONFormatter(FormatterPlugin): class JSONFormatter(FormatterPlugin):
def format_body(self, body, mime): def format_body(self, body, mime):
maybe_json = [ if 'json' in mime:
'json',
'javascript',
'text',
]
if (self.kwargs['explicit_json']
or any(token in mime for token in maybe_json)):
try: try:
obj = json.loads(body) obj = json.loads(body)
except ValueError: except ValueError:
pass # Invalid JSON, ignore. # Invalid JSON, ignore.
pass
else: else:
# Indent, sort keys by name, and avoid # Indent, sort keys by name, and avoid
# unicode escapes to improve readability. # unicode escapes to improve readability.
body = json.dumps( body = json.dumps(obj,
obj=obj,
sort_keys=True, sort_keys=True,
ensure_ascii=False, ensure_ascii=False,
indent=DEFAULT_INDENT indent=DEFAULT_INDENT)
)
return body return body

View File

@ -0,0 +1,61 @@
from __future__ import absolute_import
import re
from xml.etree import ElementTree
from httpie.plugins import FormatterPlugin
DECLARATION_RE = re.compile('<\?xml[^\n]+?\?>', flags=re.I)
DOCTYPE_RE = re.compile('<!DOCTYPE[^\n]+?>', flags=re.I)
DEFAULT_INDENT = 4
def indent(elem, indent_text=' ' * DEFAULT_INDENT):
"""
In-place prettyprint formatter
C.f. http://effbot.org/zone/element-lib.htm#prettyprint
"""
def _indent(elem, level=0):
i = "\n" + level * indent_text
if len(elem):
if not elem.text or not elem.text.strip():
elem.text = i + indent_text
if not elem.tail or not elem.tail.strip():
elem.tail = i
for elem in elem:
_indent(elem, level + 1)
if not elem.tail or not elem.tail.strip():
elem.tail = i
else:
if level and (not elem.tail or not elem.tail.strip()):
elem.tail = i
return _indent(elem)
class XMLFormatter(FormatterPlugin):
# TODO: tests
def format_body(self, body, mime):
if 'xml' in mime:
# FIXME: orig NS names get forgotten during the conversion, etc.
try:
root = ElementTree.fromstring(body.encode('utf8'))
except ElementTree.ParseError:
# Ignore invalid XML errors (skips attempting to pretty print)
pass
else:
indent(root)
# Use the original declaration
declaration = DECLARATION_RE.match(body)
doctype = DOCTYPE_RE.match(body)
body = ElementTree.tostring(root, encoding='utf-8')\
.decode('utf8')
if doctype:
body = '%s\n%s' % (doctype.group(0), body)
if declaration:
body = '%s\n%s' % (declaration.group(0), body)
return body

View File

@ -24,7 +24,7 @@ class BinarySuppressedError(Exception):
message = BINARY_SUPPRESSED_NOTICE message = BINARY_SUPPRESSED_NOTICE
def write_stream(stream, outfile, flush): def write(stream, outfile, flush):
"""Write the output stream.""" """Write the output stream."""
try: try:
# Writing bytes so we use the buffer interface (Python 3). # Writing bytes so we use the buffer interface (Python 3).
@ -38,7 +38,7 @@ def write_stream(stream, outfile, flush):
outfile.flush() outfile.flush()
def write_stream_with_colors_win_py3(stream, outfile, flush): def write_with_colors_win_py3(stream, outfile, flush):
"""Like `write`, but colorized chunks are written as text """Like `write`, but colorized chunks are written as text
directly to `outfile` to ensure it gets processed by colorama. directly to `outfile` to ensure it gets processed by colorama.
Applies only to Windows with Python 3 and colorized terminal output. Applies only to Windows with Python 3 and colorized terminal output.
@ -55,15 +55,15 @@ def write_stream_with_colors_win_py3(stream, outfile, flush):
outfile.flush() outfile.flush()
def build_output_stream(args, env, request, response, output_options): def build_output_stream(args, env, request, response):
"""Build and return a chain of iterators over the `request`-`response` """Build and return a chain of iterators over the `request`-`response`
exchange each of which yields `bytes` chunks. exchange each of which yields `bytes` chunks.
""" """
req_h = OUT_REQ_HEAD in output_options req_h = OUT_REQ_HEAD in args.output_options
req_b = OUT_REQ_BODY in output_options req_b = OUT_REQ_BODY in args.output_options
resp_h = OUT_RESP_HEAD in output_options resp_h = OUT_RESP_HEAD in args.output_options
resp_b = OUT_RESP_BODY in output_options resp_b = OUT_RESP_BODY in args.output_options
req = req_h or req_b req = req_h or req_b
resp = resp_h or resp_b resp = resp_h or resp_b
@ -112,12 +112,8 @@ def get_stream_type(env, args):
PrettyStream if args.stream else BufferedPrettyStream, PrettyStream if args.stream else BufferedPrettyStream,
env=env, env=env,
conversion=Conversion(), conversion=Conversion(),
formatting=Formatting( formatting=Formatting(env=env, groups=args.prettify,
env=env, color_scheme=args.style),
groups=args.prettify,
color_scheme=args.style,
explicit_json=args.json,
),
) )
else: else:
Stream = partial(EncodedStream, env=env) Stream = partial(EncodedStream, env=env)

View File

@ -11,6 +11,7 @@ from httpie.plugins.manager import PluginManager
from httpie.plugins.builtin import BasicAuthPlugin, DigestAuthPlugin from httpie.plugins.builtin import BasicAuthPlugin, DigestAuthPlugin
from httpie.output.formatters.headers import HeadersFormatter from httpie.output.formatters.headers import HeadersFormatter
from httpie.output.formatters.json import JSONFormatter from httpie.output.formatters.json import JSONFormatter
from httpie.output.formatters.xml import XMLFormatter
from httpie.output.formatters.colors import ColorFormatter from httpie.output.formatters.colors import ColorFormatter
@ -19,4 +20,5 @@ plugin_manager.register(BasicAuthPlugin,
DigestAuthPlugin) DigestAuthPlugin)
plugin_manager.register(HeadersFormatter, plugin_manager.register(HeadersFormatter,
JSONFormatter, JSONFormatter,
XMLFormatter,
ColorFormatter) ColorFormatter)

View File

@ -15,41 +15,15 @@ class AuthPlugin(BasePlugin):
""" """
Base auth plugin class. Base auth plugin class.
See <https://github.com/httpie/httpie-ntlm> for an example auth plugin. See <https://github.com/jakubroztocil/httpie-ntlm> for an example auth plugin.
See also `test_auth_plugins.py`
""" """
# The value that should be passed to --auth-type # The value that should be passed to --auth-type
# to use this auth plugin. Eg. "my-auth" # to use this auth plugin. Eg. "my-auth"
auth_type = None auth_type = None
# Set to `False` to make it possible to invoke this auth def get_auth(self, username, password):
# plugin without requiring the user to specify credentials
# through `--auth, -a`.
auth_require = True
# By default the `-a` argument is parsed for `username:password`.
# Set this to `False` to disable the parsing and error handling.
auth_parse = True
# If both `auth_parse` and `prompt_password` are set to `True`,
# and the value of `-a` lacks the password part,
# then the user will be prompted to type the password in.
prompt_password = True
# Will be set to the raw value of `-a` (if provided) before
# `get_auth()` gets called.
raw_auth = None
def get_auth(self, username=None, password=None):
""" """
If `auth_parse` is set to `True`, then `username`
and `password` contain the parsed credentials.
Use `self.raw_auth` to access the raw value passed through
`--auth, -a`.
Return a ``requests.auth.AuthBase`` subclass instance. Return a ``requests.auth.AuthBase`` subclass instance.
""" """

View File

@ -5,7 +5,6 @@ import requests.auth
from httpie.plugins.base import AuthPlugin from httpie.plugins.base import AuthPlugin
# noinspection PyAbstractClass
class BuiltinAuthPlugin(AuthPlugin): class BuiltinAuthPlugin(AuthPlugin):
package_name = '(builtin)' package_name = '(builtin)'
@ -36,7 +35,6 @@ class BasicAuthPlugin(BuiltinAuthPlugin):
name = 'Basic HTTP auth' name = 'Basic HTTP auth'
auth_type = 'basic' auth_type = 'basic'
# noinspection PyMethodOverriding
def get_auth(self, username, password): def get_auth(self, username, password):
return HTTPBasicAuth(username, password) return HTTPBasicAuth(username, password)
@ -46,6 +44,5 @@ class DigestAuthPlugin(BuiltinAuthPlugin):
name = 'Digest HTTP auth' name = 'Digest HTTP auth'
auth_type = 'digest' auth_type = 'digest'
# noinspection PyMethodOverriding
def get_auth(self, username, password): def get_auth(self, username, password):
return requests.auth.HTTPDigestAuth(username, password) return requests.auth.HTTPDigestAuth(username, password)

View File

@ -24,9 +24,6 @@ class PluginManager(object):
for plugin in plugins: for plugin in plugins:
self._plugins.append(plugin) self._plugins.append(plugin)
def unregister(self, plugin):
self._plugins.remove(plugin)
def load_installed_plugins(self): def load_installed_plugins(self):
for entry_point_name in ENTRY_POINT_NAMES: for entry_point_name in ENTRY_POINT_NAMES:
for entry_point in iter_entry_points(entry_point_name): for entry_point in iter_entry_points(entry_point_name):
@ -39,7 +36,8 @@ class PluginManager(object):
return [plugin for plugin in self if issubclass(plugin, AuthPlugin)] return [plugin for plugin in self if issubclass(plugin, AuthPlugin)]
def get_auth_plugin_mapping(self): def get_auth_plugin_mapping(self):
return {plugin.auth_type: plugin for plugin in self.get_auth_plugins()} return dict((plugin.auth_type, plugin)
for plugin in self.get_auth_plugins())
def get_auth_plugin(self, auth_type): def get_auth_plugin(self, auth_type):
return self.get_auth_plugin_mapping()[auth_type] return self.get_auth_plugin_mapping()[auth_type]
@ -62,6 +60,6 @@ class PluginManager(object):
if issubclass(plugin, ConverterPlugin)] if issubclass(plugin, ConverterPlugin)]
# Adapters # Adapters
def get_transport_plugins(self): def get_trasnsport_plugins(self):
return [plugin for plugin in self return [plugin for plugin in self
if issubclass(plugin, TransportPlugin)] if issubclass(plugin, TransportPlugin)]

View File

@ -22,7 +22,7 @@ SESSION_IGNORED_HEADER_PREFIXES = ['Content-', 'If-']
def get_response(requests_session, session_name, def get_response(requests_session, session_name,
config_dir, args, read_only=False): config_dir, args, read_only=False):
"""Like `client.get_responses`, but applies permanent """Like `client.get_response`, but applies permanent
aspects of the session to the request. aspects of the session to the request.
""" """
@ -51,10 +51,11 @@ def get_response(requests_session, session_name,
dump_request(kwargs) dump_request(kwargs)
session.update_headers(kwargs['headers']) session.update_headers(kwargs['headers'])
if args.auth_plugin: if args.auth:
session.auth = { session.auth = {
'type': args.auth_plugin.auth_type, 'type': args.auth_type,
'raw_auth': args.auth_plugin.raw_auth, 'username': args.auth.key,
'password': args.auth.value,
} }
elif session.auth: elif session.auth:
kwargs['auth'] = session.auth kwargs['auth'] = session.auth
@ -74,7 +75,7 @@ def get_response(requests_session, session_name,
class Session(BaseConfigDict): class Session(BaseConfigDict):
helpurl = 'https://httpie.org/doc#sessions' helpurl = 'https://github.com/jakubroztocil/httpie#sessions'
about = 'HTTPie session file' about = 'HTTPie session file'
def __init__(self, path, *args, **kwargs): def __init__(self, path, *args, **kwargs):
@ -100,10 +101,6 @@ class Session(BaseConfigDict):
""" """
for name, value in request_headers.items(): for name, value in request_headers.items():
if value is None:
continue # Ignore explicitely unset headers
value = value.decode('utf8') value = value.decode('utf8')
if name == 'User-Agent' and value.startswith('HTTPie/'): if name == 'User-Agent' and value.startswith('HTTPie/'):
continue continue
@ -136,41 +133,20 @@ class Session(BaseConfigDict):
stored_attrs = ['value', 'path', 'secure', 'expires'] stored_attrs = ['value', 'path', 'secure', 'expires']
self['cookies'] = {} self['cookies'] = {}
for cookie in jar: for cookie in jar:
self['cookies'][cookie.name] = { self['cookies'][cookie.name] = dict(
attname: getattr(cookie, attname) (attname, getattr(cookie, attname))
for attname in stored_attrs for attname in stored_attrs
} )
@property @property
def auth(self): def auth(self):
auth = self.get('auth', None) auth = self.get('auth', None)
if not auth or not auth['type']: if not auth or not auth['type']:
return return
auth_plugin = plugin_manager.get_auth_plugin(auth['type'])()
plugin = plugin_manager.get_auth_plugin(auth['type'])() return auth_plugin.get_auth(auth['username'], auth['password'])
credentials = {'username': None, 'password': None}
try:
# New style
plugin.raw_auth = auth['raw_auth']
except KeyError:
# Old style
credentials = {
'username': auth['username'],
'password': auth['password'],
}
else:
if plugin.auth_parse:
from httpie.input import parse_auth
parsed = parse_auth(plugin.raw_auth)
credentials = {
'username': parsed.key,
'password': parsed.value,
}
return plugin.get_auth(**credentials)
@auth.setter @auth.setter
def auth(self, auth): def auth(self, auth):
assert set(['type', 'raw_auth']) == set(auth.keys()) assert set(['type', 'username', 'password']) == set(auth.keys())
self['auth'] = auth self['auth'] = auth

View File

@ -1,28 +1,15 @@
from __future__ import division from __future__ import division
import json import json
from collections import OrderedDict
from httpie.compat import is_py26, OrderedDict
def load_json_preserve_order(s): def load_json_preserve_order(s):
if is_py26:
return json.loads(s)
return json.loads(s, object_pairs_hook=OrderedDict) return json.loads(s, object_pairs_hook=OrderedDict)
def repr_dict_nice(d):
def prepare_dict(d):
for k, v in d.items():
if isinstance(v, dict):
v = dict(prepare_dict(v))
elif isinstance(v, bytes):
v = v.decode('utf8')
elif not isinstance(v, (int, str)):
v = repr(v)
yield k, v
return json.dumps(
dict(prepare_dict(d)),
indent=4, sort_keys=True,
)
def humanize_bytes(n, precision=2): def humanize_bytes(n, precision=2):
# Author: Doug Latornell # Author: Doug Latornell
# Licence: MIT # Licence: MIT
@ -67,3 +54,4 @@ def humanize_bytes(n, precision=2):
# noinspection PyUnboundLocalVariable # noinspection PyUnboundLocalVariable
return '%.*f %s' % (precision, n / factor, suffix) return '%.*f %s' % (precision, n / factor, suffix)

View File

@ -1,8 +1,6 @@
tox tox
mock
pytest pytest
pytest-cov pytest-cov
pytest-httpbin>=0.0.6 pytest-httpbin
docutils docutils
wheel wheel
pycodestyle

View File

@ -1,19 +1,2 @@
[wheel] [wheel]
universal = 1 universal = 1
[tool:pytest]
# <https://docs.pytest.org/en/latest/customize.html>
norecursedirs = tests/fixtures
[pycodestyle]
# <http://pycodestyle.pycqa.org/en/latest/intro.html#configuration>
exclude = .git,.idea,__pycache__,build,dist,.tox,.pytest_cache,*.egg-info
# <http://pycodestyle.pycqa.org/en/latest/intro.html#error-codes>
# E241 - multiple spaces after ,
# E501 - line too long
# W503 - line break before binary operator
ignore = E241,E501,W503

View File

@ -14,6 +14,7 @@ class PyTest(TestCommand):
# and runs the tests with no fancy stuff like parallel execution. # and runs the tests with no fancy stuff like parallel execution.
def finalize_options(self): def finalize_options(self):
TestCommand.finalize_options(self) TestCommand.finalize_options(self)
self.test_suite = True
self.test_args = [ self.test_args = [
'--doctest-modules', '--verbose', '--doctest-modules', '--verbose',
'./httpie', './tests' './httpie', './tests'
@ -30,22 +31,20 @@ tests_require = [
# https://bitbucket.org/pypa/setuptools/issue/196/ # https://bitbucket.org/pypa/setuptools/issue/196/
'pytest-httpbin', 'pytest-httpbin',
'pytest', 'pytest',
'mock',
] ]
install_requires = [ install_requires = [
'requests>=2.18.4', 'requests>=2.3.0',
'Pygments>=2.1.3' 'Pygments>=1.5'
] ]
### Conditional dependencies:
# Conditional dependencies:
# sdist # sdist
if 'bdist_wheel' not in sys.argv: if not 'bdist_wheel' in sys.argv:
try: try:
# noinspection PyUnresolvedReferences #noinspection PyUnresolvedReferences
import argparse import argparse
except ImportError: except ImportError:
install_requires.append('argparse>=1.2.1') install_requires.append('argparse>=1.2.1')
@ -57,8 +56,10 @@ if 'bdist_wheel' not in sys.argv:
# bdist_wheel # bdist_wheel
extras_require = { extras_require = {
# http://wheel.readthedocs.io/en/latest/#defining-conditional-dependencies # http://wheel.readthedocs.org/en/latest/#defining-conditional-dependencies
'python_version == "3.0" or python_version == "3.1"': ['argparse>=1.2.1'], ':python_version == "2.6"'
' or python_version == "3.0"'
' or python_version == "3.1" ': ['argparse>=1.2.1'],
':sys_platform == "win32"': ['colorama>=0.2.4'], ':sys_platform == "win32"': ['colorama>=0.2.4'],
} }
@ -67,7 +68,6 @@ def long_description():
with codecs.open('README.rst', encoding='utf8') as f: with codecs.open('README.rst', encoding='utf8') as f:
return f.read() return f.read()
setup( setup(
name='httpie', name='httpie',
version=httpie.__version__, version=httpie.__version__,
@ -76,7 +76,7 @@ setup(
url='http://httpie.org/', url='http://httpie.org/',
download_url='https://github.com/jakubroztocil/httpie', download_url='https://github.com/jakubroztocil/httpie',
author=httpie.__author__, author=httpie.__author__,
author_email='jakub@roztocil.co', author_email='jakub@roztocil.name',
license=httpie.__licence__, license=httpie.__licence__,
packages=find_packages(), packages=find_packages(),
entry_points={ entry_points={
@ -91,14 +91,14 @@ setup(
classifiers=[ classifiers=[
'Development Status :: 5 - Production/Stable', 'Development Status :: 5 - Production/Stable',
'Programming Language :: Python', 'Programming Language :: Python',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.1', 'Programming Language :: Python :: 3.1',
'Programming Language :: Python :: 3.2', 'Programming Language :: Python :: 3.2',
'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Environment :: Console', 'Environment :: Console',
'Intended Audience :: Developers', 'Intended Audience :: Developers',
'Intended Audience :: System Administrators', 'Intended Audience :: System Administrators',

View File

@ -1,24 +0,0 @@
import pytest
from pytest_httpbin import certs
@pytest.fixture(scope='function', autouse=True)
def httpbin_add_ca_bundle(monkeypatch):
"""
Make pytest-httpbin's CA trusted by default.
(Same as `httpbin_ca_bundle`, just auto-used.).
"""
monkeypatch.setenv('REQUESTS_CA_BUNDLE', certs.where())
@pytest.fixture(scope='function')
def httpbin_secure_untrusted(monkeypatch, httpbin_secure):
"""
Like the `httpbin_secure` fixture, but without the
make-CA-trusted-by-default.
"""
monkeypatch.delenv('REQUESTS_CA_BUNDLE')
return httpbin_secure

View File

@ -12,7 +12,7 @@ def patharg(path):
return path.replace('\\', '\\\\\\') return path.replace('\\', '\\\\\\')
FIXTURES_ROOT = path.join(path.abspath(path.dirname(__file__))) FIXTURES_ROOT = path.join(path.abspath(path.dirname(__file__)), 'fixtures')
FILE_PATH = path.join(FIXTURES_ROOT, 'test.txt') FILE_PATH = path.join(FIXTURES_ROOT, 'test.txt')
JSON_FILE_PATH = path.join(FIXTURES_ROOT, 'test.json') JSON_FILE_PATH = path.join(FIXTURES_ROOT, 'test.json')
BIN_FILE_PATH = path.join(FIXTURES_ROOT, 'test.bin') BIN_FILE_PATH = path.join(FIXTURES_ROOT, 'test.bin')
@ -38,3 +38,4 @@ with open(BIN_FILE_PATH, 'rb') as f:
BIN_FILE_CONTENT = f.read() BIN_FILE_CONTENT = f.read()
UNICODE = FILE_CONTENT UNICODE = FILE_CONTENT

View File

@ -1,75 +1,62 @@
"""HTTP authentication-related tests.""" """HTTP authentication-related tests."""
import mock import requests
import pytest import pytest
from utils import http, add_auth, HTTP_OK, MockEnvironment from utils import http, add_auth, HTTP_OK, TestEnvironment
import httpie.input import httpie.input
import httpie.cli import httpie.cli
def test_basic_auth(httpbin_both): class TestAuth:
def test_basic_auth(self, httpbin):
r = http('--auth=user:password', r = http('--auth=user:password',
'GET', httpbin_both + '/basic-auth/user/password') 'GET', httpbin.url + '/basic-auth/user/password')
assert HTTP_OK in r assert HTTP_OK in r
assert r.json == {'authenticated': True, 'user': 'user'} assert r.json == {'authenticated': True, 'user': 'user'}
@pytest.mark.skipif(
@pytest.mark.parametrize('argument_name', ['--auth-type', '-A']) requests.__version__ == '0.13.6',
def test_digest_auth(httpbin_both, argument_name): reason='Redirects with prefetch=False are broken in Requests 0.13.6')
r = http(argument_name + '=digest', '--auth=user:password', def test_digest_auth(self, httpbin):
'GET', httpbin_both.url + '/digest-auth/auth/user/password') r = http('--auth-type=digest', '--auth=user:password',
'GET', httpbin.url + '/digest-auth/auth/user/password')
assert HTTP_OK in r assert HTTP_OK in r
assert r.json == {'authenticated': True, 'user': 'user'} assert r.json == {'authenticated': True, 'user': 'user'}
def test_password_prompt(self, httpbin):
@mock.patch('httpie.input.AuthCredentials._getpass', httpie.input.AuthCredentials._getpass = lambda self, prompt: 'password'
new=lambda self, prompt: 'password')
def test_password_prompt(httpbin):
r = http('--auth', 'user', r = http('--auth', 'user',
'GET', httpbin.url + '/basic-auth/user/password') 'GET', httpbin.url + '/basic-auth/user/password')
assert HTTP_OK in r assert HTTP_OK in r
assert r.json == {'authenticated': True, 'user': 'user'} assert r.json == {'authenticated': True, 'user': 'user'}
def test_credentials_in_url(self, httpbin):
def test_credentials_in_url(httpbin_both): url = add_auth(httpbin.url + '/basic-auth/user/password',
url = add_auth(httpbin_both.url + '/basic-auth/user/password',
auth='user:password') auth='user:password')
r = http('GET', url) r = http('GET', url)
assert HTTP_OK in r assert HTTP_OK in r
assert r.json == {'authenticated': True, 'user': 'user'} assert r.json == {'authenticated': True, 'user': 'user'}
def test_credentials_in_url_auth_flag_has_priority(self, httpbin):
def test_credentials_in_url_auth_flag_has_priority(httpbin_both):
"""When credentials are passed in URL and via -a at the same time, """When credentials are passed in URL and via -a at the same time,
then the ones from -a are used.""" then the ones from -a are used."""
url = add_auth(httpbin_both.url + '/basic-auth/user/password', url = add_auth(httpbin.url + '/basic-auth/user/password',
auth='user:wrong') auth='user:wrong')
r = http('--auth=user:password', 'GET', url) r = http('--auth=user:password', 'GET', url)
assert HTTP_OK in r assert HTTP_OK in r
assert r.json == {'authenticated': True, 'user': 'user'} assert r.json == {'authenticated': True, 'user': 'user'}
@pytest.mark.parametrize('url', [
@pytest.mark.parametrize('url', [
'username@example.org', 'username@example.org',
'username:@example.org', 'username:@example.org',
]) ])
def test_only_username_in_url(url): def test_only_username_in_url(self, url):
""" """
https://github.com/jakubroztocil/httpie/issues/242 https://github.com/jakubroztocil/httpie/issues/242
""" """
args = httpie.cli.parser.parse_args(args=[url], env=MockEnvironment()) args = httpie.cli.parser.parse_args(args=[url], env=TestEnvironment())
assert args.auth assert args.auth
assert args.auth.username == 'username' assert args.auth.key == 'username'
assert args.auth.password == '' assert args.auth.value == ''
def test_missing_auth(httpbin):
r = http(
'--auth-type=basic',
'GET',
httpbin + '/basic-auth/user/password',
error_exit_ok=True
)
assert HTTP_OK not in r
assert '--auth required' in r.stderr

View File

@ -1,133 +0,0 @@
from mock import mock
from httpie.input import SEP_CREDENTIALS
from httpie.plugins import AuthPlugin, plugin_manager
from utils import http, HTTP_OK
# TODO: run all these tests in session mode as well
USERNAME = 'user'
PASSWORD = 'password'
# Basic auth encoded `USERNAME` and `PASSWORD`
# noinspection SpellCheckingInspection
BASIC_AUTH_HEADER_VALUE = 'Basic dXNlcjpwYXNzd29yZA=='
BASIC_AUTH_URL = '/basic-auth/{0}/{1}'.format(USERNAME, PASSWORD)
AUTH_OK = {'authenticated': True, 'user': USERNAME}
def basic_auth(header=BASIC_AUTH_HEADER_VALUE):
def inner(r):
r.headers['Authorization'] = header
return r
return inner
def test_auth_plugin_parse_auth_false(httpbin):
class Plugin(AuthPlugin):
auth_type = 'test-parse-false'
auth_parse = False
def get_auth(self, username=None, password=None):
assert username is None
assert password is None
assert self.raw_auth == BASIC_AUTH_HEADER_VALUE
return basic_auth(self.raw_auth)
plugin_manager.register(Plugin)
try:
r = http(
httpbin + BASIC_AUTH_URL,
'--auth-type',
Plugin.auth_type,
'--auth',
BASIC_AUTH_HEADER_VALUE,
)
assert HTTP_OK in r
assert r.json == AUTH_OK
finally:
plugin_manager.unregister(Plugin)
def test_auth_plugin_require_auth_false(httpbin):
class Plugin(AuthPlugin):
auth_type = 'test-require-false'
auth_require = False
def get_auth(self, username=None, password=None):
assert self.raw_auth is None
assert username is None
assert password is None
return basic_auth()
plugin_manager.register(Plugin)
try:
r = http(
httpbin + BASIC_AUTH_URL,
'--auth-type',
Plugin.auth_type,
)
assert HTTP_OK in r
assert r.json == AUTH_OK
finally:
plugin_manager.unregister(Plugin)
def test_auth_plugin_require_auth_false_and_auth_provided(httpbin):
class Plugin(AuthPlugin):
auth_type = 'test-require-false-yet-provided'
auth_require = False
def get_auth(self, username=None, password=None):
assert self.raw_auth == USERNAME + SEP_CREDENTIALS + PASSWORD
assert username == USERNAME
assert password == PASSWORD
return basic_auth()
plugin_manager.register(Plugin)
try:
r = http(
httpbin + BASIC_AUTH_URL,
'--auth-type',
Plugin.auth_type,
'--auth',
USERNAME + SEP_CREDENTIALS + PASSWORD,
)
assert HTTP_OK in r
assert r.json == AUTH_OK
finally:
plugin_manager.unregister(Plugin)
@mock.patch('httpie.input.AuthCredentials._getpass',
new=lambda self, prompt: 'UNEXPECTED_PROMPT_RESPONSE')
def test_auth_plugin_prompt_password_false(httpbin):
class Plugin(AuthPlugin):
auth_type = 'test-prompt-false'
prompt_password = False
def get_auth(self, username=None, password=None):
assert self.raw_auth == USERNAME
assert username == USERNAME
assert password is None
return basic_auth()
plugin_manager.register(Plugin)
try:
r = http(
httpbin + BASIC_AUTH_URL,
'--auth-type',
Plugin.auth_type,
'--auth',
USERNAME,
)
assert HTTP_OK in r
assert r.json == AUTH_OK
finally:
plugin_manager.unregister(Plugin)

View File

@ -1,16 +1,14 @@
"""Tests for dealing with binary request and response data.""" """Tests for dealing with binary request and response data."""
import requests from httpie.compat import urlopen
from fixtures import BIN_FILE_PATH, BIN_FILE_CONTENT, BIN_FILE_PATH_ARG
from httpie.output.streams import BINARY_SUPPRESSED_NOTICE from httpie.output.streams import BINARY_SUPPRESSED_NOTICE
from utils import MockEnvironment, http from utils import TestEnvironment, http
from fixtures import BIN_FILE_PATH, BIN_FILE_CONTENT, BIN_FILE_PATH_ARG
class TestBinaryRequestData: class TestBinaryRequestData:
def test_binary_stdin(self, httpbin): def test_binary_stdin(self, httpbin):
with open(BIN_FILE_PATH, 'rb') as stdin: with open(BIN_FILE_PATH, 'rb') as stdin:
env = MockEnvironment( env = TestEnvironment(
stdin=stdin, stdin=stdin,
stdin_isatty=False, stdin_isatty=False,
stdout_isatty=False stdout_isatty=False
@ -19,32 +17,38 @@ class TestBinaryRequestData:
assert r == BIN_FILE_CONTENT assert r == BIN_FILE_CONTENT
def test_binary_file_path(self, httpbin): def test_binary_file_path(self, httpbin):
env = MockEnvironment(stdin_isatty=True, stdout_isatty=False) env = TestEnvironment(stdin_isatty=True, stdout_isatty=False)
r = http('--print=B', 'POST', httpbin.url + '/post', r = http('--print=B', 'POST', httpbin.url + '/post',
'@' + BIN_FILE_PATH_ARG, env=env, ) '@' + BIN_FILE_PATH_ARG, env=env, )
assert r == BIN_FILE_CONTENT assert r == BIN_FILE_CONTENT
def test_binary_file_form(self, httpbin): def test_binary_file_form(self, httpbin):
env = MockEnvironment(stdin_isatty=True, stdout_isatty=False) env = TestEnvironment(stdin_isatty=True, stdout_isatty=False)
r = http('--print=B', '--form', 'POST', httpbin.url + '/post', r = http('--print=B', '--form', 'POST', httpbin.url + '/post',
'test@' + BIN_FILE_PATH_ARG, env=env) 'test@' + BIN_FILE_PATH_ARG, env=env)
assert bytes(BIN_FILE_CONTENT) in bytes(r) assert bytes(BIN_FILE_CONTENT) in bytes(r)
class TestBinaryResponseData: class TestBinaryResponseData:
url = 'http://www.google.com/favicon.ico'
def test_binary_suppresses_when_terminal(self, httpbin): @property
r = http('GET', httpbin + '/bytes/1024') def bindata(self):
if not hasattr(self, '_bindata'):
self._bindata = urlopen(self.url).read()
return self._bindata
def test_binary_suppresses_when_terminal(self):
r = http('GET', self.url)
assert BINARY_SUPPRESSED_NOTICE.decode() in r assert BINARY_SUPPRESSED_NOTICE.decode() in r
def test_binary_suppresses_when_not_terminal_but_pretty(self, httpbin): def test_binary_suppresses_when_not_terminal_but_pretty(self):
env = MockEnvironment(stdin_isatty=True, stdout_isatty=False) env = TestEnvironment(stdin_isatty=True, stdout_isatty=False)
r = http('--pretty=all', 'GET', httpbin + '/bytes/1024', env=env) r = http('--pretty=all', 'GET', self.url,
env=env)
assert BINARY_SUPPRESSED_NOTICE.decode() in r assert BINARY_SUPPRESSED_NOTICE.decode() in r
def test_binary_included_and_correct_when_suitable(self, httpbin): def test_binary_included_and_correct_when_suitable(self):
env = MockEnvironment(stdin_isatty=True, stdout_isatty=False) env = TestEnvironment(stdin_isatty=True, stdout_isatty=False)
url = httpbin + '/bytes/1024?seed=1' r = http('GET', self.url, env=env)
r = http('GET', url, env=env) assert r == self.bindata
expected = requests.get(url).content
assert r == expected

View File

@ -10,7 +10,7 @@ from httpie import input
from httpie.input import KeyValue, KeyValueArgType, DataDict from httpie.input import KeyValue, KeyValueArgType, DataDict
from httpie import ExitStatus from httpie import ExitStatus
from httpie.cli import parser from httpie.cli import parser
from utils import MockEnvironment, http, HTTP_OK from utils import TestEnvironment, http, HTTP_OK
from fixtures import ( from fixtures import (
FILE_PATH_ARG, JSON_FILE_PATH_ARG, FILE_PATH_ARG, JSON_FILE_PATH_ARG,
JSON_FILE_CONTENT, FILE_CONTENT, FILE_PATH JSON_FILE_CONTENT, FILE_CONTENT, FILE_PATH
@ -49,9 +49,9 @@ class TestItemParsing:
assert 'bar@baz' in items.files assert 'bar@baz' in items.files
@pytest.mark.parametrize(('string', 'key', 'sep', 'value'), [ @pytest.mark.parametrize(('string', 'key', 'sep', 'value'), [
('path=c:\\windows', 'path', '=', 'c:\\windows'), ('path=c:\windows', 'path', '=', 'c:\windows'),
('path=c:\\windows\\', 'path', '=', 'c:\\windows\\'), ('path=c:\windows\\', 'path', '=', 'c:\windows\\'),
('path\\==c:\\windows', 'path=', '=', 'c:\\windows'), ('path\==c:\windows', 'path=', '=', 'c:\windows'),
]) ])
def test_backslash_before_non_special_character_does_not_escape( def test_backslash_before_non_special_character_does_not_escape(
self, string, key, sep, value): self, string, key, sep, value):
@ -68,11 +68,10 @@ class TestItemParsing:
def test_valid_items(self): def test_valid_items(self):
items = input.parse_items([ items = input.parse_items([
self.key_value('string=value'), self.key_value('string=value'),
self.key_value('Header:value'), self.key_value('header:value'),
self.key_value('Unset-Header:'),
self.key_value('Empty-Header;'),
self.key_value('list:=["a", 1, {}, false]'), self.key_value('list:=["a", 1, {}, false]'),
self.key_value('obj:={"a": "b"}'), self.key_value('obj:={"a": "b"}'),
self.key_value('eh:'),
self.key_value('ed='), self.key_value('ed='),
self.key_value('bool:=true'), self.key_value('bool:=true'),
self.key_value('file@' + FILE_PATH_ARG), self.key_value('file@' + FILE_PATH_ARG),
@ -84,11 +83,7 @@ class TestItemParsing:
# Parsed headers # Parsed headers
# `requests.structures.CaseInsensitiveDict` => `dict` # `requests.structures.CaseInsensitiveDict` => `dict`
headers = dict(items.headers._store.values()) headers = dict(items.headers._store.values())
assert headers == { assert headers == {'header': 'value', 'eh': ''}
'Header': 'value',
'Unset-Header': None,
'Empty-Header': ''
}
# Parsed data # Parsed data
raw_json_embed = items.data.pop('raw-json-embed') raw_json_embed = items.data.pop('raw-json-embed')
@ -108,8 +103,8 @@ class TestItemParsing:
# Parsed file fields # Parsed file fields
assert 'file' in items.files assert 'file' in items.files
assert (items.files['file'][1].read().strip(). assert (items.files['file'][1].read().strip().decode('utf8')
decode('utf8') == FILE_CONTENT) == FILE_CONTENT)
def test_multiple_file_fields_with_same_field_name(self): def test_multiple_file_fields_with_same_field_name(self):
items = input.parse_items([ items = input.parse_items([
@ -159,46 +154,46 @@ class TestQuerystring:
assert '"url": "%s"' % url in r assert '"url": "%s"' % url in r
class TestLocalhostShorthand: class TestURLshorthand:
def test_expand_localhost_shorthand(self): def test_expand_localhost_shorthand(self):
args = parser.parse_args(args=[':'], env=MockEnvironment()) args = parser.parse_args(args=[':'], env=TestEnvironment())
assert args.url == 'http://localhost' assert args.url == 'http://localhost'
def test_expand_localhost_shorthand_with_slash(self): def test_expand_localhost_shorthand_with_slash(self):
args = parser.parse_args(args=[':/'], env=MockEnvironment()) args = parser.parse_args(args=[':/'], env=TestEnvironment())
assert args.url == 'http://localhost/' assert args.url == 'http://localhost/'
def test_expand_localhost_shorthand_with_port(self): def test_expand_localhost_shorthand_with_port(self):
args = parser.parse_args(args=[':3000'], env=MockEnvironment()) args = parser.parse_args(args=[':3000'], env=TestEnvironment())
assert args.url == 'http://localhost:3000' assert args.url == 'http://localhost:3000'
def test_expand_localhost_shorthand_with_path(self): def test_expand_localhost_shorthand_with_path(self):
args = parser.parse_args(args=[':/path'], env=MockEnvironment()) args = parser.parse_args(args=[':/path'], env=TestEnvironment())
assert args.url == 'http://localhost/path' assert args.url == 'http://localhost/path'
def test_expand_localhost_shorthand_with_port_and_slash(self): def test_expand_localhost_shorthand_with_port_and_slash(self):
args = parser.parse_args(args=[':3000/'], env=MockEnvironment()) args = parser.parse_args(args=[':3000/'], env=TestEnvironment())
assert args.url == 'http://localhost:3000/' assert args.url == 'http://localhost:3000/'
def test_expand_localhost_shorthand_with_port_and_path(self): def test_expand_localhost_shorthand_with_port_and_path(self):
args = parser.parse_args(args=[':3000/path'], env=MockEnvironment()) args = parser.parse_args(args=[':3000/path'], env=TestEnvironment())
assert args.url == 'http://localhost:3000/path' assert args.url == 'http://localhost:3000/path'
def test_dont_expand_shorthand_ipv6_as_shorthand(self): def test_dont_expand_shorthand_ipv6_as_shorthand(self):
args = parser.parse_args(args=['::1'], env=MockEnvironment()) args = parser.parse_args(args=['::1'], env=TestEnvironment())
assert args.url == 'http://::1' assert args.url == 'http://::1'
def test_dont_expand_longer_ipv6_as_shorthand(self): def test_dont_expand_longer_ipv6_as_shorthand(self):
args = parser.parse_args( args = parser.parse_args(
args=['::ffff:c000:0280'], args=['::ffff:c000:0280'],
env=MockEnvironment() env=TestEnvironment()
) )
assert args.url == 'http://::ffff:c000:0280' assert args.url == 'http://::ffff:c000:0280'
def test_dont_expand_full_ipv6_as_shorthand(self): def test_dont_expand_full_ipv6_as_shorthand(self):
args = parser.parse_args( args = parser.parse_args(
args=['0000:0000:0000:0000:0000:0000:0000:0001'], args=['0000:0000:0000:0000:0000:0000:0000:0001'],
env=MockEnvironment() env=TestEnvironment()
) )
assert args.url == 'http://0000:0000:0000:0000:0000:0000:0000:0001' assert args.url == 'http://0000:0000:0000:0000:0000:0000:0000:0001'
@ -206,7 +201,7 @@ class TestLocalhostShorthand:
class TestArgumentParser: class TestArgumentParser:
def setup_method(self, method): def setup_method(self, method):
self.parser = input.HTTPieArgumentParser() self.parser = input.Parser()
def test_guess_when_method_set_and_valid(self): def test_guess_when_method_set_and_valid(self):
self.parser.args = argparse.Namespace() self.parser.args = argparse.Namespace()
@ -215,7 +210,7 @@ class TestArgumentParser:
self.parser.args.items = [] self.parser.args.items = []
self.parser.args.ignore_stdin = False self.parser.args.ignore_stdin = False
self.parser.env = MockEnvironment() self.parser.env = TestEnvironment()
self.parser._guess_method() self.parser._guess_method()
@ -229,7 +224,7 @@ class TestArgumentParser:
self.parser.args.url = 'http://example.com/' self.parser.args.url = 'http://example.com/'
self.parser.args.items = [] self.parser.args.items = []
self.parser.args.ignore_stdin = False self.parser.args.ignore_stdin = False
self.parser.env = MockEnvironment() self.parser.env = TestEnvironment()
self.parser._guess_method() self.parser._guess_method()
@ -243,7 +238,7 @@ class TestArgumentParser:
self.parser.args.url = 'data=field' self.parser.args.url = 'data=field'
self.parser.args.items = [] self.parser.args.items = []
self.parser.args.ignore_stdin = False self.parser.args.ignore_stdin = False
self.parser.env = MockEnvironment() self.parser.env = TestEnvironment()
self.parser._guess_method() self.parser._guess_method()
assert self.parser.args.method == 'POST' assert self.parser.args.method == 'POST'
@ -262,7 +257,7 @@ class TestArgumentParser:
self.parser.args.items = [] self.parser.args.items = []
self.parser.args.ignore_stdin = False self.parser.args.ignore_stdin = False
self.parser.env = MockEnvironment() self.parser.env = TestEnvironment()
self.parser._guess_method() self.parser._guess_method()
@ -285,7 +280,7 @@ class TestArgumentParser:
] ]
self.parser.args.ignore_stdin = False self.parser.args.ignore_stdin = False
self.parser.env = MockEnvironment() self.parser.env = TestEnvironment()
self.parser._guess_method() self.parser._guess_method()
@ -314,7 +309,7 @@ class TestIgnoreStdin:
def test_ignore_stdin(self, httpbin): def test_ignore_stdin(self, httpbin):
with open(FILE_PATH) as f: with open(FILE_PATH) as f:
env = MockEnvironment(stdin=f, stdin_isatty=False) env = TestEnvironment(stdin=f, stdin_isatty=False)
r = http('--ignore-stdin', '--verbose', httpbin.url + '/get', r = http('--ignore-stdin', '--verbose', httpbin.url + '/get',
env=env) env=env)
assert HTTP_OK in r assert HTTP_OK in r
@ -330,18 +325,8 @@ class TestIgnoreStdin:
class TestSchemes: class TestSchemes:
def test_invalid_custom_scheme(self): def test_custom_scheme(self):
# InvalidSchema is expected because HTTPie # InvalidSchema is expected because HTTPie
# shouldn't touch a formally valid scheme. # shouldn't touch a formally valid scheme.
with pytest.raises(InvalidSchema): with pytest.raises(InvalidSchema):
http('foo+bar-BAZ.123://bah') http('foo+bar-BAZ.123://bah')
def test_invalid_scheme_via_via_default_scheme(self):
# InvalidSchema is expected because HTTPie
# shouldn't touch a formally valid scheme.
with pytest.raises(InvalidSchema):
http('bah', '--default=scheme=foo+bar-BAZ.123')
def test_default_scheme(self, httpbin_secure):
url = '{0}:{1}'.format(httpbin_secure.host, httpbin_secure.port)
assert HTTP_OK in http(url, '--default-scheme=https')

View File

@ -1,40 +0,0 @@
from httpie import __version__
from utils import MockEnvironment, http
from httpie.context import Environment
def test_default_options(httpbin):
env = MockEnvironment()
env.config['default_options'] = ['--form']
env.config.save()
r = http(httpbin.url + '/post', 'foo=bar', env=env)
assert r.json['form'] == {"foo": "bar"}
def test_default_options_overwrite(httpbin):
env = MockEnvironment()
env.config['default_options'] = ['--form']
env.config.save()
r = http('--json', httpbin.url + '/post', 'foo=bar', env=env)
assert r.json['json'] == {"foo": "bar"}
def test_migrate_implicit_content_type():
config = MockEnvironment().config
config['implicit_content_type'] = 'json'
config.save()
config.load()
assert 'implicit_content_type' not in config
assert not config['default_options']
config['implicit_content_type'] = 'form'
config.save()
config.load()
assert 'implicit_content_type' not in config
assert config['default_options'] == ['--form']
def test_current_version():
version = Environment().config['__meta__']['httpie']
assert version == __version__

View File

@ -2,26 +2,10 @@
Tests for the provided defaults regarding HTTP method, and --json vs. --form. Tests for the provided defaults regarding HTTP method, and --json vs. --form.
""" """
from httpie.client import JSON_ACCEPT from utils import TestEnvironment, http, HTTP_OK, no_content_type
from utils import MockEnvironment, http, HTTP_OK
from fixtures import FILE_PATH from fixtures import FILE_PATH
def test_default_headers_case_insensitive(httpbin):
"""
<https://github.com/jakubroztocil/httpie/issues/644>
"""
r = http(
'--debug',
'--print=H',
httpbin.url + '/post',
'CONTENT-TYPE:application/json-patch+json',
'a=b',
)
assert 'CONTENT-TYPE: application/json-patch+json' in r
assert 'Content-Type' not in r
class TestImplicitHTTPMethod: class TestImplicitHTTPMethod:
def test_implicit_GET(self, httpbin): def test_implicit_GET(self, httpbin):
r = http(httpbin.url + '/get') r = http(httpbin.url + '/get')
@ -44,7 +28,7 @@ class TestImplicitHTTPMethod:
def test_implicit_POST_stdin(self, httpbin): def test_implicit_POST_stdin(self, httpbin):
with open(FILE_PATH) as f: with open(FILE_PATH) as f:
env = MockEnvironment(stdin_isatty=False, stdin=f) env = TestEnvironment(stdin_isatty=False, stdin=f)
r = http('--form', httpbin.url + '/post', env=env) r = http('--form', httpbin.url + '/post', env=env)
assert HTTP_OK in r assert HTTP_OK in r
@ -62,7 +46,7 @@ class TestAutoContentTypeAndAcceptHeaders:
r = http('GET', httpbin.url + '/headers') r = http('GET', httpbin.url + '/headers')
assert HTTP_OK in r assert HTTP_OK in r
assert r.json['headers']['Accept'] == '*/*' assert r.json['headers']['Accept'] == '*/*'
assert 'Content-Type' not in r.json['headers'] assert no_content_type(r.json['headers'])
def test_POST_no_data_no_auto_headers(self, httpbin): def test_POST_no_data_no_auto_headers(self, httpbin):
# JSON headers shouldn't be automatically set for POST with no data. # JSON headers shouldn't be automatically set for POST with no data.
@ -74,20 +58,20 @@ class TestAutoContentTypeAndAcceptHeaders:
def test_POST_with_data_auto_JSON_headers(self, httpbin): def test_POST_with_data_auto_JSON_headers(self, httpbin):
r = http('POST', httpbin.url + '/post', 'a=b') r = http('POST', httpbin.url + '/post', 'a=b')
assert HTTP_OK in r assert HTTP_OK in r
assert r.json['headers']['Accept'] == JSON_ACCEPT assert '"Accept": "application/json"' in r
assert r.json['headers']['Content-Type'] == 'application/json' assert '"Content-Type": "application/json' in r
def test_GET_with_data_auto_JSON_headers(self, httpbin): def test_GET_with_data_auto_JSON_headers(self, httpbin):
# JSON headers should automatically be set also for GET with data. # JSON headers should automatically be set also for GET with data.
r = http('POST', httpbin.url + '/post', 'a=b') r = http('POST', httpbin.url + '/post', 'a=b')
assert HTTP_OK in r assert HTTP_OK in r
assert r.json['headers']['Accept'] == JSON_ACCEPT assert '"Accept": "application/json"' in r, r
assert r.json['headers']['Content-Type'] == 'application/json' assert '"Content-Type": "application/json' in r
def test_POST_explicit_JSON_auto_JSON_accept(self, httpbin): def test_POST_explicit_JSON_auto_JSON_accept(self, httpbin):
r = http('--json', 'POST', httpbin.url + '/post') r = http('--json', 'POST', httpbin.url + '/post')
assert HTTP_OK in r assert HTTP_OK in r
assert r.json['headers']['Accept'] == JSON_ACCEPT assert r.json['headers']['Accept'] == 'application/json'
# Make sure Content-Type gets set even with no data. # Make sure Content-Type gets set even with no data.
# https://github.com/jakubroztocil/httpie/issues/137 # https://github.com/jakubroztocil/httpie/issues/137
assert 'application/json' in r.json['headers']['Content-Type'] assert 'application/json' in r.json['headers']['Content-Type']
@ -112,11 +96,11 @@ class TestAutoContentTypeAndAcceptHeaders:
assert '"Content-Type": "application/xml"' in r assert '"Content-Type": "application/xml"' in r
def test_print_only_body_when_stdout_redirected_by_default(self, httpbin): def test_print_only_body_when_stdout_redirected_by_default(self, httpbin):
env = MockEnvironment(stdin_isatty=True, stdout_isatty=False) env = TestEnvironment(stdin_isatty=True, stdout_isatty=False)
r = http('GET', httpbin.url + '/get', env=env) r = http('GET', httpbin.url + '/get', env=env)
assert 'HTTP/' not in r assert 'HTTP/' not in r
def test_print_overridable_when_stdout_redirected(self, httpbin): def test_print_overridable_when_stdout_redirected(self, httpbin):
env = MockEnvironment(stdin_isatty=True, stdout_isatty=False) env = TestEnvironment(stdin_isatty=True, stdout_isatty=False)
r = http('--print=h', 'GET', httpbin.url + '/get', env=env) r = http('--print=h', 'GET', httpbin.url + '/get', env=env)
assert HTTP_OK in r assert HTTP_OK in r

View File

@ -9,7 +9,7 @@ from utils import TESTS_ROOT
def has_docutils(): def has_docutils():
try: try:
# noinspection PyUnresolvedReferences #noinspection PyUnresolvedReferences
import docutils import docutils
return True return True
except ImportError: except ImportError:
@ -36,4 +36,4 @@ def test_rst_file_syntax(filename):
stdout=subprocess.PIPE stdout=subprocess.PIPE
) )
err = p.communicate()[1] err = p.communicate()[1]
assert p.returncode == 0, err.decode('utf8') assert p.returncode == 0, err

View File

@ -2,15 +2,14 @@ import os
import time import time
import pytest import pytest
import mock
from requests.structures import CaseInsensitiveDict from requests.structures import CaseInsensitiveDict
from httpie.compat import urlopen from httpie.compat import urlopen
from httpie.downloads import ( from httpie.downloads import (
parse_content_range, filename_from_content_disposition, filename_from_url, parse_content_range, filename_from_content_disposition, filename_from_url,
get_unique_filename, ContentRangeError, Downloader, get_unique_filename, ContentRangeError, Download,
) )
from utils import http, MockEnvironment from utils import http, TestEnvironment
class Response(object): class Response(object):
@ -75,31 +74,7 @@ class TestDownloadUtils:
content_type='x-foo/bar' content_type='x-foo/bar'
) )
@pytest.mark.parametrize( def test_unique_filename(self):
'orig_name, unique_on_attempt, expected',
[
# Simple
('foo.bar', 0, 'foo.bar'),
('foo.bar', 1, 'foo.bar-1'),
('foo.bar', 10, 'foo.bar-10'),
# Trim
('A' * 20, 0, 'A' * 10),
('A' * 20, 1, 'A' * 8 + '-1'),
('A' * 20, 10, 'A' * 7 + '-10'),
# Trim before ext
('A' * 20 + '.txt', 0, 'A' * 6 + '.txt'),
('A' * 20 + '.txt', 1, 'A' * 4 + '.txt-1'),
# Trim at the end
('foo.' + 'A' * 20, 0, 'foo.' + 'A' * 6),
('foo.' + 'A' * 20, 1, 'foo.' + 'A' * 4 + '-1'),
('foo.' + 'A' * 20, 10, 'foo.' + 'A' * 3 + '-10'),
]
)
@mock.patch('httpie.downloads.get_filename_max_length')
def test_unique_filename(self, get_filename_max_length,
orig_name, unique_on_attempt,
expected):
def attempts(unique_on_attempt=0): def attempts(unique_on_attempt=0):
# noinspection PyUnresolvedReferences,PyUnusedLocal # noinspection PyUnresolvedReferences,PyUnusedLocal
def exists(filename): def exists(filename):
@ -111,55 +86,54 @@ class TestDownloadUtils:
exists.attempt = 0 exists.attempt = 0
return exists return exists
get_filename_max_length.return_value = 10 assert 'foo.bar' == get_unique_filename('foo.bar', attempts(0))
assert 'foo.bar-1' == get_unique_filename('foo.bar', attempts(1))
actual = get_unique_filename(orig_name, attempts(unique_on_attempt)) assert 'foo.bar-10' == get_unique_filename('foo.bar', attempts(10))
assert expected == actual
class TestDownloads: class TestDownloads:
# TODO: more tests # TODO: more tests
def test_actual_download(self, httpbin_both, httpbin): def test_actual_download(self, httpbin):
robots_txt = '/robots.txt' url = httpbin.url + '/robots.txt'
body = urlopen(httpbin + robots_txt).read().decode() body = urlopen(url).read().decode()
env = MockEnvironment(stdin_isatty=True, stdout_isatty=False) env = TestEnvironment(stdin_isatty=True, stdout_isatty=False)
r = http('--download', httpbin_both.url + robots_txt, env=env) r = http('--download', url, env=env)
assert 'Downloading' in r.stderr assert 'Downloading' in r.stderr
assert '[K' in r.stderr assert '[K' in r.stderr
assert 'Done' in r.stderr assert 'Done' in r.stderr
assert body == r assert body == r
def test_download_with_Content_Length(self, httpbin_both): def test_download_with_Content_Length(self, httpbin):
devnull = open(os.devnull, 'w') devnull = open(os.devnull, 'w')
downloader = Downloader(output_file=devnull, progress_file=devnull) download = Download(output_file=devnull, progress_file=devnull)
downloader.start(Response( download.start(Response(
url=httpbin_both.url + '/', url=httpbin.url + '/',
headers={'Content-Length': 10} headers={'Content-Length': 10}
)) ))
time.sleep(1.1) time.sleep(1.1)
downloader.chunk_downloaded(b'12345') download.chunk_downloaded(b'12345')
time.sleep(1.1) time.sleep(1.1)
downloader.chunk_downloaded(b'12345') download.chunk_downloaded(b'12345')
downloader.finish() download.finish()
assert not downloader.interrupted assert not download.interrupted
def test_download_no_Content_Length(self, httpbin_both): def test_download_no_Content_Length(self, httpbin):
devnull = open(os.devnull, 'w') devnull = open(os.devnull, 'w')
downloader = Downloader(output_file=devnull, progress_file=devnull) download = Download(output_file=devnull, progress_file=devnull)
downloader.start(Response(url=httpbin_both.url + '/')) download.start(Response(url=httpbin.url + '/'))
time.sleep(1.1) time.sleep(1.1)
downloader.chunk_downloaded(b'12345') download.chunk_downloaded(b'12345')
downloader.finish() download.finish()
assert not downloader.interrupted assert not download.interrupted
def test_download_interrupted(self, httpbin_both): def test_download_interrupted(self, httpbin):
devnull = open(os.devnull, 'w') devnull = open(os.devnull, 'w')
downloader = Downloader(output_file=devnull, progress_file=devnull) download = Download(output_file=devnull, progress_file=devnull)
downloader.start(Response( download.start(Response(
url=httpbin_both.url + '/', url=httpbin.url + '/',
headers={'Content-Length': 5} headers={'Content-Length': 5}
)) ))
downloader.chunk_downloaded(b'1234') download.chunk_downloaded(b'1234')
downloader.finish() download.finish()
assert downloader.interrupted assert download.interrupted

View File

@ -1,49 +0,0 @@
import mock
from pytest import raises
from requests import Request, Timeout
from requests.exceptions import ConnectionError
from httpie import ExitStatus
from httpie.core import main
error_msg = None
@mock.patch('httpie.core.get_response')
def test_error(get_response):
def error(msg, *args, **kwargs):
global error_msg
error_msg = msg % args
exc = ConnectionError('Connection aborted')
exc.request = Request(method='GET', url='http://www.google.com')
get_response.side_effect = exc
ret = main(['--ignore-stdin', 'www.google.com'], custom_log_error=error)
assert ret == ExitStatus.ERROR
assert error_msg == (
'ConnectionError: '
'Connection aborted while doing GET request to URL: '
'http://www.google.com')
@mock.patch('httpie.core.get_response')
def test_error_traceback(get_response):
exc = ConnectionError('Connection aborted')
exc.request = Request(method='GET', url='http://www.google.com')
get_response.side_effect = exc
with raises(ConnectionError):
main(['--ignore-stdin', '--traceback', 'www.google.com'])
@mock.patch('httpie.core.get_response')
def test_timeout(get_response):
def error(msg, *args, **kwargs):
global error_msg
error_msg = msg % args
exc = Timeout('Request timed out')
exc.request = Request(method='GET', url='http://www.google.com')
get_response.side_effect = exc
ret = main(['--ignore-stdin', 'www.google.com'], custom_log_error=error)
assert ret == ExitStatus.ERROR_TIMEOUT
assert error_msg == 'Request timed out (30s).'

View File

@ -1,46 +1,35 @@
import mock import requests
import pytest
from httpie import ExitStatus from httpie import ExitStatus
from utils import MockEnvironment, http, HTTP_OK from utils import TestEnvironment, http, HTTP_OK
def test_keyboard_interrupt_during_arg_parsing_exit_status(httpbin): class TestExitStatus:
with mock.patch('httpie.cli.parser.parse_args', def test_ok_response_exits_0(self, httpbin):
side_effect=KeyboardInterrupt()): r = http('GET', httpbin.url + '/status/200')
r = http('GET', httpbin.url + '/get', error_exit_ok=True)
assert r.exit_status == ExitStatus.ERROR_CTRL_C
def test_keyboard_interrupt_in_program_exit_status(httpbin):
with mock.patch('httpie.core.program',
side_effect=KeyboardInterrupt()):
r = http('GET', httpbin.url + '/get', error_exit_ok=True)
assert r.exit_status == ExitStatus.ERROR_CTRL_C
def test_ok_response_exits_0(httpbin):
r = http('GET', httpbin.url + '/get')
assert HTTP_OK in r assert HTTP_OK in r
assert r.exit_status == ExitStatus.SUCCESS assert r.exit_status == ExitStatus.OK
def test_error_response_exits_0_without_check_status(self, httpbin):
def test_error_response_exits_0_without_check_status(httpbin):
r = http('GET', httpbin.url + '/status/500') r = http('GET', httpbin.url + '/status/500')
assert '500 INTERNAL SERVER ERROR' in r assert '500 INTERNAL SERVER ERRO' in r
assert r.exit_status == ExitStatus.SUCCESS assert r.exit_status == ExitStatus.OK
assert not r.stderr assert not r.stderr
@pytest.mark.skipif(
tuple(map(int, requests.__version__.split('.'))) < (2, 3, 0),
reason='timeout broken in requests prior v2.3.0 (#185)'
)
def test_timeout_exit_status(self, httpbin):
def test_timeout_exit_status(httpbin): r = http('--timeout=0.5', 'GET', httpbin.url + '/delay/1',
r = http('--timeout=0.01', 'GET', httpbin.url + '/delay/0.5',
error_exit_ok=True) error_exit_ok=True)
assert r.exit_status == ExitStatus.ERROR_TIMEOUT assert r.exit_status == ExitStatus.ERROR_TIMEOUT
def test_3xx_check_status_exits_3_and_stderr_when_stdout_redirected(
def test_3xx_check_status_exits_3_and_stderr_when_stdout_redirected( self, httpbin):
httpbin): env = TestEnvironment(stdout_isatty=False)
env = MockEnvironment(stdout_isatty=False)
r = http('--check-status', '--headers', r = http('--check-status', '--headers',
'GET', httpbin.url + '/status/301', 'GET', httpbin.url + '/status/301',
env=env, error_exit_ok=True) env=env, error_exit_ok=True)
@ -48,17 +37,18 @@ def test_3xx_check_status_exits_3_and_stderr_when_stdout_redirected(
assert r.exit_status == ExitStatus.ERROR_HTTP_3XX assert r.exit_status == ExitStatus.ERROR_HTTP_3XX
assert '301 moved permanently' in r.stderr.lower() assert '301 moved permanently' in r.stderr.lower()
@pytest.mark.skipif(
def test_3xx_check_status_redirects_allowed_exits_0(httpbin): requests.__version__ == '0.13.6',
reason='Redirects with prefetch=False are broken in Requests 0.13.6')
def test_3xx_check_status_redirects_allowed_exits_0(self, httpbin):
r = http('--check-status', '--follow', r = http('--check-status', '--follow',
'GET', httpbin.url + '/status/301', 'GET', httpbin.url + '/status/301',
error_exit_ok=True) error_exit_ok=True)
# The redirect will be followed so 200 is expected. # The redirect will be followed so 200 is expected.
assert HTTP_OK in r assert HTTP_OK in r
assert r.exit_status == ExitStatus.SUCCESS assert r.exit_status == ExitStatus.OK
def test_4xx_check_status_exits_4(self, httpbin):
def test_4xx_check_status_exits_4(httpbin):
r = http('--check-status', 'GET', httpbin.url + '/status/401', r = http('--check-status', 'GET', httpbin.url + '/status/401',
error_exit_ok=True) error_exit_ok=True)
assert '401 UNAUTHORIZED' in r assert '401 UNAUTHORIZED' in r
@ -66,8 +56,7 @@ def test_4xx_check_status_exits_4(httpbin):
# Also stderr should be empty since stdout isn't redirected. # Also stderr should be empty since stdout isn't redirected.
assert not r.stderr assert not r.stderr
def test_5xx_check_status_exits_5(self, httpbin):
def test_5xx_check_status_exits_5(httpbin):
r = http('--check-status', 'GET', httpbin.url + '/status/500', r = http('--check-status', 'GET', httpbin.url + '/status/500',
error_exit_ok=True) error_exit_ok=True)
assert '500 INTERNAL SERVER ERROR' in r assert '500 INTERNAL SERVER ERROR' in r

View File

@ -1,113 +1,78 @@
"""High-level tests.""" """High-level tests."""
import pytest import pytest
from utils import TestEnvironment, http, HTTP_OK
from httpie.input import ParseError
from utils import MockEnvironment, http, HTTP_OK
from fixtures import FILE_PATH, FILE_CONTENT from fixtures import FILE_PATH, FILE_CONTENT
import httpie import httpie
from httpie.compat import is_py26
def test_debug(): class TestHTTPie:
def test_debug(self):
r = http('--debug') r = http('--debug')
assert r.exit_status == httpie.ExitStatus.SUCCESS assert r.exit_status == httpie.ExitStatus.OK
assert 'HTTPie %s' % httpie.__version__ in r.stderr assert 'HTTPie %s' % httpie.__version__ in r.stderr
assert 'HTTPie data:' in r.stderr
def test_help(self):
def test_help():
r = http('--help', error_exit_ok=True) r = http('--help', error_exit_ok=True)
assert r.exit_status == httpie.ExitStatus.SUCCESS assert r.exit_status == httpie.ExitStatus.OK
assert 'https://github.com/jakubroztocil/httpie/issues' in r assert 'https://github.com/jakubroztocil/httpie/issues' in r
def test_version(self):
def test_version():
r = http('--version', error_exit_ok=True) r = http('--version', error_exit_ok=True)
assert r.exit_status == httpie.ExitStatus.SUCCESS assert r.exit_status == httpie.ExitStatus.OK
# FIXME: py3 has version in stdout, py2 in stderr # FIXME: py3 has version in stdout, py2 in stderr
assert httpie.__version__ == r.stderr.strip() + r.strip() assert httpie.__version__ == r.stderr.strip() + r.strip()
def test_GET(self, httpbin):
def test_GET(httpbin_both): r = http('GET', httpbin.url + '/get')
r = http('GET', httpbin_both + '/get')
assert HTTP_OK in r assert HTTP_OK in r
def test_DELETE(self, httpbin):
def test_DELETE(httpbin_both): r = http('DELETE', httpbin.url + '/delete')
r = http('DELETE', httpbin_both + '/delete')
assert HTTP_OK in r assert HTTP_OK in r
def test_PUT(self, httpbin):
def test_PUT(httpbin_both): r = http('PUT', httpbin.url + '/put', 'foo=bar')
r = http('PUT', httpbin_both + '/put', 'foo=bar')
assert HTTP_OK in r assert HTTP_OK in r
assert r.json['json']['foo'] == 'bar' assert r.json['json']['foo'] == 'bar'
def test_POST_JSON_data(self, httpbin):
def test_POST_JSON_data(httpbin_both): r = http('POST', httpbin.url + '/post', 'foo=bar')
r = http('POST', httpbin_both + '/post', 'foo=bar')
assert HTTP_OK in r assert HTTP_OK in r
assert r.json['json']['foo'] == 'bar' assert r.json['json']['foo'] == 'bar'
def test_POST_form(self, httpbin):
def test_POST_form(httpbin_both): r = http('--form', 'POST', httpbin.url + '/post', 'foo=bar')
r = http('--form', 'POST', httpbin_both + '/post', 'foo=bar')
assert HTTP_OK in r assert HTTP_OK in r
assert '"foo": "bar"' in r assert '"foo": "bar"' in r
def test_POST_form_multiple_values(self, httpbin):
def test_POST_form_multiple_values(httpbin_both): r = http('--form', 'POST', httpbin.url + '/post', 'foo=bar', 'foo=baz')
r = http('--form', 'POST', httpbin_both + '/post', 'foo=bar', 'foo=baz')
assert HTTP_OK in r assert HTTP_OK in r
assert r.json['form'] == {'foo': ['bar', 'baz']} assert r.json['form'] == {'foo': ['bar', 'baz']}
def test_POST_stdin(self, httpbin):
def test_POST_stdin(httpbin_both):
with open(FILE_PATH) as f: with open(FILE_PATH) as f:
env = MockEnvironment(stdin=f, stdin_isatty=False) env = TestEnvironment(stdin=f, stdin_isatty=False)
r = http('--form', 'POST', httpbin_both + '/post', env=env) r = http('--form', 'POST', httpbin.url + '/post', env=env)
assert HTTP_OK in r assert HTTP_OK in r
assert FILE_CONTENT in r assert FILE_CONTENT in r
def test_headers(self, httpbin):
def test_headers(httpbin_both): r = http('GET', httpbin.url + '/headers', 'Foo:bar')
r = http('GET', httpbin_both + '/headers', 'Foo:bar')
assert HTTP_OK in r assert HTTP_OK in r
assert '"User-Agent": "HTTPie' in r, r assert '"User-Agent": "HTTPie' in r, r
assert '"Foo": "bar"' in r assert '"Foo": "bar"' in r
@pytest.mark.skipif(
def test_headers_unset(httpbin_both): is_py26,
r = http('GET', httpbin_both + '/headers') reason='the `object_pairs_hook` arg for `json.loads()` is Py>2.6 only'
assert 'Accept' in r.json['headers'] # default Accept present )
def test_json_input_preserve_order(self, httpbin):
r = http('GET', httpbin_both + '/headers', 'Accept:') r = http('PATCH', httpbin.url + '/patch',
assert 'Accept' not in r.json['headers'] # default Accept unset
@pytest.mark.skip('unimplemented')
def test_unset_host_header(httpbin_both):
r = http('GET', httpbin_both + '/headers')
assert 'Host' in r.json['headers'] # default Host present
r = http('GET', httpbin_both + '/headers', 'Host:')
assert 'Host' not in r.json['headers'] # default Host unset
def test_headers_empty_value(httpbin_both):
r = http('GET', httpbin_both + '/headers')
assert r.json['headers']['Accept'] # default Accept has value
r = http('GET', httpbin_both + '/headers', 'Accept;')
assert r.json['headers']['Accept'] == '' # Accept has no value
def test_headers_empty_value_with_value_gives_error(httpbin):
with pytest.raises(ParseError):
http('GET', httpbin + '/headers', 'Accept;SYNTAX_ERROR')
def test_json_input_preserve_order(httpbin_both):
r = http('PATCH', httpbin_both + '/patch',
'order:={"map":{"1":"first","2":"second"}}') 'order:={"map":{"1":"first","2":"second"}}')
assert HTTP_OK in r assert HTTP_OK in r
assert r.json['data'] == \ assert r.json['data'] == \

View File

@ -1,30 +1,10 @@
import os
from tempfile import gettempdir
import pytest import pytest
from utils import MockEnvironment, http, HTTP_OK, COLOR, CRLF from utils import TestEnvironment, http, HTTP_OK, COLOR, CRLF
from httpie import ExitStatus from httpie import ExitStatus
from httpie.compat import urlopen
from httpie.output.formatters.colors import get_lexer from httpie.output.formatters.colors import get_lexer
@pytest.mark.parametrize('stdout_isatty', [True, False])
def test_output_option(httpbin, stdout_isatty):
output_filename = os.path.join(gettempdir(), test_output_option.__name__)
url = httpbin + '/robots.txt'
r = http('--output', output_filename, url,
env=MockEnvironment(stdout_isatty=stdout_isatty))
assert r == ''
expected_body = urlopen(url).read().decode()
with open(output_filename, 'r') as f:
actual_body = f.read()
assert actual_body == expected_body
class TestVerboseFlag: class TestVerboseFlag:
def test_verbose(self, httpbin): def test_verbose(self, httpbin):
r = http('--verbose', r = http('--verbose',
@ -45,38 +25,25 @@ class TestVerboseFlag:
assert HTTP_OK in r assert HTTP_OK in r
assert '"baz": "bar"' in r assert '"baz": "bar"' in r
def test_verbose_implies_all(self, httpbin):
r = http('--verbose', '--follow', httpbin + '/redirect/1')
assert 'GET /redirect/1 HTTP/1.1' in r
assert 'HTTP/1.1 302 FOUND' in r
assert 'GET /get HTTP/1.1' in r
assert HTTP_OK in r
class TestColors: class TestColors:
@pytest.mark.parametrize( @pytest.mark.parametrize('mime', [
argnames=['mime', 'explicit_json', 'body', 'expected_lexer_name'], 'application/json',
argvalues=[ 'application/json+foo',
('application/json', False, None, 'JSON'), 'application/foo+json',
('application/json+foo', False, None, 'JSON'), 'application/json-foo',
('application/foo+json', False, None, 'JSON'), 'application/x-json',
('application/json-foo', False, None, 'JSON'), 'foo/json',
('application/x-json', False, None, 'JSON'), 'foo/json+bar',
('foo/json', False, None, 'JSON'), 'foo/bar+json',
('foo/json+bar', False, None, 'JSON'), 'foo/json-foo',
('foo/bar+json', False, None, 'JSON'), 'foo/x-json',
('foo/json-foo', False, None, 'JSON'), ])
('foo/x-json', False, None, 'JSON'), def test_get_lexer(self, mime):
('application/vnd.comverge.grid+hal+json', False, None, 'JSON'), lexer = get_lexer(mime)
('text/plain', True, '{}', 'JSON'),
('text/plain', True, 'foo', 'Text only'),
]
)
def test_get_lexer(self, mime, explicit_json, body, expected_lexer_name):
lexer = get_lexer(mime, body=body, explicit_json=explicit_json)
assert lexer is not None assert lexer is not None
assert lexer.name == expected_lexer_name 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
@ -86,7 +53,7 @@ class TestPrettyOptions:
"""Test the --pretty flag handling.""" """Test the --pretty flag handling."""
def test_pretty_enabled_by_default(self, httpbin): def test_pretty_enabled_by_default(self, httpbin):
env = MockEnvironment(colors=256) env = TestEnvironment(colors=256)
r = http('GET', httpbin.url + '/get', env=env) r = http('GET', httpbin.url + '/get', env=env)
assert COLOR in r assert COLOR in r
@ -95,7 +62,7 @@ class TestPrettyOptions:
assert COLOR not in r assert COLOR not in r
def test_force_pretty(self, httpbin): def test_force_pretty(self, httpbin):
env = MockEnvironment(stdout_isatty=False, colors=256) env = TestEnvironment(stdout_isatty=False, colors=256)
r = http('--pretty=all', 'GET', httpbin.url + '/get', env=env, ) r = http('--pretty=all', 'GET', httpbin.url + '/get', env=env, )
assert COLOR in r assert COLOR in r
@ -108,13 +75,13 @@ class TestPrettyOptions:
match any lexer. match any lexer.
""" """
env = MockEnvironment(colors=256) env = TestEnvironment(colors=256)
r = http('--print=B', '--pretty=all', httpbin.url + '/post', r = http('--print=B', '--pretty=all', httpbin.url + '/post',
'Content-Type:text/foo+json', 'a=b', env=env) 'Content-Type:text/foo+json', 'a=b', env=env)
assert COLOR in r assert COLOR in r
def test_colors_option(self, httpbin): def test_colors_option(self, httpbin):
env = MockEnvironment(colors=256) env = TestEnvironment(colors=256)
r = http('--print=B', '--pretty=colors', r = http('--print=B', '--pretty=colors',
'GET', httpbin.url + '/get', 'a=b', 'GET', httpbin.url + '/get', 'a=b',
env=env) env=env)
@ -123,7 +90,7 @@ class TestPrettyOptions:
assert COLOR in r assert COLOR in r
def test_format_option(self, httpbin): def test_format_option(self, httpbin):
env = MockEnvironment(colors=256) env = TestEnvironment(colors=256)
r = http('--print=B', '--pretty=format', r = http('--print=B', '--pretty=format',
'GET', httpbin.url + '/get', 'a=b', 'GET', httpbin.url + '/get', 'a=b',
env=env) env=env)
@ -161,7 +128,7 @@ class TestLineEndings:
def test_CRLF_formatted_response(self, httpbin): def test_CRLF_formatted_response(self, httpbin):
r = http('--pretty=format', 'GET', httpbin.url + '/get') r = http('--pretty=format', 'GET', httpbin.url + '/get')
assert r.exit_status == ExitStatus.SUCCESS assert r.exit_status == ExitStatus.OK
self._validate_crlf(r) self._validate_crlf(r)
def test_CRLF_ugly_request(self, httpbin): def test_CRLF_ugly_request(self, httpbin):

View File

@ -1,47 +0,0 @@
"""High-level tests."""
import pytest
from httpie import ExitStatus
from utils import http, HTTP_OK
def test_follow_all_redirects_shown(httpbin):
r = http('--follow', '--all', httpbin.url + '/redirect/2')
assert r.count('HTTP/1.1') == 3
assert r.count('HTTP/1.1 302 FOUND', 2)
assert HTTP_OK in r
@pytest.mark.parametrize('follow_flag', ['--follow', '-F'])
def test_follow_without_all_redirects_hidden(httpbin, follow_flag):
r = http(follow_flag, httpbin.url + '/redirect/2')
assert r.count('HTTP/1.1') == 1
assert HTTP_OK in r
def test_follow_all_output_options_used_for_redirects(httpbin):
r = http('--check-status',
'--follow',
'--all',
'--print=H',
httpbin.url + '/redirect/2')
assert r.count('GET /') == 3
assert HTTP_OK not in r
def test_follow_redirect_output_options(httpbin):
r = http('--check-status',
'--follow',
'--all',
'--print=h',
'--history-print=H',
httpbin.url + '/redirect/2')
assert r.count('GET /') == 2
assert 'HTTP/1.1 302 FOUND' not in r
assert HTTP_OK in r
def test_max_redirects(httpbin):
r = http('--max-redirects=1', '--follow', httpbin.url + '/redirect/3',
error_exit_ok=True)
assert r.exit_status == ExitStatus.ERROR_TOO_MANY_REDIRECTS

View File

@ -2,12 +2,12 @@
import os import os
import shutil import shutil
import sys import sys
from tempfile import gettempdir
import pytest import pytest
from httpie.plugins.builtin import HTTPBasicAuth from httpie.plugins.builtin import HTTPBasicAuth
from utils import MockEnvironment, mk_config_dir, http, HTTP_OK from utils import TestEnvironment, mk_config_dir, http, HTTP_OK, \
no_content_type
from fixtures import UNICODE from fixtures import UNICODE
@ -29,7 +29,7 @@ class SessionTestBase(object):
for session files being reused. for session files being reused.
""" """
return MockEnvironment(config_dir=self.config_dir) return TestEnvironment(config_dir=self.config_dir)
class TestSessionFlow(SessionTestBase): class TestSessionFlow(SessionTestBase):
@ -64,31 +64,27 @@ class TestSessionFlow(SessionTestBase):
def test_session_update(self, httpbin): def test_session_update(self, httpbin):
self.start_session(httpbin) self.start_session(httpbin)
# Get a response to a request from the original session. # Get a response to a request from the original session.
r2 = http('--session=test', 'GET', httpbin.url + '/get', r2 = http('--session=test', 'GET', httpbin.url + '/get', env=self.env())
env=self.env())
assert HTTP_OK in r2 assert HTTP_OK in r2
# Make a request modifying the session data. # Make a request modifying the session data.
r3 = http('--follow', '--session=test', '--auth=username:password2', r3 = http('--follow', '--session=test', '--auth=username:password2',
'GET', httpbin.url + '/cookies/set?hello=world2', 'GET', httpbin.url + '/cookies/set?hello=world2', 'Hello:World2',
'Hello:World2',
env=self.env()) env=self.env())
assert HTTP_OK in r3 assert HTTP_OK in r3
# Get a response to a request from the updated session. # Get a response to a request from the updated session.
r4 = http('--session=test', 'GET', httpbin.url + '/get', r4 = http('--session=test', 'GET', httpbin.url + '/get', env=self.env())
env=self.env())
assert HTTP_OK in r4 assert HTTP_OK in r4
assert r4.json['headers']['Hello'] == 'World2' assert r4.json['headers']['Hello'] == 'World2'
assert r4.json['headers']['Cookie'] == 'hello=world2' assert r4.json['headers']['Cookie'] == 'hello=world2'
assert (r2.json['headers']['Authorization'] assert (r2.json['headers']['Authorization'] !=
!= r4.json['headers']['Authorization']) r4.json['headers']['Authorization'])
def test_session_read_only(self, httpbin): def test_session_read_only(self, httpbin):
self.start_session(httpbin) self.start_session(httpbin)
# Get a response from the original session. # Get a response from the original session.
r2 = http('--session=test', 'GET', httpbin.url + '/get', r2 = http('--session=test', 'GET', httpbin.url + '/get', env=self.env())
env=self.env())
assert HTTP_OK in r2 assert HTTP_OK in r2
# Make a request modifying the session data but # Make a request modifying the session data but
@ -100,8 +96,7 @@ class TestSessionFlow(SessionTestBase):
assert HTTP_OK in r3 assert HTTP_OK in r3
# Get a response from the updated session. # Get a response from the updated session.
r4 = http('--session=test', 'GET', httpbin.url + '/get', r4 = http('--session=test', 'GET', httpbin.url + '/get', env=self.env())
env=self.env())
assert HTTP_OK in r4 assert HTTP_OK in r4
# Origin can differ on Travis. # Origin can differ on Travis.
@ -122,10 +117,10 @@ class TestSession(SessionTestBase):
'If-Unmodified-Since: Sat, 29 Oct 1994 19:43:31 GMT', 'If-Unmodified-Since: Sat, 29 Oct 1994 19:43:31 GMT',
env=self.env()) env=self.env())
assert HTTP_OK in r1 assert HTTP_OK in r1
r2 = http('--session=test', 'GET', httpbin.url + '/get',
env=self.env()) r2 = http('--session=test', 'GET', httpbin.url + '/get', env=self.env())
assert HTTP_OK in r2 assert HTTP_OK in r2
assert 'Content-Type' not in r2.json['headers'] assert no_content_type(r2.json['headers'])
assert 'If-Unmodified-Since' not in r2.json['headers'] assert 'If-Unmodified-Since' not in r2.json['headers']
def test_session_by_path(self, httpbin): def test_session_by_path(self, httpbin):
@ -174,14 +169,3 @@ class TestSession(SessionTestBase):
r2 = http('--session=test', httpbin.url + '/headers', env=self.env()) r2 = http('--session=test', httpbin.url + '/headers', env=self.env())
assert HTTP_OK in r2 assert HTTP_OK in r2
assert r2.json['headers']['User-Agent'] == 'custom' assert r2.json['headers']['User-Agent'] == 'custom'
def test_download_in_session(self, httpbin):
# https://github.com/jakubroztocil/httpie/issues/412
self.start_session(httpbin)
cwd = os.getcwd()
os.chdir(gettempdir())
try:
http('--session=test', '--download',
httpbin.url + '/get', env=self.env())
finally:
os.chdir(cwd)

View File

@ -5,7 +5,6 @@ import pytest_httpbin.certs
from requests.exceptions import SSLError from requests.exceptions import SSLError
from httpie import ExitStatus from httpie import ExitStatus
from httpie.input import SSL_VERSION_ARG_MAPPING
from utils import http, HTTP_OK, TESTS_ROOT from utils import http, HTTP_OK, TESTS_ROOT
@ -13,44 +12,17 @@ CLIENT_CERT = os.path.join(TESTS_ROOT, 'client_certs', 'client.crt')
CLIENT_KEY = os.path.join(TESTS_ROOT, 'client_certs', 'client.key') CLIENT_KEY = os.path.join(TESTS_ROOT, 'client_certs', 'client.key')
CLIENT_PEM = os.path.join(TESTS_ROOT, 'client_certs', 'client.pem') CLIENT_PEM = os.path.join(TESTS_ROOT, 'client_certs', 'client.pem')
# FIXME:
# We test against a local httpbin instance which uses a self-signed cert. # We test against a local httpbin instance which uses a self-signed cert.
# Requests without --verify=<CA_BUNDLE> will fail with a verification error. # Requests without --verify=<CA_BUNDLE> will fail with a verification error.
# See: https://github.com/kevin1024/pytest-httpbin#https-support # See: https://github.com/kevin1024/pytest-httpbin#https-support
CA_BUNDLE = pytest_httpbin.certs.where() CA_BUNDLE = pytest_httpbin.certs.where()
@pytest.mark.parametrize('ssl_version', SSL_VERSION_ARG_MAPPING.keys()) class TestClientSSLCertHandling(object):
def test_ssl_version(httpbin_secure, ssl_version):
try:
r = http(
'--ssl', ssl_version,
httpbin_secure + '/get'
)
assert HTTP_OK in r
except SSLError as e:
if ssl_version == 'ssl3':
# pytest-httpbin doesn't support ssl3
assert 'SSLV3_ALERT_HANDSHAKE_FAILURE' in str(e)
else:
raise
class TestClientCert:
def test_cert_and_key(self, httpbin_secure):
r = http(httpbin_secure + '/get',
'--cert', CLIENT_CERT,
'--cert-key', CLIENT_KEY)
assert HTTP_OK in r
def test_cert_pem(self, httpbin_secure):
r = http(httpbin_secure + '/get',
'--cert', CLIENT_PEM)
assert HTTP_OK in r
def test_cert_file_not_found(self, httpbin_secure): def test_cert_file_not_found(self, httpbin_secure):
r = http(httpbin_secure + '/get', r = http(httpbin_secure + '/get',
'--verify', CA_BUNDLE,
'--cert', '/__not_found__', '--cert', '/__not_found__',
error_exit_ok=True) error_exit_ok=True)
assert r.exit_status == ExitStatus.ERROR assert r.exit_status == ExitStatus.ERROR
@ -59,39 +31,47 @@ class TestClientCert:
def test_cert_file_invalid(self, httpbin_secure): def test_cert_file_invalid(self, httpbin_secure):
with pytest.raises(SSLError): with pytest.raises(SSLError):
http(httpbin_secure + '/get', http(httpbin_secure + '/get',
'--verify', CA_BUNDLE,
'--cert', __file__) '--cert', __file__)
def test_cert_ok_but_missing_key(self, httpbin_secure): def test_cert_ok_but_missing_key(self, httpbin_secure):
with pytest.raises(SSLError): with pytest.raises(SSLError):
http(httpbin_secure + '/get', http(httpbin_secure + '/get',
'--verify', CA_BUNDLE,
'--cert', CLIENT_CERT) '--cert', CLIENT_CERT)
def test_cert_and_key(self, httpbin_secure):
r = http(httpbin_secure + '/get',
'--verify', CA_BUNDLE,
'--cert', CLIENT_CERT,
'--cert-key', CLIENT_KEY)
assert HTTP_OK in r
class TestServerCert: def test_cert_pem(self, httpbin_secure):
r = http(httpbin_secure + '/get',
'--verify', CA_BUNDLE,
'--cert', CLIENT_PEM)
assert HTTP_OK in r
class TestServerSSLCertHandling(object):
def test_self_signed_server_cert_by_default_raises_ssl_error(
self, httpbin_secure):
with pytest.raises(SSLError):
http(httpbin_secure.url + '/get')
def test_verify_no_OK(self, httpbin_secure): def test_verify_no_OK(self, httpbin_secure):
r = http(httpbin_secure.url + '/get', '--verify=no') r = http(httpbin_secure.url + '/get', '--verify=no')
assert HTTP_OK in r assert HTTP_OK in r
@pytest.mark.parametrize('verify_value', ['false', 'fALse'])
def test_verify_false_OK(self, httpbin_secure, verify_value):
r = http(httpbin_secure.url + '/get', '--verify', verify_value)
assert HTTP_OK in r
def test_verify_custom_ca_bundle_path( def test_verify_custom_ca_bundle_path(
self, httpbin_secure_untrusted): self, httpbin_secure):
r = http(httpbin_secure_untrusted + '/get', '--verify', CA_BUNDLE) r = http(httpbin_secure.url + '/get', '--verify', CA_BUNDLE)
assert HTTP_OK in r assert HTTP_OK in r
def test_self_signed_server_cert_by_default_raises_ssl_error(
self,
httpbin_secure_untrusted):
with pytest.raises(SSLError):
http(httpbin_secure_untrusted.url + '/get')
def test_verify_custom_ca_bundle_invalid_path(self, httpbin_secure): def test_verify_custom_ca_bundle_invalid_path(self, httpbin_secure):
# since 2.14.0 requests raises IOError with pytest.raises(SSLError):
with pytest.raises((SSLError, IOError)):
http(httpbin_secure.url + '/get', '--verify', '/__not_found__') http(httpbin_secure.url + '/get', '--verify', '/__not_found__')
def test_verify_custom_ca_bundle_invalid_bundle(self, httpbin_secure): def test_verify_custom_ca_bundle_invalid_bundle(self, httpbin_secure):

View File

@ -2,41 +2,39 @@ import pytest
from httpie.compat import is_windows from httpie.compat import is_windows
from httpie.output.streams import BINARY_SUPPRESSED_NOTICE from httpie.output.streams import BINARY_SUPPRESSED_NOTICE
from utils import http, MockEnvironment from utils import http, TestEnvironment
from fixtures import BIN_FILE_CONTENT, BIN_FILE_PATH from fixtures import BIN_FILE_CONTENT, BIN_FILE_PATH
# GET because httpbin 500s with binary POST body. class TestStream:
# GET because httpbin 500s with binary POST body.
@pytest.mark.skipif(is_windows,
@pytest.mark.skipif(is_windows,
reason='Pretty redirect not supported under Windows') reason='Pretty redirect not supported under Windows')
def test_pretty_redirected_stream(httpbin): def test_pretty_redirected_stream(self, httpbin):
"""Test that --stream works with prettified redirected output.""" """Test that --stream works with prettified redirected output."""
with open(BIN_FILE_PATH, 'rb') as f: with open(BIN_FILE_PATH, 'rb') as f:
env = MockEnvironment(colors=256, stdin=f, env = TestEnvironment(colors=256, stdin=f,
stdin_isatty=False, stdin_isatty=False,
stdout_isatty=False) stdout_isatty=False)
r = http('--verbose', '--pretty=all', '--stream', 'GET', r = http('--verbose', '--pretty=all', '--stream', 'GET',
httpbin.url + '/get', env=env) httpbin.url + '/get', env=env)
assert BINARY_SUPPRESSED_NOTICE.decode() in r assert BINARY_SUPPRESSED_NOTICE.decode() in r
def test_encoded_stream(self, httpbin):
def test_encoded_stream(httpbin):
"""Test that --stream works with non-prettified """Test that --stream works with non-prettified
redirected terminal output.""" redirected terminal output."""
with open(BIN_FILE_PATH, 'rb') as f: with open(BIN_FILE_PATH, 'rb') as f:
env = MockEnvironment(stdin=f, stdin_isatty=False) env = TestEnvironment(stdin=f, stdin_isatty=False)
r = http('--pretty=none', '--stream', '--verbose', 'GET', r = http('--pretty=none', '--stream', '--verbose', 'GET',
httpbin.url + '/get', env=env) httpbin.url + '/get', env=env)
assert BINARY_SUPPRESSED_NOTICE.decode() in r assert BINARY_SUPPRESSED_NOTICE.decode() in r
def test_redirected_stream(self, httpbin):
def test_redirected_stream(httpbin):
"""Test that --stream works with non-prettified """Test that --stream works with non-prettified
redirected terminal output.""" redirected terminal output."""
with open(BIN_FILE_PATH, 'rb') as f: with open(BIN_FILE_PATH, 'rb') as f:
env = MockEnvironment(stdout_isatty=False, env = TestEnvironment(stdout_isatty=False,
stdin_isatty=False, stdin_isatty=False,
stdin=f) stdin=f)
r = http('--pretty=none', '--stream', '--verbose', 'GET', r = http('--pretty=none', '--stream', '--verbose', 'GET',

View File

@ -7,89 +7,79 @@ from utils import http, HTTP_OK
from fixtures import UNICODE from fixtures import UNICODE
def test_unicode_headers(httpbin): class TestUnicode:
def test_unicode_headers(self, httpbin):
# httpbin doesn't interpret utf8 headers # httpbin doesn't interpret utf8 headers
r = http(httpbin.url + '/headers', u'Test:%s' % UNICODE) r = http(httpbin.url + '/headers', u'Test:%s' % UNICODE)
assert HTTP_OK in r assert HTTP_OK in r
def test_unicode_headers_verbose(self, httpbin):
def test_unicode_headers_verbose(httpbin):
# httpbin doesn't interpret utf8 headers # httpbin doesn't interpret utf8 headers
r = http('--verbose', httpbin.url + '/headers', u'Test:%s' % UNICODE) r = http('--verbose', httpbin.url + '/headers', u'Test:%s' % UNICODE)
assert HTTP_OK in r assert HTTP_OK in r
assert UNICODE in r assert UNICODE in r
def test_unicode_form_item(self, httpbin):
def test_unicode_form_item(httpbin):
r = http('--form', 'POST', httpbin.url + '/post', u'test=%s' % UNICODE) r = http('--form', 'POST', httpbin.url + '/post', u'test=%s' % UNICODE)
assert HTTP_OK in r assert HTTP_OK in r
assert r.json['form'] == {'test': UNICODE} assert r.json['form'] == {'test': UNICODE}
def test_unicode_form_item_verbose(self, httpbin):
def test_unicode_form_item_verbose(httpbin):
r = http('--verbose', '--form', r = http('--verbose', '--form',
'POST', httpbin.url + '/post', u'test=%s' % UNICODE) 'POST', httpbin.url + '/post', u'test=%s' % UNICODE)
assert HTTP_OK in r assert HTTP_OK in r
assert UNICODE in r assert UNICODE in r
def test_unicode_json_item(self, httpbin):
def test_unicode_json_item(httpbin):
r = http('--json', 'POST', httpbin.url + '/post', u'test=%s' % UNICODE) r = http('--json', 'POST', httpbin.url + '/post', u'test=%s' % UNICODE)
assert HTTP_OK in r assert HTTP_OK in r
assert r.json['json'] == {'test': UNICODE} assert r.json['json'] == {'test': UNICODE}
def test_unicode_json_item_verbose(self, httpbin):
def test_unicode_json_item_verbose(httpbin):
r = http('--verbose', '--json', r = http('--verbose', '--json',
'POST', httpbin.url + '/post', u'test=%s' % UNICODE) 'POST', httpbin.url + '/post', u'test=%s' % UNICODE)
assert HTTP_OK in r assert HTTP_OK in r
assert UNICODE in r assert UNICODE in r
def test_unicode_raw_json_item(self, httpbin):
def test_unicode_raw_json_item(httpbin):
r = http('--json', 'POST', httpbin.url + '/post', r = http('--json', 'POST', httpbin.url + '/post',
u'test:={ "%s" : [ "%s" ] }' % (UNICODE, UNICODE)) u'test:={ "%s" : [ "%s" ] }' % (UNICODE, UNICODE))
assert HTTP_OK in r assert HTTP_OK in r
assert r.json['json'] == {'test': {UNICODE: [UNICODE]}} assert r.json['json'] == {'test': {UNICODE: [UNICODE]}}
def test_unicode_raw_json_item_verbose(self, httpbin):
def test_unicode_raw_json_item_verbose(httpbin):
r = http('--json', 'POST', httpbin.url + '/post', r = http('--json', 'POST', httpbin.url + '/post',
u'test:={ "%s" : [ "%s" ] }' % (UNICODE, UNICODE)) u'test:={ "%s" : [ "%s" ] }' % (UNICODE, UNICODE))
assert HTTP_OK in r assert HTTP_OK in r
assert r.json['json'] == {'test': {UNICODE: [UNICODE]}} assert r.json['json'] == {'test': {UNICODE: [UNICODE]}}
def test_unicode_url_query_arg_item(self, httpbin):
def test_unicode_url_query_arg_item(httpbin):
r = http(httpbin.url + '/get', u'test==%s' % UNICODE) r = http(httpbin.url + '/get', u'test==%s' % UNICODE)
assert HTTP_OK in r assert HTTP_OK in r
assert r.json['args'] == {'test': UNICODE}, r assert r.json['args'] == {'test': UNICODE}, r
def test_unicode_url_query_arg_item_verbose(self, httpbin):
def test_unicode_url_query_arg_item_verbose(httpbin):
r = http('--verbose', httpbin.url + '/get', u'test==%s' % UNICODE) r = http('--verbose', httpbin.url + '/get', u'test==%s' % UNICODE)
assert HTTP_OK in r assert HTTP_OK in r
assert UNICODE in r assert UNICODE in r
def test_unicode_url(self, httpbin):
def test_unicode_url(httpbin):
r = http(httpbin.url + u'/get?test=' + UNICODE) r = http(httpbin.url + u'/get?test=' + UNICODE)
assert HTTP_OK in r assert HTTP_OK in r
assert r.json['args'] == {'test': UNICODE} assert r.json['args'] == {'test': UNICODE}
# def test_unicode_url_verbose(self): # def test_unicode_url_verbose(self):
# r = http(httpbin.url + '--verbose', u'/get?test=' + UNICODE) # r = http(httpbin.url + '--verbose', u'/get?test=' + UNICODE)
# assert HTTP_OK in r # assert HTTP_OK in r
def test_unicode_basic_auth(self, httpbin):
def test_unicode_basic_auth(httpbin):
# it doesn't really authenticate us because httpbin # it doesn't really authenticate us because httpbin
# doesn't interpret the utf8-encoded auth # doesn't interpret the utf8-encoded auth
http('--verbose', '--auth', u'test:%s' % UNICODE, http('--verbose', '--auth', u'test:%s' % UNICODE,
httpbin.url + u'/basic-auth/test/' + UNICODE) httpbin.url + u'/basic-auth/test/' + UNICODE)
def test_unicode_digest_auth(self, httpbin):
def test_unicode_digest_auth(httpbin):
# it doesn't really authenticate us because httpbin # it doesn't really authenticate us because httpbin
# doesn't interpret the utf8-encoded auth # doesn't interpret the utf8-encoded auth
http('--auth-type=digest', http('--auth-type=digest',

View File

@ -3,7 +3,7 @@ import os
import pytest import pytest
from httpie.input import ParseError from httpie.input import ParseError
from utils import MockEnvironment, http, HTTP_OK from utils import TestEnvironment, http, HTTP_OK
from fixtures import FILE_PATH_ARG, FILE_PATH, FILE_CONTENT from fixtures import FILE_PATH_ARG, FILE_PATH, FILE_CONTENT
@ -23,7 +23,6 @@ class TestMultipartFormDataFileUpload:
' filename="%s"' % os.path.basename(FILE_PATH) in r ' filename="%s"' % os.path.basename(FILE_PATH) in r
assert FILE_CONTENT in r assert FILE_CONTENT in r
assert '"foo": "bar"' in r assert '"foo": "bar"' in r
assert 'Content-Type: text/plain' in r
def test_upload_multiple_fields_with_the_same_name(self, httpbin): def test_upload_multiple_fields_with_the_same_name(self, httpbin):
r = http('--form', '--verbose', 'POST', httpbin.url + '/post', r = http('--form', '--verbose', 'POST', httpbin.url + '/post',
@ -35,7 +34,6 @@ class TestMultipartFormDataFileUpload:
# Should be 4, but is 3 because httpbin # Should be 4, but is 3 because httpbin
# doesn't seem to support filed field lists # doesn't seem to support filed field lists
assert r.count(FILE_CONTENT) in [3, 4] assert r.count(FILE_CONTENT) in [3, 4]
assert r.count('Content-Type: text/plain') == 2
class TestRequestBodyFromFilePath: class TestRequestBodyFromFilePath:
@ -62,14 +60,14 @@ class TestRequestBodyFromFilePath:
def test_request_body_from_file_by_path_no_field_name_allowed( def test_request_body_from_file_by_path_no_field_name_allowed(
self, httpbin): self, httpbin):
env = MockEnvironment(stdin_isatty=True) env = TestEnvironment(stdin_isatty=True)
r = http('POST', httpbin.url + '/post', 'field-name@' + FILE_PATH_ARG, r = http('POST', httpbin.url + '/post', 'field-name@' + FILE_PATH_ARG,
env=env, error_exit_ok=True) env=env, error_exit_ok=True)
assert 'perhaps you meant --form?' in r.stderr assert 'perhaps you meant --form?' in r.stderr
def test_request_body_from_file_by_path_no_data_items_allowed( def test_request_body_from_file_by_path_no_data_items_allowed(
self, httpbin): self, httpbin):
env = MockEnvironment(stdin_isatty=False) env = TestEnvironment(stdin_isatty=False)
r = http('POST', httpbin.url + '/post', '@' + FILE_PATH_ARG, 'foo=bar', r = http('POST', httpbin.url + '/post', '@' + FILE_PATH_ARG, 'foo=bar',
env=env, error_exit_ok=True) env=env, error_exit_ok=True)
assert 'cannot be mixed' in r.stderr assert 'cannot be mixed' in r.stderr

View File

@ -4,7 +4,7 @@ import tempfile
import pytest import pytest
from httpie.context import Environment from httpie.context import Environment
from utils import MockEnvironment, http from utils import TestEnvironment, http
from httpie.compat import is_windows from httpie.compat import is_windows
@ -20,11 +20,9 @@ class TestWindowsOnly:
class TestFakeWindows: class TestFakeWindows:
def test_output_file_pretty_not_allowed_on_windows(self, httpbin): def test_output_file_pretty_not_allowed_on_windows(self, httpbin):
env = MockEnvironment(is_windows=True) env = TestEnvironment(is_windows=True)
output_file = os.path.join( output_file = os.path.join(
tempfile.gettempdir(), tempfile.gettempdir(), '__httpie_test_output__')
self.test_output_file_pretty_not_allowed_on_windows.__name__
)
r = http('--output', output_file, r = http('--output', output_file,
'--pretty=all', 'GET', httpbin.url + '/get', '--pretty=all', 'GET', httpbin.url + '/get',
env=env, error_exit_ok=True) env=env, error_exit_ok=True)

View File

@ -1,18 +1,23 @@
# coding=utf-8 # coding=utf-8
"""Utilities for HTTPie test suite.""" """Utilities used by HTTPie tests.
"""
import os import os
import sys import sys
import time import time
import json import json
import shutil
import tempfile import tempfile
from httpie import ExitStatus, EXIT_STATUS_LABELS import httpie
from httpie.context import Environment from httpie.context import Environment
from httpie.core import main from httpie.core import main
from httpie.compat import bytes, str from httpie.compat import bytes, str
TESTS_ROOT = os.path.abspath(os.path.dirname(__file__)) TESTS_ROOT = os.path.abspath(os.path.dirname(__file__))
CRLF = '\r\n' CRLF = '\r\n'
COLOR = '\x1b[' COLOR = '\x1b['
HTTP_OK = '200 OK' HTTP_OK = '200 OK'
@ -23,9 +28,14 @@ HTTP_OK_COLOR = (
) )
def mk_config_dir(): def no_content_type(headers):
dirname = tempfile.mkdtemp(prefix='httpie_config_') return (
return dirname 'Content-Type' not in headers
# We need to do also this because of this issue:
# <https://github.com/kevin1024/pytest-httpbin/issues/5>
# TODO: remove this function once the issue is if fixed
or headers['Content-Type'] == 'text/plain'
)
def add_auth(url, auth): def add_auth(url, auth):
@ -33,45 +43,137 @@ def add_auth(url, auth):
return proto + '://' + auth + '@' + rest return proto + '://' + auth + '@' + rest
class MockEnvironment(Environment): class TestEnvironment(Environment):
"""Environment subclass with reasonable defaults for testing.""" """
Environment subclass with reasonable defaults suitable for testing.
"""
colors = 0 colors = 0
stdin_isatty = True, stdin_isatty = True,
stdout_isatty = True stdout_isatty = True
is_windows = False is_windows = False
_shutil = shutil # needed by __del__ (would get gc'd)
def __init__(self, **kwargs): def __init__(self, **kwargs):
if 'stdout' not in kwargs: if 'stdout' not in kwargs:
kwargs['stdout'] = tempfile.TemporaryFile( kwargs['stdout'] = tempfile.TemporaryFile('w+b')
mode='w+b',
prefix='httpie_stdout'
)
if 'stderr' not in kwargs: if 'stderr' not in kwargs:
kwargs['stderr'] = tempfile.TemporaryFile( kwargs['stderr'] = tempfile.TemporaryFile('w+t')
mode='w+t',
prefix='httpie_stderr'
)
super(MockEnvironment, self).__init__(**kwargs)
self._delete_config_dir = False
@property self.delete_config_dir = False
def config(self): if 'config_dir' not in kwargs:
if not self.config_dir.startswith(tempfile.gettempdir()): kwargs['config_dir'] = mk_config_dir()
self.config_dir = mk_config_dir() self.delete_config_dir = True
self._delete_config_dir = True
return super(MockEnvironment, self).config
def cleanup(self): super(TestEnvironment, self).__init__(**kwargs)
if self._delete_config_dir:
assert self.config_dir.startswith(tempfile.gettempdir())
from shutil import rmtree
rmtree(self.config_dir)
def __del__(self): def __del__(self):
if self.delete_config_dir:
self._shutil.rmtree(self.config_dir)
def http(*args, **kwargs):
"""
Run HTTPie and capture stderr/out and exit status.
Invoke `httpie.core.main()` with `args` and `kwargs`,
and return a `CLIResponse` subclass instance.
The return value is either a `StrCLIResponse`, or `BytesCLIResponse`
if unable to decode the output.
The response has the following attributes:
`stdout` is represented by the instance itself (print r)
`stderr`: text written to stderr
`exit_status`: the exit status
`json`: decoded JSON (if possible) or `None`
Exceptions are propagated.
If you pass ``error_exit_ok=True``, then error exit statuses
won't result into an exception.
Example:
$ http --auth=user:password GET httpbin.org/basic-auth/user/password
>>> r = http('-a', 'user:pw', 'httpbin.org/basic-auth/user/pw')
>>> type(r) == StrCLIResponse
True
>>> r.exit_status
0
>>> r.stderr
''
>>> 'HTTP/1.1 200 OK' in r
True
>>> r.json == {'authenticated': True, 'user': 'user'}
True
"""
error_exit_ok = kwargs.pop('error_exit_ok', False)
env = kwargs.get('env')
if not env:
env = kwargs['env'] = TestEnvironment()
stdout = env.stdout
stderr = env.stderr
args = list(args)
if '--debug' not in args and '--traceback' not in args:
args = ['--traceback'] + args
def dump_stderr():
stderr.seek(0)
sys.stderr.write(stderr.read())
try: try:
self.cleanup() try:
exit_status = main(args=args, **kwargs)
if '--download' in args:
# Let the progress reporter thread finish.
time.sleep(.5)
except SystemExit:
if error_exit_ok:
exit_status = httpie.ExitStatus.ERROR
else:
dump_stderr()
raise
except Exception: except Exception:
pass stderr.seek(0)
sys.stderr.write(stderr.read())
raise
else:
if exit_status != httpie.ExitStatus.OK and not error_exit_ok:
dump_stderr()
raise Exception('Unexpected exit status: %s', exit_status)
stdout.seek(0)
stderr.seek(0)
output = stdout.read()
try:
output = output.decode('utf8')
except UnicodeDecodeError:
# noinspection PyArgumentList
r = BytesCLIResponse(output)
else:
# noinspection PyArgumentList
r = StrCLIResponse(output)
r.stderr = stderr.read()
r.exit_status = exit_status
if r.exit_status != httpie.ExitStatus.OK:
sys.stderr.write(r.stderr)
return r
finally:
stdout.close()
stderr.close()
class BaseCLIResponse(object): class BaseCLIResponse(object):
@ -135,120 +237,5 @@ class StrCLIResponse(str, BaseCLIResponse):
return self._json return self._json
class ExitStatusError(Exception): def mk_config_dir():
pass return tempfile.mkdtemp(prefix='httpie_test_config_dir_')
def http(*args, **kwargs):
# noinspection PyUnresolvedReferences
"""
Run HTTPie and capture stderr/out and exit status.
Invoke `httpie.core.main()` with `args` and `kwargs`,
and return a `CLIResponse` subclass instance.
The return value is either a `StrCLIResponse`, or `BytesCLIResponse`
if unable to decode the output.
The response has the following attributes:
`stdout` is represented by the instance itself (print r)
`stderr`: text written to stderr
`exit_status`: the exit status
`json`: decoded JSON (if possible) or `None`
Exceptions are propagated.
If you pass ``error_exit_ok=True``, then error exit statuses
won't result into an exception.
Example:
$ http --auth=user:password GET httpbin.org/basic-auth/user/password
>>> httpbin = getfixture('httpbin')
>>> r = http('-a', 'user:pw', httpbin.url + '/basic-auth/user/pw')
>>> type(r) == StrCLIResponse
True
>>> r.exit_status
0
>>> r.stderr
''
>>> 'HTTP/1.1 200 OK' in r
True
>>> r.json == {'authenticated': True, 'user': 'user'}
True
"""
error_exit_ok = kwargs.pop('error_exit_ok', False)
env = kwargs.get('env')
if not env:
env = kwargs['env'] = MockEnvironment()
stdout = env.stdout
stderr = env.stderr
args = list(args)
args_with_config_defaults = args + env.config.default_options
add_to_args = []
if '--debug' not in args_with_config_defaults:
if not error_exit_ok and '--traceback' not in args_with_config_defaults:
add_to_args.append('--traceback')
if not any('--timeout' in arg for arg in args_with_config_defaults):
add_to_args.append('--timeout=3')
args = add_to_args + args
def dump_stderr():
stderr.seek(0)
sys.stderr.write(stderr.read())
try:
try:
exit_status = main(args=args, **kwargs)
if '--download' in args:
# Let the progress reporter thread finish.
time.sleep(.5)
except SystemExit:
if error_exit_ok:
exit_status = ExitStatus.ERROR
else:
dump_stderr()
raise
except Exception:
stderr.seek(0)
sys.stderr.write(stderr.read())
raise
else:
if not error_exit_ok and exit_status != ExitStatus.SUCCESS:
dump_stderr()
raise ExitStatusError(
'httpie.core.main() unexpectedly returned'
' a non-zero exit status: {0} ({1})'.format(
exit_status,
EXIT_STATUS_LABELS[exit_status]
)
)
stdout.seek(0)
stderr.seek(0)
output = stdout.read()
try:
output = output.decode('utf8')
except UnicodeDecodeError:
# noinspection PyArgumentList
r = BytesCLIResponse(output)
else:
# noinspection PyArgumentList
r = StrCLIResponse(output)
r.stderr = stderr.read()
r.exit_status = exit_status
if r.exit_status != ExitStatus.SUCCESS:
sys.stderr.write(r.stderr)
return r
finally:
stdout.close()
stderr.close()
env.cleanup()

21
tox.ini
View File

@ -1,26 +1,21 @@
# Tox (http://tox.testrun.org/) is a tool for running tests # Tox (http://tox.testrun.org/) is a tool for running tests
# in multiple virtualenvs. See ./CONTRIBUTING.rst # in multiple virtualenvs.
# Run:
# $ pip install -r requirements-dev.txt
# $ tox
[tox] [tox]
# pypy3 currently fails because of a Flask issue envlist = py26, py27, py34, pypy
envlist = py27, py37, pypy
[testenv] [testenv]
deps = deps =
mock
pytest pytest
pytest-httpbin>=0.0.6 pytest-httpbin>=0.0.6
commands = commands =
# NOTE: the order of the directories in posargs seems to matter. py.test --verbose --doctest-modules --basetemp={envtmpdir} {posargs:./tests ./httpie}
# When changed, then many ImportMismatchError exceptions occurrs.
py.test \
--verbose \
--doctest-modules \
{posargs:./httpie ./tests}
[testenv:py27-osx-builtin] [pytest]
basepython = /usr/bin/python2.7 addopts = --tb=native