Compare commits

...

24 Commits

Author SHA1 Message Date
af5e9897c9 Bump peter-evans/create-pull-request from 4 to 5
Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 4 to 5.
- [Release notes](https://github.com/peter-evans/create-pull-request/releases)
- [Commits](https://github.com/peter-evans/create-pull-request/compare/v4...v5)

---
updated-dependencies:
- dependency-name: peter-evans/create-pull-request
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-05 14:58:07 +00:00
47e9b99ba1 Bump actions/stale from 7 to 8 (#1492)
Bumps [actions/stale](https://github.com/actions/stale) from 7 to 8.
- [Release notes](https://github.com/actions/stale/releases)
- [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/stale/compare/v7...v8)

---
updated-dependencies:
- dependency-name: actions/stale
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-28 18:30:30 -07:00
Sid
265841f866 docs: improve clarity of sentences (#1489)
* docs: improve clarity of sentences

Improved clarity by rephrasing sentences in the best practices section.

* docs: improve best practices section

* use appropriate formatting for stdin
* include EOF in abbreviated form

* docs: clarify sentence

* change 'know that' -> 'note that'
* use neither nor for better clarity
2023-03-22 13:03:18 -07:00
b16392fbb9 Remove redundant imports (#1466) 2023-01-15 11:35:36 -08:00
e73c3e6c24 Fix failing tests with responses ≥ 0.22.0
Close #1461
Close #1467

Thanks, @alexshpilkin!
2023-01-15 17:43:17 +01:00
f0563deb7f Bump mislav/bump-homebrew-formula-action from 1 to 2 (#1453)
Bumps [mislav/bump-homebrew-formula-action](https://github.com/mislav/bump-homebrew-formula-action) from 1 to 2.
- [Release notes](https://github.com/mislav/bump-homebrew-formula-action/releases)
- [Commits](https://github.com/mislav/bump-homebrew-formula-action/compare/v1...v2)

---
updated-dependencies:
- dependency-name: mislav/bump-homebrew-formula-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-04 03:18:15 -08:00
4894b4c0fc Bump actions/stale from 6 to 7 (#1459)
Bumps [actions/stale](https://github.com/actions/stale) from 6 to 7.
- [Release notes](https://github.com/actions/stale/releases)
- [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/stale/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/stale
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-04 03:17:54 -08:00
3a123c4125 Fix ci status badge error (#1464) 2023-01-04 03:17:18 -08:00
621042a048 Update README.md 2022-10-01 04:00:56 -07:00
0689b55e1d Clean up and refactor nested JSON parsing & interpreting (#1440) 2022-10-01 03:38:19 -07:00
a7321d8ac4 🔥 Remove $ from code fenced examples on readme (#1435)
* 🔥 Remove $ from code fenced examples on readme

* 🚨 FIx markdownlint errors

README.md:8: MD009 Trailing spaces
README.md:10: MD009 Trailing spaces
2022-10-01 03:37:50 -07:00
d9a73cd8eb Fix typos (#1431)
Found via `codespell -L datas`.
2022-10-01 03:34:41 -07:00
930cd9081a Use grep -E instead of egrep (#1436) 2022-10-01 03:32:17 -07:00
3549ee8342 Bump actions/stale from 5 to 6 (#1437)
Bumps [actions/stale](https://github.com/actions/stale) from 5 to 6.
- [Release notes](https://github.com/actions/stale/releases)
- [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/stale/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/stale
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-10-01 03:31:54 -07:00
810bb1c77b Update README.md 2022-08-10 09:13:49 -07:00
767f3c3a19 Update README.md 2022-08-10 07:58:09 -07:00
1236793272 Update README.md 2022-08-10 07:48:57 -07:00
c3a2f87dd2 Update README.md 2022-08-10 07:41:13 -07:00
1121d695a8 Update README.md 2022-08-10 07:38:40 -07:00
5794a070e1 place the logo in the middle in README.md (#1393) 2022-07-12 08:33:57 -07:00
4736a16698 docs: Fix a few typos (#1419) 2022-07-03 14:54:34 +02:00
3ad408add7 Fix paths to run benchmarking script (#1416) 2022-06-19 00:20:22 -07:00
91cdb22a4b Update Requests documentation links (#1414) 2022-06-17 14:04:42 -07:00
c995fd9b24 Bump actions/setup-python from 3 to 4 (#1412)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 3 to 4.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-06-09 10:04:06 -07:00
35 changed files with 602 additions and 540 deletions

View File

@ -12,7 +12,7 @@ jobs:
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v3
- uses: actions/setup-python@v4
with:
python-version: 3.9
@ -20,7 +20,7 @@ jobs:
- name: Create Pull Request
id: cpr
uses: peter-evans/create-pull-request@v4
uses: peter-evans/create-pull-request@v5
with:
commit-message: "[automated] Update auto-generated files"
title: "[automated] Update auto-generated files"

View File

@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v3
- uses: actions/setup-python@v4
with:
python-version: "3.9"

View File

@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v3
- uses: actions/setup-python@v4
with:
python-version: 3.9
- run: make venv

View File

@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v3
- uses: actions/setup-python@v4
with:
python-version: "3.10"
- run: make install

View File

@ -18,7 +18,7 @@ jobs:
with:
ref: ${{ github.event.inputs.branch }}
- uses: mislav/bump-homebrew-formula-action@v1
- uses: mislav/bump-homebrew-formula-action@v2
with:
formula-name: httpie
tag-name: ${{ github.events.inputs.branch }}

View File

@ -24,7 +24,7 @@ jobs:
with:
ref: ${{ github.event.inputs.branch }}
- uses: actions/setup-python@v3
- uses: actions/setup-python@v4
with:
python-version: 3.9

View File

@ -17,7 +17,7 @@ jobs:
with:
ref: ${{ github.event.inputs.branch }}
- uses: actions/setup-python@v3
- uses: actions/setup-python@v4
with:
python-version: 3.9

View File

@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v5
- uses: actions/stale@v8
with:
close-pr-message: 'Thanks for the pull request, but since it was stale for more than a 30 days we are closing it. If you want to work back on it, feel free to re-open it or create a new one.'
stale-pr-label: 'stale'

View File

@ -30,7 +30,7 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v3
- uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Windows setup

View File

@ -154,7 +154,7 @@ with the master branch of your repository (or a fresh checkout of HTTPie master,
`--fresh`) and report the results back.
```bash
$ python extras/benchmarks/run.py
$ python extras/profiling/run.py
```
The benchmarks can also be run on the CI. Since it is a long process, it requires manual

View File

@ -34,7 +34,7 @@ default: list-tasks
list-tasks:
@echo Available tasks:
@echo ----------------
@$(MAKE) -pRrq -f $(lastword $(MAKEFILE_LIST)) : 2>/dev/null | awk -v RS= -F: '/^# File/,/^# Finished Make data base/ {if ($$1 !~ "^[#.]") {print $$1}}' | sort | egrep -v -e '^[^[:alnum:]]' -e '^$@$$'
@$(MAKE) -pRrq -f $(lastword $(MAKEFILE_LIST)) : 2>/dev/null | awk -v RS= -F: '/^# File/,/^# Finished Make data base/ {if ($$1 !~ "^[#.]") {print $$1}}' | sort | grep -E -v -e '^[^[:alnum:]]' -e '^$@$$'
@echo

View File

@ -1,10 +1,30 @@
<br/>
<a href="https://httpie.io" target="blank_">
<img height="100" alt="HTTPie" src="https://raw.githubusercontent.com/httpie/httpie/master/docs/httpie-logo.svg" />
</a>
<br/>
<h2 align="center">
<a href="https://httpie.io" target="blank_">
<img height="100" alt="HTTPie" src="https://raw.githubusercontent.com/httpie/httpie/master/docs/httpie-logo.svg" />
</a>
<br>
HTTPie for Terminal: human-friendly CLI HTTP client for the API era
</h2>
# HTTPie: human-friendly CLI HTTP client for the API era
<div align="center">
[![HTTPie for Desktop](https://img.shields.io/static/v1?label=HTTPie&message=for%20Desktop&color=4B78E6)](https://httpie.io/product)
[![](https://img.shields.io/static/v1?label=HTTPie&message=for%20Web%20%26%20Mobile&color=73DC8C)](https://httpie.io/app)
[![](https://img.shields.io/static/v1?label=HTTPie&message=for%20Terminal&color=FA9BFA)](https://httpie.io/cli)
[![Twitter](https://img.shields.io/twitter/follow/httpie?style=flat&color=%234B78E6&logoColor=%234B78E6)](https://twitter.com/httpie)
[![Chat](https://img.shields.io/discord/725351238698270761?style=flat&label=Chat%20on%20Discord&color=%23FA9BFA)](https://httpie.io/discord)
</div>
<div align="center">
[![Docs](https://img.shields.io/badge/stable%20docs-httpie.io%2Fdocs%2Fcli-brightgreen?style=flat&color=%2373DC8C&label=Docs)](https://httpie.org/docs/cli)
[![Latest version](https://img.shields.io/pypi/v/httpie.svg?style=flat&label=Latest&color=%234B78E6&logo=&logoColor=white)](https://pypi.python.org/pypi/httpie)
[![Build](https://img.shields.io/github/actions/workflow/status/httpie/httpie/tests.yml?branch=master&color=%23FA9BFA&label=Build)](https://github.com/httpie/httpie/actions)
[![Coverage](https://img.shields.io/codecov/c/github/httpie/httpie?style=flat&label=Coverage&color=%2373DC8C)](https://codecov.io/gh/httpie/httpie)
</div>
HTTPie (pronounced _aitch-tee-tee-pie_) is a command-line HTTP client.
Its goal is to make CLI interaction with web services as human-friendly as possible.
@ -12,16 +32,16 @@ HTTPie is designed for testing, debugging, and generally interacting with APIs &
The `http` & `https` commands allow for creating and sending arbitrary HTTP requests.
They use simple and natural syntax and provide formatted and colorized output.
[![Docs](https://img.shields.io/badge/stable%20docs-httpie.io%2Fdocs-brightgreen?style=flat&color=%2373DC8C&label=Docs)](https://httpie.org/docs)
[![Latest version](https://img.shields.io/pypi/v/httpie.svg?style=flat&label=Latest&color=%234B78E6&logo=&logoColor=white)](https://pypi.python.org/pypi/httpie)
[![Build](https://img.shields.io/github/workflow/status/httpie/httpie/Build?color=%23FA9BFA&label=Build)](https://github.com/httpie/httpie/actions)
[![Coverage](https://img.shields.io/codecov/c/github/httpie/httpie?style=flat&label=Coverage&color=%2373DC8C)](https://codecov.io/gh/httpie/httpie)
[![Twitter](https://img.shields.io/twitter/follow/httpie?style=flat&color=%234B78E6&logoColor=%234B78E6)](https://twitter.com/httpie)
[![Chat](https://img.shields.io/badge/chat-Discord-brightgreen?style=flat&label=Chat%20on&color=%23FA9BFA)](https://httpie.io/discord)
<div align="center">
<img src="https://raw.githubusercontent.com/httpie/httpie/master/docs/httpie-animation.gif" alt="HTTPie in action" width="100%"/>
</div>
## We lost 54k GitHub stars
Please note we recently accidentally made this repo private for a moment, and GitHub deleted our community that took a decade to build. Read the full story here: https://httpie.io/blog/stardust
@ -53,25 +73,25 @@ Please note we recently accidentally made this repo private for a moment, and Gi
Hello World:
```bash
$ https httpie.io/hello
https httpie.io/hello
```
Custom [HTTP method](https://httpie.io/docs#http-method), [HTTP headers](https://httpie.io/docs#http-headers) and [JSON](https://httpie.io/docs#json) data:
```bash
$ http PUT pie.dev/put X-API-Token:123 name=John
http PUT pie.dev/put X-API-Token:123 name=John
```
Build and print a request without sending it using [offline mode](https://httpie.io/docs#offline-mode):
```bash
$ http --offline pie.dev/post hello=offline
http --offline pie.dev/post hello=offline
```
Use [GitHub API](https://developer.github.com/v3/issues/comments/#create-a-comment) to post a comment on an [Issue](https://github.com/httpie/httpie/issues/83) with [authentication](https://httpie.io/docs#authentication):
```bash
$ http -a USERNAME POST https://api.github.com/repos/httpie/httpie/issues/83/comments body='HTTPie is awesome! :heart:'
http -a USERNAME POST https://api.github.com/repos/httpie/httpie/issues/83/comments body='HTTPie is awesome! :heart:'
```
[See more examples →](https://httpie.io/docs#examples)

View File

@ -1453,7 +1453,8 @@ $ http --proxy=http:http://user:pass@10.10.1.10:3128 example.org
## HTTPS
### Server SSL certificate verification
To skip the hosts SSL certificate verification, you can pass `--verify=no` (default is `yes`):
To skip the hosts SSL certificate verification, you can pass `--verify=no` (default is `yes`):
```bash
$ http --verify=no https://pie.dev/get
@ -2394,12 +2395,9 @@ fi
You can check whether a new update is available for your system by running `httpie cli check-updates`:
```bash-termible
You can check whether a new update is available for your system by running `httpie cli check-updates`:
```bash-termible
$ httpie cli check-updates
$ httpie cli check-updates
```
#### `httpie cli export-args`
`httpie cli export-args` command can expose the parser specification of `http`/`https` commands
@ -2553,7 +2551,7 @@ HTTPie has the following community channels:
See [github.com/httpie/httpie/security/policy](https://github.com/httpie/httpie/security/policy).
### Change log
### Change log
See [CHANGELOG](https://github.com/httpie/httpie/blob/master/CHANGELOG.md).

View File

@ -1,3 +1,3 @@
Here we maintain a database of contributors, from which we generate credits on release blog posts and social medias.
Here we maintain a database of contributors, from which we generate credits on release blog posts and social media.
For the HTTPie blog see: <https://httpie.io/blog>.

View File

@ -55,7 +55,7 @@ def build_docs_structure(database: Database):
tree = database[KEY_DOC_STRUCTURE]
structure = []
for platform, tools_ids in tree.items():
assert platform.isalnum(), f'{platform=} must be alpha-numeric for generated links to work'
assert platform.isalnum(), f'{platform=} must be alphanumeric for generated links to work'
platform_tools = [tools[tool_id] for tool_id in tools_ids]
structure.append((platform, platform_tools))
return structure

View File

@ -10,7 +10,7 @@ Ensure the following requirements are satisfied:
- Python 3.7+
- `pyperf`
Then, run the `extras/benchmarks/run.py`:
Then, run the `extras/profiling/run.py`:
```console
$ python extras/profiling/run.py

View File

@ -9,11 +9,11 @@ timings.
The benchmarks are run through 'pyperf', which allows to
do get very precise results. For micro-benchmarks like startup,
please run `pyperf system tune` to get even more acurrate results.
please run `pyperf system tune` to get even more accurate results.
Examples:
# Run everything as usual, the default is that we do 3 warmup runs
# Run everything as usual, the default is that we do 3 warm-up runs
# and 5 actual runs.
$ python extras/profiling/benchmarks.py
@ -188,7 +188,7 @@ DownloadRunner('download', '`http --download :/big_file.txt` (3GB)', '3G')
def main() -> None:
# PyPerf will bring it's own argument parser, so configure the script.
# The somewhat fast and also precise enough configuration is this. We run
# benchmarks 3 times to warmup (e.g especially for download benchmark, this
# benchmarks 3 times to warm up (e.g especially for download benchmark, this
# is important). And then 5 actual runs where we record.
sys.argv.extend(
['--worker', '--loops=1', '--warmup=3', '--values=5', '--processes=2']

View File

@ -19,19 +19,19 @@ which would include additional dependencies like pyOpenSSL.
Examples:
# Run everything as usual, and compare last commit with master
$ python extras/benchmarks/run.py
$ python extras/profiling/run.py
# Include complex environments
$ python extras/benchmarks/run.py --complex
$ python extras/profiling/run.py --complex
# Compare against a fresh copy
$ python extras/benchmarks/run.py --fresh
$ python extras/profiling/run.py --fresh
# Compare against a custom branch of a custom repo
$ python extras/benchmarks/run.py --target-repo my_repo --target-branch my_branch
$ python extras/profiling/run.py --target-repo my_repo --target-branch my_branch
# Debug changes made on this script (only run benchmarks once)
$ python extras/benchmarks/run.py --debug
$ python extras/profiling/run.py --debug
"""
import dataclasses

View File

@ -9,7 +9,7 @@ from httpie.cli.options import ParserSpec
from httpie.manager.cli import options as manager_options
from httpie.output.ui.rich_help import OptionsHighlighter, to_usage
from httpie.output.ui.rich_utils import render_as_string
from httpie.utils import split
from httpie.utils import split_iterable
# Escape certain characters so they are rendered properly on

View File

@ -92,7 +92,3 @@ class MultipartRequestDataDict(MultiValueOrderedDict):
class RequestFilesDict(RequestDataDict):
pass
class NestedJSONArray(list):
"""Denotes a top-level JSON array."""

View File

@ -1,404 +0,0 @@
from enum import Enum, auto
from typing import (
Any,
Iterator,
NamedTuple,
Optional,
List,
NoReturn,
Type,
Union,
)
from .dicts import NestedJSONArray
EMPTY_STRING = ''
HIGHLIGHTER = '^'
OPEN_BRACKET = '['
CLOSE_BRACKET = ']'
BACKSLASH = '\\'
class HTTPieSyntaxError(ValueError):
def __init__(
self,
source: str,
token: Optional['Token'],
message: str,
message_kind: str = 'Syntax',
) -> None:
self.source = source
self.token = token
self.message = message
self.message_kind = message_kind
def __str__(self):
lines = [f'HTTPie {self.message_kind} Error: {self.message}']
if self.token is not None:
lines.append(self.source)
lines.append(
' ' * self.token.start
+ HIGHLIGHTER * (self.token.end - self.token.start)
)
return '\n'.join(lines)
class TokenKind(Enum):
TEXT = auto()
NUMBER = auto()
LEFT_BRACKET = auto()
RIGHT_BRACKET = auto()
def to_name(self) -> str:
for key, value in OPERATORS.items():
if value is self:
return repr(key)
else:
return 'a ' + self.name.lower()
OPERATORS = {
OPEN_BRACKET: TokenKind.LEFT_BRACKET,
CLOSE_BRACKET: TokenKind.RIGHT_BRACKET,
}
SPECIAL_CHARS = OPERATORS.keys() | {BACKSLASH}
LITERAL_TOKENS = [
TokenKind.TEXT,
TokenKind.NUMBER,
]
class Token(NamedTuple):
kind: TokenKind
value: Union[str, int]
start: int
end: int
def assert_cant_happen() -> NoReturn:
raise ValueError('Unexpected value')
def check_escaped_int(value: str) -> str:
if not value.startswith(BACKSLASH):
raise ValueError('Not an escaped int')
try:
int(value[1:])
except ValueError as exc:
raise ValueError('Not an escaped int') from exc
else:
return value[1:]
def tokenize(source: str) -> Iterator[Token]:
cursor = 0
backslashes = 0
buffer = []
def send_buffer() -> Iterator[Token]:
nonlocal backslashes
if not buffer:
return None
value = ''.join(buffer)
kind = TokenKind.TEXT
if not backslashes:
for variation, kind in [
(int, TokenKind.NUMBER),
(check_escaped_int, TokenKind.TEXT),
]:
try:
value = variation(value)
except ValueError:
continue
else:
break
yield Token(
kind, value, start=cursor - (len(buffer) + backslashes), end=cursor
)
buffer.clear()
backslashes = 0
def can_advance() -> bool:
return cursor < len(source)
while can_advance():
index = source[cursor]
if index in OPERATORS:
yield from send_buffer()
yield Token(OPERATORS[index], index, cursor, cursor + 1)
elif index == BACKSLASH and can_advance():
if source[cursor + 1] in SPECIAL_CHARS:
backslashes += 1
else:
buffer.append(index)
buffer.append(source[cursor + 1])
cursor += 1
else:
buffer.append(index)
cursor += 1
yield from send_buffer()
class PathAction(Enum):
KEY = auto()
INDEX = auto()
APPEND = auto()
# Pseudo action, used by the interpreter
SET = auto()
def to_string(self) -> str:
return self.name.lower()
class Path:
def __init__(
self,
kind: PathAction,
accessor: Optional[Union[str, int]] = None,
tokens: Optional[List[Token]] = None,
is_root: bool = False,
):
self.kind = kind
self.accessor = accessor
self.tokens = tokens or []
self.is_root = is_root
def reconstruct(self) -> str:
if self.kind is PathAction.KEY:
if self.is_root:
return str(self.accessor)
return OPEN_BRACKET + self.accessor + CLOSE_BRACKET
elif self.kind is PathAction.INDEX:
return OPEN_BRACKET + str(self.accessor) + CLOSE_BRACKET
elif self.kind is PathAction.APPEND:
return OPEN_BRACKET + CLOSE_BRACKET
else:
assert_cant_happen()
def parse(source: str) -> Iterator[Path]:
"""
start: root_path path*
root_path: (literal | index_path | append_path)
literal: TEXT | NUMBER
path:
key_path
| index_path
| append_path
key_path: LEFT_BRACKET TEXT RIGHT_BRACKET
index_path: LEFT_BRACKET NUMBER RIGHT_BRACKET
append_path: LEFT_BRACKET RIGHT_BRACKET
"""
tokens = list(tokenize(source))
cursor = 0
def can_advance():
return cursor < len(tokens)
def expect(*kinds):
nonlocal cursor
assert len(kinds) > 0
if can_advance():
token = tokens[cursor]
cursor += 1
if token.kind in kinds:
return token
elif tokens:
token = tokens[-1]._replace(
start=tokens[-1].end + 0, end=tokens[-1].end + 1
)
else:
token = None
if len(kinds) == 1:
suffix = kinds[0].to_name()
else:
suffix = ', '.join(kind.to_name() for kind in kinds[:-1])
suffix += ' or ' + kinds[-1].to_name()
message = f'Expecting {suffix}'
raise HTTPieSyntaxError(source, token, message)
def parse_root():
tokens = []
if not can_advance():
return Path(
PathAction.KEY,
EMPTY_STRING,
is_root=True
)
# (literal | index_path | append_path)?
token = expect(*LITERAL_TOKENS, TokenKind.LEFT_BRACKET)
tokens.append(token)
if token.kind in LITERAL_TOKENS:
action = PathAction.KEY
value = str(token.value)
elif token.kind is TokenKind.LEFT_BRACKET:
token = expect(TokenKind.NUMBER, TokenKind.RIGHT_BRACKET)
tokens.append(token)
if token.kind is TokenKind.NUMBER:
action = PathAction.INDEX
value = token.value
tokens.append(expect(TokenKind.RIGHT_BRACKET))
elif token.kind is TokenKind.RIGHT_BRACKET:
action = PathAction.APPEND
value = None
else:
assert_cant_happen()
else:
assert_cant_happen()
return Path(
action,
value,
tokens=tokens,
is_root=True
)
yield parse_root()
# path*
while can_advance():
path_tokens = []
path_tokens.append(expect(TokenKind.LEFT_BRACKET))
token = expect(
TokenKind.TEXT, TokenKind.NUMBER, TokenKind.RIGHT_BRACKET
)
path_tokens.append(token)
if token.kind is TokenKind.RIGHT_BRACKET:
path = Path(PathAction.APPEND, tokens=path_tokens)
elif token.kind is TokenKind.TEXT:
path = Path(PathAction.KEY, token.value, tokens=path_tokens)
path_tokens.append(expect(TokenKind.RIGHT_BRACKET))
elif token.kind is TokenKind.NUMBER:
path = Path(PathAction.INDEX, token.value, tokens=path_tokens)
path_tokens.append(expect(TokenKind.RIGHT_BRACKET))
else:
assert_cant_happen()
yield path
JSON_TYPE_MAPPING = {
dict: 'object',
list: 'array',
int: 'number',
float: 'number',
str: 'string',
}
def interpret(context: Any, key: str, value: Any) -> Any:
cursor = context
paths = list(parse(key))
paths.append(Path(PathAction.SET, value))
def type_check(index: int, path: Path, expected_type: Type[Any]) -> None:
if not isinstance(cursor, expected_type):
if path.tokens:
pseudo_token = Token(
None, None, path.tokens[0].start, path.tokens[-1].end
)
else:
pseudo_token = None
cursor_type = JSON_TYPE_MAPPING.get(
type(cursor), type(cursor).__name__
)
required_type = JSON_TYPE_MAPPING[expected_type]
message = f"Can't perform {path.kind.to_string()!r} based access on "
message += repr(
''.join(path.reconstruct() for path in paths[:index])
)
message += (
f' which has a type of {cursor_type!r} but this operation'
)
message += f' requires a type of {required_type!r}.'
raise HTTPieSyntaxError(
key, pseudo_token, message, message_kind='Type'
)
def object_for(kind: str) -> Any:
if kind is PathAction.KEY:
return {}
elif kind in {PathAction.INDEX, PathAction.APPEND}:
return []
else:
assert_cant_happen()
for index, (path, next_path) in enumerate(zip(paths, paths[1:])):
# If there is no context yet, set it.
if cursor is None:
context = cursor = object_for(path.kind)
if path.kind is PathAction.KEY:
type_check(index, path, dict)
if next_path.kind is PathAction.SET:
cursor[path.accessor] = next_path.accessor
break
cursor = cursor.setdefault(
path.accessor, object_for(next_path.kind)
)
elif path.kind is PathAction.INDEX:
type_check(index, path, list)
if path.accessor < 0:
raise HTTPieSyntaxError(
key,
path.tokens[1],
'Negative indexes are not supported.',
message_kind='Value',
)
cursor.extend([None] * (path.accessor - len(cursor) + 1))
if next_path.kind is PathAction.SET:
cursor[path.accessor] = next_path.accessor
break
if cursor[path.accessor] is None:
cursor[path.accessor] = object_for(next_path.kind)
cursor = cursor[path.accessor]
elif path.kind is PathAction.APPEND:
type_check(index, path, list)
if next_path.kind is PathAction.SET:
cursor.append(next_path.accessor)
break
cursor.append(object_for(next_path.kind))
cursor = cursor[-1]
else:
assert_cant_happen()
return context
def wrap_with_dict(context):
if context is None:
return {}
elif isinstance(context, list):
return {EMPTY_STRING: NestedJSONArray(context)}
else:
assert isinstance(context, dict)
return context
def interpret_nested_json(pairs):
context = None
for key, value in pairs:
context = interpret(context, key, value)
return wrap_with_dict(context)

View File

@ -0,0 +1,20 @@
"""
A library for parsing the HTTPie nested JSON key syntax and constructing the resulting objects.
<https://httpie.io/docs/cli/nested-json>
It has no dependencies.
"""
from .interpret import interpret_nested_json, unwrap_top_level_list_if_needed
from .errors import NestedJSONSyntaxError
from .tokens import EMPTY_STRING, NestedJSONArray
__all__ = [
'interpret_nested_json',
'unwrap_top_level_list_if_needed',
'EMPTY_STRING',
'NestedJSONArray',
'NestedJSONSyntaxError'
]

View File

@ -0,0 +1,27 @@
from typing import Optional
from .tokens import Token, HIGHLIGHTER
class NestedJSONSyntaxError(ValueError):
def __init__(
self,
source: str,
token: Optional[Token],
message: str,
message_kind: str = 'Syntax',
) -> None:
self.source = source
self.token = token
self.message = message
self.message_kind = message_kind
def __str__(self):
lines = [f'HTTPie {self.message_kind} Error: {self.message}']
if self.token is not None:
lines.append(self.source)
lines.append(
' ' * self.token.start
+ HIGHLIGHTER * (self.token.end - self.token.start)
)
return '\n'.join(lines)

View File

@ -0,0 +1,129 @@
from typing import Type, Union, Any, Iterable, Tuple
from .parse import parse, assert_cant_happen
from .errors import NestedJSONSyntaxError
from .tokens import EMPTY_STRING, TokenKind, Token, PathAction, Path, NestedJSONArray
__all__ = [
'interpret_nested_json',
'unwrap_top_level_list_if_needed',
]
JSONType = Type[Union[dict, list, int, float, str]]
JSON_TYPE_MAPPING = {
dict: 'object',
list: 'array',
int: 'number',
float: 'number',
str: 'string',
}
def interpret_nested_json(pairs: Iterable[Tuple[str, str]]) -> dict:
context = None
for key, value in pairs:
context = interpret(context, key, value)
return wrap_with_dict(context)
def interpret(context: Any, key: str, value: Any) -> Any:
cursor = context
paths = list(parse(key))
paths.append(Path(PathAction.SET, value))
# noinspection PyShadowingNames
def type_check(index: int, path: Path, expected_type: JSONType):
if not isinstance(cursor, expected_type):
if path.tokens:
pseudo_token = Token(
kind=TokenKind.PSEUDO,
value='',
start=path.tokens[0].start,
end=path.tokens[-1].end,
)
else:
pseudo_token = None
cursor_type = JSON_TYPE_MAPPING.get(type(cursor), type(cursor).__name__)
required_type = JSON_TYPE_MAPPING[expected_type]
message = f'Cannot perform {path.kind.to_string()!r} based access on '
message += repr(''.join(path.reconstruct() for path in paths[:index]))
message += f' which has a type of {cursor_type!r} but this operation'
message += f' requires a type of {required_type!r}.'
raise NestedJSONSyntaxError(
source=key,
token=pseudo_token,
message=message,
message_kind='Type',
)
def object_for(kind: PathAction) -> Any:
if kind is PathAction.KEY:
return {}
elif kind in {PathAction.INDEX, PathAction.APPEND}:
return []
else:
assert_cant_happen()
for index, (path, next_path) in enumerate(zip(paths, paths[1:])):
# If there is no context yet, set it.
if cursor is None:
context = cursor = object_for(path.kind)
if path.kind is PathAction.KEY:
type_check(index, path, dict)
if next_path.kind is PathAction.SET:
cursor[path.accessor] = next_path.accessor
break
cursor = cursor.setdefault(path.accessor, object_for(next_path.kind))
elif path.kind is PathAction.INDEX:
type_check(index, path, list)
if path.accessor < 0:
raise NestedJSONSyntaxError(
source=key,
token=path.tokens[1],
message='Negative indexes are not supported.',
message_kind='Value',
)
cursor.extend([None] * (path.accessor - len(cursor) + 1))
if next_path.kind is PathAction.SET:
cursor[path.accessor] = next_path.accessor
break
if cursor[path.accessor] is None:
cursor[path.accessor] = object_for(next_path.kind)
cursor = cursor[path.accessor]
elif path.kind is PathAction.APPEND:
type_check(index, path, list)
if next_path.kind is PathAction.SET:
cursor.append(next_path.accessor)
break
cursor.append(object_for(next_path.kind))
cursor = cursor[-1]
else:
assert_cant_happen()
return context
def wrap_with_dict(context):
if context is None:
return {}
elif isinstance(context, list):
return {
EMPTY_STRING: NestedJSONArray(context),
}
else:
assert isinstance(context, dict)
return context
def unwrap_top_level_list_if_needed(data: dict):
"""
Propagate the top-level list, if thats what we got.
"""
if len(data) == 1:
key, value = list(data.items())[0]
if isinstance(value, NestedJSONArray):
assert key == EMPTY_STRING
return value
return data

View File

@ -0,0 +1,193 @@
from typing import Iterator
from .errors import NestedJSONSyntaxError
from .tokens import (
EMPTY_STRING,
BACKSLASH,
TokenKind,
OPERATORS,
SPECIAL_CHARS,
LITERAL_TOKENS,
Token,
PathAction,
Path,
)
__all__ = [
'parse',
'assert_cant_happen',
]
def parse(source: str) -> Iterator[Path]:
"""
start: root_path path*
root_path: (literal | index_path | append_path)
literal: TEXT | NUMBER
path:
key_path
| index_path
| append_path
key_path: LEFT_BRACKET TEXT RIGHT_BRACKET
index_path: LEFT_BRACKET NUMBER RIGHT_BRACKET
append_path: LEFT_BRACKET RIGHT_BRACKET
"""
tokens = list(tokenize(source))
cursor = 0
def can_advance():
return cursor < len(tokens)
# noinspection PyShadowingNames
def expect(*kinds):
nonlocal cursor
assert kinds
if can_advance():
token = tokens[cursor]
cursor += 1
if token.kind in kinds:
return token
elif tokens:
token = tokens[-1]._replace(
start=tokens[-1].end + 0,
end=tokens[-1].end + 1,
)
else:
token = None
if len(kinds) == 1:
suffix = kinds[0].to_name()
else:
suffix = ', '.join(kind.to_name() for kind in kinds[:-1])
suffix += ' or ' + kinds[-1].to_name()
message = f'Expecting {suffix}'
raise NestedJSONSyntaxError(source, token, message)
# noinspection PyShadowingNames
def parse_root():
tokens = []
if not can_advance():
return Path(
kind=PathAction.KEY,
accessor=EMPTY_STRING,
is_root=True
)
# (literal | index_path | append_path)?
token = expect(*LITERAL_TOKENS, TokenKind.LEFT_BRACKET)
tokens.append(token)
if token.kind in LITERAL_TOKENS:
action = PathAction.KEY
value = str(token.value)
elif token.kind is TokenKind.LEFT_BRACKET:
token = expect(TokenKind.NUMBER, TokenKind.RIGHT_BRACKET)
tokens.append(token)
if token.kind is TokenKind.NUMBER:
action = PathAction.INDEX
value = token.value
tokens.append(expect(TokenKind.RIGHT_BRACKET))
elif token.kind is TokenKind.RIGHT_BRACKET:
action = PathAction.APPEND
value = None
else:
assert_cant_happen()
else:
assert_cant_happen()
# noinspection PyUnboundLocalVariable
return Path(
kind=action,
accessor=value,
tokens=tokens,
is_root=True
)
yield parse_root()
# path*
while can_advance():
path_tokens = [expect(TokenKind.LEFT_BRACKET)]
token = expect(TokenKind.TEXT, TokenKind.NUMBER, TokenKind.RIGHT_BRACKET)
path_tokens.append(token)
if token.kind is TokenKind.RIGHT_BRACKET:
path = Path(PathAction.APPEND, tokens=path_tokens)
elif token.kind is TokenKind.TEXT:
path = Path(PathAction.KEY, token.value, tokens=path_tokens)
path_tokens.append(expect(TokenKind.RIGHT_BRACKET))
elif token.kind is TokenKind.NUMBER:
path = Path(PathAction.INDEX, token.value, tokens=path_tokens)
path_tokens.append(expect(TokenKind.RIGHT_BRACKET))
else:
assert_cant_happen()
# noinspection PyUnboundLocalVariable
yield path
def tokenize(source: str) -> Iterator[Token]:
cursor = 0
backslashes = 0
buffer = []
def send_buffer() -> Iterator[Token]:
nonlocal backslashes
if not buffer:
return None
value = ''.join(buffer)
kind = TokenKind.TEXT
if not backslashes:
for variation, kind in [
(int, TokenKind.NUMBER),
(check_escaped_int, TokenKind.TEXT),
]:
try:
value = variation(value)
except ValueError:
continue
else:
break
yield Token(
kind=kind,
value=value,
start=cursor - (len(buffer) + backslashes),
end=cursor,
)
buffer.clear()
backslashes = 0
def can_advance() -> bool:
return cursor < len(source)
while can_advance():
index = source[cursor]
if index in OPERATORS:
yield from send_buffer()
yield Token(OPERATORS[index], index, cursor, cursor + 1)
elif index == BACKSLASH and can_advance():
if source[cursor + 1] in SPECIAL_CHARS:
backslashes += 1
else:
buffer.append(index)
buffer.append(source[cursor + 1])
cursor += 1
else:
buffer.append(index)
cursor += 1
yield from send_buffer()
def check_escaped_int(value: str) -> str:
if not value.startswith(BACKSLASH):
raise ValueError('Not an escaped int')
try:
int(value[1:])
except ValueError as exc:
raise ValueError('Not an escaped int') from exc
else:
return value[1:]
def assert_cant_happen():
raise ValueError('Unexpected value')

View File

@ -0,0 +1,80 @@
from enum import Enum, auto
from typing import NamedTuple, Union, Optional, List
EMPTY_STRING = ''
HIGHLIGHTER = '^'
OPEN_BRACKET = '['
CLOSE_BRACKET = ']'
BACKSLASH = '\\'
class TokenKind(Enum):
TEXT = auto()
NUMBER = auto()
LEFT_BRACKET = auto()
RIGHT_BRACKET = auto()
PSEUDO = auto() # Not a real token, use when representing location only.
def to_name(self) -> str:
for key, value in OPERATORS.items():
if value is self:
return repr(key)
else:
return 'a ' + self.name.lower()
OPERATORS = {
OPEN_BRACKET: TokenKind.LEFT_BRACKET,
CLOSE_BRACKET: TokenKind.RIGHT_BRACKET,
}
SPECIAL_CHARS = OPERATORS.keys() | {BACKSLASH}
LITERAL_TOKENS = [
TokenKind.TEXT,
TokenKind.NUMBER,
]
class Token(NamedTuple):
kind: TokenKind
value: Union[str, int]
start: int
end: int
class PathAction(Enum):
KEY = auto()
INDEX = auto()
APPEND = auto()
# Pseudo action, used by the interpreter
SET = auto()
def to_string(self) -> str:
return self.name.lower()
class Path:
def __init__(
self,
kind: PathAction,
accessor: Optional[Union[str, int]] = None,
tokens: Optional[List[Token]] = None,
is_root: bool = False,
):
self.kind = kind
self.accessor = accessor
self.tokens = tokens or []
self.is_root = is_root
def reconstruct(self) -> str:
if self.kind is PathAction.KEY:
if self.is_root:
return str(self.accessor)
return OPEN_BRACKET + self.accessor + CLOSE_BRACKET
elif self.kind is PathAction.INDEX:
return OPEN_BRACKET + str(self.accessor) + CLOSE_BRACKET
elif self.kind is PathAction.APPEND:
return OPEN_BRACKET + CLOSE_BRACKET
class NestedJSONArray(list):
"""Denotes a top-level JSON array."""

View File

@ -18,7 +18,7 @@ from .dicts import (
)
from .exceptions import ParseError
from .nested_json import interpret_nested_json
from ..utils import get_content_type, load_json_preserve_order_and_dupe_keys, split
from ..utils import get_content_type, load_json_preserve_order_and_dupe_keys, split_iterable
class RequestItems:
@ -78,25 +78,28 @@ class RequestItems:
instance.data,
),
SEPARATOR_DATA_RAW_JSON: (
json_only(instance, process_data_raw_json_embed_arg),
convert_json_value_to_form_if_needed(
in_json_mode=instance.is_json,
processor=process_data_raw_json_embed_arg
),
instance.data,
),
SEPARATOR_DATA_EMBED_RAW_JSON_FILE: (
json_only(instance, process_data_embed_raw_json_file_arg),
convert_json_value_to_form_if_needed(
in_json_mode=instance.is_json,
processor=process_data_embed_raw_json_file_arg,
),
instance.data,
),
}
if instance.is_json:
json_item_args, request_item_args = split(
request_item_args,
lambda arg: arg.sep in SEPARATOR_GROUP_NESTED_JSON_ITEMS
json_item_args, request_item_args = split_iterable(
iterable=request_item_args,
key=lambda arg: arg.sep in SEPARATOR_GROUP_NESTED_JSON_ITEMS
)
if json_item_args:
pairs = [
(arg.key, rules[arg.sep][0](arg))
for arg in json_item_args
]
pairs = [(arg.key, rules[arg.sep][0](arg)) for arg in json_item_args]
processor_func, target_dict = rules[SEPARATOR_GROUP_NESTED_JSON_ITEMS]
value = processor_func(pairs)
target_dict.update(value)
@ -159,6 +162,30 @@ def process_file_upload_arg(arg: KeyValueArg) -> Tuple[str, IO, str]:
)
def convert_json_value_to_form_if_needed(in_json_mode: bool, processor: Callable[[KeyValueArg], JSONType]) -> Callable[[], str]:
"""
We allow primitive values to be passed to forms via JSON key/value syntax.
But complex values lead to an error because theres no clear way to serialize them.
"""
if in_json_mode:
return processor
@functools.wraps(processor)
def wrapper(*args, **kwargs) -> str:
try:
output = processor(*args, **kwargs)
except ParseError:
output = None
if isinstance(output, (str, int, float)):
return str(output)
else:
raise ParseError('Cannot use complex JSON value types with --form/--multipart.')
return wrapper
def process_data_item_arg(arg: KeyValueArg) -> str:
return arg.value
@ -167,29 +194,6 @@ def process_data_embed_file_contents_arg(arg: KeyValueArg) -> str:
return load_text_file(arg)
def json_only(items: RequestItems, func: Callable[[KeyValueArg], JSONType]) -> str:
if items.is_json:
return func
@functools.wraps(func)
def wrapper(*args, **kwargs) -> str:
try:
ret = func(*args, **kwargs)
except ParseError:
ret = None
# If it is a basic type, then allow it
if isinstance(ret, (str, int, float)):
return str(ret)
else:
raise ParseError(
'Can\'t use complex JSON value types with '
'--form/--multipart.'
)
return wrapper
def process_data_embed_raw_json_file_arg(arg: KeyValueArg) -> JSONType:
contents = load_text_file(arg)
value = load_json(arg, contents)

View File

@ -10,12 +10,13 @@ from urllib.parse import urlparse, urlunparse
import requests
# noinspection PyPackageRequirements
import urllib3
from . import __version__
from .adapters import HTTPieHTTPAdapter
from .context import Environment
from .cli.constants import HTTP_OPTIONS
from .cli.nested_json import EMPTY_STRING
from .cli.dicts import HTTPHeadersDict, NestedJSONArray
from .cli.dicts import HTTPHeadersDict
from .cli.nested_json import unwrap_top_level_list_if_needed
from .context import Environment
from .encoding import UTF8
from .models import RequestsMessage
from .plugins.registry import plugin_manager
@ -306,21 +307,13 @@ def make_send_kwargs_mergeable_from_env(args: argparse.Namespace) -> dict:
def json_dict_to_request_body(data: Dict[str, Any]) -> str:
# Propagate the top-level list if there is only one
# item in the object, with an en empty key.
if len(data) == 1:
[(key, value)] = data.items()
if isinstance(value, NestedJSONArray):
assert key == EMPTY_STRING
data = value
data = unwrap_top_level_list_if_needed(data)
if data:
data = json.dumps(data)
else:
# We need to set data to an empty string to prevent requests
# from assigning an empty list to `response.request.data`.
data = ''
return data

View File

@ -11,7 +11,7 @@ from requests import __version__ as requests_version
from . import __version__ as httpie_version
from .cli.constants import OUT_REQ_BODY
from .cli.nested_json import HTTPieSyntaxError
from .cli.nested_json import NestedJSONSyntaxError
from .client import collect_messages
from .context import Environment, LogLevel
from .downloads import Downloader
@ -78,7 +78,7 @@ def raw_main(
args=args,
env=env,
)
except HTTPieSyntaxError as exc:
except NestedJSONSyntaxError as exc:
env.stderr.write(str(exc) + "\n")
if include_traceback:
raise

View File

@ -52,7 +52,6 @@ def program():
try:
exit_status = main()
except KeyboardInterrupt:
from httpie.status import ExitStatus
exit_status = ExitStatus.ERROR_CTRL_C
return exit_status

View File

@ -43,7 +43,6 @@ def _discover_system_pip() -> List[str]:
def _run_pip_subprocess(pip_executable: List[str], args: List[str]) -> bytes:
import subprocess
cmd = [*pip_executable, *args]
try:

View File

@ -16,7 +16,6 @@ from .cli.constants import (
from .compat import cached_property
from .utils import split_cookies, parse_content_type_header
ELAPSED_TIME_LABEL = 'Elapsed time'
@ -67,27 +66,10 @@ class HTTPResponse(HTTPMessage):
def iter_lines(self, chunk_size):
return ((line, b'\n') for line in self._orig.iter_lines(chunk_size))
# noinspection PyProtectedMember
@property
def headers(self):
try:
raw = self._orig.raw
if getattr(raw, '_original_response', None):
raw_version = raw._original_response.version
else:
raw_version = raw.version
except AttributeError:
# Assume HTTP/1.1
raw_version = 11
version = {
9: '0.9',
10: '1.0',
11: '1.1',
20: '2.0',
}[raw_version]
original = self._orig
status_line = f'HTTP/{version} {original.status_code} {original.reason}'
status_line = f'HTTP/{self.version} {original.status_code} {original.reason}'
headers = [status_line]
headers.extend(
': '.join(header)
@ -117,6 +99,32 @@ class HTTPResponse(HTTPMessage):
for key, value in data.items()
)
@property
def version(self) -> str:
"""
Return the HTTP version used by the server, e.g. '1.1'.
Assume HTTP/1.1 if version is not available.
"""
mapping = {
9: '0.9',
10: '1.0',
11: '1.1',
20: '2.0',
}
fallback = 11
version = None
try:
raw = self._orig.raw
if getattr(raw, '_original_response', None):
version = raw._original_response.version
else:
version = raw.version
except AttributeError:
pass
return mapping[version or fallback]
class HTTPRequest(HTTPMessage):
"""A :class:`requests.models.Request` wrapper."""

View File

@ -24,7 +24,7 @@ def enable_highlighter(
console: Console,
highlighter: Highlighter,
) -> Iterator[Console]:
"""Enable a higlighter temporarily."""
"""Enable a highlighter temporarily."""
original_highlighter = console.highlighter
try:

View File

@ -245,7 +245,7 @@ def get_site_paths(path: Path) -> Iterable[Path]:
yield as_site(path)
def split(iterable: Iterable[T], key: Callable[[T], bool]) -> Tuple[List[T], List[T]]:
def split_iterable(iterable: Iterable[T], key: Callable[[T], bool]) -> Tuple[List[T], List[T]]:
left, right = [], []
for item in iterable:
if key(item):

View File

@ -5,7 +5,7 @@ import responses
from httpie.cli.constants import PRETTY_MAP
from httpie.cli.exceptions import ParseError
from httpie.cli.nested_json import HTTPieSyntaxError
from httpie.cli.nested_json import NestedJSONSyntaxError
from httpie.output.formatters.colors import ColorFormatter
from httpie.utils import JsonDictPreservingDuplicateKeys
@ -157,7 +157,7 @@ def test_complex_json_arguments_with_non_json(httpbin, request_type, value):
f'option:={json.dumps(value)}',
)
cm.match("Can't use complex JSON value types")
cm.match('Cannot use complex JSON value types')
@pytest.mark.parametrize(
@ -508,23 +508,23 @@ def test_nested_json_syntax(input_json, expected_json, httpbin):
),
(
['foo=1', 'foo[key]:=2'],
"HTTPie Type Error: Can't perform 'key' based access on 'foo' which has a type of 'string' but this operation requires a type of 'object'.\nfoo[key]\n ^^^^^",
"HTTPie Type Error: Cannot perform 'key' based access on 'foo' which has a type of 'string' but this operation requires a type of 'object'.\nfoo[key]\n ^^^^^",
),
(
['foo=1', 'foo[0]:=2'],
"HTTPie Type Error: Can't perform 'index' based access on 'foo' which has a type of 'string' but this operation requires a type of 'array'.\nfoo[0]\n ^^^",
"HTTPie Type Error: Cannot perform 'index' based access on 'foo' which has a type of 'string' but this operation requires a type of 'array'.\nfoo[0]\n ^^^",
),
(
['foo=1', 'foo[]:=2'],
"HTTPie Type Error: Can't perform 'append' based access on 'foo' which has a type of 'string' but this operation requires a type of 'array'.\nfoo[]\n ^^",
"HTTPie Type Error: Cannot perform 'append' based access on 'foo' which has a type of 'string' but this operation requires a type of 'array'.\nfoo[]\n ^^",
),
(
['data[key]=value', 'data[key 2]=value 2', 'data[0]=value'],
"HTTPie Type Error: Can't perform 'index' based access on 'data' which has a type of 'object' but this operation requires a type of 'array'.\ndata[0]\n ^^^",
"HTTPie Type Error: Cannot perform 'index' based access on 'data' which has a type of 'object' but this operation requires a type of 'array'.\ndata[0]\n ^^^",
),
(
['data[key]=value', 'data[key 2]=value 2', 'data[]=value'],
"HTTPie Type Error: Can't perform 'append' based access on 'data' which has a type of 'object' but this operation requires a type of 'array'.\ndata[]\n ^^",
"HTTPie Type Error: Cannot perform 'append' based access on 'data' which has a type of 'object' but this operation requires a type of 'array'.\ndata[]\n ^^",
),
(
[
@ -532,7 +532,7 @@ def test_nested_json_syntax(input_json, expected_json, httpbin):
'foo[bar][baz][5][]:=4',
'foo[bar][baz][key][]:=5',
],
"HTTPie Type Error: Can't perform 'key' based access on 'foo[bar][baz]' which has a type of 'array' but this operation requires a type of 'object'.\nfoo[bar][baz][key][]\n ^^^^^",
"HTTPie Type Error: Cannot perform 'key' based access on 'foo[bar][baz]' which has a type of 'array' but this operation requires a type of 'object'.\nfoo[bar][baz][key][]\n ^^^^^",
),
(
['foo[-10]:=[1,2]'],
@ -540,32 +540,32 @@ def test_nested_json_syntax(input_json, expected_json, httpbin):
),
(
['foo[0]:=1', 'foo[]:=2', 'foo[\\2]:=3'],
"HTTPie Type Error: Can't perform 'key' based access on 'foo' which has a type of 'array' but this operation requires a type of 'object'.\nfoo[\\2]\n ^^^^",
"HTTPie Type Error: Cannot perform 'key' based access on 'foo' which has a type of 'array' but this operation requires a type of 'object'.\nfoo[\\2]\n ^^^^",
),
(
['foo[\\1]:=2', 'foo[5]:=3'],
"HTTPie Type Error: Can't perform 'index' based access on 'foo' which has a type of 'object' but this operation requires a type of 'array'.\nfoo[5]\n ^^^",
"HTTPie Type Error: Cannot perform 'index' based access on 'foo' which has a type of 'object' but this operation requires a type of 'array'.\nfoo[5]\n ^^^",
),
(
['x=y', '[]:=2'],
"HTTPie Type Error: Can't perform 'append' based access on '' which has a type of 'object' but this operation requires a type of 'array'.",
"HTTPie Type Error: Cannot perform 'append' based access on '' which has a type of 'object' but this operation requires a type of 'array'.",
),
(
['[]:=2', 'x=y'],
"HTTPie Type Error: Can't perform 'key' based access on '' which has a type of 'array' but this operation requires a type of 'object'.",
"HTTPie Type Error: Cannot perform 'key' based access on '' which has a type of 'array' but this operation requires a type of 'object'.",
),
(
[':=[1,2,3]', '[]:=4'],
"HTTPie Type Error: Can't perform 'append' based access on '' which has a type of 'object' but this operation requires a type of 'array'.",
"HTTPie Type Error: Cannot perform 'append' based access on '' which has a type of 'object' but this operation requires a type of 'array'.",
),
(
['[]:=4', ':=[1,2,3]'],
"HTTPie Type Error: Can't perform 'key' based access on '' which has a type of 'array' but this operation requires a type of 'object'.",
"HTTPie Type Error: Cannot perform 'key' based access on '' which has a type of 'array' but this operation requires a type of 'object'.",
),
],
)
def test_nested_json_errors(input_json, expected_error, httpbin):
with pytest.raises(HTTPieSyntaxError) as exc:
with pytest.raises(NestedJSONSyntaxError) as exc:
http(httpbin + '/post', *input_json)
exc_lines = str(exc.value).splitlines()