Compare commits

...

408 Commits
0.9.0 ... 1.0.1

Author SHA1 Message Date
d5032ca859 Fix changelog 2018-11-14 11:45:57 +01:00
f6a19cf552 Don't call external URLs from tests #729 2018-11-14 11:42:59 +01:00
74979f3b33 Brew 2018-11-06 11:37:33 +01:00
698eb51e60 Update screenshot 2018-11-03 18:08:43 +01:00
ae8030c930 Homebrew formula for v1.0.0 2018-11-02 17:18:04 +01:00
2e96d7ffbb Update CHANGELOG.rst 2018-11-02 16:28:17 +01:00
b5625e3d75 v1.0.0 2018-11-02 16:24:35 +01:00
932d3224f4 Cleanup 2018-11-02 16:23:17 +01:00
b596fedf13 exit 0 constant: OK => SUCCESS to avoid confusion w/ HTTP 200 OK 2018-11-02 16:07:39 +01:00
96444f3345 Changelog 2018-11-02 15:13:53 +01:00
89b66f1608 Merge remote-tracking branch 'origin/master' 2018-11-02 14:58:08 +01:00
a7d570916d #722: Add support for tls1.3 (#724)
* #722: Add support for tls1.3

* #722: Document the potential support for tls1.3
2018-11-02 14:57:53 +01:00
ab5a50cee8 Finish --style=auto for terminal ANSI colors and make it the default.
Previously (only in the development version), this was called 'preset'.
2018-11-02 14:53:05 +01:00
91961c6b51 Fixed some lines (#723) 2018-10-31 19:17:44 +01:00
256ea7d49d Add prog parameter to HTTPieArgumentParser (#715) 2018-10-30 18:41:56 +01:00
2cd6ea3050 Fix some broken documentation links (#703) 2018-09-07 19:10:04 +02:00
37dddf5bf7 Fix for broken Travis builds on macOS with Python 3.7 (#704) (#705) 2018-09-07 19:09:30 +02:00
e508c631f2 Fix Tox using different Python than expected on macOS (#688) (#706) 2018-09-07 19:08:37 +02:00
55530c8c6d fixed output for escaping rules (#700) 2018-09-03 20:04:18 +02:00
eb929cbc04 Travis CI: Add Python 3.7 on linux to the testing (#690)
* Travis CI: Add Python 3.7 on linux to the testing

`sudo: true` and `dist: xenial` are currently required https://github.com/travis-ci/travis-ci/issues/9069

* NEWEST_PYTHON=3.7
2018-07-25 14:02:00 +02:00
2490bb25ca Add v 0.9.9 CHANGELOG link 2018-07-22 17:58:52 +02:00
2038fa02e3 Mention v0.9.9 in CHANGELOG
#620
2018-07-22 17:57:52 +02:00
59d51ad513 Travis 2018-07-12 21:52:01 +02:00
61568f1def Travis 2018-07-12 21:46:00 +02:00
f93f4fa7c7 Travis CI Python versions; install fix 2018-07-12 21:33:12 +02:00
bf73b5701e Fix travis.yml syntax 2018-07-12 21:23:32 +02:00
7917f1b40c Build fixes and clean-up
* reflect Python 3.7 release
* fix `pycodestyle` errors
* update `pycodestyle` config
* move `pytest` and `pycodestyle` config to `setup.cfg`
* add `make pycodestyle`
* add `make coveralls`
* etc.
2018-07-12 21:16:16 +02:00
a50660cc70 Test --timeout with longer delay
test_timeout_exit_status fails on Python 2.7
https://travis-ci.org/jakubroztocil/httpie/jobs/390072675#L325
2018-07-12 00:39:31 +02:00
749b1e2aca Fix pytest configuration 2018-06-09 11:59:34 +02:00
137889a267 Doc improvements 2018-05-30 14:07:52 +02:00
c9c6f0fae5 Formatting 2018-05-30 14:02:46 +02:00
6fd1ea0e5a Section ordering 2018-05-30 13:56:35 +02:00
8f7676a2a9 Add Cookies section to the docs 2018-05-30 13:55:06 +02:00
87e661c5f1 Support using styles from Pygments plugins (#663)
`pygments.styles.STYLE_MAP` contains only styles built directly into
Pygments library. To list all available styles (including styles
registered by plugins), one should use `get_all_styles` generator.

For respective Pygments documentation, see:
http://pygments.org/docs/styles/#getting-a-list-of-available-styles
2018-04-14 15:25:59 -05:00
8ca333dda0 Use parentheses in describing sessions (#664)
It's a little more readable than using em dashes.
2018-04-11 20:04:02 -05:00
0f4dce98c7 Make default HTTP headers case-insensitive
Cloase #644
2018-02-22 12:52:57 +01:00
05547224ce Remove a Python 2.6 mention from extras_require 2017-12-28 18:33:31 +01:00
6301fee3d2 Upgrade to latest requests 2017-12-28 18:32:29 +01:00
a803e845a5 More robust urllib3 import 2017-12-28 18:32:12 +01:00
11be041e06 Rename TestEnvironment to MockEnvironment to avoid pytest warnings
Close #621
2017-12-28 18:17:48 +01:00
7f5fd130c5 Start using dict comprehensions 2017-12-28 18:15:17 +01:00
ec899d70b7 Removed Python 2.6 support
* Travis CI doesn't support it anymore.
* It had EOL more than 4 years ago
2017-12-28 18:03:37 +01:00
4d3b4fa0be Fix rst 2017-12-22 14:48:08 +01:00
27c557e983 Update README.rst
test
2017-12-22 14:40:48 +01:00
7f24f7d34c Delete appveyor.yml 2017-12-22 14:36:13 +01:00
4b61108005 Remove AppVeyor II. 2017-12-22 14:35:23 +01:00
8b189725fd Remove AppVeyor
@appveyor  https://help.appveyor.com/discussions/problems/10507-pip-install-fails-with-access-is-denied-error
2017-12-22 14:34:20 +01:00
1719ebded6 Fix README (#641) 2017-12-22 03:37:04 +01:00
c5d6a4ad8e OS X => macOS
Close #634
2017-12-17 19:45:46 +01:00
91e1fe2d0f appveyor fix attempt II. 2017-12-13 21:32:37 +01:00
ca7f41de53 appveyor fix attempt 2017-12-13 21:29:51 +01:00
46e24dd6b5 Use function as source of styles for Fish completion 2017-12-13 21:22:53 +01:00
803127e8c9 Remove duplicate option from Fish completion list 2017-12-13 21:22:53 +01:00
4c138959ea Merge pull request #633 from darshanime/version_number_fix
fix env version attribute
2017-12-13 21:18:38 +01:00
91a28973bd Merge pull request #631 from CrazyPython/patch-2
Fix Travis Build by removing 2.6
2017-12-13 21:16:05 +01:00
02b28093a8 Merge pull request #630 from CrazyPython/patch-1
Clarify error message
2017-12-13 21:15:43 +01:00
d64e7d8a6a Merge pull request #638 from gtback/update-contributing-rst
Update CONTRIBUTING.rst to include correct Makefile targets.
2017-12-13 21:14:31 +01:00
8841b8bf46 Update CONTRIBUTING.rst to include correct Makefile targets. 2017-12-07 04:39:32 +00:00
6472ca55e1 fix env version attribute 2017-11-18 19:01:26 +05:30
37c3307018 Remove 2.6 2017-11-14 09:18:10 -05:00
0aab796960 Clarify error message 2017-11-13 07:23:52 -05:00
95c33e31a2 Merge pull request #614 from watersalesman/master
List DNF as Fedora package manager in README
2017-10-04 12:09:07 -05:00
9af833da30 List DNF as Fedora package manager in README 2017-10-02 16:55:35 -04:00
dfe6245cd6 Update AppVeyor 2017-09-07 13:57:15 +02:00
555761f3cb Update copyright year 2017-09-06 01:42:16 +02:00
643735ef23 Fix Gitter link
Close #590
2017-09-06 01:14:56 +02:00
7a45f14542 Merge pull request #584 from scorphus/hotfix/new-requests
Support requests>=2.14.0
2017-07-20 07:54:08 +02:00
e993f83355 Merge pull request #589 from alappe/patch-1
Update README.rst, fix typo…
2017-07-20 07:53:30 +02:00
d726a4cd92 Merge pull request #591 from DavidOliver/patch-1
Fix sentence on overriding default timeout in readme
2017-07-20 07:52:57 +02:00
8d3f09497b Fix sentence on overriding default timeout in readme 2017-06-30 14:54:49 +02:00
31c78c2885 Update README.rst 2017-06-26 13:00:46 +02:00
9776a6dea0 Support requests>=2.14.0
From that release onwards, `cert_verify` raises `IOError` [1].

    1: https://github.com/kennethreitz/requests/commit/7d8b87c
2017-05-17 20:31:10 -03:00
f1d4861fae Merge pull request #568 from dsego/dsego/ansi-colors
Follow terminal ANSI color styles

Close #524
2017-03-12 22:44:05 +01:00
d99e1ff492 Fix link 2017-03-12 13:31:03 +01:00
a196d1d451 Travis cache: pip 2017-03-12 13:18:39 +01:00
02209c2db1 Oops, remove semicolons 2017-03-11 18:12:00 +01:00
9886f01f91 New style option that applies the terminal ANSI color scheme 2017-03-11 18:00:35 +01:00
a4f796fe69 Revert "Follow terminal ANSI color styles"
This reverts commit b0fde07cfd.
2017-03-11 16:58:50 +01:00
c948f98b05 Update links 2017-03-10 11:27:38 +01:00
b0fde07cfd Follow terminal ANSI color styles
Removes the default solarized color scheme and custom http lexer class.
2017-03-06 01:05:50 +01:00
f74670fac1 Update README.rst 2017-03-01 12:40:26 +01:00
7321b9fa4e Add --verify true/false tests and CHANGELOG 2017-02-17 00:56:07 +01:00
cf8d5eb3e8 Merge pull request #560 from hangtwenty/dummyproof-cli-param-verify
Add --verify=(true|false) as an alternative to (yes|no) and make the boolean value case-insensitive
2017-02-17 00:43:22 +01:00
64af72eb88 Turn --verify=False/True to --verify=no/yes
One way to address #559 -- https://github.com/jkbrzt/httpie/issues/559
-- instead of warning or throwing an error, just accept "True" and "False"
as synonyms of yes/no

(Updated to reflect feedback given at https://github.com/jkbrzt/httpie/pull/560 )
2017-02-13 18:30:55 -06:00
de38f86730 Merge pull request #558 from RobDesideri/patch-1
Update pip official website url
2017-02-09 21:13:47 +01:00
244ad15c92 Update pip official website url 2017-02-09 15:25:07 +01:00
586f45e634 Merge pull request #494 from keik/patch-1
Fix typo
2017-02-07 20:50:43 +01:00
b1b4743663 Merge pull request #555 from rootulp/patch-1
Gitter Badge: flat-square style
2017-02-07 20:50:01 +01:00
5600b4a2d3 Merge pull request #557 from robertbenjamin/fix-doc-typo
Update README.rst
2017-02-07 20:49:34 +01:00
9261167a1f Fix typo in the docs 2017-02-02 11:45:58 -08:00
519654e21b Gitter Badge: flat-square style
To match the other badges
2017-01-22 20:58:58 +00:00
4840499a43 Merge pull request #552 from duboviy/master
Add Python 3.6 support
2017-01-08 19:57:56 +01:00
ee6cdf4ab3 Update setup.py 2017-01-08 16:20:53 +02:00
98003f545d Update appveyor.yml 2017-01-08 16:19:26 +02:00
0046ed73c6 Update .travis.yml 2017-01-08 16:18:19 +02:00
66a6475064 Update tox.ini 2017-01-08 16:12:31 +02:00
97804802c0 Alternatives 2016-12-17 03:10:52 +01:00
c9296a9a45 Added link to httpcat 2016-12-17 03:06:48 +01:00
64a41c2601 README 2016-12-17 03:04:59 +01:00
0af6ae1be4 Fix PyPi README rendering
Close #540
2016-12-09 00:26:55 +01:00
d0fc10cf1a AWS / Amazon S3 auth plugin link 2016-12-08 21:48:38 +01:00
fe1d0b0a1e Doc 2016-12-08 21:48:18 +01:00
f133dbf22c Update README with new plugin repos location 2016-12-08 21:48:11 +01:00
9d93b07a9d Redme 2016-12-08 05:38:25 +01:00
761cdbf8be Update Homebrew formula 2016-12-08 05:25:50 +01:00
3a3aecca45 0.9.8 2016-12-08 05:22:20 +01:00
fb3a26586a Fix --auth-type help 2016-12-08 05:16:22 +01:00
cc9083f541 Keep the latest submitted Homebrew formula in extras/ for testing 2016-12-08 04:58:49 +01:00
9ae86f3b4f 0.9.7 2016-12-08 04:47:32 +01:00
3a6fd074a1 Added ExitStatus.PLUGIN_ERROR (7) 2016-12-08 04:42:17 +01:00
da59381b0b Fix PyPi link 2016-12-07 18:54:53 +01:00
6de2d6c2cb Docs 2016-12-07 06:20:01 +01:00
b9b033ed0c Docs 2016-12-07 06:00:51 +01:00
64d6363565 Docs 2016-12-07 05:59:27 +01:00
923b7acbe6 Docs 2016-12-07 05:56:53 +01:00
2efc0db8d4 Cleanup 2016-11-24 00:58:41 +01:00
2bf71af286 pep8 2016-11-23 23:36:46 +01:00
0b84180485 Fix Python 2.6 2016-11-23 23:20:52 +01:00
5a1bd4ba83 Cleanup 2016-11-23 23:15:18 +01:00
3f7ed35238 Add more plugin API tests 2016-11-23 23:09:45 +01:00
47fd392c74 Cleanup 2016-11-23 22:33:22 +01:00
54a63a810e Cleanup/docstring 2016-11-23 22:29:36 +01:00
a49774d3ab Extend auth plugin API
This extends the `AuthPlugin` API by the following attributes:

* `auth_require`: set to `False` to make `--auth, -a` optional
* `auth_parse`: set to `False` to disable `username:password` parsing
  (access the raw value passed to `-a` via `self.raw_auth`).
* `prompt_password`: set to`False` to disable password prompt when
   no password provided (only relevant when `auth_parse == True`)

 These changes should be 100% backwards-compatible.

 What needs more testing is auth support in sessions.

Close #433
Close #431
Close #378
Ping teracyhq/httpie-jwt-auth#3
2016-11-23 22:02:12 +01:00
b879d38b07 Test case for Host header removal (unimplemented feature) 2016-11-23 22:02:12 +01:00
0913e8b2ef Merge pull request #533 from kigawas/patch-1
Update README.rst
2016-10-28 18:11:55 +02:00
4fef4b9a75 Update README.rst
Change "you shell" to "your shell"
2016-10-28 12:02:21 +08:00
bfc23b1412 Changelog 2016-10-26 12:18:53 +02:00
6267f21f21 Clean-up 2016-10-26 11:58:47 +02:00
e9aba543b1 Changelog 2016-10-26 11:54:35 +02:00
9b23a4ac9a Exit with status 130 on CTRL-C
http://www.tldp.org/LDP/abs/html/exitcodes.html

 #531
2016-10-26 11:53:01 +02:00
b96eba336d Fixed test 2016-10-26 11:28:17 +02:00
48a6d234cb Need a main()
#531
2016-10-26 11:21:30 +02:00
c6f2b32e36 Stricter KeyboardInterrupt silencing
Relates to #531, but doesn't solve it completely.
2016-10-26 11:16:39 +02:00
64f6f69037 Add Twitter link 2016-09-17 15:58:05 +02:00
6bdfc7a071 Update config and session file help URLs 2016-09-12 10:57:30 +02:00
497a91711a README 2016-09-12 09:13:37 +02:00
f515ef72d0 README 2016-09-12 09:12:07 +02:00
22a2fddc79 README 2016-09-12 08:59:55 +02:00
1847eaa299 Updated config docs 2016-09-11 18:48:56 +02:00
e387c1d43e Updated config docs 2016-09-11 18:46:33 +02:00
fc6d89913f README 2016-09-11 11:39:03 +02:00
d584686744 README 2016-09-11 01:16:07 +02:00
b565be4318 CHANGELOG 2016-09-06 11:53:52 +01:00
87e44ae639 Handle curses-free Pythons 2016-09-06 11:50:56 +01:00
0d08732397 Merge pull request #516 from dongweiming/fix-496
Fix the handling of zero REQUEST_ITEM arguments 

Close  #496
2016-09-06 11:06:45 +01:00
c53a778f60 Fix Issue #496 2016-09-01 17:46:34 +08:00
5efc9010cc Update CHANGELOG.rst 2016-08-14 11:36:21 +02:00
08e883fcfe Merge pull request #503 from zquestz/patch-1
Updated README.rst to add Arch Linux install docs.
2016-08-14 04:09:50 +02:00
c4b309164f Updated README.rst to add Arch Linux install docs. 2016-08-13 19:08:37 -07:00
8e96238323 v0.9.6 2016-08-13 23:01:05 +02:00
8a9206eceb Fixed Makefile 2016-08-13 22:57:44 +02:00
8ac3c5961c Upgrade Pygments version 2016-08-13 22:57:33 +02:00
487c7a9221 v0.9.5 2016-08-13 22:51:42 +02:00
6d65668355 Strip request header values 2016-08-13 22:40:01 +02:00
3e5115e4a2 Merge pull request #501 from ii-v/master
Fixed spelling mistake in the AUTHORS.rst file
2016-08-11 08:37:41 +02:00
2b8b572f22 Merge pull request #1 from ii-v/ii-v-patch-1
Fixed spelling mistake `GitHib` to `GitHub`
2016-08-11 01:44:04 +02:00
af737fd338 Fixed spelling mistage GitHib to GitHub 2016-08-11 01:43:15 +02:00
ee375b6942 Merge pull request #493 from medecau/codestyle_environment
Codestyle environment
2016-07-29 23:17:00 +02:00
6b06d92a59 Fix typo 2016-07-27 09:54:26 +09:00
becb63de9a useful info 2016-07-26 21:59:34 +01:00
86c8abc485 force os to be linux (+1 squashed commit)
Squashed commits:
[444c56d] no vars for you (+1 squashed commit)
Squashed commits:
[c7d1bf9] added pycodestyle environment to travis config
2016-07-26 21:43:13 +01:00
8f6bee9196 codestyle fixes 2016-07-19 17:23:40 +01:00
9c2c058ae5 separate environment to test codestyle as proposed by @sigmavirus24 2016-07-19 17:23:18 +01:00
6238b59e72 Fix formatting 2016-07-08 15:05:43 +02:00
702c21aa91 Added related projects 2016-07-08 15:03:48 +02:00
aab5cd9da0 PEP8. clean-up 2016-07-04 20:30:55 +02:00
8c0f0b578c Clean-up 2016-07-02 18:44:02 +02:00
bb4881a873 Fixed README 2016-07-02 18:30:04 +02:00
3a1726b4ed Fixed README 2016-07-02 15:04:19 +02:00
e1fa57d228 Added -I as a shortcut for --ignore-stdin 2016-07-02 15:01:46 +02:00
bfc64bce21 Upgrade requests to 2.10.0 to enable optional SOCKS support
Closes #86
2016-07-02 14:58:34 +02:00
595dc51b2d Fish shell completion 2016-07-02 14:33:04 +02:00
83fa772247 Merge pull request #459 from dickeyxxx/fish-completion
added completions for fish shell
2016-07-02 14:31:06 +02:00
49a0fb6e0f More liberal default JSON Accept header
Closes #470
2016-07-02 14:18:36 +02:00
41e822ca2f Clean-up 2016-07-02 12:51:35 +02:00
1124d68946 Added --default-scheme <URL_SCHEME>
Closes #289
2016-07-02 12:47:02 +02:00
c3735d0422 Merge pull request #401 from lgarron/default-scheme
Add a --default-scheme argument.
2016-07-02 12:32:07 +02:00
364b91cbc4 Skip pypy3 tests on TravisCI 2016-07-02 12:03:52 +02:00
c8e06b55e1 Fix tests 2016-07-02 12:03:19 +02:00
5acbc904b7 Added the ability to unset headers
Closes #476
2016-07-02 11:50:30 +02:00
0c7c248dce Fix CHANGELOG 2016-07-02 11:17:38 +02:00
caf60cbc65 Typos 2016-07-02 11:11:06 +02:00
2b0e642842 Document preference for Python 3
Also mention that the Homebrew formula depends on Python 3 starting with HTTPie 0.9.4.
2016-07-02 11:07:46 +02:00
e25948f6a0 1.0.0-dev 2016-07-01 19:17:31 +02:00
b565b4628e v0.9.4 2016-07-01 19:02:34 +02:00
65081b2f12 Cleanup 2016-07-01 19:00:06 +02:00
963b2746f5 Be more liberal when detecting JSON in the formatter
Closes #485
2016-07-01 18:57:13 +02:00
098257c0be Rename --print-others to --history-print. 2016-07-01 18:49:27 +02:00
30eb0c2f26 Merge pull request #468 from Natim/master
Update readthedocs links.
2016-04-28 23:14:52 +08:00
9fbe745987 Update readthedocs links. 2016-04-28 12:28:20 +02:00
01a546eedd Merge pull request #463 from KyleAMathews/patch-1
Remove extra backtick.
2016-04-20 11:17:40 +08:00
eba6b63c55 Remove extra backtick. 2016-04-18 15:11:04 -07:00
ec245a1e80 added completions for fish shell 2016-04-06 11:28:03 -07:00
33eb9acd92 Updated README 2016-03-18 09:20:19 +08:00
293295cad6 Removed XML formatter
Closes #443
Closes #389
Closes #415
Closes #384
Closes #394
2016-03-18 09:16:39 +08:00
557911b606 Handle that os.pathconf is posix-only 2016-03-17 16:14:14 +08:00
5300b0b490 Fixed #451 - OSError: [Errno 36] File name too long 2016-03-17 15:58:01 +08:00
001bda1945 README 2016-03-17 15:00:50 +08:00
7c68d87c10 README 2016-03-10 14:27:33 +08:00
35a99fe04b Added test for -F shortcut 2016-03-09 21:58:34 +08:00
76e15b227c Added test_verbose_implies_all 2016-03-09 21:58:11 +08:00
8881ebf033 Changed the version icon label to include to word "stable" 2016-03-09 13:49:00 +08:00
25d1e8e418 Add Accept-Encoding: identity for --download
#423
2016-03-07 11:46:59 +08:00
7ce6eb148e Typo 2016-03-07 07:09:58 +08:00
6e1dbadff9 Replace --show-redirects with --all and add --print-others, -P
With --all, any intermediary requests/responses are shown (such as redirects
 or the initial unauthorized Digest auth request).

 The --print-others, -P option works like --print, -p, but only applies to
 intermediary requests/responses. The default behaviour is to inherit
 the value of -p.
2016-03-07 07:04:23 +08:00
a6ebc44a48 Run tests against both HTTP and HTTPS
Some of the tests now use the `httpbin_both` fixture from pytest-httpbin.
Also, made httpbin's CA trusted by default and added `httpbin_secure_untrusted`
fixture  to allow overriding that for particular tests.
2016-03-06 17:42:35 +08:00
5e03aeceb7 Make fruity default style one Windows
(again)
2016-03-06 08:33:40 +08:00
13ee9389aa Add link to contributors 2016-03-05 19:48:35 +08:00
bb49a1f979 Improved --debug output 2016-03-05 01:42:48 +08:00
4e574e6b8e Cleanup tests 2016-03-03 18:50:18 +08:00
529981af7a Fix CHANGELOG 2016-03-03 18:46:58 +08:00
6731cb881a README 2016-03-03 17:26:47 +08:00
f7d1b739e2 README 2016-03-03 17:24:46 +08:00
5bdf4a3bae Fixed test_rst_file_syntax error message 2016-03-03 17:22:12 +08:00
2d9414d34c Fixed README 2016-03-03 17:21:51 +08:00
20823c1702 Removed the "implicit_content_type" config option
If you used:

    "implicit_content_type": "form"

 You can achieve the the same result with:

     "default_options": ["--form"]

If you used:

    "implicit_content_type": "json"

 Then it's the default behaviour and it can be removed.

 In either case HTTPie will migrate your config file on the next invocation.
2016-03-03 17:14:39 +08:00
5dbd104c3b Nobody ain't got time for that 2016-03-03 17:09:34 +08:00
13a979ad11 Cleanup 2016-03-02 14:20:35 +08:00
4cfa143bfe Fixed coverage 2016-03-02 13:31:40 +08:00
d24f30d0af Cleanup 2016-03-02 13:31:23 +08:00
66e168b2af Improved failed test output 2016-03-02 13:16:41 +08:00
564670566c Fix coveralls integration 2016-03-02 12:25:19 +08:00
ecbbad816a Fix coveralls integration 2016-03-02 12:24:46 +08:00
0432694661 Changel 2016-03-02 12:24:24 +08:00
dc4da527db Added --ssl=<PROTOCOL_VERSION>
Closes #98
2016-03-02 12:12:05 +08:00
38e8ef14ec Run positive tests first
Trying to debug failing SSL tests on Travis - kevin1024/pytest-httpbin#32
2016-03-02 10:35:40 +08:00
c73dcaf63d CI 2016-03-02 10:08:20 +08:00
fb85509e91 CI 2016-03-02 09:58:50 +08:00
a2dca1e3bb CI 2016-03-02 09:44:39 +08:00
c2dae62af0 Appveyor: added Python 3.5 build 2016-03-02 09:37:58 +08:00
ae7008ee96 Appveyor 2016-03-02 09:33:43 +08:00
f6824f7ade Cleanup 2016-03-02 02:53:23 +08:00
7fd46e0b0d Cleanup 2016-03-02 01:02:11 +08:00
d4067fcb6d Added a short timeout for test requests 2016-03-02 00:31:00 +08:00
20f01709ea Mention URL escaping
Closes #311
2016-03-01 23:48:13 +08:00
56afd1adb9 Test suite cleanup 2016-03-01 23:22:50 +08:00
5e87a2d7e5 Cleanup 2016-03-01 23:13:45 +08:00
d30e28c2c7 Test suite improvements 2016-03-01 23:11:06 +08:00
0d2d24eac7 Copy 2016-03-01 21:37:26 +08:00
e2751e5fa3 Fixed args for Python 2.x 2016-03-01 21:28:10 +08:00
2a25d71aa4 Refactored main() into program() + main() 2016-03-01 21:10:54 +08:00
01ca7f0eb2 Ignore redirected stdout with --output, -o
This makes it easier to use HTTPie in cron jobs and scripts.

Closes #259
2016-03-01 20:24:50 +08:00
4f8d6c013b Fixed get_lexer() 2016-03-01 16:55:12 +08:00
e83e554ffb README 2016-03-01 16:50:30 +08:00
345f5a02a2 Fixed json absolute import 2016-03-01 16:39:50 +08:00
f96f0ef9ed JSON detection improvements 2016-03-01 16:22:54 +08:00
74e4d0b678 Added JSON detection when `--json, -j` is set
To correctly format JSON responses even when an incorrect ``Content-Type`` is returned.

Closes #92
Closes #349
Closes #368
2016-03-01 14:57:15 +08:00
0fc1f61f3d Fixed README 2016-03-01 00:45:54 +08:00
c50413a9c1 Added support section 2016-03-01 00:24:52 +08:00
9f8c452e7e Added gitter chat 2016-03-01 00:12:55 +08:00
776328c818 Added gitter chat 2016-03-01 00:08:07 +08:00
9312fabc01 Capitalization II 2016-02-29 22:03:08 +08:00
48ce934dfa Capitalization 2016-02-29 22:01:05 +08:00
3625bb6fa1 Updated travis badge title 2016-02-29 22:00:18 +08:00
a97f0d52f6 Travis 2016-02-29 17:20:50 +08:00
41b0286f37 Travis 2016-02-29 17:19:12 +08:00
fee54b04d8 Travis 2016-02-29 17:09:55 +08:00
73e0455896 Travis 2016-02-29 16:52:33 +08:00
3b217daddc Travis 2016-02-29 16:43:18 +08:00
e5e5d0ce6d Travis 2016-02-29 16:36:25 +08:00
f43e473de1 Travis 2016-02-29 16:35:09 +08:00
0a002ec554 Fix travis 2016-02-29 15:38:59 +08:00
576ee83d82 Fix travis 2016-02-29 15:35:56 +08:00
e42f7b8fc9 OSX build 2016-02-29 15:32:10 +08:00
b44e16ed0f Fix appveyor 2016-02-29 15:14:43 +08:00
ed08ab133e Refactoring 2016-02-29 15:00:17 +08:00
5408fb0fb9 Cleanup 2016-02-29 14:31:27 +08:00
e18b609ef7 Fixed --max-redirects 2016-02-29 14:21:25 +08:00
356e043651 Added --show-redirects and --max-redirects
Closes #157, #183, #188, #246
2016-02-29 14:12:09 +08:00
c6d4f6cdf6 Show redirects WIP 2016-02-29 14:07:08 +08:00
dc1371d4d6 Implemented --max-redirects option
Added argument to argparse, changed client so that it uses a new
requests Session() with the number of redirects and a single test to
show the setting works.
2016-02-29 14:07:08 +08:00
e2235e56dc Update CHANGELOG.rst 2016-02-29 02:38:09 +08:00
763935b77f Update CHANGELOG.rst 2016-02-29 02:37:36 +08:00
6435532f72 Updated CHANGELOG 2016-02-28 21:37:49 +08:00
11a37067e7 Document -A as a short name for --auth-type 2016-02-28 21:01:21 +08:00
25f0156502 Merge pull request #432 from hangtwenty/master
Add `-A` as short arg for `--auth-type`
2016-02-28 20:58:23 +08:00
0f8d04b4df More robust mime type parsing
Closes #344
2016-02-28 20:12:16 +08:00
e385ed6a99 Fix README 2016-02-28 19:32:19 +08:00
01fdab55e9 Explain how to send fieldnames and headers starting with '-'
Closes #355
2016-02-28 19:31:43 +08:00
1127557742 Cleanup 2016-02-28 19:15:35 +08:00
5898879395 Fixed --download with --session
Closes #412
2016-02-28 19:14:10 +08:00
8c33e5e3d3 Parser => HTTPieArgumentParser 2016-02-28 19:01:54 +08:00
10da7b63a3 Mention MacPorts installation method
Closes #395
2016-02-28 16:54:33 +08:00
df193a373f Updated tarball URL 2016-02-28 16:20:19 +08:00
c2f8c36952 Updated download example URLs 2016-02-28 16:19:18 +08:00
56f498c153 Detect Content Type of file uploaded in multipart/form-data request
Closes #271 #285 #398

This adds filename-based detection. It's still not possible to specify the
content type manually, though.
2016-02-28 15:49:01 +08:00
59e22b16b8 When possible, guess the content-type of the file being sent
Refined PR #285 by rasky to pass all tests
2016-02-28 15:47:43 +08:00
d32d6f29a9 Typo 2016-02-17 14:53:00 +08:00
274dddfb45 Changed the default color style back to solarized
Closes #440
2016-02-17 14:46:35 +08:00
deb7b747cc Small fix for Python 2.6 compatibility.
Relates to #430 / #432.
2016-01-22 18:46:36 -06:00
018e1f68de Merge remote-tracking branch 'upstream/master' 2016-01-22 18:40:53 -06:00
ac69d4311b add -A as short arg for --auth-type
Addresses #430
comes with unit test
2016-01-22 18:37:30 -06:00
c75c4fa2a6 Merge pull request #429 from pra85/patch-1
Update license year range to 2016
2016-01-18 11:26:27 -03:00
a6a79e92e4 Update license year range to 2016 2016-01-18 11:20:24 +05:30
ea76542150 Added guardian/httpie-hmac-auth 2016-01-15 14:07:41 -03:00
c6690e0182 Makefile improvements 2016-01-02 14:33:48 -03:00
c82c9f0ae4 Makefile improvements 2016-01-02 14:28:46 -03:00
84b81c00ea Fixed tox.ini and improved tests and CONTRIBUTING.txt 2016-01-02 14:07:00 -03:00
34c6958dc8 1.0.0-dev 2016-01-01 19:38:21 -03:00
4722076335 v0.9.3 2016-01-01 19:27:07 -03:00
f14a0ad37d Fix pytest configuration 2016-01-01 19:11:22 -03:00
4cadc1d4c0 Update pytest configuration
* Move it from tox.ini to pytest.ini
* Ignore tests/fixtures
2016-01-01 19:08:27 -03:00
c3e5456aba Update setuptools on Appveyor 2016-01-01 19:01:39 -03:00
33489c9a91 Update pip on Appveyor 2016-01-01 18:51:17 -03:00
4e2b6b0ccc Update get-pip.py URL 2016-01-01 18:46:01 -03:00
b034c8703a PEP8 2016-01-01 18:41:58 -03:00
ab3d2656af Undo 'Fix "mock requires setuptools>=17.1. Aborting installation" on Win+Py3' 2016-01-01 18:38:06 -03:00
c42bd0051a Merge pull request #396 from pathcl/master
PEP8 errors
2016-01-01 18:37:49 -03:00
288cb4fdeb Fix "mock requires setuptools>=17.1. Aborting installation" on Win+Py3 2016-01-01 18:27:43 -03:00
8771d759fe Merge pull request #382 from konopski/master
[#381] Fixed --auth prompt on Windows
2016-01-01 18:18:23 -03:00
2cdca36960 Merge pull request #386 from honorabrutroll/dev
Fixed --pretty on Windows (closes #372)
2016-01-01 18:15:50 -03:00
8dc4f04fda Merge pull request #419 from hangtwenty/master
Fail gracefully if disable_warnings not available
2016-01-01 17:58:23 -03:00
dadc0cd27c Merge pull request #417 from yansal/patch-1
Fix typo in CONTRIBUTING.rst
2016-01-01 17:57:48 -03:00
59fd42244a Merge pull request #421 from Altreus/patch-1
Aitch already has an official spelling - use it
2016-01-01 17:56:52 -03:00
6afe9c32c4 Merge pull request #424 from t-mart/master
Remove duplicate setup.py test option
2015-12-28 13:56:14 -03:00
cc0ba03290 Remove duplicate setup.py test option 2015-12-28 10:38:07 -06:00
fad84a962e Aitch already has an official spelling - use it
http://www.oxforddictionaries.com/definition/english/aitch
2015-12-15 14:39:26 +00:00
4f755a8bde Fail gracefully if disable_warnings not available
Addresses #418. Rationale explained there.
2015-12-02 11:50:48 -06:00
21ee981fc6 Fix typo in CONTRIBUTING.rst 2015-11-27 15:02:23 +01:00
6259b5dd3b Add a --default-scheme argument. 2015-10-28 15:06:04 -07:00
45df860124 PEP8 errors 2015-10-22 14:32:16 -03:00
277da1ff93 str conversion 2015-10-21 21:57:06 +02:00
1ded5c2a97 Merge pull request #387 from yurimalheiros/patch-1
Update README.rst
2015-10-07 19:41:24 +02:00
69bd72ce95 Update README.rst
Fix a simple typo
2015-10-07 14:17:53 -03:00
8bf6db471b Fixed AUTHORS.rst 2015-10-04 17:16:41 -05:00
b1cc069fce Add Edward Yang to AUTHORS.rst 2015-10-04 17:06:54 -05:00
ed484c278b Change pretty option processor to only raise error when using output file 2015-10-04 17:06:00 -05:00
aec0f04f5d [#381] --auth fails on windows 2015-09-21 16:30:46 +02:00
8eb460a6f3 Disable OSX travis builds for now
OSX/Python support isn't probably not ready on Travis' side yet. 

https://travis-ci.org/jkbrzt/httpie/jobs/78219565#L27

// cc @msabramo — any idea?
2015-09-11 16:24:42 +02:00
5fe5958b06 Update README.rst 2015-09-01 15:11:36 +02:00
0e1c17daa1 Merge pull request #375 from hoatle/jwt-auth-plugin-reference
mention httpie-jwt-auth plugin on README
2015-09-01 15:10:41 +02:00
307517e7ef Merge pull request #377 from mblayman/fix-trasnsport
Fix trasnsport typo in plugin manager.
2015-08-31 06:20:01 +02:00
d60a04da2d Add Matt Layman to AUTHORS. 2015-08-30 21:41:14 -04:00
9ea89ffefe Fix typo in method name of plugin manager. 2015-08-30 21:37:47 -04:00
bebeb2100d mention httpie-jwt-auth plugin on README 2015-08-24 01:58:00 +07:00
2b51cb6687 Updated links II. 2015-07-03 18:55:45 +02:00
fa4bd033ef Updated links. 2015-07-03 03:40:38 +02:00
f8c1104429 Fixed link to httpie-edgegrid 2015-06-30 11:51:40 +02:00
be9d9281b7 Added a link to the httpie-edgegrid plugin. 2015-06-30 11:50:11 +02:00
ced0838598 Converted tabs to spaces. 2015-06-26 16:39:23 +02:00
d8b819b03f Merge pull request #337 from joaodelgado/json-serialization
Only serialize json if data is a dict instance
2015-04-24 15:07:16 +02:00
6fd0f23f39 Only serialize json if data is a dict instance 2015-04-11 02:11:22 +01:00
483546d781 Added mock to tests_require 2015-03-25 22:52:49 +01:00
daf3573908 Update CHANGELOG.rst 2015-03-25 22:37:48 +01:00
62407f781f Update CHANGELOG.rst 2015-03-25 22:35:36 +01:00
cbbaac13ea Merge pull request #300 from msabramo/print_info_about_request_on_error
Print info about request on error
2015-03-25 22:21:18 +01:00
6aad79d71c Merge pull request #319 from fay-jai/license
Update license with up-to-date year
2015-03-25 22:18:42 +01:00
c1f26347fc Merge pull request #330 from mihirvj/bash-completion
Bash auto completion
2015-03-25 22:08:13 +01:00
29a0147dd5 See #326
Adds bash completion to http command line interface.

Installing the script:
You can copy it to /etc/bash_completion.d/ (or something else on your
machine) and source it using following command

	$ source /etc/profile

Now whenever you encounter a "-*" on your CLI, it presents you with the
options specified.

Couple of things that are still under work:
1) Adding this bash script to setup, so that user won't need manual
installation
2) Adding more options for HTTP (GET, PUT and so on) and other
options
2015-03-24 22:26:10 -04:00
ab0d1fd8d0 Added .editorconfig. 2015-03-13 17:17:17 +01:00
35a3dd2855 Merge pull request #321 from ifdattic/patch-1
Fix typos, improve readability
2015-03-10 09:37:40 +01:00
ece85c0f0c Fix typos, improve readability 2015-03-10 10:05:13 +02:00
798cd4f0ec Update license with up-to-date year 2015-03-08 11:29:33 -07:00
1a43c0e5f7 Fixed --debug output 2015-02-28 17:02:05 +01:00
fdabbc6048 Typo 2015-02-24 16:50:02 +01:00
5f3de558cb README 2015-02-24 16:41:34 +01:00
fdae686e12 Clean up compat and fix is_pypy. 2015-02-24 08:18:03 +01:00
1c181a5d25 1.0.0-dev 2015-02-24 07:52:34 +01:00
a228399801 0.9.2 2015-02-24 07:50:15 +01:00
bada3b45f1 Use absolute links to LICENCE, etc. 2015-02-24 07:50:15 +01:00
e4bc363f9e Don't depend on requests.compat
#314
2015-02-24 07:50:15 +01:00
24957e3b61 Update requirements-dev.txt
dd7f1c4
2015-02-16 21:55:40 +01:00
fb437591da Include AUTHORS.rst in dist; metadata cleanup 2015-02-16 21:42:09 +01:00
b7fc89acdc README fixes 2015-02-16 21:29:40 +01:00
2e88aa53cf Extracted changes from README into a proper CHANGELOG file
Inspired by keepachangelog.com
2015-02-16 21:16:39 +01:00
9e62151bec Merge pull request #312 from msabramo/patch-5
tox.ini: Use pytest-httpbin>=0.0.6
2015-02-16 20:27:35 +01:00
ecc59591f1 Disable urllib3's "Unverified HTTPS request is being made" warnings 2015-02-16 19:36:02 +01:00
f855de16c2 Increase test coverage for error handling 2015-02-15 23:22:52 -08:00
7f8adad313 Print info about request on error
This can help in diagnosing certain issues. For example, if I were
trying to use a "http+unix" URL but I don't have #299, then I'll get the
following:

    [marca@marca-mac2 httpie]$ http http+unix://%2Ftmp%2Fprofilesvc.sock/status/pid

    http: error: ConnectionError: ('Connection aborted.', gaierror(8, 'nodename nor servname provided, or not known'))
    while doing GET request to URL: http://http+unix//%2Ftmp%2Fprofilesvc.sock/status/pid

Having the URL in the error message is super useful here so that I know an
extra `http://` is getting prepended and it's not doing what I expected.
2015-02-15 23:22:52 -08:00
51c19cfe10 test_ssl.py: Remove skip failures on PyPy
Revert 985f65e which skipped SSL tests that failed on PyPy because @kevin1024 fixed the problem in pytest-httpbin 0.0.6 (commit f38a312446)
2015-02-15 21:42:34 -08:00
dd7f1c4cce tox.ini: Use pytest-httpbin>=0.0.6
This hopefully fixes SSL timeout errors. 

Fixes #308
2015-02-15 20:33:57 -08:00
45784c7260 Fixed TOC 2015-02-15 12:57:57 +01:00
868baaba4e README 2015-02-15 12:53:57 +01:00
5760b780a0 README 2015-02-15 11:28:53 +01:00
2e5d14238f Tweak badge style 2015-02-15 00:54:49 +01:00
3b3eff01b7 Use shields.io badges 2015-02-15 00:51:58 +01:00
42f454eb6b README 2015-02-15 00:43:24 +01:00
40d95b650c README 2015-02-15 00:42:41 +01:00
bc0d17c04c Added a PyPy incompatibility workaround. 2015-02-15 00:36:55 +01:00
985f65ef52 Temporarily skip SSL tests on PyPy due to #308 2015-02-14 23:14:06 +01:00
dd0a4ab87a Default --style to "monokai"
419ca85
2015-02-14 22:51:31 +01:00
07aaefa232 Updated screenshot 2015-02-14 18:18:43 +01:00
419ca85e62 The default color --style is now "fruity"
It's experimental - please let me know should you dislike this change.

To make Solarized default again, add this to your ~/.config.json:

  "default_options": [
    "--style=solarized"
  ],
2015-02-14 18:18:04 +01:00
596fdc8c7e Update README examples with the new default Accept-Encoding value used by Requests. 2015-02-14 17:55:34 +01:00
6e7e2f2eea Changed the default JSON Content-Type to application/json. 2015-02-14 17:45:15 +01:00
748794257c Merge pull request #306 from msabramo/patch-4
.travis.yml: sudo false for Docker containers
2015-02-10 17:41:32 +01:00
55fa975ae5 .travis.yml: sudo false for Docker containers
Enables new Docker container infrastructure.
2015-02-10 07:41:38 -08:00
e6e94398ae Merge pull request #303 from msabramo/coveralls_only_one
.travis.yml: Only do coveralls on the latest Python version
2015-02-10 16:38:54 +01:00
fbd44640e6 .travis.yml: Only do coveralls on newest python
Testing theory that it has to do with different python version subjobs
completing in different orders and the last one wins.
2015-02-10 07:37:03 -08:00
43915b5fc0 Merge pull request #304 from msabramo/patch-2
compat.py: Add pragma no covers
2015-02-10 16:00:59 +01:00
f1e1299104 Merge pull request #305 from msabramo/patch-3
Conditionally skip test_session_unicode on Py3k
2015-02-10 16:00:24 +01:00
86ebb9b741 compat.py: Add pragma no covers
Cuz this is a lot of version-specific stuff and it can be confusing to have different coverage per version, especially with coveralls.
2015-02-10 06:54:59 -08:00
873102d5eb Mark test_session_unicode as xfail
There are known problems with unicode in headers.
See https://github.com/jakubroztocil/httpie/issues/282
2015-02-10 06:52:51 -08:00
337c05f95c README 2015-02-07 18:06:49 +01:00
a786f17997 1.0.0-dev 2015-02-07 17:04:33 +01:00
753a8d04e4 v0.9.1 2015-02-07 17:04:13 +01:00
3ff03524ff HTTP/2 has no minor versions.
https://github.com/jakubroztocil/httpie-http2/issues/1
2015-02-07 16:31:42 +01:00
a5a83c5b77 Prevent a circular import issue. 2015-02-07 16:29:17 +01:00
9682f955b5 Handle HTTP/2 responses
https://github.com/jakubroztocil/httpie-http2/issues/1#issuecomment-73301801
2015-02-06 21:13:57 +01:00
0d21ff022e Added a link to @pd's httpie-api-auth plugin 2015-02-06 20:06:50 +01:00
996e314482 Cleanup 2015-02-05 15:55:20 +01:00
687a6a734d Added support for transport adapter plugins
#276, #298
2015-02-05 15:25:00 +01:00
b125ce5eae Allow custom URL schemes
Closes #299

See also #276
2015-02-05 14:35:34 +01:00
92a4352f10 Added a coveralls badge. 2015-01-31 17:49:48 +01:00
c0f1fb61ac Merge pull request #297 from msabramo/patch-1
README.rst: suppor => support
2015-01-31 16:15:42 +01:00
17358be1ae README.rst: suppor => support 2015-01-31 07:01:54 -08:00
338d39c841 Fixed version link 2015-01-31 13:23:52 +01:00
530d6c5e27 1.0.0-dev 2015-01-31 13:22:17 +01:00
63 changed files with 3618 additions and 1717 deletions

17
.editorconfig Normal file
View File

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

18
.gitignore vendored
View File

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

View File

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

View File

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

350
CHANGELOG.rst Normal file
View File

@ -0,0 +1,350 @@
==========
Change Log
==========
This document records all notable changes to `HTTPie <http://httpie.org>`_.
This project adheres to `Semantic Versioning <http://semver.org/>`_.
`1.0.1`_ (2018-11-14)
-------------------------
* Removed external URL calls from tests.
`1.0.0`_ (2018-11-02)
-------------------------
* Added ``--style=auto`` which follows the terminal ANSI color styles.
* Added support for selecting TLS 1.3 via ``--ssl=tls1.3``
(available once implemented in upstream libraries).
* Added ``true``/``false`` as valid values for ``--verify``
(in addition to ``yes``/``no``) and the boolean value is case-insensitive.
* Changed the default ``--style`` from ``solarized`` to ``auto`` (on Windows it stays ``fruity``).
* Fixed default headers being incorrectly case-sensitive.
* Removed Python 2.6 support.
`0.9.9`_ (2016-12-08)
---------------------
* Fixed README.
`0.9.8`_ (2016-12-08)
---------------------
* Extended auth plugin API.
* Added exit status code ``7`` for plugin errors.
* Added support for ``curses``-less Python installations.
* Fixed ``REQUEST_ITEM`` arg incorrectly being reported as required.
* Improved ``CTRL-C`` interrupt handling.
* Added the standard exit status code ``130`` for keyboard interrupts.
`0.9.6`_ (2016-08-13)
---------------------
* Added Python 3 as a dependency for Homebrew installations
to ensure some of the newer HTTP features work out of the box
for macOS users (starting with HTTPie 0.9.4.).
* Added the ability to unset a request header with ``Header:``, and send an
empty value with ``Header;``.
* Added ``--default-scheme <URL_SCHEME>`` to enable things like
``$ alias https='http --default-scheme=https``.
* Added ``-I`` as a shortcut for ``--ignore-stdin``.
* Added fish shell completion (located in ``extras/httpie-completion.fish``
in the Github repo).
* Updated ``requests`` to 2.10.0 so that SOCKS support can be added via
``pip install requests[socks]``.
* Changed the default JSON ``Accept`` header from ``application/json``
to ``application/json, */*``.
* Changed the pre-processing of request HTTP headers so that any leading
and trailing whitespace is removed.
`0.9.4`_ (2016-07-01)
---------------------
* Added ``Content-Type`` of files uploaded in ``multipart/form-data`` requests
* Added ``--ssl=<PROTOCOL>`` to specify the desired SSL/TLS protocol version
to use for HTTPS requests.
* Added JSON detection with ``--json, -j`` to work around incorrect
``Content-Type``
* Added ``--all`` to show intermediate responses such as redirects (with ``--follow``)
* Added ``--history-print, -P WHAT`` to specify formatting of intermediate responses
* Added ``--max-redirects=N`` (default 30)
* Added ``-A`` as short name for ``--auth-type``
* Added ``-F`` as short name for ``--follow``
* Removed the ``implicit_content_type`` config option
(use ``"default_options": ["--form"]`` instead)
* Redirected ``stdout`` doesn't trigger an error anymore when ``--output FILE``
is set
* Changed the default ``--style`` back to ``solarized`` for better support
of light and dark terminals
* Improved ``--debug`` output
* Fixed ``--session`` when used with ``--download``
* Fixed ``--download`` to trim too long filenames before saving the file
* Fixed the handling of ``Content-Type`` with multiple ``+subtype`` parts
* Removed the XML formatter as the implementation suffered from multiple issues
`0.9.3`_ (2016-01-01)
---------------------
* Changed the default color ``--style`` from ``solarized`` to ``monokai``
* Added basic Bash autocomplete support (need to be installed manually)
* Added request details to connection error messages
* Fixed ``'requests.packages.urllib3' has no attribute 'disable_warnings'``
errors that occurred in some installations
* Fixed colors and formatting on Windows
* Fixed ``--auth`` prompt on Windows
`0.9.2`_ (2015-02-24)
---------------------
* Fixed compatibility with Requests 2.5.1
* Changed the default JSON ``Content-Type`` to ``application/json`` as UTF-8
is the default JSON encoding
`0.9.1`_ (2015-02-07)
---------------------
* Added support for Requests transport adapter plugins
(see `httpie-unixsocket <https://github.com/httpie/httpie-unixsocket>`_
and `httpie-http2 <https://github.com/httpie/httpie-http2>`_)
`0.9.0`_ (2015-01-31)
---------------------
* Added ``--cert`` and ``--cert-key`` parameters to specify a client side
certificate and private key for SSL
* Improved unicode support
* Improved terminal color depth detection via ``curses``
* To make it easier to deal with Windows paths in request items, ``\``
now only escapes special characters (the ones that are used as key-value
separators by HTTPie)
* Switched from ``unittest`` to ``pytest``
* Added Python `wheel` support
* Various test suite improvements
* Added ``CONTRIBUTING``
* Fixed ``User-Agent`` overwriting when used within a session
* Fixed handling of empty passwords in URL credentials
* Fixed multiple file uploads with the same form field name
* Fixed ``--output=/dev/null`` on Linux
* Miscellaneous bugfixes
`0.8.0`_ (2014-01-25)
---------------------
* Added ``field=@file.txt`` and ``field:=@file.json`` for embedding
the contents of text and JSON files into request data
* Added curl-style shorthand for localhost
* Fixed request ``Host`` header value output so that it doesn't contain
credentials, if included in the URL
`0.7.1`_ (2013-09-24)
---------------------
* Added ``--ignore-stdin``
* Added support for auth plugins
* Improved ``--help`` output
* Improved ``Content-Disposition`` parsing for ``--download`` mode
* Update to Requests 2.0.0
`0.6.0`_ (2013-06-03)
---------------------
* XML data is now formatted
* ``--session`` and ``--session-read-only`` now also accept paths to
session files (eg. ``http --session=/tmp/session.json example.org``)
`0.5.1`_ (2013-05-13)
---------------------
* ``Content-*`` and ``If-*`` request headers are not stored in sessions
anymore as they are request-specific
`0.5.0`_ (2013-04-27)
---------------------
* Added a download mode via ``--download``
* Fixes miscellaneous bugs
`0.4.1`_ (2013-02-26)
---------------------
* Fixed ``setup.py``
`0.4.0`_ (2013-02-22)
---------------------
* Added Python 3.3 compatibility
* Added Requests >= v1.0.4 compatibility
* Added support for credentials in URL
* Added ``--no-option`` for every ``--option`` to be config-friendly
* Mutually exclusive arguments can be specified multiple times. The
last value is used
`0.3.0`_ (2012-09-21)
---------------------
* Allow output redirection on Windows
* Added configuration file
* Added persistent session support
* Renamed ``--allow-redirects`` to ``--follow``
* Improved the usability of ``http --help``
* Fixed installation on Windows with Python 3
* Fixed colorized output on Windows with Python 3
* CRLF HTTP header field separation in the output
* Added exit status code ``2`` for timed-out requests
* Added the option to separate colorizing and formatting
(``--pretty=all``, ``--pretty=colors`` and ``--pretty=format``)
``--ugly`` has bee removed in favor of ``--pretty=none``
`0.2.7`_ (2012-08-07)
---------------------
* Added compatibility with Requests 0.13.6
* Added streamed terminal output. ``--stream, -S`` can be used to enable
streaming also with ``--pretty`` and to ensure a more frequent output
flushing
* Added support for efficient large file downloads
* Sort headers by name (unless ``--pretty=none``)
* Response body is fetched only when needed (e.g., not with ``--headers``)
* Improved content type matching
* Updated Solarized color scheme
* Windows: Added ``--output FILE`` to store output into a file
(piping results in corrupted data on Windows)
* Proper handling of binary requests and responses
* Fixed printing of ``multipart/form-data`` requests
* Renamed ``--traceback`` to ``--debug``
`0.2.6`_ (2012-07-26)
---------------------
* The short option for ``--headers`` is now ``-h`` (``-t`` has been
removed, for usage use ``--help``)
* Form data and URL parameters can have multiple fields with the same name
(e.g.,``http -f url a=1 a=2``)
* Added ``--check-status`` to exit with an error on HTTP 3xx, 4xx and
5xx (3, 4, and 5, respectively)
* If the output is piped to another program or redirected to a file,
the default behaviour is to only print the response body
(It can still be overwritten via the ``--print`` flag.)
* Improved highlighting of HTTP headers
* Added query string parameters (``param==value``)
* Added support for terminal colors under Windows
`0.2.5`_ (2012-07-17)
---------------------
* Unicode characters in prettified JSON now don't get escaped for
improved readability
* --auth now prompts for a password if only a username provided
* Added support for request payloads from a file path with automatic
``Content-Type`` (``http URL @/path``)
* Fixed missing query string when displaying the request headers via
``--verbose``
* Fixed Content-Type for requests with no data
`0.2.2`_ (2012-06-24)
---------------------
* The ``METHOD`` positional argument can now be omitted (defaults to
``GET``, or to ``POST`` with data)
* Fixed --verbose --form
* Added support for Tox
`0.2.1`_ (2012-06-13)
---------------------
* Added compatibility with ``requests-0.12.1``
* Dropped custom JSON and HTTP lexers in favor of the ones newly included
in ``pygments-1.5``
`0.2.0`_ (2012-04-25)
---------------------
* Added Python 3 support
* Added the ability to print the HTTP request as well as the response
(see ``--print`` and ``--verbose``)
* Added support for Digest authentication
* Added file upload support
(``http -f POST file_field_name@/path/to/file``)
* Improved syntax highlighting for JSON
* Added support for field name escaping
* Many bug fixes
`0.1.6`_ (2012-03-04)
---------------------
* Fixed ``setup.py``
`0.1.5`_ (2012-03-04)
---------------------
* Many improvements and bug fixes
`0.1.4`_ (2012-02-28)
---------------------
* Many improvements and bug fixes
`0.1`_ (2012-02-25)
-------------------
* Initial public release
.. _`0.1`: https://github.com/jakubroztocil/httpie/commit/b966efa
.. _0.1.4: https://github.com/jakubroztocil/httpie/compare/b966efa...0.1.4
.. _0.1.5: https://github.com/jakubroztocil/httpie/compare/0.1.4...0.1.5
.. _0.1.6: https://github.com/jakubroztocil/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.1: https://github.com/jakubroztocil/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.5: https://github.com/jakubroztocil/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.7: https://github.com/jakubroztocil/httpie/compare/0.2.5...0.2.7
.. _0.3.0: https://github.com/jakubroztocil/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.1: https://github.com/jakubroztocil/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.1: https://github.com/jakubroztocil/httpie/compare/0.5.0...0.5.1
.. _0.6.0: https://github.com/jakubroztocil/httpie/compare/0.5.1...0.6.0
.. _0.7.1: https://github.com/jakubroztocil/httpie/compare/0.6.0...0.7.1
.. _0.8.0: https://github.com/jakubroztocil/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.1: https://github.com/jakubroztocil/httpie/compare/0.9.0...0.9.1
.. _0.9.2: https://github.com/jakubroztocil/httpie/compare/0.9.1...0.9.2
.. _0.9.3: https://github.com/jakubroztocil/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.6: https://github.com/jakubroztocil/httpie/compare/0.9.4...0.9.6
.. _0.9.8: https://github.com/jakubroztocil/httpie/compare/0.9.6...0.9.8
.. _0.9.9: https://github.com/jakubroztocil/httpie/compare/0.9.8...0.9.9
.. _1.0.0: https://github.com/jakubroztocil/httpie/compare/0.9.9...1.0.0
.. _1.0.1: https://github.com/jakubroztocil/httpie/compare/1.0.0...1.0.1

View File

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

View File

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

View File

@ -1 +1,4 @@
include README.rst LICENSE
include LICENSE
include README.rst
include CHANGELOG.rst
include AUTHORS.rst

104
Makefile
View File

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

1147
README.rst

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

69
extras/httpie.rb Normal file
View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 446 KiB

After

Width:  |  Height:  |  Size: 681 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -48,11 +48,18 @@ class HTTPResponse(HTTPMessage):
def iter_lines(self, chunk_size):
return ((line, b'\n') for line in self._orig.iter_lines(chunk_size))
#noinspection PyProtectedMember
# noinspection PyProtectedMember
@property
def headers(self):
original = self._orig.raw._original_response
version = {9: '0.9', 10: '1.0', 11: '1.1'}[original.version]
version = {
9: '0.9',
10: '1.0',
11: '1.1',
20: '2',
}[original.version]
status_line = 'HTTP/{version} {status} {reason}'.format(
version=version,
status=original.status,

View File

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

View File

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

View File

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

View File

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

View File

@ -1,9 +1,16 @@
from httpie.plugins.base import AuthPlugin, FormatterPlugin, ConverterPlugin
"""
WARNING: The plugin API is still work in progress and will
probably be completely reworked by v1.0.0.
"""
from httpie.plugins.base import (
AuthPlugin, FormatterPlugin,
ConverterPlugin, TransportPlugin
)
from httpie.plugins.manager import PluginManager
from httpie.plugins.builtin import BasicAuthPlugin, DigestAuthPlugin
from httpie.output.formatters.headers import HeadersFormatter
from httpie.output.formatters.json import JSONFormatter
from httpie.output.formatters.xml import XMLFormatter
from httpie.output.formatters.colors import ColorFormatter
@ -12,5 +19,4 @@ plugin_manager.register(BasicAuthPlugin,
DigestAuthPlugin)
plugin_manager.register(HeadersFormatter,
JSONFormatter,
XMLFormatter,
ColorFormatter)

View File

@ -15,21 +15,66 @@ class AuthPlugin(BasePlugin):
"""
Base auth plugin class.
See <https://github.com/jkbr/httpie-ntlm> for an example auth plugin.
See <https://github.com/httpie/httpie-ntlm> for an example auth plugin.
See also `test_auth_plugins.py`
"""
# The value that should be passed to --auth-type
# to use this auth plugin. Eg. "my-auth"
auth_type = None
def get_auth(self, username, password):
# Set to `False` to make it possible to invoke this auth
# plugin without requiring the user to specify credentials
# through `--auth, -a`.
auth_require = True
# By default the `-a` argument is parsed for `username:password`.
# Set this to `False` to disable the parsing and error handling.
auth_parse = True
# If both `auth_parse` and `prompt_password` are set to `True`,
# and the value of `-a` lacks the password part,
# then the user will be prompted to type the password in.
prompt_password = True
# Will be set to the raw value of `-a` (if provided) before
# `get_auth()` gets called.
raw_auth = None
def get_auth(self, username=None, password=None):
"""
If `auth_parse` is set to `True`, then `username`
and `password` contain the parsed credentials.
Use `self.raw_auth` to access the raw value passed through
`--auth, -a`.
Return a ``requests.auth.AuthBase`` subclass instance.
"""
raise NotImplementedError()
class TransportPlugin(BasePlugin):
"""
http://docs.python-requests.org/en/latest/user/advanced/#transport-adapters
"""
# The URL prefix the adapter should be mount to.
prefix = None
def get_adapter(self):
"""
Return a ``requests.adapters.BaseAdapter`` subclass instance to be
mounted to ``self.prefix``.
"""
raise NotImplementedError()
class ConverterPlugin(object):
def __init__(self, mime):

View File

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

View File

@ -1,12 +1,14 @@
from itertools import groupby
from pkg_resources import iter_entry_points
from httpie.plugins import AuthPlugin, FormatterPlugin, ConverterPlugin
from httpie.plugins.base import TransportPlugin
ENTRY_POINT_NAMES = [
'httpie.plugins.auth.v1',
'httpie.plugins.formatter.v1',
'httpie.plugins.converter.v1',
'httpie.plugins.transport.v1',
]
@ -22,6 +24,9 @@ class PluginManager(object):
for plugin in plugins:
self._plugins.append(plugin)
def unregister(self, plugin):
self._plugins.remove(plugin)
def load_installed_plugins(self):
for entry_point_name in ENTRY_POINT_NAMES:
for entry_point in iter_entry_points(entry_point_name):
@ -34,8 +39,7 @@ class PluginManager(object):
return [plugin for plugin in self if issubclass(plugin, AuthPlugin)]
def get_auth_plugin_mapping(self):
return dict((plugin.auth_type, plugin)
for plugin in self.get_auth_plugins())
return {plugin.auth_type: plugin for plugin in self.get_auth_plugins()}
def get_auth_plugin(self, auth_type):
return self.get_auth_plugin_mapping()[auth_type]
@ -56,3 +60,8 @@ class PluginManager(object):
def get_converters(self):
return [plugin for plugin in self
if issubclass(plugin, ConverterPlugin)]
# Adapters
def get_transport_plugins(self):
return [plugin for plugin in self
if issubclass(plugin, TransportPlugin)]

View File

@ -4,7 +4,6 @@
import re
import os
import requests
from requests.cookies import RequestsCookieJar, create_cookie
from httpie.compat import urlsplit
@ -21,8 +20,9 @@ VALID_SESSION_NAME_PATTERN = re.compile('^[a-zA-Z0-9_.-]+$')
SESSION_IGNORED_HEADER_PREFIXES = ['Content-', 'If-']
def get_response(session_name, config_dir, args, read_only=False):
"""Like `client.get_response`, but applies permanent
def get_response(requests_session, session_name,
config_dir, args, read_only=False):
"""Like `client.get_responses`, but applies permanent
aspects of the session to the request.
"""
@ -32,7 +32,9 @@ def get_response(session_name, config_dir, args, read_only=False):
else:
hostname = (args.headers.get('Host', None)
or urlsplit(args.url).netloc.split('@')[-1])
assert re.match('^[a-zA-Z0-9_.:-]+$', hostname)
if not hostname:
# HACK/FIXME: httpie-unixsocket's URLs have no hostname.
hostname = 'localhost'
# host:port => host_port
hostname = hostname.replace(':', '_')
@ -44,25 +46,23 @@ def get_response(session_name, config_dir, args, read_only=False):
session = Session(path)
session.load()
requests_kwargs = get_requests_kwargs(args, base_headers=session.headers)
kwargs = get_requests_kwargs(args, base_headers=session.headers)
if args.debug:
dump_request(requests_kwargs)
session.update_headers(requests_kwargs['headers'])
dump_request(kwargs)
session.update_headers(kwargs['headers'])
if args.auth:
if args.auth_plugin:
session.auth = {
'type': args.auth_type,
'username': args.auth.key,
'password': args.auth.value,
'type': args.auth_plugin.auth_type,
'raw_auth': args.auth_plugin.raw_auth,
}
elif session.auth:
requests_kwargs['auth'] = session.auth
kwargs['auth'] = session.auth
requests_session = requests.Session()
requests_session.cookies = session.cookies
try:
response = requests_session.request(**requests_kwargs)
response = requests_session.request(**kwargs)
except Exception:
raise
else:
@ -74,7 +74,7 @@ def get_response(session_name, config_dir, args, read_only=False):
class Session(BaseConfigDict):
helpurl = 'https://github.com/jakubroztocil/httpie#sessions'
helpurl = 'https://httpie.org/doc#sessions'
about = 'HTTPie session file'
def __init__(self, path, *args, **kwargs):
@ -100,6 +100,10 @@ class Session(BaseConfigDict):
"""
for name, value in request_headers.items():
if value is None:
continue # Ignore explicitely unset headers
value = value.decode('utf8')
if name == 'User-Agent' and value.startswith('HTTPie/'):
continue
@ -132,20 +136,41 @@ class Session(BaseConfigDict):
stored_attrs = ['value', 'path', 'secure', 'expires']
self['cookies'] = {}
for cookie in jar:
self['cookies'][cookie.name] = dict(
(attname, getattr(cookie, attname))
self['cookies'][cookie.name] = {
attname: getattr(cookie, attname)
for attname in stored_attrs
)
}
@property
def auth(self):
auth = self.get('auth', None)
if not auth or not auth['type']:
return
auth_plugin = plugin_manager.get_auth_plugin(auth['type'])()
return auth_plugin.get_auth(auth['username'], auth['password'])
plugin = plugin_manager.get_auth_plugin(auth['type'])()
credentials = {'username': None, 'password': None}
try:
# New style
plugin.raw_auth = auth['raw_auth']
except KeyError:
# Old style
credentials = {
'username': auth['username'],
'password': auth['password'],
}
else:
if plugin.auth_parse:
from httpie.input import parse_auth
parsed = parse_auth(plugin.raw_auth)
credentials = {
'username': parsed.key,
'password': parsed.value,
}
return plugin.get_auth(**credentials)
@auth.setter
def auth(self, auth):
assert set(['type', 'username', 'password']) == set(auth.keys())
assert set(['type', 'raw_auth']) == set(auth.keys())
self['auth'] = auth

View File

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

View File

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

View File

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

View File

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

24
tests/conftest.py Normal file
View File

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

View File

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

View File

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

133
tests/test_auth_plugins.py Normal file
View File

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

View File

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

View File

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

40
tests/test_config.py Normal file
View File

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

View File

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

View File

@ -9,7 +9,7 @@ from utils import TESTS_ROOT
def has_docutils():
try:
#noinspection PyUnresolvedReferences
# noinspection PyUnresolvedReferences
import docutils
return True
except ImportError:
@ -32,8 +32,8 @@ assert filenames
def test_rst_file_syntax(filename):
p = subprocess.Popen(
['rst2pseudoxml.py', '--report=1', '--exit-status=1', filename],
stderr=subprocess.PIPE,
stdout=subprocess.PIPE
stderr=subprocess.PIPE,
stdout=subprocess.PIPE
)
err = p.communicate()[1]
assert p.returncode == 0, err
assert p.returncode == 0, err.decode('utf8')

View File

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

49
tests/test_errors.py Normal file
View File

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

View File

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

View File

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

View File

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

47
tests/test_redirects.py Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

23
tox.ini
View File

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