Compare commits

...

99 Commits
2.2.0 ... 2.4.0

Author SHA1 Message Date
bb36897054 2.4.0 2021-02-06 11:17:24 +01:00
173e622567 Update upload command 2021-02-06 11:17:14 +01:00
3426030370 Fix README formatting 2021-02-06 11:13:04 +01:00
d014498713 Changelog entry for cookie expiration based on Set-Cookie: max-age=<n>
#1029
2021-02-06 11:02:26 +01:00
5414d1853e Refactoring
#1029
2021-02-06 10:58:36 +01:00
1ac8f69651 Add more output matching tests 2021-02-06 10:52:30 +01:00
3c07a25326 Add support for max-age=0 cookie expiry (#1029)
Close #998
2021-02-06 10:50:34 +01:00
cf78a12e46 Show --check-status warning with --quiet as well. (#1026)
Fixes #1028
2021-01-31 00:58:56 +01:00
0f1e098cc4 Fix incorrect separators and introduce assert_output_matches() (close #1027) 2021-01-30 22:14:57 +01:00
0401d7b31c fixed typo (#1024) 2021-01-21 22:35:41 +01:00
795627f965 Update chat icon 2021-01-13 22:24:20 +01:00
21cc008cb2 Add Linux Solus install to README (#1018) 2021-01-13 21:52:00 +01:00
77b8c37cb0 Update changelog
#1020
2021-01-13 21:49:53 +01:00
db685d58b5 Decode headers using utf-8 only if they are not str (#1020) 2021-01-13 21:45:56 +01:00
44ae67685d Add HTTPie Discord link to README (#1016)
* add discord link

* update link
2021-01-12 15:27:45 +01:00
6922a0c912 Switch from httpbin.org to pie.dev 2020-12-24 21:34:30 +01:00
2afdc958c6 Update URLs 2020-12-23 22:07:27 +01:00
57b1baf1d1 Add a test to reproduce #1006 2020-12-22 22:56:45 +01:00
1828da6a50 Update --stream example comment 2020-12-21 12:17:04 +01:00
0629f2ff42 Fix --stream example II 2020-12-21 12:14:41 +01:00
d71b7eee81 Fix --stream example
Close #1002
2020-12-21 12:12:11 +01:00
9883a46575 Cleanup (#993) 2020-12-21 12:03:25 +01:00
2409077a6d Clarify 2020-12-21 11:51:19 +01:00
02971b938d Fix documentation on file upload (#1000)
As documented later on in "File upload forms", the correct syntax to set
the mimetype of the upload is `field@file;type=filetype`
2020-12-21 11:47:47 +01:00
f7e77efe4b Test on Python 3.9 (#986) 2020-12-21 11:42:21 +01:00
5d8bd0da7c python -m pip (#1005) 2020-12-21 11:38:00 +01:00
3c6e7c73fe Update setup.py 2020-12-19 14:07:31 +01:00
d64c0ee415 Remove funding request 2020-12-18 17:54:13 +01:00
311a5ede70 Simplify Hello World 2020-10-29 10:07:45 +01:00
f64c90010f Simplify Hello World 2020-10-29 10:06:26 +01:00
8456ddb27c Update brew instructions for dev 2020-10-25 21:48:09 +01:00
cf254680b7 Update homebrew 2020-10-25 21:43:20 +01:00
42c4a7596b 2.4.0-dev 2020-10-25 21:36:24 +01:00
1573058811 v2.3.0 2020-10-25 21:12:38 +01:00
51bc8fb2c6 Update setup.py 2020-10-03 12:01:36 +02:00
a69d6f44fd New Twitter handle — @httpie 2020-10-03 11:01:08 +02:00
507cd6e255 Fix table formatting
reStructuredText is a mess
2020-09-29 21:26:38 +02:00
759e4400d0 Cleanup 2020-09-28 17:02:22 +02:00
8cb1af7376 Cleanup 2020-09-28 16:58:59 +02:00
2f8d330b57 Fix --offline --chunked, add more tests 2020-09-28 16:40:16 +02:00
32d8b481e9 Fix --offline --multipart, add more tests 2020-09-28 16:22:34 +02:00
75f1e02215 README 2020-09-28 12:55:39 +02:00
70ba84dc48 Fix fixture encoding on Windows 2020-09-28 12:53:28 +02:00
5a5b42340f PEP8 2020-09-28 12:50:45 +02:00
299250b3c3 Merge branch 'feature/uploads2020' 2020-09-28 12:43:09 +02:00
6925d930da Add support for streamed uploads, --chunked, finish --multipart, etc.
Close #201
Close #753
Close #684
Close #903
Related: #452
2020-09-28 12:16:57 +02:00
c1948f8340 Update README.rst 2020-09-25 22:27:54 +02:00
b80ba040ac Update README.rst 2020-09-25 22:26:39 +02:00
b7754f92ce Merge branch 'master' into feature/uploads2020
# Conflicts:
#	httpie/cli/argparser.py
#	httpie/uploads.py
2020-09-25 14:46:19 +02:00
e4e40e5b06 Request content type 2020-09-25 14:44:22 +02:00
d12af4a569 WIP 2020-09-25 13:44:28 +02:00
c431ed7728 CHANGELOG
#963
2020-09-20 09:30:18 +02:00
16ef08a159 Gracefully ignore cookie expiry dates in invalid format
Close #963
2020-09-20 09:21:10 +02:00
100872b5cf pep8 2020-08-19 10:39:13 +02:00
664cebfbcc Update README.rst 2020-08-19 10:31:20 +02:00
743f9738a3 Update README.rst 2020-08-19 10:30:40 +02:00
69445c106c Cleanup 2020-08-19 10:25:47 +02:00
1813cf6156 Add --multipart and --boundary 2020-08-19 10:22:50 +02:00
a23b0e39e5 Update README.rst 2020-08-17 13:34:23 +02:00
06dec4e6c6 Update README.rst 2020-08-17 13:31:51 +02:00
ce185bd0fa Update __init__.py 2020-08-17 13:16:57 +02:00
1e1dbfeba0 Update README.rst 2020-08-17 13:14:52 +02:00
5a908aa411 pep8 2020-08-15 17:51:43 +02:00
6cd934d1b8 Add support for multipart upload streaming
Close #684, #201
2020-08-15 17:50:00 +02:00
d32c8cab12 Syntax 2020-08-15 15:34:31 +02:00
5ce7c190e9 Add a --quiet example 2020-08-15 15:33:24 +02:00
1aa1366f99 Finish --quiet 2020-08-15 15:26:29 +02:00
2c7f24e3e5 Added additional tests to verify downloads work properly with quiet flag 2020-08-15 15:26:29 +02:00
c90d039a0b fixed issues related to downloading and using quiet at the same time 2020-08-15 15:26:29 +02:00
ae22d4e754 Additional Aesthetic changes 2020-08-15 15:26:29 +02:00
69e1067a2c Aesthetic changes 2020-08-15 15:26:29 +02:00
7e38f9ccf0 Added additional tests for flag and better documentation 2020-08-15 15:26:29 +02:00
d546081340 Solved issue pertaining to downloads and added additional testing functionality for devnull 2020-08-15 15:26:29 +02:00
6421c145d9 Added changes suggested in the PR review 2020-08-15 15:26:29 +02:00
61e7cd786e Added a documentation entry and modified CHANGELOG 2020-08-15 15:26:29 +02:00
4bd2e622a5 Added tests for --quiet flag 2020-08-15 15:26:29 +02:00
a4a1e8d43b Added a quiet functionality 2020-08-15 15:26:28 +02:00
ebf2139fd5 Introduce CurliPie to convert from cURL to HTTPie (#843) 2020-08-14 15:27:49 +02:00
6c84cebed4 Update build.yml 2020-08-06 22:35:35 +02:00
10246366da Quieten ssl tests (#952)
* Add skip when required TLS version unsupported

Allow tests to skip, rather than fail from SSL errors with unsupported
TLS version, e.g. if Openssl is configured with MinProtocol higher than
the version being tested.

* Regenerate test certificate and key

Regenerate these with more secure settings for the sake of future
proofing, regenerate the key using RSA 4096 and sign the certificate
with SHA512.

This fixes test failures in tests/test_ssl.py when the user's OpenSSL
security level is set to a value greater than 1 and resolves issue #948

* Suppress SSL warnings in no verify tests
2020-08-06 22:24:03 +02:00
a448b0d928 Fix minor typos in the README and CONTRIBUTING docs (#956)
* Fix typo in CONTRIBUTING docs

* Fix minor typo in README (exists --> exits)
2020-07-16 22:47:39 +02:00
0541490dda Add test to test auth plugin reused in session (#938)
* Add test to test auth plugin reused in session

* Remove unnecessary assertion in auth-plugin test

* Fixed auth test to use same session file

* Add test for password prompt behaviour in session

* Edit auth readme for plugin clarity
2020-07-10 12:48:26 +02:00
3704db9b6d Install setuptools & wheel in CI II. 2020-07-07 13:39:02 +02:00
d1665b08d2 Install setuptools & wheel in CI 2020-07-07 13:38:27 +02:00
1a4e0c2646 Add wheel to tests_require
Speculative. Not sure why it’s suddenly needed.

https://github.com/jakubroztocil/httpie/runs/845338452
2020-07-07 13:35:09 +02:00
0d480139e4 Split session JSON serialization and writing to file
To avoid writing invalid JSON in case of presence of unserializable data due to an internal bug.
2020-07-07 13:26:47 +02:00
9931747901 Update CONTRIBUTING.rst 2020-07-07 13:19:42 +02:00
8891afa3b7 Reorganize and expand CONTRIBUTING.rst 2020-06-26 17:48:04 +02:00
4f493d51f8 Add Windows setup instructions (#941)
* Added a sub-section specifically for windows

* Wrote instructions for creating/activating venv

* Split the PS and CMD examples into two separate code blocks

* Specified language for the code blocks

* Added test instructions for windows

* Converted slash to backward
2020-06-26 17:25:26 +02:00
cf937b6b79 Remove tox (#944)
* Removed the instructions of tox testing

* Deleted tox.ini

* removed tox from requirements

* removed tox from setup.cfg

* removed tox from the Makefile

* removed tox from contributing docs

* updated the CHANGELOG

* removed tox from .gitignore
2020-06-26 17:22:06 +02:00
14677bd25d Cleanup inline to-dos
I.
2020-06-25 11:36:09 +02:00
49e71d252f Fixed test_ciphers_none_can_be_selected on OpenBSD
Thanks @juped!
2020-06-19 18:26:08 +02:00
d6f25b1017 Update Brew formula for v2.2.0 2020-06-19 02:23:12 +02:00
a434cddd42 Fix install_requires 2020-06-19 01:13:11 +02:00
55d7af86fd Install requests[socks] by default for out of the box SOCKS support
Close #904
2020-06-19 00:56:30 +02:00
978aace86c Update README.rst (#737) 2020-06-19 00:25:29 +02:00
ecdeffe7c8 CHANGELOG + README 2020-06-18 23:23:10 +02:00
9500ce136a Combine cookies from original request and session file
Close #932
Co-authored-by: kbanc <katherine.bancoft@gmail.com>
Co-authored-by: Gabriel Cruz <gabs.oficial98@gmail.com>
2020-06-18 23:17:33 +02:00
93d07cfe57 v2.3.0-dev 2020-06-18 22:25:07 +02:00
59 changed files with 2502 additions and 839 deletions

12
.github/FUNDING.yml vendored
View File

@ -1,12 +0,0 @@
# These are supported funding model platforms
github: jakubroztocil # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: https://paypal.me/roztocil

View File

@ -8,8 +8,8 @@ jobs:
- uses: actions/checkout@v1 - uses: actions/checkout@v1
- uses: actions/setup-python@v1 - uses: actions/setup-python@v1
with: with:
python-version: 3.8 python-version: 3.9
- run: pip install --upgrade pip - run: python -m pip install --upgrade pip setuptools wheel
- run: make install - run: make install
- run: make pycodestyle - run: make pycodestyle
- run: make test-cover - run: make test-cover
@ -23,15 +23,12 @@ jobs:
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest, macOS-latest, windows-latest] os: [ubuntu-latest, macOS-latest, windows-latest]
python-version: [3.6, 3.7, 3.8] python-version: [3.6, 3.7, 3.8, 3.9]
exclude:
- os: windows-latest
python-version: 3.8
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v1
- uses: actions/setup-python@v1 - uses: actions/setup-python@v1
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- run: python -m pip install --upgrade pip - run: python -m pip install --upgrade pip setuptools wheel
- run: pip install --upgrade --editable . - run: python -m pip install --upgrade --editable .
- run: python setup.py test - run: python setup.py test

1
.gitignore vendored
View File

@ -52,7 +52,6 @@ pip-delete-this-directory.txt
# Unit test / coverage reports # Unit test / coverage reports
htmlcov/ htmlcov/
.tox/
.nox/ .nox/
.coverage .coverage
.coverage.* .coverage.*

View File

@ -8,7 +8,7 @@ HTTPie authors
Patches and ideas Patches and ideas
----------------- -----------------
`Complete list of contributors on GitHub <https://github.com/jakubroztocil/httpie/graphs/contributors>`_ `Complete list of contributors on GitHub <https://github.com/httpie/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>`_
@ -38,5 +38,6 @@ Patches and ideas
* `Edward Yang <https://github.com/honorabrutroll>`_ * `Edward Yang <https://github.com/honorabrutroll>`_
* `Aleksandr Vinokurov <https://github.com/aleksandr-vin>`_ * `Aleksandr Vinokurov <https://github.com/aleksandr-vin>`_
* `Jeff Byrnes <https://github.com/jeffbyrnes>`_ * `Jeff Byrnes <https://github.com/jeffbyrnes>`_
* `Denis Belavin <https://github.com/LuckyDenis>`_

View File

@ -6,6 +6,32 @@ This document records all notable changes to `HTTPie <https://httpie.org>`_.
This project adheres to `Semantic Versioning <https://semver.org/>`_. This project adheres to `Semantic Versioning <https://semver.org/>`_.
`2.4.0`_ (2021-02-06)
---------------------
* Added support for ``--session`` cookie expiration based on ``Set-Cookie: max-age=<n>``. (`#1029`_)
* Show a ``--check-status`` warning with ``--quiet`` as well, not only when the output si redirected. (`#1026`_)
* Fixed upload with ``--session`` (`#1020`_).
* Fixed a missing blank line between request and response (`#1006`_).
`2.3.0`_ (2020-10-25)
-------------------------
* Added support for streamed uploads (`#201`_).
* Added support for multipart upload streaming (`#684`_).
* Added support for body-from-file upload streaming (``http pie.dev/post @file``).
* Added ``--chunked`` to enable chunked transfer encoding (`#753`_).
* Added ``--multipart`` to allow ``multipart/form-data`` encoding for non-file ``--form`` requests as well.
* Added support for preserving field order in multipart requests (`#903`_).
* Added ``--boundary`` to allow a custom boundary string for ``multipart/form-data`` requests.
* Added support for combining cookies specified on the CLI and in a session file (`#932`_).
* Added out of the box SOCKS support with no extra installation (`#904`_).
* Added ``--quiet, -q`` flag to enforce silent behaviour.
* Fixed the handling of invalid ``expires`` dates in ``Set-Cookie`` headers (`#963`_).
* Removed Tox testing entirely (`#943`_).
`2.2.0`_ (2020-06-18) `2.2.0`_ (2020-06-18)
------------------------- -------------------------
@ -404,51 +430,65 @@ This project adheres to `Semantic Versioning <https://semver.org/>`_.
* Initial public release * Initial public release
.. _`0.1.0`: https://github.com/jakubroztocil/httpie/commit/b966efa .. _`0.1.0`: https://github.com/httpie/httpie/commit/b966efa
.. _0.1.4: https://github.com/jakubroztocil/httpie/compare/b966efa...0.1.4 .. _0.1.4: https://github.com/httpie/httpie/compare/b966efa...0.1.4
.. _0.1.5: https://github.com/jakubroztocil/httpie/compare/0.1.4...0.1.5 .. _0.1.5: https://github.com/httpie/httpie/compare/0.1.4...0.1.5
.. _0.1.6: https://github.com/jakubroztocil/httpie/compare/0.1.5...0.1.6 .. _0.1.6: https://github.com/httpie/httpie/compare/0.1.5...0.1.6
.. _0.2.0: https://github.com/jakubroztocil/httpie/compare/0.1.6...0.2.0 .. _0.2.0: https://github.com/httpie/httpie/compare/0.1.6...0.2.0
.. _0.2.1: https://github.com/jakubroztocil/httpie/compare/0.2.0...0.2.1 .. _0.2.1: https://github.com/httpie/httpie/compare/0.2.0...0.2.1
.. _0.2.2: https://github.com/jakubroztocil/httpie/compare/0.2.1...0.2.2 .. _0.2.2: https://github.com/httpie/httpie/compare/0.2.1...0.2.2
.. _0.2.5: https://github.com/jakubroztocil/httpie/compare/0.2.2...0.2.5 .. _0.2.5: https://github.com/httpie/httpie/compare/0.2.2...0.2.5
.. _0.2.6: https://github.com/jakubroztocil/httpie/compare/0.2.5...0.2.6 .. _0.2.6: https://github.com/httpie/httpie/compare/0.2.5...0.2.6
.. _0.2.7: https://github.com/jakubroztocil/httpie/compare/0.2.5...0.2.7 .. _0.2.7: https://github.com/httpie/httpie/compare/0.2.5...0.2.7
.. _0.3.0: https://github.com/jakubroztocil/httpie/compare/0.2.7...0.3.0 .. _0.3.0: https://github.com/httpie/httpie/compare/0.2.7...0.3.0
.. _0.4.0: https://github.com/jakubroztocil/httpie/compare/0.3.0...0.4.0 .. _0.4.0: https://github.com/httpie/httpie/compare/0.3.0...0.4.0
.. _0.4.1: https://github.com/jakubroztocil/httpie/compare/0.4.0...0.4.1 .. _0.4.1: https://github.com/httpie/httpie/compare/0.4.0...0.4.1
.. _0.5.0: https://github.com/jakubroztocil/httpie/compare/0.4.1...0.5.0 .. _0.5.0: https://github.com/httpie/httpie/compare/0.4.1...0.5.0
.. _0.5.1: https://github.com/jakubroztocil/httpie/compare/0.5.0...0.5.1 .. _0.5.1: https://github.com/httpie/httpie/compare/0.5.0...0.5.1
.. _0.6.0: https://github.com/jakubroztocil/httpie/compare/0.5.1...0.6.0 .. _0.6.0: https://github.com/httpie/httpie/compare/0.5.1...0.6.0
.. _0.7.1: https://github.com/jakubroztocil/httpie/compare/0.6.0...0.7.1 .. _0.7.1: https://github.com/httpie/httpie/compare/0.6.0...0.7.1
.. _0.8.0: https://github.com/jakubroztocil/httpie/compare/0.7.1...0.8.0 .. _0.8.0: https://github.com/httpie/httpie/compare/0.7.1...0.8.0
.. _0.9.0: https://github.com/jakubroztocil/httpie/compare/0.8.0...0.9.0 .. _0.9.0: https://github.com/httpie/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/httpie/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/httpie/httpie/compare/0.9.1...0.9.2
.. _0.9.3: https://github.com/jakubroztocil/httpie/compare/0.9.2...0.9.3 .. _0.9.3: https://github.com/httpie/httpie/compare/0.9.2...0.9.3
.. _0.9.4: https://github.com/jakubroztocil/httpie/compare/0.9.3...0.9.4 .. _0.9.4: https://github.com/httpie/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.6: https://github.com/httpie/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.8: https://github.com/httpie/httpie/compare/0.9.6...0.9.8
.. _0.9.9: https://github.com/jakubroztocil/httpie/compare/0.9.8...0.9.9 .. _0.9.9: https://github.com/httpie/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.0: https://github.com/httpie/httpie/compare/0.9.9...1.0.0
.. _1.0.1: https://github.com/jakubroztocil/httpie/compare/1.0.0...1.0.1 .. _1.0.1: https://github.com/httpie/httpie/compare/1.0.0...1.0.1
.. _1.0.2: https://github.com/jakubroztocil/httpie/compare/1.0.1...1.0.2 .. _1.0.2: https://github.com/httpie/httpie/compare/1.0.1...1.0.2
.. _1.0.3: https://github.com/jakubroztocil/httpie/compare/1.0.2...1.0.3 .. _1.0.3: https://github.com/httpie/httpie/compare/1.0.2...1.0.3
.. _2.0.0: https://github.com/jakubroztocil/httpie/compare/1.0.3...2.0.0 .. _2.0.0: https://github.com/httpie/httpie/compare/1.0.3...2.0.0
.. _2.1.0: https://github.com/jakubroztocil/httpie/compare/2.0.0...2.1.0 .. _2.1.0: https://github.com/httpie/httpie/compare/2.0.0...2.1.0
.. _2.2.0: https://github.com/jakubroztocil/httpie/compare/2.1.0...2.2.0 .. _2.2.0: https://github.com/httpie/httpie/compare/2.1.0...2.2.0
.. _2.3.0: https://github.com/httpie/httpie/compare/2.2.0...2.3.0
.. _2.4.0: https://github.com/httpie/httpie/compare/2.3.0...2.4.0
.. _2.5.0-dev: https://github.com/httpie/httpie/compare/2.4.0...master
.. _#128: https://github.com/httpie/httpie/issues/128
.. _#128: https://github.com/jakubroztocil/httpie/issues/128 .. _#201: https://github.com/httpie/httpie/issues/201
.. _#488: https://github.com/jakubroztocil/httpie/issues/488 .. _#488: https://github.com/httpie/httpie/issues/488
.. _#668: https://github.com/jakubroztocil/httpie/issues/668 .. _#668: https://github.com/httpie/httpie/issues/668
.. _#718: https://github.com/jakubroztocil/httpie/issues/718 .. _#684: https://github.com/httpie/httpie/issues/684
.. _#719: https://github.com/jakubroztocil/httpie/issues/719 .. _#718: https://github.com/httpie/httpie/issues/718
.. _#840: https://github.com/jakubroztocil/httpie/issues/840 .. _#719: https://github.com/httpie/httpie/issues/719
.. _#853: https://github.com/jakubroztocil/httpie/issues/853 .. _#753: https://github.com/httpie/httpie/issues/753
.. _#852: https://github.com/jakubroztocil/httpie/issues/852 .. _#840: https://github.com/httpie/httpie/issues/840
.. _#870: https://github.com/jakubroztocil/httpie/issues/870 .. _#853: https://github.com/httpie/httpie/issues/853
.. _#895: https://github.com/jakubroztocil/httpie/issues/895 .. _#852: https://github.com/httpie/httpie/issues/852
.. _#920: https://github.com/jakubroztocil/httpie/issues/920 .. _#870: https://github.com/httpie/httpie/issues/870
.. _#925: https://github.com/jakubroztocil/httpie/issues/925 .. _#895: https://github.com/httpie/httpie/issues/895
.. _#934: https://github.com/jakubroztocil/httpie/issues/934 .. _#903: https://github.com/httpie/httpie/issues/903
.. _#920: https://github.com/httpie/httpie/issues/920
.. _#904: https://github.com/httpie/httpie/issues/904
.. _#925: https://github.com/httpie/httpie/issues/925
.. _#932: https://github.com/httpie/httpie/issues/932
.. _#934: https://github.com/httpie/httpie/issues/934
.. _#943: https://github.com/httpie/httpie/issues/943
.. _#963: https://github.com/httpie/httpie/issues/963
.. _#1006: https://github.com/httpie/httpie/issues/1006
.. _#1020: https://github.com/httpie/httpie/issues/1020
.. _#1026: https://github.com/httpie/httpie/issues/1026
.. _#1029: https://github.com/httpie/httpie/issues/1029

View File

@ -16,17 +16,34 @@ to your bug report, e.g.:
.. code-block:: bash .. code-block:: bash
$ http --debug [COMPLETE ARGUMENT LIST THAT TRIGGERS THE ERROR] $ http --debug <COMPLETE ARGUMENT LIST THAT TRIGGERS THE ERROR>
[COMPLETE OUTPUT]
<COMPLETE OUTPUT>
2. Contributing Code and Docs 2. Contributing Code and Docs
============================= =============================
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 previously been discussed.
is a bigger one, it's always good to discuss before you start working on
it. If your change alters HTTPies behaviour or interface, it's a good idea to
discuss it before you start working on it.
If you are fixing an issue, the first step should be to create a test case that
reproduces the incorrect behaviour. That will also help you to build an
understanding of the issue at hand.
**Pull requests introducing code changes without tests
will generally not get merged. The same goes for PRs changing HTTPies
behaviour and not providing documentation.**
Conversely, PRs consisting of documentation improvements or tests
for existing-yet-previously-untested behavior will very likely be merged.
Therefore, docs and tests improvements are a great candidate for your first
contribution.
Consider also adding a ``CHANGELOG`` entry for your changes.
Development Environment Development Environment
@ -36,7 +53,7 @@ Development Environment
Getting the code Getting the code
**************** ****************
Go to https://github.com/jakubroztocil/httpie and fork the project repository. Go to https://github.com/httpie/httpie and fork the project repository.
.. code-block:: bash .. code-block:: bash
@ -87,7 +104,7 @@ activate the environment — we have created a symlink for you. Its a bit of
a hack but it works™.) a hack but it works™.)
You should now see ``(httpie)`` next to your shell prompt, and You should now see ``(httpie)`` next to your shell prompt, and
the ``http`` should point to you development copy: the ``http`` command should point to your development copy:
.. code-block:: .. code-block::
@ -115,7 +132,7 @@ Testing & CI
Please add tests for any new features and bug fixes. Please add tests for any new features and bug fixes.
When you open a pull request, When you open a pull request,
`GitHub Actions <https://github.com/jakubroztocil/httpie/actions>`_ `GitHub Actions <https://github.com/httpie/httpie/actions>`_
will automatically run HTTPies `test suite`_ against your code will automatically run HTTPies `test suite`_ against your code
so please make sure all checks pass. so please make sure all checks pass.
@ -123,8 +140,7 @@ so please make sure all checks pass.
Running tests locally Running tests locally
********************* *********************
HTTPie uses the `pytest`_ runner. It also uses `Tox`_ which allows you to run HTTPie uses the `pytest`_ runner.
tests on multiple Python versions even when testing locally.
.. code-block:: bash .. code-block:: bash
@ -135,9 +151,6 @@ tests on multiple Python versions even when testing locally.
# Run tests with coverage # Run tests with coverage
make test-cover make test-cover
# Run all tests in all of the supported and available Pythons via Tox
make test-tox
# Test PEP8 compliance # Test PEP8 compliance
make pycodestyle make pycodestyle
@ -158,26 +171,69 @@ can run specific tests from the terminal:
py.test tests/test_uploads.py::TestMultipartFormDataFileUpload py.test tests/test_uploads.py::TestMultipartFormDataFileUpload
py.test tests/test_uploads.py::TestMultipartFormDataFileUpload::test_upload_ok 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. See `Makefile`_ for additional development utilities.
Windows
*******
Finally, don't forget to add yourself to `AUTHORS`_! If you are on a Windows machine and not able to run ``make``,
follow the next steps for a basic setup. As a prerequisite, you need to have
Python 3.6+ installed.
Create a virtual environment and activate it:
.. code-block:: powershell
python -m venv --prompt httpie venv
venv\Scripts\activate
Install HTTPie in editable mode with all the dependencies:
.. code-block:: powershell
pip install --upgrade -e . -r requirements-dev.txt
You should now see ``(httpie)`` next to your shell prompt, and
the ``http`` command should point to your development copy:
.. code-block:: powershell
# In PowerShell:
(httpie) PS C:\Users\ovezovs\httpie> Get-Command http
CommandType Name Version Source
----------- ---- ------- ------
Application http.exe 0.0.0.0 C:\Users\ovezovs\httpie\venv\Scripts\http.exe
.. code-block:: bash
# In CMD:
(httpie) C:\Users\ovezovs\httpie> where http
C:\Users\ovezovs\httpie\venv\Scripts\http.exe
C:\Users\ovezovs\AppData\Local\Programs\Python\Python38-32\Scripts\http.exe
(httpie) C:\Users\ovezovs\httpie> http --version
2.3.0-dev
Use ``pytest`` to run tests locally with an active virtual environment:
.. code-block:: bash
# Run all tests
py.test
.. _Tox: http://tox.testrun.org -----
.. _supported Python environments: https://github.com/jakubroztocil/httpie/blob/master/tox.ini
.. _existing issues: https://github.com/jakubroztocil/httpie/issues?state=open
.. _AUTHORS: https://github.com/jakubroztocil/httpie/blob/master/AUTHORS.rst Finally, feel free to add yourself to `AUTHORS`_!
.. _Makefile: https://github.com/jakubroztocil/httpie/blob/master/Makefile
.. _existing issues: https://github.com/httpie/httpie/issues?state=open
.. _AUTHORS: https://github.com/httpie/httpie/blob/master/AUTHORS.rst
.. _Makefile: https://github.com/httpie/httpie/blob/master/Makefile
.. _venv: https://docs.python.org/3/library/venv.html .. _venv: https://docs.python.org/3/library/venv.html
.. _pytest: https://pytest.org/ .. _pytest: https://pytest.org/
.. _Style Guide for Python Code: https://python.org/dev/peps/pep-0008/ .. _Style Guide for Python Code: https://python.org/dev/peps/pep-0008/
.. _test suite: https://github.com/jakubroztocil/httpie/tree/master/tests .. _test suite: https://github.com/httpie/httpie/tree/master/tests

View File

@ -3,5 +3,5 @@ include README.rst
include CHANGELOG.rst include CHANGELOG.rst
include AUTHORS.rst include AUTHORS.rst
# <https://github.com/jakubroztocil/httpie/issues/182> # <https://github.com/httpie/httpie/issues/182>
recursive-include tests/ * recursive-include tests/ *

View File

@ -2,6 +2,8 @@
# See ./CONTRIBUTING.rst # See ./CONTRIBUTING.rst
############################################################################### ###############################################################################
.PHONY: build
ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
VERSION=$(shell grep __version__ httpie/__init__.py) VERSION=$(shell grep __version__ httpie/__init__.py)
REQUIREMENTS=requirements-dev.txt REQUIREMENTS=requirements-dev.txt
@ -38,7 +40,7 @@ clean:
rm -rf $(VENV_ROOT) rm -rf $(VENV_ROOT)
# Remove symlink for virtualenvwrapper, if weve created one. # Remove symlink for virtualenvwrapper, if weve created one.
[ -n "$(WORKON_HOME)" -a -L "$(WORKON_HOME)/httpie" -a -f "$(WORKON_HOME)/httpie" ] && rm $(WORKON_HOME)/httpie || true [ -n "$(WORKON_HOME)" -a -L "$(WORKON_HOME)/httpie" -a -f "$(WORKON_HOME)/httpie" ] && rm $(WORKON_HOME)/httpie || true
rm -rf .tox *.egg dist build .coverage .cache .pytest_cache httpie.egg-info rm -rf *.egg dist build .coverage .cache .pytest_cache httpie.egg-info
find . -name '__pycache__' -delete -o -name '*.pyc' -delete find . -name '__pycache__' -delete -o -name '*.pyc' -delete
@echo @echo
@ -86,7 +88,7 @@ test-cover: test
# test-all is meant to test everything — even this Makefile # test-all is meant to test everything — even this Makefile
test-all: clean install test test-tox test-dist pycodestyle test-all: clean install test test-dist pycodestyle
@echo @echo
@ -94,12 +96,6 @@ test-dist: test-sdist test-bdist-wheel
@echo @echo
test-tox: uninstall-httpie install
@echo $(H1)Running tests on all Pythons via Tox$(H1END)
$(VENV_BIN)/tox
@echo
test-sdist: clean venv test-sdist: clean venv
@echo $(H1)Testing sdist build an installation$(H1END) @echo $(H1)Testing sdist build an installation$(H1END)
$(VENV_PYTHON) setup.py sdist $(VENV_PYTHON) setup.py sdist
@ -117,6 +113,9 @@ test-bdist-wheel: clean venv
@echo @echo
twine-check:
twine check dist/*
pycodestyle: pycodestyle:
@echo $(H1)Running pycodestyle$(H1END) @echo $(H1)Running pycodestyle$(H1END)
@[ -f $(VENV_BIN)/pycodestyle ] || $(VENV_PIP) install pycodestyle @[ -f $(VENV_BIN)/pycodestyle ] || $(VENV_PIP) install pycodestyle
@ -137,6 +136,11 @@ codecov-upload:
############################################################################### ###############################################################################
build:
rm -rf build/
$(VENV_PYTHON) setup.py sdist bdist_wheel
publish: test-all publish-no-test publish: test-all publish-no-test
@ -144,8 +148,9 @@ publish-no-test:
@echo $(H1)Testing wheel build an installation$(H1END) @echo $(H1)Testing wheel build an installation$(H1END)
@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 || echo ok
$(VENV_PYTHON) setup.py sdist bdist_wheel make build
$(VENV_BIN)/twine upload dist/* make twine-check
$(VENV_BIN)/twine upload --repository=httpie dist/*
@echo @echo
@ -170,7 +175,6 @@ uninstall-httpie:
############################################################################### ###############################################################################
pdf: pdf:
# NOTE: rst2pdf needs to be installed manually and against a Python 2
@echo "Converting README.rst to PDF…" @echo "Converting README.rst to PDF…"
rst2pdf \ rst2pdf \
--strip-elements-with-class=no-pdf \ --strip-elements-with-class=no-pdf \

File diff suppressed because it is too large Load Diff

View File

@ -16,6 +16,7 @@ PACKAGES = [
'httpie', 'httpie',
'Pygments', 'Pygments',
'requests', 'requests',
'requests-toolbelt',
'certifi', 'certifi',
'urllib3', 'urllib3',
'idna', 'idna',

View File

@ -9,42 +9,45 @@ class Httpie < Formula
desc "User-friendly cURL replacement (command-line HTTP client)" desc "User-friendly cURL replacement (command-line HTTP client)"
homepage "https://httpie.org/" homepage "https://httpie.org/"
url "https://files.pythonhosted.org/packages/e2/79/da6aec7b4356e8b325561b987c940e5b1e4de1200a5c3db7c57a97d61ca1/httpie-2.1.0.tar.gz" url "https://files.pythonhosted.org/packages/b4/d4/712645808103f2d15c281b9eacd184c88754ef7e9a322d9a30ba343fd341/httpie-2.3.0.tar.gz"
sha256 "a76f1c72e83bd03cde3478c5f345d5570fdb2967ed19d68d09518088640b9e8e" sha256 "d540571991d07329d217c31bf1ff95fd217957da2aa2def09bcfa0c0fca0cf96"
head "https://github.com/jakubroztocil/httpie.git" license "BSD-3-Clause"
head "https://github.com/httpie/httpie.git"
bottle do livecheck do
cellar :any_skip_relocation url :stable
sha256 "1fb33d9c85dc462c2549a03cf08670edad8014a5fdf0a7cb26493c64af40283d" => :catalina
sha256 "a22030f0b96c698c90265286ee80ffbb03079d1d008a80c0bdb3ea15a17d3fbb" => :mojave
sha256 "9f994ecf826efe53a3a49d1c3193e271629068d11306df55adeea2842a8afb8c" => :high_sierra
end end
depends_on "python@3.8" depends_on "python@3.9"
resource "Pygments" do resource "Pygments" do
url "https://files.pythonhosted.org/packages/6e/4d/4d2fe93a35dfba417311a4ff627489a947b01dc0cc377a3673c00cf7e4b2/Pygments-2.6.1.tar.gz" url "https://files.pythonhosted.org/packages/5d/0e/ff13c055b014d634ed17e9e9345a312c28ec6a06448ba6d6ccfa77c3b5e8/Pygments-2.7.2.tar.gz"
sha256 "647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44" sha256 "381985fcc551eb9d37c52088a32914e00517e57f4a21609f48141ba08e193fa0"
end end
resource "requests" do resource "requests" do
url "https://files.pythonhosted.org/packages/f5/4f/280162d4bd4d8aad241a21aecff7a6e46891b905a4341e7ab549ebaf7915/requests-2.23.0.tar.gz" url "https://files.pythonhosted.org/packages/da/67/672b422d9daf07365259958912ba533a0ecab839d4084c487a5fe9a5405f/requests-2.24.0.tar.gz"
sha256 "b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6" sha256 "b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b"
end
resource "requests-toolbelt" do
url "https://files.pythonhosted.org/packages/28/30/7bf7e5071081f761766d46820e52f4b16c8a08fef02d2eb4682ca7534310/requests-toolbelt-0.9.1.tar.gz"
sha256 "968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"
end end
resource "certifi" do resource "certifi" do
url "https://files.pythonhosted.org/packages/b8/e2/a3a86a67c3fc8249ed305fc7b7d290ebe5e4d46ad45573884761ef4dea7b/certifi-2020.4.5.1.tar.gz" url "https://files.pythonhosted.org/packages/40/a7/ded59fa294b85ca206082306bba75469a38ea1c7d44ea7e1d64f5443d67a/certifi-2020.6.20.tar.gz"
sha256 "51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519" sha256 "5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3"
end end
resource "urllib3" do resource "urllib3" do
url "https://files.pythonhosted.org/packages/05/8c/40cd6949373e23081b3ea20d5594ae523e681b6f472e600fbc95ed046a36/urllib3-1.25.9.tar.gz" url "https://files.pythonhosted.org/packages/76/d9/bbbafc76b18da706451fa91bc2ebe21c0daf8868ef3c30b869ac7cb7f01d/urllib3-1.25.11.tar.gz"
sha256 "3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527" sha256 "8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2"
end end
resource "idna" do resource "idna" do
url "https://files.pythonhosted.org/packages/cb/19/57503b5de719ee45e83472f339f617b0c01ad75cba44aba1e4c97c2b0abd/idna-2.9.tar.gz" url "https://files.pythonhosted.org/packages/ea/b7/e0e3c1c467636186c39925827be42f16fee389dc404ac29e930e9136be70/idna-2.10.tar.gz"
sha256 "7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb" sha256 "b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"
end end
resource "chardet" do resource "chardet" do

View File

@ -1,8 +1,8 @@
""" """
HTTPie - a CLI, cURL-like tool for humans. HTTPie: command-line HTTP client for the API era.
""" """
__version__ = '2.2.0' __version__ = '2.4.0'
__author__ = 'Jakub Roztocil' __author__ = 'Jakub Roztocil'
__licence__ = 'BSD' __licence__ = 'BSD'

View File

@ -15,12 +15,11 @@ from httpie.cli.argtypes import (
parse_format_options, parse_format_options,
) )
from httpie.cli.constants import ( from httpie.cli.constants import (
DEFAULT_FORMAT_OPTIONS, HTTP_GET, HTTP_POST, OUTPUT_OPTIONS, HTTP_GET, HTTP_POST, OUTPUT_OPTIONS, OUTPUT_OPTIONS_DEFAULT,
OUTPUT_OPTIONS_DEFAULT, OUTPUT_OPTIONS_DEFAULT_OFFLINE, OUTPUT_OPTIONS_DEFAULT_STDOUT_REDIRECTED,
OUTPUT_OPTIONS_DEFAULT_STDOUT_REDIRECTED, OUT_RESP_BODY, PRETTY_MAP, OUT_RESP_BODY, PRETTY_MAP, PRETTY_STDOUT_TTY_ONLY, RequestType,
PRETTY_STDOUT_TTY_ONLY, SEPARATOR_CREDENTIALS, SEPARATOR_GROUP_ALL_ITEMS, SEPARATOR_CREDENTIALS,
SEPARATOR_GROUP_DATA_ITEMS, URL_SCHEME_RE, SEPARATOR_GROUP_ALL_ITEMS, SEPARATOR_GROUP_DATA_ITEMS, URL_SCHEME_RE,
OUTPUT_OPTIONS_DEFAULT_OFFLINE,
) )
from httpie.cli.exceptions import ParseError from httpie.cli.exceptions import ParseError
from httpie.cli.requestitems import RequestItems from httpie.cli.requestitems import RequestItems
@ -75,18 +74,16 @@ class HTTPieArgumentParser(argparse.ArgumentParser):
) -> argparse.Namespace: ) -> argparse.Namespace:
self.env = env self.env = env
self.args, no_options = super().parse_known_args(args, namespace) self.args, no_options = super().parse_known_args(args, namespace)
if self.args.debug: if self.args.debug:
self.args.traceback = True self.args.traceback = True
self.has_stdin_data = ( self.has_stdin_data = (
self.env.stdin self.env.stdin
and not self.args.ignore_stdin and not self.args.ignore_stdin
and not self.env.stdin_isatty and not self.env.stdin_isatty
) )
# Arguments processing and environment setup. # Arguments processing and environment setup.
self._apply_no_options(no_options) self._apply_no_options(no_options)
self._process_request_type()
self._process_download_options() self._process_download_options()
self._setup_standard_streams() self._setup_standard_streams()
self._process_output_options() self._process_output_options()
@ -94,14 +91,35 @@ class HTTPieArgumentParser(argparse.ArgumentParser):
self._process_format_options() self._process_format_options()
self._guess_method() self._guess_method()
self._parse_items() self._parse_items()
if self.has_stdin_data: if self.has_stdin_data:
self._body_from_file(self.env.stdin) self._body_from_file(self.env.stdin)
self._process_url()
self._process_auth()
if self.args.compress:
# TODO: allow --compress with --chunked / --multipart
if self.args.chunked:
self.error('cannot combine --compress and --chunked')
if self.args.multipart:
self.error('cannot combine --compress and --multipart')
return self.args
def _process_request_type(self):
request_type = self.args.request_type
self.args.json = request_type is RequestType.JSON
self.args.multipart = request_type is RequestType.MULTIPART
self.args.form = request_type in {
RequestType.FORM,
RequestType.MULTIPART,
}
def _process_url(self):
if not URL_SCHEME_RE.match(self.args.url): if not URL_SCHEME_RE.match(self.args.url):
if os.path.basename(env.program_name) == 'https': if os.path.basename(self.env.program_name) == 'https':
scheme = 'https://' scheme = 'https://'
else: else:
scheme = self.args.default_scheme + "://" scheme = self.args.default_scheme + '://'
# 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)
@ -114,9 +132,6 @@ class HTTPieArgumentParser(argparse.ArgumentParser):
self.args.url += rest self.args.url += rest
else: else:
self.args.url = scheme + self.args.url self.args.url = scheme + self.args.url
self._process_auth()
return self.args
# noinspection PyShadowingBuiltins # noinspection PyShadowingBuiltins
def _print_message(self, message, file=None): def _print_message(self, message, file=None):
@ -135,6 +150,7 @@ class HTTPieArgumentParser(argparse.ArgumentParser):
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) self.args.output_file_specified = bool(self.args.output_file)
if self.args.download: if self.args.download:
# FIXME: Come up with a cleaner solution. # FIXME: Come up with a cleaner solution.
@ -147,6 +163,7 @@ class HTTPieArgumentParser(argparse.ArgumentParser):
# The response body will be treated separately. # The response body will be treated separately.
self.env.stdout = self.env.stderr self.env.stdout = self.env.stderr
self.env.stdout_isatty = self.env.stderr_isatty self.env.stdout_isatty = self.env.stderr_isatty
elif self.args.output_file: elif self.args.output_file:
# When not `--download`ing, then `--output` simply replaces # When not `--download`ing, then `--output` simply replaces
# `stdout`. The file is opened for appending, which isn't what # `stdout`. The file is opened for appending, which isn't what
@ -163,6 +180,11 @@ class HTTPieArgumentParser(argparse.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
if self.args.quiet:
self.env.stderr = self.env.devnull
if not (self.args.output_file_specified and not self.args.download):
self.env.stdout = self.env.devnull
def _process_auth(self): def _process_auth(self):
# TODO: refactor & simplify this method. # TODO: refactor & simplify this method.
self.args.auth_plugin = None self.args.auth_plugin = None
@ -271,7 +293,7 @@ class HTTPieArgumentParser(argparse.ArgumentParser):
'data (key=value) cannot be mixed. Pass ' 'data (key=value) cannot be mixed. Pass '
'--ignore-stdin to let key/value take priority. ' '--ignore-stdin to let key/value take priority. '
'See https://httpie.org/doc#scripting for details.') 'See https://httpie.org/doc#scripting for details.')
self.args.data = getattr(fd, 'buffer', fd).read() self.args.data = getattr(fd, 'buffer', fd)
def _guess_method(self): def _guess_method(self):
"""Set `args.method` if not specified to either POST or GET """Set `args.method` if not specified to either POST or GET
@ -332,6 +354,7 @@ class HTTPieArgumentParser(argparse.ArgumentParser):
self.args.data = request_items.data self.args.data = request_items.data
self.args.files = request_items.files self.args.files = request_items.files
self.args.params = request_items.params self.args.params = request_items.params
self.args.multipart_data = request_items.multipart_data
if self.args.files and not self.args.form: if self.args.files and not self.args.form:
# `http url @/path/to/file` # `http url @/path/to/file`

View File

@ -242,9 +242,3 @@ PARSED_DEFAULT_FORMAT_OPTIONS = parse_format_options(
s=','.join(DEFAULT_FORMAT_OPTIONS), s=','.join(DEFAULT_FORMAT_OPTIONS),
defaults=None, defaults=None,
) )
class UnsortedAction(argparse.Action):
def __call__(self, *args, **kwargs):
return 1

View File

@ -1,16 +1,10 @@
"""Parsing and processing of CLI input (args, auth credentials, files, stdin). """Parsing and processing of CLI input (args, auth credentials, files, stdin).
""" """
import enum
import re import re
import ssl
# TODO: Use MultiDict for headers once added to `requests`.
# <https://github.com/jakubroztocil/httpie/issues/130>
# ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
# <https://tools.ietf.org/html/rfc3986#section-3.1>
URL_SCHEME_RE = re.compile(r'^[a-z][a-z0-9.+-]*://', re.IGNORECASE) URL_SCHEME_RE = re.compile(r'^[a-z][a-z0-9.+-]*://', re.IGNORECASE)
HTTP_POST = 'POST' HTTP_POST = 'POST'
@ -38,6 +32,12 @@ SEPARATOR_GROUP_DATA_ITEMS = frozenset({
SEPARATOR_DATA_EMBED_RAW_JSON_FILE SEPARATOR_DATA_EMBED_RAW_JSON_FILE
}) })
SEPARATORS_GROUP_MULTIPART = frozenset({
SEPARATOR_DATA_STRING,
SEPARATOR_DATA_EMBED_FILE_CONTENTS,
SEPARATOR_FILE_UPLOAD,
})
# Separators for items whose value is a filename to be embedded # Separators for items whose value is a filename to be embedded
SEPARATOR_GROUP_DATA_EMBED_ITEMS = frozenset({ SEPARATOR_GROUP_DATA_EMBED_ITEMS = frozenset({
SEPARATOR_DATA_EMBED_FILE_CONTENTS, SEPARATOR_DATA_EMBED_FILE_CONTENTS,
@ -103,3 +103,9 @@ UNSORTED_FORMAT_OPTIONS_STRING = ','.join(
OUTPUT_OPTIONS_DEFAULT = OUT_RESP_HEAD + OUT_RESP_BODY 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
OUTPUT_OPTIONS_DEFAULT_OFFLINE = OUT_REQ_HEAD + OUT_REQ_BODY OUTPUT_OPTIONS_DEFAULT_OFFLINE = OUT_REQ_HEAD + OUT_REQ_BODY
class RequestType(enum.Enum):
FORM = enum.auto()
MULTIPART = enum.auto()
JSON = enum.auto()

View File

@ -15,7 +15,8 @@ from httpie.cli.constants import (
DEFAULT_FORMAT_OPTIONS, OUTPUT_OPTIONS, DEFAULT_FORMAT_OPTIONS, OUTPUT_OPTIONS,
OUTPUT_OPTIONS_DEFAULT, OUT_REQ_BODY, OUT_REQ_HEAD, OUTPUT_OPTIONS_DEFAULT, OUT_REQ_BODY, OUT_REQ_HEAD,
OUT_RESP_BODY, OUT_RESP_HEAD, PRETTY_MAP, PRETTY_STDOUT_TTY_ONLY, OUT_RESP_BODY, OUT_RESP_HEAD, PRETTY_MAP, PRETTY_STDOUT_TTY_ONLY,
SEPARATOR_GROUP_ALL_ITEMS, SEPARATOR_PROXY, SORTED_FORMAT_OPTIONS_STRING, RequestType, SEPARATOR_GROUP_ALL_ITEMS, SEPARATOR_PROXY,
SORTED_FORMAT_OPTIONS_STRING,
UNSORTED_FORMAT_OPTIONS_STRING, UNSORTED_FORMAT_OPTIONS_STRING,
) )
from httpie.output.formatters.colors import ( from httpie.output.formatters.colors import (
@ -36,7 +37,7 @@ parser = HTTPieArgumentParser(
Suggestions and bug reports are greatly appreciated: Suggestions and bug reports are greatly appreciated:
https://github.com/jakubroztocil/httpie/issues https://github.com/httpie/httpie/issues
'''), '''),
) )
@ -110,7 +111,7 @@ positional.add_argument(
awesome:=true amount:=42 colors:='["red", "green", "blue"]' awesome:=true amount:=42 colors:='["red", "green", "blue"]'
'@' Form file fields (only with --form, -f): '@' Form file fields (only with --form or --multipart):
cv@~/Documents/CV.pdf cv@~/Documents/CV.pdf
cv@'~/Documents/CV.pdf;type=application/pdf' cv@'~/Documents/CV.pdf;type=application/pdf'
@ -141,7 +142,9 @@ content_type = parser.add_argument_group(
content_type.add_argument( content_type.add_argument(
'--json', '-j', '--json', '-j',
action='store_true', action='store_const',
const=RequestType.JSON,
dest='request_type',
help=''' help='''
(default) Data items from the command line are serialized as a JSON object. (default) Data items from the command line are serialized as a JSON object.
The Content-Type and Accept headers are set to application/json The Content-Type and Accept headers are set to application/json
@ -151,7 +154,9 @@ content_type.add_argument(
) )
content_type.add_argument( content_type.add_argument(
'--form', '-f', '--form', '-f',
action='store_true', action='store_const',
const=RequestType.FORM,
dest='request_type',
help=''' help='''
Data items from the command line are serialized as form fields. Data items from the command line are serialized as form fields.
@ -161,6 +166,26 @@ content_type.add_argument(
''' '''
) )
content_type.add_argument(
'--multipart',
action='store_const',
const=RequestType.MULTIPART,
dest='request_type',
help='''
Similar to --form, but always sends a multipart/form-data
request (i.e., even without files).
'''
)
content_type.add_argument(
'--boundary',
help='''
Specify a custom boundary string for multipart/form-data requests.
Only has effect only together with --form.
'''
)
####################################################################### #######################################################################
# Content processing. # Content processing.
@ -375,7 +400,7 @@ output_options.add_argument(
action='store_true', action='store_true',
default=False, default=False,
help=''' help='''
Always stream the output by line, i.e., behave like `tail -f'. Always stream the response body by line, i.e., behave like `tail -f'.
Without --stream and with --pretty (either set or implied), Without --stream and with --pretty (either set or implied),
HTTPie fetches the whole response before it outputs the processed data. HTTPie fetches the whole response before it outputs the processed data.
@ -426,6 +451,17 @@ output_options.add_argument(
''' '''
) )
output_options.add_argument(
'--quiet', '-q',
action='store_true',
default=False,
help='''
Do not print to stdout or stderr.
stdout is still redirected if --output is specified.
Flag doesn't affect behaviour of download beyond not printing to terminal.
'''
)
####################################################################### #######################################################################
# Sessions # Sessions
####################################################################### #######################################################################
@ -627,6 +663,15 @@ network.add_argument(
''' '''
) )
network.add_argument(
'--chunked',
default=False,
action='store_true',
help="""
"""
)
####################################################################### #######################################################################
# SSL # SSL
####################################################################### #######################################################################
@ -643,7 +688,7 @@ ssl.add_argument(
''' '''
) )
ssl.add_argument( ssl.add_argument(
'--ssl', # TODO: Maybe something more general, such as --secure-protocol? '--ssl',
dest='ssl_version', dest='ssl_version',
choices=list(sorted(AVAILABLE_SSL_VERSION_ARG_MAPPING.keys())), choices=list(sorted(AVAILABLE_SSL_VERSION_ARG_MAPPING.keys())),
help=''' help='''

View File

@ -34,20 +34,25 @@ class MultiValueOrderedDict(OrderedDict):
super().__setitem__(key, [self[key]]) super().__setitem__(key, [self[key]])
self[key].append(value) self[key].append(value)
class RequestQueryParamsDict(MultiValueOrderedDict):
pass
class RequestDataDict(MultiValueOrderedDict):
def items(self): def items(self):
for key, values in super(MultiValueOrderedDict, self).items(): for key, values in super().items():
if not isinstance(values, list): if not isinstance(values, list):
values = [values] values = [values]
for value in values: for value in values:
yield key, value yield key, value
class RequestQueryParamsDict(MultiValueOrderedDict):
pass
class RequestDataDict(MultiValueOrderedDict):
pass
class MultipartRequestDataDict(MultiValueOrderedDict):
pass
class RequestFilesDict(RequestDataDict): class RequestFilesDict(RequestDataDict):
pass pass

View File

@ -1,16 +1,17 @@
import os import os
from io import BytesIO
from typing import Callable, Dict, IO, List, Optional, Tuple, Union from typing import Callable, Dict, IO, List, Optional, Tuple, Union
from httpie.cli.argtypes import KeyValueArg from httpie.cli.argtypes import KeyValueArg
from httpie.cli.constants import ( from httpie.cli.constants import (
SEPARATOR_DATA_EMBED_FILE_CONTENTS, SEPARATOR_DATA_EMBED_RAW_JSON_FILE, SEPARATORS_GROUP_MULTIPART, SEPARATOR_DATA_EMBED_FILE_CONTENTS,
SEPARATOR_DATA_EMBED_RAW_JSON_FILE,
SEPARATOR_DATA_RAW_JSON, SEPARATOR_DATA_STRING, SEPARATOR_FILE_UPLOAD, SEPARATOR_DATA_RAW_JSON, SEPARATOR_DATA_STRING, SEPARATOR_FILE_UPLOAD,
SEPARATOR_FILE_UPLOAD_TYPE, SEPARATOR_HEADER, SEPARATOR_HEADER_EMPTY, SEPARATOR_FILE_UPLOAD_TYPE, SEPARATOR_HEADER, SEPARATOR_HEADER_EMPTY,
SEPARATOR_QUERY_PARAM, SEPARATOR_QUERY_PARAM,
) )
from httpie.cli.dicts import ( from httpie.cli.dicts import (
RequestDataDict, RequestFilesDict, RequestHeadersDict, RequestJSONDataDict, MultipartRequestDataDict, RequestDataDict, RequestFilesDict,
RequestHeadersDict, RequestJSONDataDict,
RequestQueryParamsDict, RequestQueryParamsDict,
) )
from httpie.cli.exceptions import ParseError from httpie.cli.exceptions import ParseError
@ -24,6 +25,8 @@ class RequestItems:
self.data = RequestDataDict() if as_form else RequestJSONDataDict() self.data = RequestDataDict() if as_form else RequestJSONDataDict()
self.files = RequestFilesDict() self.files = RequestFilesDict()
self.params = RequestQueryParamsDict() self.params = RequestQueryParamsDict()
# To preserve the order of fields in file upload multipart requests.
self.multipart_data = MultipartRequestDataDict()
@classmethod @classmethod
def from_args( def from_args(
@ -69,7 +72,11 @@ class RequestItems:
for arg in request_item_args: for arg in request_item_args:
processor_func, target_dict = rules[arg.sep] processor_func, target_dict = rules[arg.sep]
target_dict[arg.key] = processor_func(arg) value = processor_func(arg)
target_dict[arg.key] = value
if arg.sep in SEPARATORS_GROUP_MULTIPART:
instance.multipart_data[arg.key] = value
return instance return instance
@ -99,15 +106,13 @@ def process_file_upload_arg(arg: KeyValueArg) -> Tuple[str, IO, str]:
parts = arg.value.split(SEPARATOR_FILE_UPLOAD_TYPE) parts = arg.value.split(SEPARATOR_FILE_UPLOAD_TYPE)
filename = parts[0] filename = parts[0]
mime_type = parts[1] if len(parts) > 1 else None mime_type = parts[1] if len(parts) > 1 else None
try: try:
with open(os.path.expanduser(filename), 'rb') as f: f = open(os.path.expanduser(filename), 'rb')
contents = f.read()
except IOError as e: except IOError as e:
raise ParseError('"%s": %s' % (arg.orig, e)) raise ParseError('"%s": %s' % (arg.orig, e))
return ( return (
os.path.basename(filename), os.path.basename(filename),
BytesIO(contents), f,
mime_type or get_content_type(filename), mime_type or get_content_type(filename),
) )

View File

@ -2,23 +2,26 @@ import argparse
import http.client import http.client
import json import json
import sys import sys
import zlib
from contextlib import contextmanager from contextlib import contextmanager
from pathlib import Path from pathlib import Path
from typing import Iterable, Union from typing import Callable, Iterable, Union
from urllib.parse import urlparse, urlunparse from urllib.parse import urlparse, urlunparse
import requests import requests
# noinspection PyPackageRequirements # noinspection PyPackageRequirements
import urllib3 import urllib3
from httpie import __version__ from httpie import __version__
from httpie.cli.dicts import RequestHeadersDict from httpie.cli.dicts import RequestHeadersDict
from httpie.plugins.registry import plugin_manager from httpie.plugins.registry import plugin_manager
from httpie.sessions import get_httpie_session from httpie.sessions import get_httpie_session
from httpie.ssl import AVAILABLE_SSL_VERSION_ARG_MAPPING, HTTPieHTTPSAdapter from httpie.ssl import AVAILABLE_SSL_VERSION_ARG_MAPPING, HTTPieHTTPSAdapter
from httpie.uploads import (
compress_request, prepare_request_body,
get_multipart_data_and_content_type,
)
from httpie.utils import get_expired_cookies, repr_dict from httpie.utils import get_expired_cookies, repr_dict
urllib3.disable_warnings() urllib3.disable_warnings()
FORM_CONTENT_TYPE = 'application/x-www-form-urlencoded; charset=utf-8' FORM_CONTENT_TYPE = 'application/x-www-form-urlencoded; charset=utf-8'
@ -30,6 +33,7 @@ DEFAULT_UA = f'HTTPie/{__version__}'
def collect_messages( def collect_messages(
args: argparse.Namespace, args: argparse.Namespace,
config_dir: Path, config_dir: Path,
request_body_read_callback: Callable[[bytes], None] = None,
) -> Iterable[Union[requests.PreparedRequest, requests.Response]]: ) -> Iterable[Union[requests.PreparedRequest, requests.Response]]:
httpie_session = None httpie_session = None
httpie_session_headers = None httpie_session_headers = None
@ -45,6 +49,7 @@ def collect_messages(
request_kwargs = make_request_kwargs( request_kwargs = make_request_kwargs(
args=args, args=args,
base_headers=httpie_session_headers, base_headers=httpie_session_headers,
request_body_read_callback=request_body_read_callback
) )
send_kwargs = make_send_kwargs(args) send_kwargs = make_send_kwargs(args)
send_kwargs_mergeable_from_env = make_send_kwargs_mergeable_from_env(args) send_kwargs_mergeable_from_env = make_send_kwargs_mergeable_from_env(args)
@ -79,7 +84,10 @@ def collect_messages(
prepped_url=prepared_request.url, prepped_url=prepared_request.url,
) )
if args.compress and prepared_request.body: if args.compress and prepared_request.body:
compress_body(prepared_request, always=args.compress > 1) compress_request(
request=prepared_request,
always=args.compress > 1,
)
response_count = 0 response_count = 0
expired_cookies = [] expired_cookies = []
while prepared_request: while prepared_request:
@ -126,7 +134,7 @@ def collect_messages(
# noinspection PyProtectedMember # noinspection PyProtectedMember
@contextmanager @contextmanager
def max_headers(limit): def max_headers(limit):
# <https://github.com/jakubroztocil/httpie/issues/802> # <https://github.com/httpie/httpie/issues/802>
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
orig = http.client._MAXHEADERS orig = http.client._MAXHEADERS
http.client._MAXHEADERS = limit or float('Inf') http.client._MAXHEADERS = limit or float('Inf')
@ -136,22 +144,6 @@ def max_headers(limit):
http.client._MAXHEADERS = orig http.client._MAXHEADERS = orig
def compress_body(request: requests.PreparedRequest, always: bool):
deflater = zlib.compressobj()
body_bytes = (
request.body
if isinstance(request.body, bytes)
else request.body.encode()
)
deflated_data = deflater.compress(body_bytes)
deflated_data += deflater.flush()
is_economical = len(deflated_data) < len(body_bytes)
if is_economical or always:
request.body = deflated_data
request.headers['Content-Encoding'] = 'deflate'
request.headers['Content-Length'] = str(len(deflated_data))
def build_requests_session( def build_requests_session(
verify: bool, verify: bool,
ssl_version: str = None, ssl_version: str = None,
@ -196,7 +188,7 @@ def finalize_headers(headers: RequestHeadersDict) -> RequestHeadersDict:
# Also, requests raises `InvalidHeader` for leading spaces. # Also, requests raises `InvalidHeader` for leading spaces.
value = value.strip() value = value.strip()
if isinstance(value, str): if isinstance(value, str):
# See <https://github.com/jakubroztocil/httpie/issues/212> # See <https://github.com/httpie/httpie/issues/212>
value = value.encode('utf8') value = value.encode('utf8')
final_headers[name] = value final_headers[name] = value
return final_headers return final_headers
@ -250,12 +242,14 @@ def make_send_kwargs_mergeable_from_env(args: argparse.Namespace) -> dict:
def make_request_kwargs( def make_request_kwargs(
args: argparse.Namespace, args: argparse.Namespace,
base_headers: RequestHeadersDict = None base_headers: RequestHeadersDict = None,
request_body_read_callback=lambda chunk: chunk
) -> dict: ) -> dict:
""" """
Translate our `args` into `requests.Request` keyword arguments. Translate our `args` into `requests.Request` keyword arguments.
""" """
files = args.files
# 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
@ -272,16 +266,32 @@ def make_request_kwargs(
if base_headers: if base_headers:
headers.update(base_headers) headers.update(base_headers)
headers.update(args.headers) headers.update(args.headers)
if args.offline and args.chunked and 'Transfer-Encoding' not in headers:
# When online, we let requests set the header instead to be able more
# easily verify chunking is taking place.
headers['Transfer-Encoding'] = 'chunked'
headers = finalize_headers(headers) headers = finalize_headers(headers)
if (args.form and files) or args.multipart:
data, headers['Content-Type'] = get_multipart_data_and_content_type(
data=args.multipart_data,
boundary=args.boundary,
content_type=args.headers.get('Content-Type'),
)
kwargs = { kwargs = {
'method': args.method.lower(), 'method': args.method.lower(),
'url': args.url, 'url': args.url,
'headers': headers, 'headers': headers,
'data': data, 'data': prepare_request_body(
body=data,
body_read_callback=request_body_read_callback,
chunked=args.chunked,
offline=args.offline,
content_length_header_value=headers.get('Content-Length'),
),
'auth': args.auth, 'auth': args.auth,
'params': args.params, 'params': args.params.items(),
'files': args.files,
} }
return kwargs return kwargs
@ -294,7 +304,7 @@ def ensure_path_as_is(orig_url: str, prepped_url: str) -> str:
untouched because other (welcome) processing on the URL might have untouched because other (welcome) processing on the URL might have
taken place. taken place.
<https://github.com/jakubroztocil/httpie/issues/895> <https://github.com/httpie/httpie/issues/895>
<https://ec.haxx.se/http/http-basics#path-as-is> <https://ec.haxx.se/http/http-basics#path-as-is>

View File

@ -108,16 +108,14 @@ class BaseConfigDict(dict):
self.ensure_directory() self.ensure_directory()
try: json_string = json.dumps(
with self.path.open('w') as f:
json.dump(
obj=self, obj=self,
fp=f,
indent=4, indent=4,
sort_keys=True, sort_keys=True,
ensure_ascii=True, ensure_ascii=True,
) )
f.write('\n') try:
self.path.write_text(json_string + '\n')
except IOError: except IOError:
if not fail_silently: if not fail_silently:
raise raise

View File

@ -1,4 +1,5 @@
import sys import sys
import os
from pathlib import Path from pathlib import Path
from typing import IO, Optional from typing import IO, Optional
@ -56,7 +57,7 @@ class Environment:
) )
del colorama del colorama
def __init__(self, **kwargs): def __init__(self, devnull=None, **kwargs):
""" """
Use keyword arguments to overwrite Use keyword arguments to overwrite
any of the class attributes for this instance. any of the class attributes for this instance.
@ -65,6 +66,10 @@ class Environment:
assert all(hasattr(type(self), attr) for attr in kwargs.keys()) assert all(hasattr(type(self), attr) for attr in kwargs.keys())
self.__dict__.update(**kwargs) self.__dict__.update(**kwargs)
# The original STDERR unaffected by --quieting.
self._orig_stderr = self.stderr
self._devnull = devnull
# Keyword arguments > stream.encoding > default utf8 # Keyword arguments > stream.encoding > default utf8
if self.stdin and self.stdin_encoding is None: if self.stdin and self.stdin_encoding is None:
self.stdin_encoding = getattr( self.stdin_encoding = getattr(
@ -108,6 +113,16 @@ class Environment:
self.log_error(e, level='warning') self.log_error(e, level='warning')
return config return config
@property
def devnull(self) -> IO:
if self._devnull is None:
self._devnull = open(os.devnull, 'w+')
return self._devnull
@devnull.setter
def devnull(self, value):
self._devnull = value
def log_error(self, msg, level='error'): def log_error(self, msg, level='error'):
assert level in ['error', 'warning'] assert level in ['error', 'warning']
self.stderr.write(f'\n{self.program_name}: {level}: {msg}\n\n') self._orig_stderr.write(f'\n{self.program_name}: {level}: {msg}\n\n')

View File

@ -2,26 +2,24 @@ import argparse
import os import os
import platform import platform
import sys import sys
from typing import List, Union from typing import List, Optional, Tuple, Union
import requests import requests
from pygments import __version__ as pygments_version from pygments import __version__ as pygments_version
from requests import __version__ as requests_version from requests import __version__ as requests_version
from httpie import __version__ as httpie_version from httpie import __version__ as httpie_version
from httpie.cli.constants import OUT_REQ_BODY, OUT_REQ_HEAD, OUT_RESP_BODY, OUT_RESP_HEAD
from httpie.client import collect_messages from httpie.client import collect_messages
from httpie.context import Environment from httpie.context import Environment
from httpie.downloads import Downloader from httpie.downloads import Downloader
from httpie.output.writer import write_message, write_stream from httpie.output.writer import write_message, write_stream, MESSAGE_SEPARATOR_BYTES
from httpie.plugins.registry import plugin_manager from httpie.plugins.registry import plugin_manager
from httpie.status import ExitStatus, http_status_to_exit_status from httpie.status import ExitStatus, http_status_to_exit_status
# noinspection PyDefaultArgument # noinspection PyDefaultArgument
def main( def main(args: List[Union[str, bytes]] = sys.argv, env=Environment()) -> ExitStatus:
args: List[Union[str, bytes]] = sys.argv,
env=Environment(),
) -> ExitStatus:
""" """
The main function. The main function.
@ -111,64 +109,98 @@ def main(
return exit_status return exit_status
def program( def get_output_options(
args: argparse.Namespace, args: argparse.Namespace,
env: Environment, message: Union[requests.PreparedRequest, requests.Response]
) -> ExitStatus: ) -> Tuple[bool, bool]:
return {
requests.PreparedRequest: (
OUT_REQ_HEAD in args.output_options,
OUT_REQ_BODY in args.output_options,
),
requests.Response: (
OUT_RESP_HEAD in args.output_options,
OUT_RESP_BODY in args.output_options,
),
}[type(message)]
def program(args: argparse.Namespace, env: Environment) -> ExitStatus:
""" """
The main program without error handling. The main program without error handling.
""" """
# TODO: Refactor and drastically simplify, especially so that the separator logic is elsewhere.
exit_status = ExitStatus.SUCCESS exit_status = ExitStatus.SUCCESS
downloader = None downloader = None
initial_request: Optional[requests.PreparedRequest] = None
final_response: Optional[requests.Response] = None
def separate():
getattr(env.stdout, 'buffer', env.stdout).write(MESSAGE_SEPARATOR_BYTES)
def request_body_read_callback(chunk: bytes):
should_pipe_to_stdout = bool(
# Request body output desired
OUT_REQ_BODY in args.output_options
# & not `.read()` already pre-request (e.g., for compression)
and initial_request
# & non-EOF chunk
and chunk
)
if should_pipe_to_stdout:
msg = requests.PreparedRequest()
msg.is_body_upload_chunk = True
msg.body = chunk
msg.headers = initial_request.headers
write_message(requests_message=msg, env=env, args=args, with_body=True, with_headers=False)
try: try:
if args.download: if args.download:
args.follow = True # --download implies --follow. args.follow = True # --download implies --follow.
downloader = Downloader( downloader = Downloader(output_file=args.output_file, progress_file=env.stderr, resume=args.download_resume)
output_file=args.output_file,
progress_file=env.stderr,
resume=args.download_resume
)
downloader.pre_request(args.headers) downloader.pre_request(args.headers)
messages = collect_messages(args=args, config_dir=env.config.directory,
request_body_read_callback=request_body_read_callback)
force_separator = False
prev_with_body = False
initial_request = None # Process messages as theyre generated
final_response = None for message in messages:
is_request = isinstance(message, requests.PreparedRequest)
for message in collect_messages(args, env.config.directory): with_headers, with_body = get_output_options(args=args, message=message)
write_message( do_write_body = with_body
requests_message=message, if prev_with_body and (with_headers or with_body) and (force_separator or not env.stdout_isatty):
env=env, # Separate after a previous message with body, if needed. See test_tokens.py.
args=args, separate()
) force_separator = False
if isinstance(message, requests.PreparedRequest): if is_request:
if not initial_request: if not initial_request:
initial_request = message initial_request = message
is_streamed_upload = not isinstance(message.body, (str, bytes))
if with_body:
do_write_body = not is_streamed_upload
force_separator = is_streamed_upload and env.stdout_isatty
else: else:
final_response = message final_response = message
if args.check_status or downloader: if args.check_status or downloader:
exit_status = http_status_to_exit_status( exit_status = http_status_to_exit_status(http_status=message.status_code, follow=args.follow)
http_status=message.status_code, if exit_status != ExitStatus.SUCCESS and (not env.stdout_isatty or args.quiet):
follow=args.follow env.log_error(f'HTTP {message.raw.status} {message.raw.reason}', level='warning')
) write_message(requests_message=message, env=env, args=args, with_headers=with_headers,
if (not env.stdout_isatty with_body=do_write_body)
and exit_status != ExitStatus.SUCCESS): prev_with_body = with_body
env.log_error(
f'HTTP {message.raw.status} {message.raw.reason}',
level='warning'
)
# Cleanup
if force_separator:
separate()
if downloader and exit_status == ExitStatus.SUCCESS: if downloader and exit_status == ExitStatus.SUCCESS:
# Last response body download. # Last response body download.
download_stream, download_to = downloader.start( download_stream, download_to = downloader.start(
initial_url=initial_request.url, initial_url=initial_request.url,
final_response=final_response, final_response=final_response,
) )
write_stream( write_stream(stream=download_stream, outfile=download_to, flush=False)
stream=download_stream,
outfile=download_to,
flush=False,
)
downloader.finish() downloader.finish()
if downloader.interrupted: if downloader.interrupted:
exit_status = ExitStatus.ERROR exit_status = ExitStatus.ERROR
@ -182,9 +214,7 @@ def program(
finally: finally:
if downloader and not downloader.finished: if downloader and not downloader.finished:
downloader.failed() downloader.failed()
if not isinstance(args, list) and args.output_file and args.output_file_specified:
if (not isinstance(args, list) and args.output_file
and args.output_file_specified):
args.output_file.close() args.output_file.close()
@ -212,6 +242,6 @@ def decode_raw_args(
""" """
return [ return [
arg.decode(stdin_encoding) arg.decode(stdin_encoding)
if type(arg) == bytes else arg if type(arg) is bytes else arg
for arg in args for arg in args
] ]

View File

@ -247,7 +247,7 @@ class Downloader:
assert not self.status.time_started assert not self.status.time_started
# FIXME: some servers still might sent Content-Encoding: gzip # FIXME: some servers still might sent Content-Encoding: gzip
# <https://github.com/jakubroztocil/httpie/issues/423> # <https://github.com/httpie/httpie/issues/423>
try: try:
total_size = int(final_response.headers['Content-Length']) total_size = int(final_response.headers['Content-Length'])
except (KeyError, ValueError, TypeError): except (KeyError, ValueError, TypeError):

View File

@ -86,7 +86,7 @@ class ColorFormatter(FormatterPlugin):
lexer=lexer, lexer=lexer,
formatter=self.formatter, formatter=self.formatter,
) )
return body.strip() return body
def get_lexer_for_body( def get_lexer_for_body(
self, mime: str, self, mime: str,

View File

@ -14,10 +14,13 @@ BINARY_SUPPRESSED_NOTICE = (
) )
class BinarySuppressedError(Exception): class DataSuppressedError(Exception):
message = None
class BinarySuppressedError(DataSuppressedError):
"""An error indicating that the body is binary and won't be written, """An error indicating that the body is binary and won't be written,
e.g., for terminal output).""" e.g., for terminal output)."""
message = BINARY_SUPPRESSED_NOTICE message = BINARY_SUPPRESSED_NOTICE
@ -63,7 +66,7 @@ class BaseStream:
yield chunk yield chunk
if self.on_body_chunk_downloaded: if self.on_body_chunk_downloaded:
self.on_body_chunk_downloaded(chunk) self.on_body_chunk_downloaded(chunk)
except BinarySuppressedError as e: except DataSuppressedError as e:
if self.with_headers: if self.with_headers:
yield b'\n' yield b'\n'
yield e.message yield e.message

View File

@ -1,6 +1,6 @@
import argparse import argparse
import errno import errno
from typing import Union, IO, TextIO, Tuple, Type from typing import IO, TextIO, Tuple, Type, Union
import requests import requests
@ -8,39 +8,30 @@ from httpie.context import Environment
from httpie.models import HTTPRequest, HTTPResponse from httpie.models import HTTPRequest, HTTPResponse
from httpie.output.processing import Conversion, Formatting from httpie.output.processing import Conversion, Formatting
from httpie.output.streams import ( from httpie.output.streams import (
RawStream, PrettyStream, BaseStream, BufferedPrettyStream, EncodedStream, PrettyStream, RawStream,
BufferedPrettyStream, EncodedStream,
BaseStream,
)
from httpie.cli.constants import (
OUT_REQ_BODY, OUT_REQ_HEAD, OUT_RESP_BODY, OUT_RESP_HEAD,
) )
MESSAGE_SEPARATOR = '\n\n'
MESSAGE_SEPARATOR_BYTES = MESSAGE_SEPARATOR.encode()
def write_message( def write_message(
requests_message: Union[requests.PreparedRequest, requests.Response], requests_message: Union[requests.PreparedRequest, requests.Response],
env: Environment, env: Environment,
args: argparse.Namespace, args: argparse.Namespace,
with_headers=False,
with_body=False,
): ):
output_options_by_message_type = { if not (with_body or with_headers):
requests.PreparedRequest: {
'with_headers': OUT_REQ_HEAD in args.output_options,
'with_body': OUT_REQ_BODY in args.output_options,
},
requests.Response: {
'with_headers': OUT_RESP_HEAD in args.output_options,
'with_body': OUT_RESP_BODY in args.output_options,
},
}
output_options = output_options_by_message_type[type(requests_message)]
if not any(output_options.values()):
return return
write_stream_kwargs = { write_stream_kwargs = {
'stream': build_output_stream_for_message( 'stream': build_output_stream_for_message(
args=args, args=args,
env=env, env=env,
requests_message=requests_message, requests_message=requests_message,
**output_options, with_body=with_body,
with_headers=with_headers,
), ),
# NOTE: `env.stdout` will in fact be `stderr` with `--download` # NOTE: `env.stdout` will in fact be `stderr` with `--download`
'outfile': env.stdout, 'outfile': env.stdout,
@ -120,10 +111,11 @@ def build_output_stream_for_message(
with_body=with_body, with_body=with_body,
**stream_kwargs, **stream_kwargs,
) )
if env.stdout_isatty and with_body: if (env.stdout_isatty and with_body
and not getattr(requests_message, 'is_body_upload_chunk', False)):
# Ensure a blank line after the response body. # Ensure a blank line after the response body.
# For terminal output only. # For terminal output only.
yield b'\n\n' yield MESSAGE_SEPARATOR_BYTES
def get_stream_type_and_kwargs( def get_stream_type_and_kwargs(

View File

@ -19,7 +19,7 @@ class HTTPBasicAuth(requests.auth.HTTPBasicAuth):
""" """
Override username/password serialization to allow unicode. Override username/password serialization to allow unicode.
See https://github.com/jakubroztocil/httpie/issues/212 See https://github.com/httpie/httpie/issues/212
""" """
# noinspection PyTypeChecker # noinspection PyTypeChecker

View File

@ -4,6 +4,8 @@ Persistent, JSON-serialized sessions.
""" """
import os import os
import re import re
from http.cookies import SimpleCookie
from pathlib import Path from pathlib import Path
from typing import Iterable, Optional, Union from typing import Iterable, Optional, Union
from urllib.parse import urlsplit from urllib.parse import urlsplit
@ -75,8 +77,16 @@ class Session(BaseConfigDict):
if value is None: if value is None:
continue # Ignore explicitly unset headers continue # Ignore explicitly unset headers
if type(value) is not str:
value = value.decode('utf8') value = value.decode('utf8')
if name == 'User-Agent' and value.startswith('HTTPie/'):
if name.lower() == 'user-agent' and value.startswith('HTTPie/'):
continue
if name.lower() == 'cookie':
for cookie_name, morsel in SimpleCookie(value).items():
self['cookies'][cookie_name] = {'value': morsel.value}
del request_headers[name]
continue continue
for prefix in SESSION_IGNORED_HEADER_PREFIXES: for prefix in SESSION_IGNORED_HEADER_PREFIXES:

View File

@ -52,7 +52,7 @@ class HTTPieHTTPSAdapter(HTTPAdapter):
verify: bool, verify: bool,
ssl_version: str = None, ssl_version: str = None,
ciphers: str = None, ciphers: str = None,
) -> ssl.SSLContext: ) -> 'ssl.SSLContext':
return create_urllib3_context( return create_urllib3_context(
ciphers=ciphers, ciphers=ciphers,
ssl_version=resolve_ssl_version(ssl_version), ssl_version=resolve_ssl_version(ssl_version),

138
httpie/uploads.py Normal file
View File

@ -0,0 +1,138 @@
import zlib
from typing import Callable, IO, Iterable, Tuple, Union
from urllib.parse import urlencode
import requests
from requests.utils import super_len
from requests_toolbelt import MultipartEncoder
from httpie.cli.dicts import MultipartRequestDataDict, RequestDataDict
class ChunkedUploadStream:
def __init__(self, stream: Iterable, callback: Callable):
self.callback = callback
self.stream = stream
def __iter__(self) -> Iterable[Union[str, bytes]]:
for chunk in self.stream:
self.callback(chunk)
yield chunk
class ChunkedMultipartUploadStream:
chunk_size = 100 * 1024
def __init__(self, encoder: MultipartEncoder):
self.encoder = encoder
def __iter__(self) -> Iterable[Union[str, bytes]]:
while True:
chunk = self.encoder.read(self.chunk_size)
if not chunk:
break
yield chunk
def prepare_request_body(
body: Union[str, bytes, IO, MultipartEncoder, RequestDataDict],
body_read_callback: Callable[[bytes], bytes],
content_length_header_value: int = None,
chunked=False,
offline=False,
) -> Union[str, bytes, IO, MultipartEncoder, ChunkedUploadStream]:
is_file_like = hasattr(body, 'read')
if isinstance(body, RequestDataDict):
body = urlencode(body, doseq=True)
if offline:
if is_file_like:
return body.read()
return body
if not is_file_like:
if chunked:
body = ChunkedUploadStream(
# Pass the entire body as one chunk.
stream=(chunk.encode() for chunk in [body]),
callback=body_read_callback,
)
else:
# File-like object.
if not super_len(body):
# Zero-length -> assume stdin.
if content_length_header_value is None and not chunked:
#
# Read the whole stdin to determine `Content-Length`.
#
# TODO: Instead of opt-in --chunked, consider making
# `Transfer-Encoding: chunked` for STDIN opt-out via
# something like --no-chunked.
# This would be backwards-incompatible so wait until v3.0.0.
#
body = body.read()
else:
orig_read = body.read
def new_read(*args):
chunk = orig_read(*args)
body_read_callback(chunk)
return chunk
body.read = new_read
if chunked:
if isinstance(body, MultipartEncoder):
body = ChunkedMultipartUploadStream(
encoder=body,
)
else:
body = ChunkedUploadStream(
stream=body,
callback=body_read_callback,
)
return body
def get_multipart_data_and_content_type(
data: MultipartRequestDataDict,
boundary: str = None,
content_type: str = None,
) -> Tuple[MultipartEncoder, str]:
encoder = MultipartEncoder(
fields=data.items(),
boundary=boundary,
)
if content_type:
content_type = content_type.strip()
if 'boundary=' not in content_type:
content_type = f'{content_type}; boundary={encoder.boundary_value}'
else:
content_type = encoder.content_type
data = encoder
return data, content_type
def compress_request(
request: requests.PreparedRequest,
always: bool,
):
deflater = zlib.compressobj()
if isinstance(request.body, str):
body_bytes = request.body.encode()
elif hasattr(request.body, 'read'):
body_bytes = request.body.read()
else:
body_bytes = request.body
deflated_data = deflater.compress(body_bytes)
deflated_data += deflater.flush()
is_economical = len(deflated_data) < len(body_bytes)
if is_economical or always:
request.body = deflated_data
request.headers['Content-Encoding'] = 'deflate'
request.headers['Content-Length'] = str(len(deflated_data))

View File

@ -6,7 +6,7 @@ import time
from collections import OrderedDict from collections import OrderedDict
from http.cookiejar import parse_ns_headers from http.cookiejar import parse_ns_headers
from pprint import pformat from pprint import pformat
from typing import List, Tuple from typing import List, Optional, Tuple
import requests.auth import requests.auth
@ -93,7 +93,12 @@ def get_expired_cookies(
headers: List[Tuple[str, str]], headers: List[Tuple[str, str]],
now: float = None now: float = None
) -> List[dict]: ) -> List[dict]:
now = now or time.time() now = now or time.time()
def is_expired(expires: Optional[float]) -> bool:
return expires is not None and expires <= now
attr_sets: List[Tuple[str, str]] = parse_ns_headers( attr_sets: List[Tuple[str, str]] = parse_ns_headers(
value for name, value in headers value for name, value in headers
if name.lower() == 'set-cookie' if name.lower() == 'set-cookie'
@ -103,11 +108,29 @@ def get_expired_cookies(
dict(attrs[1:], name=attrs[0][0]) dict(attrs[1:], name=attrs[0][0])
for attrs in attr_sets for attrs in attr_sets
] ]
_max_age_to_expires(cookies=cookies, now=now)
return [ return [
{ {
'name': cookie['name'], 'name': cookie['name'],
'path': cookie.get('path', '/') 'path': cookie.get('path', '/')
} }
for cookie in cookies for cookie in cookies
if cookie.get('expires', float('Inf')) <= now if is_expired(expires=cookie.get('expires'))
] ]
def _max_age_to_expires(cookies, now):
"""
Translate `max-age` into `expires` for Requests to take it into account.
HACK/FIXME: <https://github.com/psf/requests/issues/5743>
"""
for cookie in cookies:
if 'expires' in cookie:
continue
max_age = cookie.get('max-age')
if max_age and max_age.isdigit():
cookie['expires'] = now + float(max_age)

View File

@ -1,4 +1,3 @@
tox
mock mock
pytest pytest
pytest-cov pytest-cov

View File

@ -4,13 +4,13 @@
[tool:pytest] [tool:pytest]
# <https://docs.pytest.org/en/latest/customize.html> # <https://docs.pytest.org/en/latest/customize.html>
norecursedirs = tests/fixtures norecursedirs = tests/fixtures
addopts = --tb=native addopts = --tb=native --doctest-modules
[pycodestyle] [pycodestyle]
# <http://pycodestyle.pycqa.org/en/latest/intro.html#configuration> # <http://pycodestyle.pycqa.org/en/latest/intro.html#configuration>
exclude = .git,.idea,__pycache__,build,dist,.tox,.pytest_cache,*.egg-info exclude = .git,.idea,__pycache__,build,dist,.pytest_cache,*.egg-info
# <http://pycodestyle.pycqa.org/en/latest/intro.html#error-codes> # <http://pycodestyle.pycqa.org/en/latest/intro.html#error-codes>
# E241 - multiple spaces after , # E241 - multiple spaces after ,

View File

@ -15,11 +15,14 @@ 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_args = [ self.test_args = [
'--doctest-modules', '--verbose', '--doctest-modules',
'./httpie', './tests' '--verbose',
'./httpie',
'./tests',
] ]
self.test_suite = True self.test_suite = True
@ -36,8 +39,9 @@ tests_require = [
install_requires = [ install_requires = [
'requests>=2.22.0', 'requests[socks]>=2.22.0',
'Pygments>=2.5.2', 'Pygments>=2.5.2',
'requests-toolbelt>=0.9.1',
] ]
install_requires_win_only = [ install_requires_win_only = [
'colorama>=0.2.4', 'colorama>=0.2.4',
@ -70,8 +74,9 @@ setup(
version=httpie.__version__, version=httpie.__version__,
description=httpie.__doc__.strip(), description=httpie.__doc__.strip(),
long_description=long_description(), long_description=long_description(),
long_description_content_type='text/x-rst',
url='https://httpie.org/', url='https://httpie.org/',
download_url=f'https://github.com/jakubroztocil/httpie/archive/{httpie.__version__}.tar.gz', download_url=f'https://github.com/httpie/httpie/archive/{httpie.__version__}.tar.gz',
author=httpie.__author__, author=httpie.__author__,
author_email='jakub@roztocil.co', author_email='jakub@roztocil.co',
license=httpie.__licence__, license=httpie.__licence__,
@ -103,10 +108,9 @@ setup(
'Topic :: Utilities' 'Topic :: Utilities'
], ],
project_urls={ project_urls={
'GitHub': 'https://github.com/httpie/httpie',
'Twitter': 'https://twitter.com/httpie',
'Documentation': 'https://httpie.org/docs', 'Documentation': 'https://httpie.org/docs',
'Source': 'https://github.com/jakubroztocil/httpie',
'Online Demo': 'https://httpie.org/run', 'Online Demo': 'https://httpie.org/run',
'Donate': 'https://httpie.org/donate',
'Twitter': 'https://twitter.com/clihttp',
}, },
) )

View File

@ -5,4 +5,4 @@ HTTPie Test Suite
Please see `CONTRIBUTING`_. Please see `CONTRIBUTING`_.
.. _CONTRIBUTING: https://github.com/jakubroztocil/httpie/blob/master/CONTRIBUTING.rst .. _CONTRIBUTING: https://github.com/httpie/httpie/blob/master/CONTRIBUTING.rst

View File

@ -1,29 +1,31 @@
-----BEGIN CERTIFICATE----- -----BEGIN CERTIFICATE-----
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
-----END CERTIFICATE----- -----END CERTIFICATE-----

View File

@ -1,51 +1,51 @@
-----BEGIN RSA PRIVATE KEY----- -----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY----- -----END RSA PRIVATE KEY-----

View File

@ -1,87 +1,82 @@
Bag Attributes
localKeyID: 93 0C 3E A7 82 62 36 37 5E 73 9B 05 C4 98 DF DC 04 5C B4 C9
subject=/C=AU/ST=Some-State/O=Internet Widgits Pty Ltd
issuer=/C=US/ST=CA/L=SF/O=HTTPie/CN=HTTPie
-----BEGIN CERTIFICATE----- -----BEGIN CERTIFICATE-----
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
-----END CERTIFICATE----- -----END CERTIFICATE-----
Bag Attributes
localKeyID: 93 0C 3E A7 82 62 36 37 5E 73 9B 05 C4 98 DF DC 04 5C B4 C9
Key Attributes: <No Attributes>
-----BEGIN RSA PRIVATE KEY----- -----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY----- -----END RSA PRIVATE KEY-----

View File

@ -1,6 +1,5 @@
"""Test data""" """Test data"""
from os import path from pathlib import Path
import codecs
def patharg(path): def patharg(path):
@ -9,32 +8,24 @@ def patharg(path):
even in Windows paths. even in Windows paths.
""" """
return path.replace('\\', '\\\\\\') return str(path).replace('\\', '\\\\\\')
FIXTURES_ROOT = path.join(path.abspath(path.dirname(__file__))) FIXTURES_ROOT = Path(__file__).parent
FILE_PATH = path.join(FIXTURES_ROOT, 'test.txt') FILE_PATH = FIXTURES_ROOT / 'test.txt'
JSON_FILE_PATH = path.join(FIXTURES_ROOT, 'test.json') JSON_FILE_PATH = FIXTURES_ROOT / 'test.json'
BIN_FILE_PATH = path.join(FIXTURES_ROOT, 'test.bin') BIN_FILE_PATH = FIXTURES_ROOT / 'test.bin'
FILE_PATH_ARG = patharg(FILE_PATH) FILE_PATH_ARG = patharg(FILE_PATH)
BIN_FILE_PATH_ARG = patharg(BIN_FILE_PATH) BIN_FILE_PATH_ARG = patharg(BIN_FILE_PATH)
JSON_FILE_PATH_ARG = patharg(JSON_FILE_PATH) JSON_FILE_PATH_ARG = patharg(JSON_FILE_PATH)
with codecs.open(FILE_PATH, encoding='utf8') as f:
# Strip because we don't want new lines in the data so that we can # Strip because we don't want new lines in the data so that we can
# easily count occurrences also when embedded in JSON (where the new # easily count occurrences also when embedded in JSON (where the new
# line would be escaped). # line would be escaped).
FILE_CONTENT = f.read().strip() FILE_CONTENT = FILE_PATH.read_text('utf8').strip()
with codecs.open(JSON_FILE_PATH, encoding='utf8') as f: JSON_FILE_CONTENT = JSON_FILE_PATH.read_text('utf8')
JSON_FILE_CONTENT = f.read() BIN_FILE_CONTENT = BIN_FILE_PATH.read_bytes()
with open(BIN_FILE_PATH, 'rb') as f:
BIN_FILE_CONTENT = f.read()
UNICODE = FILE_CONTENT UNICODE = FILE_CONTENT

View File

@ -58,7 +58,7 @@ def test_credentials_in_url_auth_flag_has_priority(httpbin_both):
]) ])
def test_only_username_in_url(url): def test_only_username_in_url(url):
""" """
https://github.com/jakubroztocil/httpie/issues/242 https://github.com/httpie/httpie/issues/242
""" """
args = httpie.cli.definition.parser.parse_args(args=[url], env=MockEnvironment()) args = httpie.cli.definition.parser.parse_args(args=[url], env=MockEnvironment())

View File

@ -15,7 +15,7 @@ from httpie.cli import constants
from httpie.cli.definition import parser from httpie.cli.definition import parser
from httpie.cli.argtypes import KeyValueArg, KeyValueArgType from httpie.cli.argtypes import KeyValueArg, KeyValueArgType
from httpie.cli.requestitems import RequestItems from httpie.cli.requestitems import RequestItems
from utils import HTTP_OK, MockEnvironment, http from utils import HTTP_OK, MockEnvironment, StdinBytesIO, http
class TestItemParsing: class TestItemParsing:
@ -312,10 +312,11 @@ class TestNoOptions:
class TestStdin: class TestStdin:
def test_ignore_stdin(self, httpbin): def test_ignore_stdin(self, httpbin):
with open(FILE_PATH) as f: env = MockEnvironment(
env = MockEnvironment(stdin=f, stdin_isatty=False) stdin=StdinBytesIO(FILE_PATH.read_bytes()),
r = http('--ignore-stdin', '--verbose', httpbin.url + '/get', stdin_isatty=False,
env=env) )
r = http('--ignore-stdin', '--verbose', httpbin.url + '/get', env=env)
assert HTTP_OK in r assert HTTP_OK in r
assert 'GET /get HTTP' in r, "Don't default to POST." assert 'GET /get HTTP' in r, "Don't default to POST."
assert FILE_CONTENT not in r, "Don't send stdin data." assert FILE_CONTENT not in r, "Don't send stdin data."

View File

@ -12,7 +12,8 @@ import base64
import zlib import zlib
from fixtures import FILE_PATH, FILE_CONTENT from fixtures import FILE_PATH, FILE_CONTENT
from utils import http, HTTP_OK, MockEnvironment from httpie.status import ExitStatus
from utils import StdinBytesIO, http, HTTP_OK, MockEnvironment
def assert_decompressed_equal(base64_compressed_data, expected_str): def assert_decompressed_equal(base64_compressed_data, expected_str):
@ -27,6 +28,20 @@ def assert_decompressed_equal(base64_compressed_data, expected_str):
assert actual_str == expected_str assert actual_str == expected_str
def test_cannot_combine_compress_with_chunked(httpbin):
r = http('--compress', '--chunked', httpbin.url + '/get',
tolerate_error_exit_status=True)
assert r.exit_status == ExitStatus.ERROR
assert 'cannot combine --compress and --chunked' in r.stderr
def test_cannot_combine_compress_with_multipart(httpbin):
r = http('--compress', '--multipart', httpbin.url + '/get',
tolerate_error_exit_status=True)
assert r.exit_status == ExitStatus.ERROR
assert 'cannot combine --compress and --multipart' in r.stderr
def test_compress_skip_negative_ratio(httpbin_both): def test_compress_skip_negative_ratio(httpbin_both):
r = http( r = http(
'--compress', '--compress',
@ -78,8 +93,10 @@ def test_compress_form(httpbin_both):
def test_compress_stdin(httpbin_both): def test_compress_stdin(httpbin_both):
with open(FILE_PATH) as f: env = MockEnvironment(
env = MockEnvironment(stdin=f, stdin_isatty=False) stdin=StdinBytesIO(FILE_PATH.read_bytes()),
stdin_isatty=False,
)
r = http( r = http(
'--compress', '--compress',
'--compress', '--compress',
@ -100,7 +117,7 @@ def test_compress_file(httpbin_both):
'--compress', '--compress',
'PUT', 'PUT',
httpbin_both + '/put', httpbin_both + '/put',
'file@' + FILE_PATH, f'file@{FILE_PATH}',
) )
assert HTTP_OK in r assert HTTP_OK in r
assert r.json['headers']['Content-Encoding'] == 'deflate' assert r.json['headers']['Content-Encoding'] == 'deflate'

View File

@ -2,6 +2,8 @@
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 io import BytesIO
from httpie.client import JSON_ACCEPT from httpie.client import JSON_ACCEPT
from utils import MockEnvironment, http, HTTP_OK from utils import MockEnvironment, http, HTTP_OK
from fixtures import FILE_PATH from fixtures import FILE_PATH
@ -9,7 +11,7 @@ from fixtures import FILE_PATH
def test_default_headers_case_insensitive(httpbin): def test_default_headers_case_insensitive(httpbin):
""" """
<https://github.com/jakubroztocil/httpie/issues/644> <https://github.com/httpie/httpie/issues/644>
""" """
r = http( r = http(
'--debug', '--debug',
@ -44,8 +46,10 @@ class TestImplicitHTTPMethod:
assert r.json['form'] == {'foo': 'bar'} assert r.json['form'] == {'foo': 'bar'}
def test_implicit_POST_stdin(self, httpbin): def test_implicit_POST_stdin(self, httpbin):
with open(FILE_PATH) as f: env = MockEnvironment(
env = MockEnvironment(stdin_isatty=False, stdin=f) stdin_isatty=False,
stdin=BytesIO(FILE_PATH.read_bytes())
)
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
@ -59,7 +63,7 @@ class TestAutoContentTypeAndAcceptHeaders:
""" """
def test_GET_no_data_no_auto_headers(self, httpbin): def test_GET_no_data_no_auto_headers(self, httpbin):
# https://github.com/jakubroztocil/httpie/issues/62 # https://github.com/httpie/httpie/issues/62
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'] == '*/*'
@ -90,7 +94,7 @@ class TestAutoContentTypeAndAcceptHeaders:
assert HTTP_OK in r assert HTTP_OK in r
assert r.json['headers']['Accept'] == JSON_ACCEPT assert r.json['headers']['Accept'] == JSON_ACCEPT
# 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/httpie/httpie/issues/137
assert 'application/json' in r.json['headers']['Content-Type'] assert 'application/json' in r.json['headers']['Content-Type']
def test_GET_explicit_JSON_explicit_headers(self, httpbin): def test_GET_explicit_JSON_explicit_headers(self, httpbin):

View File

@ -40,13 +40,17 @@ assert filenames
# HACK: hardcoded paths, venv should be irrelevant, etc. # HACK: hardcoded paths, venv should be irrelevant, etc.
# TODO: replaces the process with Python code # TODO: simplify by using the Python API instead of a subprocess
# then we wontt need the paths.
VENV_BIN = Path(__file__).parent.parent / 'venv/bin' VENV_BIN = Path(__file__).parent.parent / 'venv/bin'
VENV_PYTHON = VENV_BIN / 'python' VENV_PYTHON = VENV_BIN / 'python'
VENV_RST2PSEUDOXML = VENV_BIN / 'rst2pseudoxml.py' VENV_RST2PSEUDOXML = VENV_BIN / 'rst2pseudoxml.py'
@pytest.mark.skipif(not os.path.exists(VENV_RST2PSEUDOXML), reason='docutils not installed') @pytest.mark.skipif(
not VENV_RST2PSEUDOXML.exists(),
reason='docutils not installed',
)
@pytest.mark.parametrize('filename', filenames) @pytest.mark.parametrize('filename', filenames)
def test_rst_file_syntax(filename): def test_rst_file_syntax(filename):
p = subprocess.Popen( p = subprocess.Popen(

View File

@ -182,7 +182,8 @@ class TestDownloads:
# Redirect from `/redirect/1` to `/get`. # Redirect from `/redirect/1` to `/get`.
expected_filename = '1.json' expected_filename = '1.json'
orig_cwd = os.getcwd() orig_cwd = os.getcwd()
os.chdir(tempfile.mkdtemp(prefix='httpie_download_test_')) with tempfile.TemporaryDirectory() as tmp_dirname:
os.chdir(tmp_dirname)
try: try:
assert os.listdir('.') == [] assert os.listdir('.') == []
http('--download', httpbin.url + '/redirect/1') http('--download', httpbin.url + '/redirect/1')

View File

@ -4,14 +4,13 @@ from unittest import mock
import pytest import pytest
import httpie
import httpie.__main__ import httpie.__main__
from fixtures import FILE_CONTENT, FILE_PATH
from httpie.cli.exceptions import ParseError
from httpie.context import Environment from httpie.context import Environment
from httpie.status import ExitStatus from httpie.status import ExitStatus
from httpie.cli.exceptions import ParseError from utils import HTTP_OK, MockEnvironment, StdinBytesIO, http
from utils import MockEnvironment, http, HTTP_OK
from fixtures import FILE_PATH, FILE_CONTENT
import httpie
def test_main_entry_point(): def test_main_entry_point():
@ -40,13 +39,12 @@ def test_debug():
def test_help(): def test_help():
r = http('--help', tolerate_error_exit_status=True) r = http('--help', tolerate_error_exit_status=True)
assert r.exit_status == ExitStatus.SUCCESS assert r.exit_status == ExitStatus.SUCCESS
assert 'https://github.com/jakubroztocil/httpie/issues' in r assert 'https://github.com/httpie/httpie/issues' in r
def test_version(): def test_version():
r = http('--version', tolerate_error_exit_status=True) r = http('--version', tolerate_error_exit_status=True)
assert r.exit_status == ExitStatus.SUCCESS assert r.exit_status == ExitStatus.SUCCESS
# FIXME: py3 has version in stdout, py2 in stderr
assert httpie.__version__ == r.strip() assert httpie.__version__ == r.strip()
@ -100,26 +98,30 @@ def test_POST_form(httpbin_both):
def test_POST_form_multiple_values(httpbin_both): def test_POST_form_multiple_values(httpbin_both):
r = http('--form', 'POST', httpbin_both + '/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(httpbin_both): def test_POST_stdin(httpbin_both):
with open(FILE_PATH) as f: env = MockEnvironment(
env = MockEnvironment(stdin=f, stdin_isatty=False) stdin=StdinBytesIO(FILE_PATH.read_bytes()),
stdin_isatty=False,
)
r = http('--form', 'POST', httpbin_both + '/post', env=env) r = http('--form', 'POST', httpbin_both + '/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_POST_file(httpbin_both): def test_POST_file(httpbin_both):
r = http('--form', 'POST', httpbin_both + '/post', 'file@' + FILE_PATH) r = http('--form', 'POST', httpbin_both + '/post', f'file@{FILE_PATH}')
assert HTTP_OK in r assert HTTP_OK in r
assert FILE_CONTENT in r assert FILE_CONTENT in r
def test_form_POST_file_redirected_stdin(httpbin): def test_form_POST_file_redirected_stdin(httpbin):
""" """
<https://github.com/jakubroztocil/httpie/issues/840> <https://github.com/httpie/httpie/issues/840>
""" """
with open(FILE_PATH) as f: with open(FILE_PATH) as f:
@ -127,10 +129,10 @@ def test_form_POST_file_redirected_stdin(httpbin):
'--form', '--form',
'POST', 'POST',
httpbin + '/post', httpbin + '/post',
'file@' + FILE_PATH, f'file@{FILE_PATH}',
tolerate_error_exit_status=True, tolerate_error_exit_status=True,
env=MockEnvironment( env=MockEnvironment(
stdin=f, stdin=StdinBytesIO(FILE_PATH.read_bytes()),
stdin_isatty=False, stdin_isatty=False,
), ),
) )
@ -181,21 +183,3 @@ def test_json_input_preserve_order(httpbin_both):
assert HTTP_OK in r assert HTTP_OK in r
assert r.json['data'] == \ assert r.json['data'] == \
'{"order": {"map": {"1": "first", "2": "second"}}}' '{"order": {"map": {"1": "first", "2": "second"}}}'
def test_offline():
r = http(
'--offline',
'https://this-should.never-resolve/foo',
)
assert 'GET /foo' in r
def test_offline_download():
"""Absence of response should be handled gracefully with --download"""
r = http(
'--offline',
'--download',
'https://this-should.never-resolve/foo',
)
assert 'GET /foo' in r

75
tests/test_offline.py Normal file
View File

@ -0,0 +1,75 @@
from fixtures import FILE_CONTENT, FILE_PATH_ARG
from utils import http
def test_offline():
r = http(
'--offline',
'https://this-should.never-resolve/foo',
)
assert 'GET /foo' in r
def test_offline_form():
r = http(
'--offline',
'--form',
'https://this-should.never-resolve/foo',
'foo=bar'
)
assert 'POST /foo' in r
assert 'foo=bar' in r
def test_offline_json():
r = http(
'--offline',
'https://this-should.never-resolve/foo',
'foo=bar'
)
assert 'POST /foo' in r
assert r.json == {'foo': 'bar'}
def test_offline_multipart():
r = http(
'--offline',
'--multipart',
'https://this-should.never-resolve/foo',
'foo=bar'
)
assert 'POST /foo' in r
assert 'name="foo"' in r
def test_offline_from_file():
r = http(
'--offline',
'https://this-should.never-resolve/foo',
f'@{FILE_PATH_ARG}'
)
assert 'POST /foo' in r
assert FILE_CONTENT in r
def test_offline_chunked():
r = http(
'--offline',
'--chunked',
'--form',
'https://this-should.never-resolve/foo',
'hello=world'
)
assert 'POST /foo' in r
assert 'Transfer-Encoding: chunked' in r, r
assert 'hello=world' in r
def test_offline_download():
"""Absence of response should be handled gracefully with --download"""
r = http(
'--offline',
'--download',
'https://this-should.never-resolve/foo',
)
assert 'GET /foo' in r

View File

@ -1,10 +1,16 @@
import argparse import argparse
from pathlib import Path
import mock
import json import json
import os import os
import tempfile
import io
from tempfile import gettempdir from tempfile import gettempdir
from urllib.request import urlopen from urllib.request import urlopen
import pytest import pytest
import requests
from httpie.cli.argtypes import ( from httpie.cli.argtypes import (
PARSED_DEFAULT_FORMAT_OPTIONS, PARSED_DEFAULT_FORMAT_OPTIONS,
@ -32,6 +38,102 @@ def test_output_option(httpbin, stdout_isatty):
assert actual_body == expected_body assert actual_body == expected_body
class TestQuietFlag:
@pytest.mark.parametrize('argument_name', ['--quiet', '-q'])
def test_quiet(self, httpbin, argument_name):
env = MockEnvironment(
stdin_isatty=True,
stdout_isatty=True,
devnull=io.BytesIO()
)
r = http(argument_name, 'GET', httpbin.url + '/get', env=env)
assert env.stdout is env.devnull
assert env.stderr is env.devnull
assert HTTP_OK in r.devnull
assert r == ''
assert r.stderr == ''
def test_quiet_with_check_status_non_zero(self, httpbin):
r = http(
'--quiet', '--check-status', httpbin + '/status/500',
tolerate_error_exit_status=True,
)
assert 'http: warning: HTTP 500' in r.stderr
def test_quiet_with_check_status_non_zero_pipe(self, httpbin):
r = http(
'--quiet', '--check-status', httpbin + '/status/500',
tolerate_error_exit_status=True,
env=MockEnvironment(stdout_isatty=False)
)
assert 'http: warning: HTTP 500' in r.stderr
@mock.patch('httpie.cli.argtypes.AuthCredentials._getpass',
new=lambda self, prompt: 'password')
def test_quiet_with_password_prompt(self, httpbin):
"""
Tests whether httpie still prompts for a password when request
requires authentication and only username is provided
"""
env = MockEnvironment(
stdin_isatty=True,
stdout_isatty=True,
devnull=io.BytesIO()
)
r = http(
'--quiet', '--auth', 'user', 'GET',
httpbin.url + '/basic-auth/user/password',
env=env
)
assert env.stdout is env.devnull
assert env.stderr is env.devnull
assert HTTP_OK in r.devnull
assert r == ''
assert r.stderr == ''
@pytest.mark.parametrize('argument_name', ['-h', '-b', '-v', '-p=hH'])
def test_quiet_with_explicit_output_options(self, httpbin, argument_name):
env = MockEnvironment(stdin_isatty=True, stdout_isatty=True)
r = http('--quiet', argument_name, httpbin.url + '/get', env=env)
assert env.stdout is env.devnull
assert env.stderr is env.devnull
assert r == ''
assert r.stderr == ''
@pytest.mark.parametrize('with_download', [True, False])
def test_quiet_with_output_redirection(self, httpbin, with_download):
url = httpbin + '/robots.txt'
output_path = Path('output.txt')
env = MockEnvironment()
orig_cwd = os.getcwd()
output = requests.get(url).text
extra_args = ['--download'] if with_download else []
with tempfile.TemporaryDirectory() as tmp_dirname:
os.chdir(tmp_dirname)
try:
assert os.listdir('.') == []
r = http(
'--quiet',
'--output', str(output_path),
*extra_args,
url,
env=env
)
assert os.listdir('.') == [str(output_path)]
assert r == ''
assert r.stderr == ''
assert env.stderr is env.devnull
if with_download:
assert env.stdout is env.devnull
else:
assert env.stdout is not env.devnull # --output swaps stdout.
assert output_path.read_text() == output
finally:
os.chdir(orig_cwd)
class TestVerboseFlag: class TestVerboseFlag:
def test_verbose(self, httpbin): def test_verbose(self, httpbin):
r = http('--verbose', r = http('--verbose',
@ -40,7 +142,7 @@ class TestVerboseFlag:
assert r.count('__test__') == 2 assert r.count('__test__') == 2
def test_verbose_form(self, httpbin): def test_verbose_form(self, httpbin):
# https://github.com/jakubroztocil/httpie/issues/53 # https://github.com/httpie/httpie/issues/53
r = http('--verbose', '--form', 'POST', httpbin.url + '/post', r = http('--verbose', '--form', 'POST', httpbin.url + '/post',
'A=B', 'C=D') 'A=B', 'C=D')
assert HTTP_OK in r assert HTTP_OK in r

View File

@ -1,16 +1,17 @@
"""Miscellaneous regression tests""" """Miscellaneous regression tests"""
import pytest import pytest
from utils import http, HTTP_OK
from httpie.compat import is_windows from httpie.compat import is_windows
from tests.utils.matching import assert_output_matches, Expect
from utils import HTTP_OK, MockEnvironment, http
def test_Host_header_overwrite(httpbin): def test_Host_header_overwrite(httpbin):
""" """
https://github.com/jakubroztocil/httpie/issues/235 https://github.com/httpie/httpie/issues/235
""" """
host = 'httpbin.org' host = 'pie.dev'
url = httpbin.url + '/get' url = httpbin.url + '/get'
r = http('--print=hH', url, 'host:{0}'.format(host)) r = http('--print=hH', url, 'host:{0}'.format(host))
assert HTTP_OK in r assert HTTP_OK in r
@ -21,7 +22,28 @@ def test_Host_header_overwrite(httpbin):
@pytest.mark.skipif(is_windows, reason='Unix-only') @pytest.mark.skipif(is_windows, reason='Unix-only')
def test_output_devnull(httpbin): def test_output_devnull(httpbin):
""" """
https://github.com/jakubroztocil/httpie/issues/252 https://github.com/httpie/httpie/issues/252
""" """
http('--output=/dev/null', httpbin + '/get') http('--output=/dev/null', httpbin + '/get')
def test_verbose_redirected_stdout_separator(httpbin):
"""
<https://github.com/httpie/httpie/issues/1006>
"""
r = http(
'-v',
httpbin.url + '/post',
'a=b',
env=MockEnvironment(stdout_isatty=False),
)
assert '}HTTP/' not in r
assert_output_matches(r, [
Expect.REQUEST_HEADERS,
Expect.BODY,
Expect.SEPARATOR,
Expect.RESPONSE_HEADERS,
Expect.BODY,
])

View File

@ -3,15 +3,20 @@ import json
import os import os
import shutil import shutil
from datetime import datetime from datetime import datetime
from mock import mock
from tempfile import gettempdir from tempfile import gettempdir
import pytest import pytest
from fixtures import UNICODE from fixtures import UNICODE
from httpie.plugins import AuthPlugin
from httpie.plugins.builtin import HTTPBasicAuth from httpie.plugins.builtin import HTTPBasicAuth
from httpie.plugins.registry import plugin_manager
from httpie.sessions import Session from httpie.sessions import Session
from httpie.utils import get_expired_cookies from httpie.utils import get_expired_cookies
from utils import MockEnvironment, mk_config_dir, http, HTTP_OK from tests.test_auth_plugins import basic_auth
from utils import HTTP_OK, MockEnvironment, http, mk_config_dir
from fixtures import FILE_PATH_ARG
class SessionTestBase: class SessionTestBase:
@ -35,6 +40,27 @@ class SessionTestBase:
return MockEnvironment(config_dir=self.config_dir) return MockEnvironment(config_dir=self.config_dir)
class CookieTestBase:
def setup_method(self, method):
self.config_dir = mk_config_dir()
orig_session = {
'cookies': {
'cookie1': {
'value': 'foo',
},
'cookie2': {
'value': 'foo',
}
}
}
self.session_path = self.config_dir / 'test-session.json'
self.session_path.write_text(json.dumps(orig_session))
def teardown_method(self, method):
shutil.rmtree(self.config_dir)
class TestSessionFlow(SessionTestBase): class TestSessionFlow(SessionTestBase):
""" """
These tests start with an existing session created in `setup_method()`. These tests start with an existing session created in `setup_method()`.
@ -136,6 +162,12 @@ class TestSession(SessionTestBase):
assert 'Content-Type' not in r2.json['headers'] assert 'Content-Type' not in r2.json['headers']
assert 'If-Unmodified-Since' not in r2.json['headers'] assert 'If-Unmodified-Since' not in r2.json['headers']
def test_session_with_upload(self, httpbin):
self.start_session(httpbin)
r = http('--session=test', '--form', '--verbose', 'POST', httpbin.url + '/post',
f'test-file@{FILE_PATH_ARG}', 'foo=bar', env=self.env())
assert HTTP_OK in r
def test_session_by_path(self, httpbin): def test_session_by_path(self, httpbin):
self.start_session(httpbin) self.start_session(httpbin)
session_path = self.config_dir / 'session-by-path.json' session_path = self.config_dir / 'session-by-path.json'
@ -168,7 +200,7 @@ class TestSession(SessionTestBase):
def test_session_default_header_value_overwritten(self, httpbin): def test_session_default_header_value_overwritten(self, httpbin):
self.start_session(httpbin) self.start_session(httpbin)
# https://github.com/jakubroztocil/httpie/issues/180 # https://github.com/httpie/httpie/issues/180
r1 = http('--session=test', r1 = http('--session=test',
httpbin.url + '/headers', 'User-Agent:custom', httpbin.url + '/headers', 'User-Agent:custom',
env=self.env()) env=self.env())
@ -180,7 +212,7 @@ class TestSession(SessionTestBase):
assert r2.json['headers']['User-Agent'] == 'custom' assert r2.json['headers']['User-Agent'] == 'custom'
def test_download_in_session(self, httpbin): def test_download_in_session(self, httpbin):
# https://github.com/jakubroztocil/httpie/issues/412 # https://github.com/httpie/httpie/issues/412
self.start_session(httpbin) self.start_session(httpbin)
cwd = os.getcwd() cwd = os.getcwd()
os.chdir(gettempdir()) os.chdir(gettempdir())
@ -190,14 +222,103 @@ class TestSession(SessionTestBase):
finally: finally:
os.chdir(cwd) os.chdir(cwd)
@pytest.mark.parametrize(
argnames=['auth_require_param', 'auth_parse_param'],
argvalues=[
(False, False),
(False, True),
(True, False)
]
)
def test_auth_type_reused_in_session(self, auth_require_param, auth_parse_param, httpbin):
self.start_session(httpbin)
session_path = self.config_dir / 'test-session.json'
class TestExpiredCookies: header = 'Custom dXNlcjpwYXNzd29yZA'
def setup_method(self, method): class Plugin(AuthPlugin):
auth_type = 'test-reused'
auth_require = auth_require_param
auth_parse = auth_parse_param
def get_auth(self, username=None, password=None):
return basic_auth(header=f'{header}==')
plugin_manager.register(Plugin)
r1 = http(
'--session', str(session_path),
httpbin + '/basic-auth/user/password',
'--auth-type',
Plugin.auth_type,
'--auth', 'user:password',
'--print=H',
)
r2 = http(
'--session', str(session_path),
httpbin + '/basic-auth/user/password',
'--print=H',
)
assert f'Authorization: {header}' in r1
assert f'Authorization: {header}' in r2
plugin_manager.unregister(Plugin)
@mock.patch('httpie.cli.argtypes.AuthCredentials._getpass',
new=lambda self, prompt: 'password')
def test_auth_plugin_prompt_password_in_session(self, httpbin):
self.start_session(httpbin)
session_path = self.config_dir / 'test-session.json'
class Plugin(AuthPlugin):
auth_type = 'test-prompted'
def get_auth(self, username=None, password=None):
return basic_auth()
plugin_manager.register(Plugin)
r1 = http(
'--session', str(session_path),
httpbin + '/basic-auth/user/password',
'--auth-type',
Plugin.auth_type,
'--auth', 'user:',
)
r2 = http(
'--session', str(session_path),
httpbin + '/basic-auth/user/password',
)
assert HTTP_OK in r1
assert HTTP_OK in r2
plugin_manager.unregister(Plugin)
def test_auth_type_stored_in_session_file(self, httpbin):
self.config_dir = mk_config_dir() self.config_dir = mk_config_dir()
self.session_path = self.config_dir / 'test-session.json'
def teardown_method(self, method): class Plugin(AuthPlugin):
shutil.rmtree(self.config_dir) auth_type = 'test-saved'
auth_require = True
def get_auth(self, username=None, password=None):
return basic_auth()
plugin_manager.register(Plugin)
http('--session', str(self.session_path),
httpbin + '/basic-auth/user/password',
'--auth-type',
Plugin.auth_type,
'--auth', 'user:password',
)
updated_session = json.loads(self.session_path.read_text())
assert updated_session['auth']['type'] == 'test-saved'
assert updated_session['auth']['raw_auth'] == "user:password"
plugin_manager.unregister(Plugin)
class TestExpiredCookies(CookieTestBase):
@pytest.mark.parametrize( @pytest.mark.parametrize(
argnames=['initial_cookie', 'expired_cookie'], argnames=['initial_cookie', 'expired_cookie'],
@ -213,29 +334,25 @@ class TestExpiredCookies:
assert expired_cookie not in session.cookies assert expired_cookie not in session.cookies
def test_expired_cookies(self, httpbin): def test_expired_cookies(self, httpbin):
orig_session = {
'cookies': {
'to_expire': {
'value': 'foo'
},
'to_stay': {
'value': 'foo'
},
}
}
session_path = self.config_dir / 'test-session.json'
session_path.write_text(json.dumps(orig_session))
r = http( r = http(
'--session', str(session_path), '--session', str(self.session_path),
'--print=H', '--print=H',
httpbin.url + '/cookies/delete?to_expire', httpbin.url + '/cookies/delete?cookie2',
) )
assert 'Cookie: to_expire=foo; to_stay=foo' in r assert 'Cookie: cookie1=foo; cookie2=foo' in r
updated_session = json.loads(session_path.read_text()) updated_session = json.loads(self.session_path.read_text())
assert 'to_stay' in updated_session['cookies'] assert 'cookie1' in updated_session['cookies']
assert 'to_expire' not in updated_session['cookies'] assert 'cookie2' not in updated_session['cookies']
def test_get_expired_cookies_using_max_age(self):
headers = [
('Set-Cookie', 'one=two; Max-Age=0; path=/; domain=.tumblr.com; HttpOnly')
]
expected_expired = [
{'name': 'one', 'path': '/'}
]
assert get_expired_cookies(headers, now=None) == expected_expired
@pytest.mark.parametrize( @pytest.mark.parametrize(
argnames=['headers', 'now', 'expected_expired'], argnames=['headers', 'now', 'expected_expired'],
@ -265,6 +382,15 @@ class TestExpiredCookies:
{'name': 'pea', 'path': '/ab'} {'name': 'pea', 'path': '/ab'}
] ]
), ),
(
# Checks we gracefully ignore expires date in invalid format.
# <https://github.com/httpie/httpie/issues/963>
[
('Set-Cookie', 'pfg=; Expires=Sat, 19-Sep-2020 06:58:14 GMT+0000; Max-Age=0; path=/; domain=.tumblr.com; secure; HttpOnly'),
],
None,
[]
),
( (
[ [
('Set-Cookie', 'hello=world; Path=/; Expires=Fri, 12 Jun 2020 12:28:55 GMT; HttpOnly'), ('Set-Cookie', 'hello=world; Path=/; Expires=Fri, 12 Jun 2020 12:28:55 GMT; HttpOnly'),
@ -272,8 +398,95 @@ class TestExpiredCookies:
], ],
datetime(2020, 6, 11).timestamp(), datetime(2020, 6, 11).timestamp(),
[] []
) ),
] ]
) )
def test_get_expired_cookies_manages_multiple_cookie_headers(self, headers, now, expected_expired): def test_get_expired_cookies_manages_multiple_cookie_headers(self, headers, now, expected_expired):
assert get_expired_cookies(headers, now=now) == expected_expired assert get_expired_cookies(headers, now=now) == expected_expired
class TestCookieStorage(CookieTestBase):
@pytest.mark.parametrize(
argnames=['new_cookies', 'new_cookies_dict', 'expected'],
argvalues=[(
'new=bar',
{'new': 'bar'},
'cookie1=foo; cookie2=foo; new=bar'
),
(
'new=bar;chocolate=milk',
{'new': 'bar', 'chocolate': 'milk'},
'chocolate=milk; cookie1=foo; cookie2=foo; new=bar'
),
(
'new=bar; chocolate=milk',
{'new': 'bar', 'chocolate': 'milk'},
'chocolate=milk; cookie1=foo; cookie2=foo; new=bar'
),
(
'new=bar;; chocolate=milk;;;',
{'new': 'bar', 'chocolate': 'milk'},
'cookie1=foo; cookie2=foo; new=bar'
),
(
'new=bar; chocolate=milk;;;',
{'new': 'bar', 'chocolate': 'milk'},
'chocolate=milk; cookie1=foo; cookie2=foo; new=bar'
)
]
)
def test_existing_and_new_cookies_sent_in_request(self, new_cookies, new_cookies_dict, expected, httpbin):
r = http(
'--session', str(self.session_path),
'--print=H',
httpbin.url,
'Cookie:' + new_cookies,
)
# Note: cookies in response are in alphabetical order
assert 'Cookie: ' + expected in r
updated_session = json.loads(self.session_path.read_text())
for name, value in new_cookies_dict.items():
assert name, value in updated_session['cookies']
assert 'Cookie' not in updated_session['headers']
@pytest.mark.parametrize(
argnames=['cli_cookie', 'set_cookie', 'expected'],
argvalues=[(
'',
'/cookies/set/cookie1/bar',
'bar'
),
(
'cookie1=not_foo',
'/cookies/set/cookie1/bar',
'bar'
),
(
'cookie1=not_foo',
'',
'not_foo'
),
(
'',
'',
'foo'
)
]
)
def test_cookie_storage_priority(self, cli_cookie, set_cookie, expected, httpbin):
"""
Expected order of priority for cookie storage in session file:
1. set-cookie (from server)
2. command line arg
3. cookie already stored in session file
"""
r = http(
'--session', str(self.session_path),
httpbin.url + set_cookie,
'Cookie:' + cli_cookie,
)
updated_session = json.loads(self.session_path.read_text())
assert updated_session['cookies']['cookie1']['value'] == expected

View File

@ -1,6 +1,8 @@
import pytest import pytest
import pytest_httpbin.certs import pytest_httpbin.certs
import requests.exceptions import requests.exceptions
import ssl
import urllib3
from httpie.ssl import AVAILABLE_SSL_VERSION_ARG_MAPPING, DEFAULT_SSL_CIPHERS from httpie.ssl import AVAILABLE_SSL_VERSION_ARG_MAPPING, DEFAULT_SSL_CIPHERS
from httpie.status import ExitStatus from httpie.status import ExitStatus
@ -9,7 +11,7 @@ from utils import HTTP_OK, TESTS_ROOT, http
try: try:
# Handle OpenSSL errors, if installed. # Handle OpenSSL errors, if installed.
# See <https://github.com/jakubroztocil/httpie/issues/729> # See <https://github.com/httpie/httpie/issues/729>
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
import OpenSSL.SSL import OpenSSL.SSL
ssl_errors = ( ssl_errors = (
@ -27,7 +29,6 @@ CLIENT_KEY = str(CERTS_ROOT / 'client.key')
CLIENT_PEM = str(CERTS_ROOT / 'client.pem') CLIENT_PEM = str(CERTS_ROOT / '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
@ -47,6 +48,12 @@ def test_ssl_version(httpbin_secure, ssl_version):
if ssl_version == 'ssl3': if ssl_version == 'ssl3':
# pytest-httpbin doesn't support ssl3 # pytest-httpbin doesn't support ssl3
pass pass
elif e.__context__ is not None: # Check if root cause was an unsupported TLS version
root = e.__context__
while root.__context__ is not None:
root = root.__context__
if isinstance(root, ssl.SSLError) and root.reason == "TLSV1_ALERT_PROTOCOL_VERSION":
pytest.skip("Unsupported TLS version: {}".format(ssl_version))
else: else:
raise raise
@ -85,11 +92,14 @@ class TestClientCert:
class TestServerCert: class TestServerCert:
def test_verify_no_OK(self, httpbin_secure): def test_verify_no_OK(self, httpbin_secure):
# Avoid warnings when explicitly testing insecure requests
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
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']) @pytest.mark.parametrize('verify_value', ['false', 'fALse'])
def test_verify_false_OK(self, httpbin_secure, verify_value): def test_verify_false_OK(self, httpbin_secure, verify_value):
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
r = http(httpbin_secure.url + '/get', '--verify', verify_value) r = http(httpbin_secure.url + '/get', '--verify', verify_value)
assert HTTP_OK in r assert HTTP_OK in r
@ -133,4 +143,9 @@ def test_ciphers_none_can_be_selected(httpbin_secure):
tolerate_error_exit_status=True, tolerate_error_exit_status=True,
) )
assert r.exit_status == ExitStatus.ERROR assert r.exit_status == ExitStatus.ERROR
assert 'No cipher can be selected.' in r.stderr # Linux/macOS:
# http: error: SSLError: ('No cipher can be selected.',)
# OpenBSD:
# <https://marc.info/?l=openbsd-ports&m=159251948515635&w=2>
# http: error: Error: [('SSL routines', '(UNKNOWN)SSL_internal', 'no cipher match')]
assert 'cipher' in r.stderr

View File

@ -2,7 +2,7 @@ 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 StdinBytesIO, http, MockEnvironment
from fixtures import BIN_FILE_CONTENT, BIN_FILE_PATH from fixtures import BIN_FILE_CONTENT, BIN_FILE_PATH
@ -13,10 +13,12 @@ from fixtures import BIN_FILE_CONTENT, BIN_FILE_PATH
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(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: env = MockEnvironment(
env = MockEnvironment(colors=256, stdin=f, colors=256,
stdin=StdinBytesIO(BIN_FILE_PATH.read_bytes()),
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
@ -25,8 +27,10 @@ def test_pretty_redirected_stream(httpbin):
def test_encoded_stream(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: env = MockEnvironment(
env = MockEnvironment(stdin=f, stdin_isatty=False) stdin=StdinBytesIO(BIN_FILE_PATH.read_bytes()),
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
@ -35,10 +39,11 @@ def test_encoded_stream(httpbin):
def test_redirected_stream(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: env = MockEnvironment(
env = MockEnvironment(stdout_isatty=False, stdout_isatty=False,
stdin_isatty=False, stdin_isatty=False,
stdin=f) stdin=StdinBytesIO(BIN_FILE_PATH.read_bytes()),
)
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 BIN_FILE_CONTENT in r assert BIN_FILE_CONTENT in r

141
tests/test_tokens.py Normal file
View File

@ -0,0 +1,141 @@
"""
The ideas behind these test and the named templates is to ensure consistent output
across all supported different scenarios:
TODO: cover more scenarios
* terminal vs. redirect stdout
* different combinations of `--print=HBhb` (request/response headers/body)
* multipart requests
* streamed uploads
"""
from tests.utils.matching import assert_output_matches, Expect
from utils import http, HTTP_OK, MockEnvironment, HTTPBIN_WITH_CHUNKED_SUPPORT
RAW_REQUEST = [
Expect.REQUEST_HEADERS,
Expect.BODY,
]
RAW_RESPONSE = [
Expect.RESPONSE_HEADERS,
Expect.BODY,
]
RAW_EXCHANGE = [
*RAW_REQUEST,
Expect.SEPARATOR, # Good choice?
*RAW_RESPONSE,
]
RAW_BODY = [
Expect.BODY,
]
TERMINAL_REQUEST = [
*RAW_REQUEST,
Expect.SEPARATOR,
]
TERMINAL_RESPONSE = [
*RAW_RESPONSE,
Expect.SEPARATOR,
]
TERMINAL_EXCHANGE = [
*TERMINAL_REQUEST,
*TERMINAL_RESPONSE,
]
TERMINAL_BODY = [
RAW_BODY,
Expect.SEPARATOR
]
def test_headers():
r = http('--print=H', '--offline', 'pie.dev')
assert_output_matches(r, [Expect.REQUEST_HEADERS])
def test_redirected_headers():
r = http('--print=H', '--offline', 'pie.dev', env=MockEnvironment(stdout_isatty=False))
assert_output_matches(r, [Expect.REQUEST_HEADERS])
def test_terminal_headers_and_body():
r = http('--print=HB', '--offline', 'pie.dev', 'AAA=BBB')
assert_output_matches(r, TERMINAL_REQUEST)
def test_terminal_request_headers_response_body(httpbin):
r = http('--print=Hb', httpbin + '/get')
assert_output_matches(r, TERMINAL_REQUEST)
def test_raw_request_headers_response_body(httpbin):
r = http('--print=Hb', httpbin + '/get', env=MockEnvironment(stdout_isatty=False))
assert_output_matches(r, RAW_REQUEST)
def test_terminal_request_headers_response_headers(httpbin):
r = http('--print=Hh', httpbin + '/get')
assert_output_matches(r, [Expect.REQUEST_HEADERS, Expect.RESPONSE_HEADERS])
def test_raw_request_headers_response_headers(httpbin):
r = http('--print=Hh', httpbin + '/get')
assert_output_matches(r, [Expect.REQUEST_HEADERS, Expect.RESPONSE_HEADERS])
def test_terminal_request_body_response_body(httpbin):
r = http('--print=Hh', httpbin + '/get')
assert_output_matches(r, [Expect.REQUEST_HEADERS, Expect.RESPONSE_HEADERS])
def test_raw_headers_and_body():
r = http(
'--print=HB', '--offline', 'pie.dev', 'AAA=BBB',
env=MockEnvironment(stdout_isatty=False),
)
assert_output_matches(r, RAW_REQUEST)
def test_raw_body():
r = http(
'--print=B', '--offline', 'pie.dev', 'AAA=BBB',
env=MockEnvironment(stdout_isatty=False),
)
assert_output_matches(r, RAW_BODY)
def test_raw_exchange(httpbin):
r = http('--verbose', httpbin + '/post', 'a=b', env=MockEnvironment(stdout_isatty=False))
assert HTTP_OK in r
assert_output_matches(r, RAW_EXCHANGE)
def test_terminal_exchange(httpbin):
r = http('--verbose', httpbin + '/post', 'a=b')
assert HTTP_OK in r
assert_output_matches(r, TERMINAL_EXCHANGE)
def test_headers_multipart_body_separator():
r = http('--print=HB', '--multipart', '--offline', 'pie.dev', 'AAA=BBB')
assert_output_matches(r, TERMINAL_REQUEST)
def test_redirected_headers_multipart_no_separator():
r = http(
'--print=HB', '--multipart', '--offline', 'pie.dev', 'AAA=BBB',
env=MockEnvironment(stdout_isatty=False),
)
assert_output_matches(r, RAW_REQUEST)
def test_verbose_chunked():
r = http('--verbose', '--chunked', HTTPBIN_WITH_CHUNKED_SUPPORT + '/post', 'hello=world')
assert HTTP_OK in r
assert 'Transfer-Encoding: chunked' in r
assert_output_matches(r, TERMINAL_EXCHANGE)
def test_request_headers_response_body(httpbin):
r = http('--print=Hb', httpbin + '/get')
assert_output_matches(r, TERMINAL_REQUEST)

View File

@ -3,11 +3,72 @@ import os
import pytest import pytest
from httpie.cli.exceptions import ParseError from httpie.cli.exceptions import ParseError
from httpie.client import FORM_CONTENT_TYPE
from httpie.status import ExitStatus from httpie.status import ExitStatus
from utils import MockEnvironment, http, HTTP_OK from utils import (
HTTPBIN_WITH_CHUNKED_SUPPORT, MockEnvironment, StdinBytesIO, http,
HTTP_OK,
)
from fixtures import FILE_PATH_ARG, FILE_PATH, FILE_CONTENT from fixtures import FILE_PATH_ARG, FILE_PATH, FILE_CONTENT
def test_chunked_json():
r = http(
'--verbose',
'--chunked',
HTTPBIN_WITH_CHUNKED_SUPPORT + '/post',
'hello=world',
)
assert HTTP_OK in r
assert 'Transfer-Encoding: chunked' in r
assert r.count('hello') == 3
def test_chunked_form():
r = http(
'--verbose',
'--chunked',
'--form',
HTTPBIN_WITH_CHUNKED_SUPPORT + '/post',
'hello=world',
)
assert HTTP_OK in r
assert 'Transfer-Encoding: chunked' in r
assert r.count('hello') == 2
def test_chunked_stdin():
r = http(
'--verbose',
'--chunked',
HTTPBIN_WITH_CHUNKED_SUPPORT + '/post',
env=MockEnvironment(
stdin=StdinBytesIO(FILE_PATH.read_bytes()),
stdin_isatty=False,
)
)
assert HTTP_OK in r
assert 'Transfer-Encoding: chunked' in r
assert r.count(FILE_CONTENT) == 2
def test_chunked_stdin_multiple_chunks():
stdin_bytes = FILE_PATH.read_bytes() + b'\n' + FILE_PATH.read_bytes()
r = http(
'--verbose',
'--chunked',
HTTPBIN_WITH_CHUNKED_SUPPORT + '/post',
env=MockEnvironment(
stdin=StdinBytesIO(stdin_bytes),
stdin_isatty=False,
stdout_isatty=True,
)
)
assert HTTP_OK in r
assert 'Transfer-Encoding: chunked' in r
assert r.count(FILE_CONTENT) == 4
class TestMultipartFormDataFileUpload: class TestMultipartFormDataFileUpload:
def test_non_existent_file_raises_parse_error(self, httpbin): def test_non_existent_file_raises_parse_error(self, httpbin):
@ -39,15 +100,120 @@ class TestMultipartFormDataFileUpload:
assert r.count('Content-Type: text/plain') == 2 assert r.count('Content-Type: text/plain') == 2
def test_upload_custom_content_type(self, httpbin): def test_upload_custom_content_type(self, httpbin):
r = http('--form', '--verbose', 'POST', httpbin.url + '/post', r = http(
f'test-file@{FILE_PATH_ARG};type=image/vnd.microsoft.icon') '--form',
'--verbose',
httpbin.url + '/post',
f'test-file@{FILE_PATH_ARG};type=image/vnd.microsoft.icon'
)
assert HTTP_OK in r assert HTTP_OK in r
# Content type is stripped from the filename # Content type is stripped from the filename
assert 'Content-Disposition: form-data; name="test-file";' \ assert 'Content-Disposition: form-data; name="test-file";' \
f' filename="{os.path.basename(FILE_PATH)}"' in r f' filename="{os.path.basename(FILE_PATH)}"' in r
assert FILE_CONTENT in r assert r.count(FILE_CONTENT) == 2
assert 'Content-Type: image/vnd.microsoft.icon' in r assert 'Content-Type: image/vnd.microsoft.icon' in r
def test_form_no_files_urlencoded(self, httpbin):
r = http(
'--form',
'--verbose',
httpbin.url + '/post',
'AAAA=AAA',
'BBB=BBB',
)
assert HTTP_OK in r
assert FORM_CONTENT_TYPE in r
def test_multipart(self, httpbin):
r = http(
'--verbose',
'--multipart',
httpbin.url + '/post',
'AAAA=AAA',
'BBB=BBB',
)
assert HTTP_OK in r
assert FORM_CONTENT_TYPE not in r
assert 'multipart/form-data' in r
def test_form_multipart_custom_boundary(self, httpbin):
boundary = 'HTTPIE_FTW'
r = http(
'--print=HB',
'--check-status',
'--multipart',
f'--boundary={boundary}',
httpbin.url + '/post',
'AAAA=AAA',
'BBB=BBB',
)
assert f'multipart/form-data; boundary={boundary}' in r
assert r.count(boundary) == 4
def test_multipart_custom_content_type_boundary_added(self, httpbin):
boundary = 'HTTPIE_FTW'
r = http(
'--print=HB',
'--check-status',
'--multipart',
f'--boundary={boundary}',
httpbin.url + '/post',
'Content-Type: multipart/magic',
'AAAA=AAA',
'BBB=BBB',
)
assert f'multipart/magic; boundary={boundary}' in r
assert r.count(boundary) == 4
def test_multipart_custom_content_type_boundary_preserved(self, httpbin):
# Allow explicit nonsense requests.
boundary_in_header = 'HEADER_BOUNDARY'
boundary_in_body = 'BODY_BOUNDARY'
r = http(
'--print=HB',
'--check-status',
'--multipart',
f'--boundary={boundary_in_body}',
httpbin.url + '/post',
f'Content-Type: multipart/magic; boundary={boundary_in_header}',
'AAAA=AAA',
'BBB=BBB',
)
assert f'multipart/magic; boundary={boundary_in_header}' in r
assert r.count(boundary_in_body) == 3
def test_multipart_chunked(self, httpbin):
r = http(
'--verbose',
'--multipart',
'--chunked',
HTTPBIN_WITH_CHUNKED_SUPPORT + '/post',
'AAA=AAA',
)
assert 'Transfer-Encoding: chunked' in r
assert 'multipart/form-data' in r
assert 'name="AAA"' in r # in request
assert '"AAA": "AAA"', r # in response
def test_multipart_preserve_order(self, httpbin):
r = http(
'--form',
'--offline',
httpbin + '/post',
'text_field=foo',
f'file_field@{FILE_PATH_ARG}',
)
assert r.index('text_field') < r.index('file_field')
r = http(
'--form',
'--offline',
httpbin + '/post',
f'file_field@{FILE_PATH_ARG}',
'text_field=foo',
)
assert r.index('text_field') > r.index('file_field')
class TestRequestBodyFromFilePath: class TestRequestBodyFromFilePath:
""" """
@ -56,12 +222,26 @@ class TestRequestBodyFromFilePath:
""" """
def test_request_body_from_file_by_path(self, httpbin): def test_request_body_from_file_by_path(self, httpbin):
r = http('--verbose', r = http(
'POST', httpbin.url + '/post', '@' + FILE_PATH_ARG) '--verbose',
'POST', httpbin.url + '/post',
'@' + FILE_PATH_ARG,
)
assert HTTP_OK in r assert HTTP_OK in r
assert FILE_CONTENT in r, r assert r.count(FILE_CONTENT) == 2
assert '"Content-Type": "text/plain"' in r assert '"Content-Type": "text/plain"' in r
def test_request_body_from_file_by_path_chunked(self, httpbin):
r = http(
'--verbose', '--chunked',
HTTPBIN_WITH_CHUNKED_SUPPORT + '/post',
'@' + FILE_PATH_ARG,
)
assert HTTP_OK in r
assert 'Transfer-Encoding: chunked' in r
assert '"Content-Type": "text/plain"' in r
assert r.count(FILE_CONTENT) == 2
def test_request_body_from_file_by_path_with_explicit_content_type( def test_request_body_from_file_by_path_with_explicit_content_type(
self, httpbin): self, httpbin):
r = http('--verbose', r = http('--verbose',

View File

@ -1,12 +1,14 @@
# coding=utf-8 # coding=utf-8
"""Utilities for HTTPie test suite.""" """Utilities for HTTPie test suite."""
import os import re
import shlex
import sys import sys
import time import time
import json import json
import tempfile import tempfile
from io import BytesIO
from pathlib import Path from pathlib import Path
from typing import Optional, Union from typing import Optional, Union, List
from httpie.status import ExitStatus from httpie.status import ExitStatus
from httpie.config import Config from httpie.config import Config
@ -14,7 +16,13 @@ from httpie.context import Environment
from httpie.core import main from httpie.core import main
TESTS_ROOT = Path(__file__).parent # pytest-httpbin currently does not support chunked requests:
# <https://github.com/kevin1024/pytest-httpbin/issues/33>
# <https://github.com/kevin1024/pytest-httpbin/issues/28>
HTTPBIN_WITH_CHUNKED_SUPPORT = 'http://pie.dev'
TESTS_ROOT = Path(__file__).parent.parent
CRLF = '\r\n' CRLF = '\r\n'
COLOR = '\x1b[' COLOR = '\x1b['
HTTP_OK = '200 OK' HTTP_OK = '200 OK'
@ -36,9 +44,14 @@ def add_auth(url, auth):
return proto + '://' + auth + '@' + rest return proto + '://' + auth + '@' + rest
class StdinBytesIO(BytesIO):
"""To be used for `MockEnvironment.stdin`"""
len = 0 # See `prepare_request_body()`
class MockEnvironment(Environment): class MockEnvironment(Environment):
"""Environment subclass with reasonable defaults for testing.""" """Environment subclass with reasonable defaults for testing."""
colors = 0 colors = 0 # For easier debugging
stdin_isatty = True, stdin_isatty = True,
stdout_isatty = True stdout_isatty = True
is_windows = False is_windows = False
@ -94,12 +107,23 @@ class BaseCLIResponse:
- stdout output: print(self) - stdout output: print(self)
- stderr output: print(self.stderr) - stderr output: print(self.stderr)
- devnull output: print(self.devnull)
- exit_status output: print(self.exit_status) - exit_status output: print(self.exit_status)
""" """
stderr: str = None stderr: str = None
devnull: str = None
json: dict = None json: dict = None
exit_status: ExitStatus = None exit_status: ExitStatus = None
command: str = None
args: List[str] = []
complete_args: List[str] = []
@property
def command(self):
cmd = ' '.join(shlex.quote(arg) for arg in ['http', *self.args])
# pytest-httpbin to real httpbin.
return re.sub(r'127\.0\.0\.1:\d+', 'httpbin.org', cmd)
class BytesCLIResponse(bytes, BaseCLIResponse): class BytesCLIResponse(bytes, BaseCLIResponse):
@ -131,10 +155,9 @@ class StrCLIResponse(str, BaseCLIResponse):
elif self.strip().startswith('{'): elif self.strip().startswith('{'):
# Looks like JSON body. # Looks like JSON body.
self._json = json.loads(self) self._json = json.loads(self)
elif (self.count('Content-Type:') == 1 elif self.count('Content-Type:') == 1:
and 'application/json' in self): # Looks like a HTTP message,
# Looks like a whole JSON HTTP message, # try to extract JSON from its body.
# try to extract its body.
try: try:
j = self.strip()[self.strip().rindex('\r\n\r\n'):] j = self.strip()[self.strip().rindex('\r\n\r\n'):]
except ValueError: except ValueError:
@ -161,17 +184,21 @@ def http(
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
""" """
Run HTTPie and capture stderr/out and exit status. Run HTTPie and capture stderr/out and exit status.
Content writtent to devnull will be captured only if
env.devnull is set manually.
Invoke `httpie.core.main()` with `args` and `kwargs`, Invoke `httpie.core.main()` with `args` and `kwargs`,
and return a `CLIResponse` subclass instance. and return a `CLIResponse` subclass instance.
The return value is either a `StrCLIResponse`, or `BytesCLIResponse` The return value is either a `StrCLIResponse`, or `BytesCLIResponse`
if unable to decode the output. if unable to decode the output. Devnull is string when possible,
bytes otherwise.
The response has the following attributes: The response has the following attributes:
`stdout` is represented by the instance itself (print r) `stdout` is represented by the instance itself (print r)
`stderr`: text written to stderr `stderr`: text written to stderr
`devnull` text written to devnull.
`exit_status`: the exit status `exit_status`: the exit status
`json`: decoded JSON (if possible) or `None` `json`: decoded JSON (if possible) or `None`
@ -182,7 +209,7 @@ def http(
Example: Example:
$ http --auth=user:password GET httpbin.org/basic-auth/user/password $ http --auth=user:password GET pie.dev/basic-auth/user/password
>>> httpbin = getfixture('httpbin') >>> httpbin = getfixture('httpbin')
>>> r = http('-a', 'user:pw', httpbin.url + '/basic-auth/user/pw') >>> r = http('-a', 'user:pw', httpbin.url + '/basic-auth/user/pw')
@ -204,6 +231,7 @@ def http(
stdout = env.stdout stdout = env.stdout
stderr = env.stderr stderr = env.stderr
devnull = env.devnull
args = list(args) args = list(args)
args_with_config_defaults = args + env.config.default_options args_with_config_defaults = args + env.config.default_options
@ -249,22 +277,35 @@ def http(
stdout.seek(0) stdout.seek(0)
stderr.seek(0) stderr.seek(0)
devnull.seek(0)
output = stdout.read() output = stdout.read()
devnull_output = devnull.read()
try: try:
output = output.decode('utf8') output = output.decode('utf8')
except UnicodeDecodeError: except UnicodeDecodeError:
r = BytesCLIResponse(output) r = BytesCLIResponse(output)
else: else:
r = StrCLIResponse(output) r = StrCLIResponse(output)
try:
devnull_output = devnull_output.decode('utf8')
except Exception:
pass
r.devnull = devnull_output
r.stderr = stderr.read() r.stderr = stderr.read()
r.exit_status = exit_status r.exit_status = exit_status
r.args = args
r.complete_args = ' '.join(complete_args)
if r.exit_status != ExitStatus.SUCCESS: if r.exit_status != ExitStatus.SUCCESS:
sys.stderr.write(r.stderr) sys.stderr.write(r.stderr)
# print(f'\n\n$ {r.command}\n')
return r return r
finally: finally:
devnull.close()
stdout.close() stdout.close()
stderr.close() stderr.close()
env.cleanup() env.cleanup()

View File

@ -0,0 +1,32 @@
from typing import Iterable
import pytest
from tests.utils.matching.parsing import OutputMatchingError, expect_tokens, Expect
__all__ = [
'assert_output_matches',
'assert_output_does_not_match',
'Expect',
]
def assert_output_matches(output: str, tokens: Iterable[Expect]):
r"""
Check the command `output` for an exact full sequence of `tokens`.
>>> out = 'GET / HTTP/1.1\r\nAAA:BBB\r\n\r\nCCC\n\n'
>>> assert_output_matches(out, [Expect.REQUEST_HEADERS, Expect.BODY, Expect.SEPARATOR])
"""
# TODO: auto-remove ansi colors to allow for testing of colorized output as well.
expect_tokens(tokens=tokens, s=output)
def assert_output_does_not_match(output: str, tokens: Iterable[Expect]):
r"""
>>> assert_output_does_not_match('\r\n', [Expect.BODY])
"""
with pytest.raises(OutputMatchingError):
assert_output_matches(output=output, tokens=tokens)

View File

@ -0,0 +1,107 @@
import re
from typing import Iterable
from enum import Enum, auto
from httpie.output.writer import MESSAGE_SEPARATOR
from tests.utils import CRLF
class Expect(Enum):
"""
Predefined token types we can expect in the output.
"""
REQUEST_HEADERS = auto()
RESPONSE_HEADERS = auto()
BODY = auto()
SEPARATOR = auto()
SEPARATOR_RE = re.compile(f'^{MESSAGE_SEPARATOR}')
def make_headers_re(message_type: Expect):
assert message_type in {Expect.REQUEST_HEADERS, Expect.RESPONSE_HEADERS}
# language=RegExp
crlf = r'[\r][\n]'
non_crlf = rf'[^{CRLF}]'
# language=RegExp
http_version = r'HTTP/\d+\.\d+'
if message_type is Expect.REQUEST_HEADERS:
# POST /post HTTP/1.1
start_line_re = fr'{non_crlf}*{http_version}{crlf}'
else:
# HTTP/1.1 200 OK
start_line_re = fr'{http_version}{non_crlf}*{crlf}'
return re.compile(
fr'''
^
{start_line_re}
({non_crlf}+:{non_crlf}+{crlf})+
{crlf}
''',
flags=re.VERBOSE
)
BODY_ENDINGS = [
MESSAGE_SEPARATOR,
CRLF, # Not really but useful for testing (just remember not to include it in a body).
]
TOKEN_REGEX_MAP = {
Expect.REQUEST_HEADERS: make_headers_re(Expect.REQUEST_HEADERS),
Expect.RESPONSE_HEADERS: make_headers_re(Expect.RESPONSE_HEADERS),
Expect.SEPARATOR: SEPARATOR_RE,
}
class OutputMatchingError(ValueError):
pass
def expect_tokens(tokens: Iterable[Expect], s: str):
for token in tokens:
s = expect_token(token, s)
if s:
raise OutputMatchingError(f'Unmatched remaining output for {tokens} in {s!r}')
def expect_token(token: Expect, s: str) -> str:
if token is Expect.BODY:
s = expect_body(s)
else:
s = expect_regex(token, s)
return s
def expect_regex(token: Expect, s: str) -> str:
match = TOKEN_REGEX_MAP[token].match(s)
if not match:
raise OutputMatchingError(f'No match for {token} in {s!r}')
return s[match.end():]
def expect_body(s: str) -> str:
"""
We require some text, and continue to read until we find an ending or until the end of the string.
"""
if 'content-disposition:' in s.lower():
# Multipart body heuristic.
final_boundary_re = re.compile('\r\n--[^-]+?--\r\n')
match = final_boundary_re.search(s)
if match:
return s[match.end():]
endings = [s.index(sep) for sep in BODY_ENDINGS if sep in s]
if not endings:
s = '' # Only body
else:
end = min(endings)
if end == 0:
raise OutputMatchingError(f'Empty body: {s!r}')
s = s[end:]
return s

View File

@ -0,0 +1,190 @@
"""
Here we test our output parsing and matching implementation, not HTTPie itself.
"""
from httpie.output.writer import MESSAGE_SEPARATOR
from tests.utils import CRLF
from tests.utils.matching import assert_output_does_not_match, assert_output_matches, Expect
def test_assert_output_matches_headers_incomplete():
assert_output_does_not_match(f'HTTP/1.1{CRLF}', [Expect.RESPONSE_HEADERS])
def test_assert_output_matches_headers_unterminated():
assert_output_does_not_match(
(
f'HTTP/1.1{CRLF}'
f'AAA:BBB'
f'{CRLF}'
),
[Expect.RESPONSE_HEADERS],
)
def test_assert_output_matches_response_headers():
assert_output_matches(
(
f'HTTP/1.1 200 OK{CRLF}'
f'AAA:BBB{CRLF}'
f'{CRLF}'
),
[Expect.RESPONSE_HEADERS],
)
def test_assert_output_matches_request_headers():
assert_output_matches(
(
f'GET / HTTP/1.1{CRLF}'
f'AAA:BBB{CRLF}'
f'{CRLF}'
),
[Expect.REQUEST_HEADERS],
)
def test_assert_output_matches_headers_and_separator():
assert_output_matches(
(
f'HTTP/1.1{CRLF}'
f'AAA:BBB{CRLF}'
f'{CRLF}'
f'{MESSAGE_SEPARATOR}'
),
[Expect.RESPONSE_HEADERS, Expect.SEPARATOR],
)
def test_assert_output_matches_body_unmatched_crlf():
assert_output_does_not_match(f'AAA{CRLF}', [Expect.BODY])
def test_assert_output_matches_body_unmatched_separator():
assert_output_does_not_match(f'AAA{MESSAGE_SEPARATOR}', [Expect.BODY])
def test_assert_output_matches_body_and_separator():
assert_output_matches(f'AAA{MESSAGE_SEPARATOR}', [Expect.BODY, Expect.SEPARATOR])
def test_assert_output_matches_body_r():
assert_output_matches(f'AAA\r', [Expect.BODY])
def test_assert_output_matches_body_n():
assert_output_matches(f'AAA\n', [Expect.BODY])
def test_assert_output_matches_body_r_body():
assert_output_matches(f'AAA\rBBB', [Expect.BODY])
def test_assert_output_matches_body_n_body():
assert_output_matches(f'AAA\nBBB', [Expect.BODY])
def test_assert_output_matches_headers_and_body():
assert_output_matches(
(
f'HTTP/1.1{CRLF}'
f'AAA:BBB{CRLF}'
f'{CRLF}'
f'CCC'
),
[Expect.RESPONSE_HEADERS, Expect.BODY]
)
def test_assert_output_matches_headers_with_body_and_separator():
assert_output_matches(
(
f'HTTP/1.1 {CRLF}'
f'AAA:BBB{CRLF}{CRLF}'
f'CCC{MESSAGE_SEPARATOR}'
),
[Expect.RESPONSE_HEADERS, Expect.BODY, Expect.SEPARATOR]
)
def test_assert_output_matches_multiple_messages():
assert_output_matches(
(
f'POST / HTTP/1.1{CRLF}'
f'AAA:BBB{CRLF}'
f'{CRLF}'
f'CCC'
f'{MESSAGE_SEPARATOR}'
f'HTTP/1.1 200 OK{CRLF}'
f'EEE:FFF{CRLF}'
f'{CRLF}'
f'GGG'
f'{MESSAGE_SEPARATOR}'
), [
Expect.REQUEST_HEADERS,
Expect.BODY,
Expect.SEPARATOR,
Expect.RESPONSE_HEADERS,
Expect.BODY,
Expect.SEPARATOR,
]
)
def test_assert_output_matches_multipart_body():
output = (
'POST / HTTP/1.1\r\n'
'User-Agent: HTTPie/2.4.0-dev\r\n'
'Accept-Encoding: gzip, deflate\r\n'
'Accept: */*\r\n'
'Connection: keep-alive\r\n'
'Content-Type: multipart/form-data; boundary=1e22169de43e4a2e8d9e41c0a1c93cc5\r\n'
'Content-Length: 212\r\n'
'Host: pie.dev\r\n'
'\r\n'
'--1e22169de43e4a2e8d9e41c0a1c93cc5\r\n'
'Content-Disposition: form-data; name="AAA"\r\n'
'\r\n'
'BBB\r\n'
'--1e22169de43e4a2e8d9e41c0a1c93cc5\r\n'
'Content-Disposition: form-data; name="CCC"\r\n'
'\r\n'
'DDD\r\n'
'--1e22169de43e4a2e8d9e41c0a1c93cc5--\r\n'
)
assert_output_matches(output, [Expect.REQUEST_HEADERS, Expect.BODY])
def test_assert_output_matches_multipart_body_with_separator():
output = (
'POST / HTTP/1.1\r\n'
'User-Agent: HTTPie/2.4.0-dev\r\n'
'Accept-Encoding: gzip, deflate\r\n'
'Accept: */*\r\n'
'Connection: keep-alive\r\n'
'Content-Type: multipart/form-data; boundary=1e22169de43e4a2e8d9e41c0a1c93cc5\r\n'
'Content-Length: 212\r\n'
'Host: pie.dev\r\n'
'\r\n'
'--1e22169de43e4a2e8d9e41c0a1c93cc5\r\n'
'Content-Disposition: form-data; name="AAA"\r\n'
'\r\n'
'BBB\r\n'
'--1e22169de43e4a2e8d9e41c0a1c93cc5\r\n'
'Content-Disposition: form-data; name="CCC"\r\n'
'\r\n'
'DDD\r\n'
'--1e22169de43e4a2e8d9e41c0a1c93cc5--\r\n'
f'{MESSAGE_SEPARATOR}'
)
assert_output_matches(output, [Expect.REQUEST_HEADERS, Expect.BODY, Expect.SEPARATOR])
def test_assert_output_matches_multiple_separators():
assert_output_matches(
MESSAGE_SEPARATOR + MESSAGE_SEPARATOR + 'AAA' + MESSAGE_SEPARATOR + MESSAGE_SEPARATOR,
[Expect.SEPARATOR, Expect.SEPARATOR, Expect.BODY, Expect.SEPARATOR, Expect.SEPARATOR]
)

23
tox.ini
View File

@ -1,23 +0,0 @@
# Tox (http://tox.testrun.org/) is a tool for running tests
# in multiple virtualenvs. See ./CONTRIBUTING.rst
[tox]
# pypy3 currently fails because of a Flask issue
envlist = py37
[testenv]
deps =
mock
pytest
pytest-httpbin>=0.0.6
commands =
# NOTE: the order of the directories in posargs seems to matter.
# When changed, then many ImportMismatchError exceptions occurrs.
py.test \
--verbose \
--doctest-modules \
{posargs:./httpie ./tests}