hexconv 0.2.7__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {hexconv-0.2.7/src/hexconv.egg-info → hexconv-0.3.0}/PKG-INFO +3 -2
- {hexconv-0.2.7 → hexconv-0.3.0}/README.md +1 -1
- {hexconv-0.2.7 → hexconv-0.3.0}/pyproject.toml +2 -1
- {hexconv-0.2.7 → hexconv-0.3.0}/src/hexconv/_core.py +58 -32
- {hexconv-0.2.7 → hexconv-0.3.0/src/hexconv.egg-info}/PKG-INFO +3 -2
- {hexconv-0.2.7 → hexconv-0.3.0}/src/hexconv.egg-info/SOURCES.txt +2 -1
- {hexconv-0.2.7 → hexconv-0.3.0}/src/hexconv.egg-info/requires.txt +1 -0
- {hexconv-0.2.7 → hexconv-0.3.0}/tests/test_hexconv.py +153 -56
- hexconv-0.3.0/tests/test_properties.py +99 -0
- {hexconv-0.2.7 → hexconv-0.3.0}/LICENSE +0 -0
- {hexconv-0.2.7 → hexconv-0.3.0}/MANIFEST.in +0 -0
- {hexconv-0.2.7 → hexconv-0.3.0}/setup.cfg +0 -0
- {hexconv-0.2.7 → hexconv-0.3.0}/src/hexconv/__init__.py +0 -0
- {hexconv-0.2.7 → hexconv-0.3.0}/src/hexconv/py.typed +0 -0
- {hexconv-0.2.7 → hexconv-0.3.0}/src/hexconv.egg-info/dependency_links.txt +0 -0
- {hexconv-0.2.7 → hexconv-0.3.0}/src/hexconv.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hexconv
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Pythonic conversion toolkit for bytes, hex, integers, arrays, binary strings, base64, and text.
|
|
5
5
|
Author: hexconv contributors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -23,6 +23,7 @@ Description-Content-Type: text/markdown
|
|
|
23
23
|
License-File: LICENSE
|
|
24
24
|
Provides-Extra: dev
|
|
25
25
|
Requires-Dist: build; extra == "dev"
|
|
26
|
+
Requires-Dist: hypothesis; extra == "dev"
|
|
26
27
|
Requires-Dist: pytest; extra == "dev"
|
|
27
28
|
Requires-Dist: twine; extra == "dev"
|
|
28
29
|
Dynamic: license-file
|
|
@@ -363,7 +364,7 @@ hx.from_hex("dead beef").bytes
|
|
|
363
364
|
hx.from_text("data").hex
|
|
364
365
|
# '64617461'
|
|
365
366
|
|
|
366
|
-
hx.convert("data",
|
|
367
|
+
hx.convert("data", hx.Text, hx.HexString)
|
|
367
368
|
# '64617461'
|
|
368
369
|
```
|
|
369
370
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "hexconv"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "Pythonic conversion toolkit for bytes, hex, integers, arrays, binary strings, base64, and text."
|
|
9
9
|
readme = { file = "README.md", content-type = "text/markdown" }
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -31,6 +31,7 @@ classifiers = [
|
|
|
31
31
|
[project.optional-dependencies]
|
|
32
32
|
dev = [
|
|
33
33
|
"build",
|
|
34
|
+
"hypothesis",
|
|
34
35
|
"pytest",
|
|
35
36
|
"twine",
|
|
36
37
|
]
|
|
@@ -8,7 +8,7 @@ import re
|
|
|
8
8
|
import struct
|
|
9
9
|
from collections.abc import Iterable
|
|
10
10
|
from dataclasses import dataclass, field
|
|
11
|
-
from typing import Any, Callable, TypeAlias
|
|
11
|
+
from typing import Any, Callable, Literal, TypeAlias, overload
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class HexConvError(ValueError):
|
|
@@ -435,9 +435,9 @@ class BitArray(_Format):
|
|
|
435
435
|
|
|
436
436
|
Format: TypeAlias = type[_Format]
|
|
437
437
|
FormatLike: TypeAlias = type[_Format] | _Format
|
|
438
|
-
Endian: TypeAlias =
|
|
439
|
-
BitOrder: TypeAlias =
|
|
440
|
-
PadSide: TypeAlias =
|
|
438
|
+
Endian: TypeAlias = Literal["big", "little"]
|
|
439
|
+
BitOrder: TypeAlias = Literal["msb", "lsb"]
|
|
440
|
+
PadSide: TypeAlias = Literal["left", "right"]
|
|
441
441
|
|
|
442
442
|
|
|
443
443
|
Hex = HexString
|
|
@@ -484,6 +484,12 @@ class Value:
|
|
|
484
484
|
"""Emit this value using a public format marker such as HexString."""
|
|
485
485
|
|
|
486
486
|
format_type = _format_type(format_)
|
|
487
|
+
if "chunk_width" in self.meta and format_type not in _CHUNK_AWARE_OUTPUTS:
|
|
488
|
+
raise HexConvError(
|
|
489
|
+
f"chunking has no effect on {format_type.__name__} output; emit to a "
|
|
490
|
+
"chunk-aware format such as HexArray, HexNumbersArray, IntArray, or "
|
|
491
|
+
"DecimalIntArray, or drop the Chunk/Group step"
|
|
492
|
+
)
|
|
487
493
|
output_options = _format_options(format_, "output")
|
|
488
494
|
output_options.update(options)
|
|
489
495
|
try:
|
|
@@ -602,18 +608,6 @@ class Value:
|
|
|
602
608
|
_validate_endian(endian)
|
|
603
609
|
return int.from_bytes(self._data, endian, signed=signed)
|
|
604
610
|
|
|
605
|
-
@property
|
|
606
|
-
def int(self) -> int:
|
|
607
|
-
"""Shortcut for to_int()."""
|
|
608
|
-
|
|
609
|
-
return self.to_int()
|
|
610
|
-
|
|
611
|
-
@property
|
|
612
|
-
def integer(self) -> int:
|
|
613
|
-
"""Shortcut for to_int()."""
|
|
614
|
-
|
|
615
|
-
return self.to_int()
|
|
616
|
-
|
|
617
611
|
def to_int_array(
|
|
618
612
|
self,
|
|
619
613
|
*,
|
|
@@ -779,6 +773,21 @@ class Value:
|
|
|
779
773
|
raise HexConvError("chunk width cannot be None")
|
|
780
774
|
return chunk_width
|
|
781
775
|
|
|
776
|
+
# `int`/`integer` shadow the builtin, so they are defined after every method
|
|
777
|
+
# whose annotations reference the builtin `int` (integer before int keeps
|
|
778
|
+
# both of these return annotations resolving to the builtin too).
|
|
779
|
+
@property
|
|
780
|
+
def integer(self) -> int:
|
|
781
|
+
"""Shortcut for to_int()."""
|
|
782
|
+
|
|
783
|
+
return self.to_int()
|
|
784
|
+
|
|
785
|
+
@property
|
|
786
|
+
def int(self) -> int:
|
|
787
|
+
"""Shortcut for to_int()."""
|
|
788
|
+
|
|
789
|
+
return self.to_int()
|
|
790
|
+
|
|
782
791
|
|
|
783
792
|
@dataclass(frozen=True)
|
|
784
793
|
class AutoResult:
|
|
@@ -1002,31 +1011,22 @@ def convert(
|
|
|
1002
1011
|
from_format: FormatLike | None = None,
|
|
1003
1012
|
to_format: FormatLike | None = None,
|
|
1004
1013
|
*,
|
|
1005
|
-
from_: FormatLike | None = None,
|
|
1006
|
-
to: FormatLike | None = None,
|
|
1007
1014
|
input_options: dict[str, Any] | None = None,
|
|
1008
1015
|
output_options: dict[str, Any] | None = None,
|
|
1009
1016
|
) -> Any:
|
|
1010
1017
|
"""One-shot conversion between explicit formats.
|
|
1011
1018
|
|
|
1012
|
-
|
|
1019
|
+
The source and target formats are positional:
|
|
1020
|
+
|
|
1013
1021
|
convert(value, Hex(), Bytes())
|
|
1014
1022
|
|
|
1015
|
-
|
|
1016
|
-
|
|
1023
|
+
Omit ``from_format`` to infer the source with ``Auto``; omit ``to_format``
|
|
1024
|
+
to emit raw ``Bytes``.
|
|
1017
1025
|
"""
|
|
1018
1026
|
|
|
1019
|
-
if from_format is not None and from_ is not None:
|
|
1020
|
-
raise HexConvError("use either positional input format or from_, not both")
|
|
1021
|
-
if to_format is not None and to is not None:
|
|
1022
|
-
raise HexConvError("use either positional output format or to, not both")
|
|
1023
|
-
|
|
1024
|
-
input_format = from_format if from_format is not None else from_
|
|
1025
|
-
output_format = to_format if to_format is not None else to
|
|
1026
|
-
|
|
1027
1027
|
return Converter(
|
|
1028
|
-
Auto if
|
|
1029
|
-
Bytes if
|
|
1028
|
+
Auto if from_format is None else from_format,
|
|
1029
|
+
Bytes if to_format is None else to_format,
|
|
1030
1030
|
input_options=input_options,
|
|
1031
1031
|
output_options=output_options,
|
|
1032
1032
|
)(value)
|
|
@@ -1082,6 +1082,18 @@ def infer(value: Any) -> AutoResult:
|
|
|
1082
1082
|
raise HexConvError(f"cannot infer source format for {type(value).__name__}")
|
|
1083
1083
|
|
|
1084
1084
|
|
|
1085
|
+
@overload
|
|
1086
|
+
def from_auto(value: Any, *, explain: Literal[False] = ...) -> Value: ...
|
|
1087
|
+
|
|
1088
|
+
|
|
1089
|
+
@overload
|
|
1090
|
+
def from_auto(value: Any, *, explain: Literal[True]) -> AutoResult: ...
|
|
1091
|
+
|
|
1092
|
+
|
|
1093
|
+
@overload
|
|
1094
|
+
def from_auto(value: Any, *, explain: bool) -> Value | AutoResult: ...
|
|
1095
|
+
|
|
1096
|
+
|
|
1085
1097
|
def from_auto(value: Any, *, explain: bool = False) -> Value | AutoResult:
|
|
1086
1098
|
"""Parse a value using convenience heuristics.
|
|
1087
1099
|
|
|
@@ -1100,6 +1112,12 @@ def from_auto(value: Any, *, explain: bool = False) -> Value | AutoResult:
|
|
|
1100
1112
|
return result if explain else result.value
|
|
1101
1113
|
|
|
1102
1114
|
|
|
1115
|
+
def _from_auto_value(value: Any) -> Value:
|
|
1116
|
+
"""Auto-format parser that always returns a Value (never AutoResult)."""
|
|
1117
|
+
|
|
1118
|
+
return from_auto(value)
|
|
1119
|
+
|
|
1120
|
+
|
|
1103
1121
|
def from_bytes(value: bytes | bytearray | memoryview | Iterable[int]) -> Value:
|
|
1104
1122
|
"""Parse a bytes-like object or a byte-sized integer sequence."""
|
|
1105
1123
|
|
|
@@ -1549,7 +1567,7 @@ def _emit_bits(value: Value, **options: Any) -> list[int]:
|
|
|
1549
1567
|
|
|
1550
1568
|
|
|
1551
1569
|
_INPUTS: dict[Format, Callable[..., Value]] = {
|
|
1552
|
-
Auto:
|
|
1570
|
+
Auto: _from_auto_value,
|
|
1553
1571
|
Bytes: from_bytes,
|
|
1554
1572
|
BytesArray: from_bytes_array,
|
|
1555
1573
|
BytesString: from_bytes_string,
|
|
@@ -1604,6 +1622,14 @@ _OUTPUTS: dict[Format, Callable[..., Any]] = {
|
|
|
1604
1622
|
}
|
|
1605
1623
|
|
|
1606
1624
|
|
|
1625
|
+
# Output formats whose emitters honor a Chunk/Group width stored on Value.meta.
|
|
1626
|
+
# Every other target silently ignores chunking, so emitting to one after a chunk
|
|
1627
|
+
# is treated as a mistake (see Value.to).
|
|
1628
|
+
_CHUNK_AWARE_OUTPUTS: frozenset[Format] = frozenset(
|
|
1629
|
+
{HexArray, HexNumbersArray, IntArray, DecimalIntArray}
|
|
1630
|
+
)
|
|
1631
|
+
|
|
1632
|
+
|
|
1607
1633
|
def _is_format_like(value: Any) -> bool:
|
|
1608
1634
|
return isinstance(value, _Format) or (
|
|
1609
1635
|
isinstance(value, type) and issubclass(value, _Format)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hexconv
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Pythonic conversion toolkit for bytes, hex, integers, arrays, binary strings, base64, and text.
|
|
5
5
|
Author: hexconv contributors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -23,6 +23,7 @@ Description-Content-Type: text/markdown
|
|
|
23
23
|
License-File: LICENSE
|
|
24
24
|
Provides-Extra: dev
|
|
25
25
|
Requires-Dist: build; extra == "dev"
|
|
26
|
+
Requires-Dist: hypothesis; extra == "dev"
|
|
26
27
|
Requires-Dist: pytest; extra == "dev"
|
|
27
28
|
Requires-Dist: twine; extra == "dev"
|
|
28
29
|
Dynamic: license-file
|
|
@@ -363,7 +364,7 @@ hx.from_hex("dead beef").bytes
|
|
|
363
364
|
hx.from_text("data").hex
|
|
364
365
|
# '64617461'
|
|
365
366
|
|
|
366
|
-
hx.convert("data",
|
|
367
|
+
hx.convert("data", hx.Text, hx.HexString)
|
|
367
368
|
# '64617461'
|
|
368
369
|
```
|
|
369
370
|
|
|
@@ -44,62 +44,150 @@ def test_converter_class_matches_proposed_usage():
|
|
|
44
44
|
|
|
45
45
|
|
|
46
46
|
def test_convert_one_shot():
|
|
47
|
-
assert hx.convert("data",
|
|
47
|
+
assert hx.convert("data", hx.Text, hx.HexString) == "64617461"
|
|
48
48
|
assert hx.convert("de ad be ef", hx.Hex(), hx.Bytes()) == b"\xde\xad\xbe\xef"
|
|
49
49
|
assert hx.convert(b"\xde\xad\xbe\xef", hx.Bytes(), hx.Hex(sep=" ")) == "de ad be ef"
|
|
50
50
|
assert hx.convert("data", hx.Text(), hx.Hex(prefix=True)) == "0x64617461"
|
|
51
51
|
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
"
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
53
|
+
_QUICK_TOUR_CASES = [
|
|
54
|
+
(
|
|
55
|
+
"hex_separators",
|
|
56
|
+
lambda: hx.from_hex("de ad:be-ef").bytes,
|
|
57
|
+
b"\xde\xad\xbe\xef",
|
|
58
|
+
),
|
|
59
|
+
(
|
|
60
|
+
"hex_sep_uppercase",
|
|
61
|
+
lambda: hx.from_bytes(b"\xde\xad\xbe\xef").to(hx.Hex(sep=":", uppercase=True)),
|
|
62
|
+
"DE:AD:BE:EF",
|
|
63
|
+
),
|
|
64
|
+
(
|
|
65
|
+
"hex_array_width2",
|
|
66
|
+
lambda: hx.from_bytes_array([0xDE, 0xAD, 0xBE, 0xEF]).to(
|
|
67
|
+
hx.HexArray(width=2, prefix=True, uppercase=True)
|
|
68
|
+
),
|
|
69
|
+
["0xDEAD", "0xBEEF"],
|
|
70
|
+
),
|
|
71
|
+
(
|
|
72
|
+
"text_utf8_prefix",
|
|
73
|
+
lambda: hx.from_text("π", encoding="utf-8").to(hx.Hex(prefix=True)),
|
|
74
|
+
"0xcf80",
|
|
75
|
+
),
|
|
76
|
+
(
|
|
77
|
+
"int_little_endian",
|
|
78
|
+
lambda: hx.from_int(0x12345678, endian="little").hex,
|
|
79
|
+
"78563412",
|
|
80
|
+
),
|
|
81
|
+
(
|
|
82
|
+
"int_array_width2",
|
|
83
|
+
lambda: hx.from_int_array([0x1234, 0x5678], width=2).to(hx.HexArray(width=2)),
|
|
84
|
+
["1234", "5678"],
|
|
85
|
+
),
|
|
86
|
+
(
|
|
87
|
+
"binary_to_text",
|
|
88
|
+
lambda: hx.from_binary("01100100 01100001 01110100 01100001").text,
|
|
89
|
+
"data",
|
|
90
|
+
),
|
|
91
|
+
(
|
|
92
|
+
"base64_urlsafe_in",
|
|
93
|
+
lambda: hx.from_base64("-_8", urlsafe=True, padding=False).hex,
|
|
94
|
+
"fbff",
|
|
95
|
+
),
|
|
96
|
+
(
|
|
97
|
+
"base32_no_padding_in",
|
|
98
|
+
lambda: hx.from_base32("MRQXIYI", padding=False).text,
|
|
99
|
+
"data",
|
|
100
|
+
),
|
|
101
|
+
(
|
|
102
|
+
"escaped_in",
|
|
103
|
+
lambda: hx.from_escaped(r"\xDE\xAD").hex,
|
|
104
|
+
"dead",
|
|
105
|
+
),
|
|
106
|
+
(
|
|
107
|
+
"struct_pack",
|
|
108
|
+
lambda: hx.from_struct((0x1234, 0x5678), fmt=">HH").hex,
|
|
109
|
+
"12345678",
|
|
110
|
+
),
|
|
111
|
+
(
|
|
112
|
+
"bits_pad_right",
|
|
113
|
+
lambda: hx.from_bits("101", pad_side="right").hex,
|
|
114
|
+
"a0",
|
|
115
|
+
),
|
|
116
|
+
(
|
|
117
|
+
"convert_hex_to_bytes",
|
|
118
|
+
lambda: hx.convert("de ad be ef", hx.Hex(), hx.Bytes()),
|
|
119
|
+
b"\xde\xad\xbe\xef",
|
|
120
|
+
),
|
|
121
|
+
(
|
|
122
|
+
"convert_bytes_to_hex",
|
|
123
|
+
lambda: hx.convert(
|
|
75
124
|
b"\xde\xad\xbe\xef",
|
|
76
125
|
hx.Bytes(),
|
|
77
126
|
hx.Hex(sep=" ", prefix=True, uppercase=True),
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
hx.
|
|
101
|
-
|
|
102
|
-
|
|
127
|
+
),
|
|
128
|
+
"0xDE AD BE EF",
|
|
129
|
+
),
|
|
130
|
+
(
|
|
131
|
+
"convert_hexarray_to_intarray",
|
|
132
|
+
lambda: hx.convert(
|
|
133
|
+
["0xDEAD", "0xBEEF"], hx.HexArray(width=2), hx.IntArray(width=2)
|
|
134
|
+
),
|
|
135
|
+
[57005, 48879],
|
|
136
|
+
),
|
|
137
|
+
(
|
|
138
|
+
"convert_hex_to_int_little",
|
|
139
|
+
lambda: hx.convert("78563412", hx.Hex(), hx.Int(endian="little")),
|
|
140
|
+
305419896,
|
|
141
|
+
),
|
|
142
|
+
(
|
|
143
|
+
"convert_binary_to_hex_lsb",
|
|
144
|
+
lambda: hx.convert("0b10100000", hx.Binary(bit_order="lsb"), hx.Hex()),
|
|
145
|
+
"05",
|
|
146
|
+
),
|
|
147
|
+
(
|
|
148
|
+
"base64_urlsafe_out",
|
|
149
|
+
lambda: hx.from_text("data").to(hx.Base64(urlsafe=True, padding=False)),
|
|
150
|
+
"ZGF0YQ",
|
|
151
|
+
),
|
|
152
|
+
(
|
|
153
|
+
"base85_out",
|
|
154
|
+
lambda: hx.from_text("data").to(hx.Base85()),
|
|
155
|
+
"WMOn+",
|
|
156
|
+
),
|
|
157
|
+
(
|
|
158
|
+
"ascii85_adobe_out",
|
|
159
|
+
lambda: hx.from_text("data").to(hx.Ascii85(adobe=True)),
|
|
160
|
+
"<~A79Rg~>",
|
|
161
|
+
),
|
|
162
|
+
(
|
|
163
|
+
"hexdump_bare_out",
|
|
164
|
+
lambda: hx.from_hex("deadbeef").to(hx.Hexdump(width=2, offset=False, ascii=False)),
|
|
165
|
+
"de ad\nbe ef",
|
|
166
|
+
),
|
|
167
|
+
(
|
|
168
|
+
"escaped_uppercase_out",
|
|
169
|
+
lambda: hx.from_hex("dead").to(hx.Escaped(uppercase=True)),
|
|
170
|
+
r"\xDE\xAD",
|
|
171
|
+
),
|
|
172
|
+
(
|
|
173
|
+
"binary_sep_prefix_lsb_out",
|
|
174
|
+
lambda: hx.from_hex("0506").to(hx.Binary(sep=" ", prefix=True, bit_order="lsb")),
|
|
175
|
+
"0b10100000 0b01100000",
|
|
176
|
+
),
|
|
177
|
+
(
|
|
178
|
+
"bits_lsb_out",
|
|
179
|
+
lambda: hx.from_hex("05").to(hx.Bits(bit_order="lsb")),
|
|
180
|
+
[1, 0, 1, 0, 0, 0, 0, 0],
|
|
181
|
+
),
|
|
182
|
+
]
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@pytest.mark.parametrize(
|
|
186
|
+
"op, expected",
|
|
187
|
+
[pytest.param(op, expected, id=name) for name, op, expected in _QUICK_TOUR_CASES],
|
|
188
|
+
)
|
|
189
|
+
def test_documented_format_options_quick_tour(op, expected):
|
|
190
|
+
assert op() == expected
|
|
103
191
|
|
|
104
192
|
|
|
105
193
|
def test_converter_options():
|
|
@@ -115,7 +203,7 @@ def test_converter_options():
|
|
|
115
203
|
def test_bytes_string_literal():
|
|
116
204
|
assert hx.from_bytes_string(r"b'\xde\xad'").to_hex() == "dead"
|
|
117
205
|
assert hx.from_hex("dead").to_bytes_string() == r"b'\xde\xad'"
|
|
118
|
-
assert hx.convert(b"\xde",
|
|
206
|
+
assert hx.convert(b"\xde", hx.Bytes, hx.BytesString) == r"b'\xde'"
|
|
119
207
|
|
|
120
208
|
|
|
121
209
|
def test_odd_hex_left_pads_by_default_and_can_be_strict():
|
|
@@ -155,7 +243,7 @@ def test_invalid_byte_array_item_reports_context():
|
|
|
155
243
|
def test_constructable_hex_and_text_specs():
|
|
156
244
|
assert hx.Hex()("de:ad").hex == "dead"
|
|
157
245
|
assert hx.from_hex("dead").to(hx.Hex(prefix=True, uppercase=True)) == "0xDEAD"
|
|
158
|
-
assert hx.convert("π",
|
|
246
|
+
assert hx.convert("π", hx.Text(encoding="utf-8"), hx.Hex()) == "cf80"
|
|
159
247
|
assert hx.from_hex("cf80").to(hx.Text(encoding="utf-8")) == "π"
|
|
160
248
|
|
|
161
249
|
|
|
@@ -174,18 +262,27 @@ def test_pipeline_transforms_pad_reverse_and_group():
|
|
|
174
262
|
assert (hx.Hex() >> hx.Group(2) >> hx.IntArray())("12345678") == [0x1234, 0x5678]
|
|
175
263
|
|
|
176
264
|
|
|
265
|
+
def test_chunk_into_non_chunk_aware_format_is_rejected():
|
|
266
|
+
with pytest.raises(hx.HexConvError, match="chunking has no effect"):
|
|
267
|
+
(hx.Hex() >> hx.Chunk(2) >> hx.Hex())("12345678")
|
|
268
|
+
with pytest.raises(hx.HexConvError, match="chunking has no effect"):
|
|
269
|
+
(hx.Hex() >> hx.Group(2) >> hx.Text())("12345678")
|
|
270
|
+
# Chunk-aware targets still accept a grouping width.
|
|
271
|
+
assert (hx.Hex() >> hx.Chunk(2) >> hx.HexArray())("12345678") == ["1234", "5678"]
|
|
272
|
+
|
|
273
|
+
|
|
177
274
|
def test_urlsafe_base64_base32_base85_and_ascii85():
|
|
178
275
|
assert hx.from_bytes(b"\xfb\xff").to(hx.Base64(urlsafe=True, padding=False)) == "-_8"
|
|
179
|
-
assert hx.convert("-_8",
|
|
276
|
+
assert hx.convert("-_8", hx.Base64(urlsafe=True, padding=False), hx.Hex()) == "fbff"
|
|
180
277
|
|
|
181
278
|
assert hx.from_text("data").to(hx.Base32(padding=False)) == "MRQXIYI"
|
|
182
|
-
assert hx.convert("MRQXIYI",
|
|
279
|
+
assert hx.convert("MRQXIYI", hx.Base32(padding=False), hx.Text()) == "data"
|
|
183
280
|
|
|
184
281
|
base85 = hx.from_text("data").to(hx.Base85())
|
|
185
|
-
assert hx.convert(base85,
|
|
282
|
+
assert hx.convert(base85, hx.Base85(), hx.Text()) == "data"
|
|
186
283
|
|
|
187
284
|
ascii85 = hx.from_text("data").to(hx.Ascii85())
|
|
188
|
-
assert hx.convert(ascii85,
|
|
285
|
+
assert hx.convert(ascii85, hx.Ascii85(), hx.Text()) == "data"
|
|
189
286
|
|
|
190
287
|
|
|
191
288
|
def test_escaped_string_format():
|
|
@@ -198,14 +295,14 @@ def test_hexdump_format_round_trip():
|
|
|
198
295
|
dump = hx.from_hex("deadbeef").to(hx.Hexdump(width=2))
|
|
199
296
|
assert "00000000" in dump
|
|
200
297
|
assert hx.from_hexdump(dump).hex == "deadbeef"
|
|
201
|
-
assert hx.convert(dump,
|
|
298
|
+
assert hx.convert(dump, hx.Hexdump(), hx.Hex()) == "deadbeef"
|
|
202
299
|
|
|
203
300
|
|
|
204
301
|
def test_struct_format_pack_and_unpack():
|
|
205
302
|
packed = hx.from_struct((0x1234, 0x5678), fmt=">HH")
|
|
206
303
|
assert packed.hex == "12345678"
|
|
207
304
|
assert packed.to(hx.Struct(">HH")) == (0x1234, 0x5678)
|
|
208
|
-
assert hx.convert((0x1234, 0x5678),
|
|
305
|
+
assert hx.convert((0x1234, 0x5678), hx.Struct(">HH"), hx.Hex()) == "12345678"
|
|
209
306
|
assert hx.Struct(">HH")((0x1234, 0x5678)).hex == "12345678"
|
|
210
307
|
assert (hx.Struct(">HH") >> hx.Hex())((0x1234, 0x5678)) == "12345678"
|
|
211
308
|
assert (hx.Hex() >> hx.Struct(">HH"))("12345678") == (0x1234, 0x5678)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Property-based round-trip invariants.
|
|
2
|
+
|
|
3
|
+
For every lossless format, parsing what we emit must reproduce the original
|
|
4
|
+
bytes (or integers). Hypothesis explores edge cases that the example-based
|
|
5
|
+
tests in ``test_hexconv.py`` do not.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from hypothesis import given
|
|
9
|
+
from hypothesis import strategies as st
|
|
10
|
+
|
|
11
|
+
import hexconv as hx
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@given(st.binary(), st.sampled_from(["", " ", ":", "-", "_", ","]))
|
|
15
|
+
def test_hex_round_trip(data, sep):
|
|
16
|
+
text = hx.from_bytes(data).to_hex(sep=sep)
|
|
17
|
+
assert hx.from_hex(text).to_bytes() == data
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@given(st.binary(), st.booleans())
|
|
21
|
+
def test_hex_prefix_and_uppercase_round_trip(data, uppercase):
|
|
22
|
+
text = hx.from_bytes(data).to_hex(prefix=True, uppercase=uppercase)
|
|
23
|
+
assert hx.from_hex(text).to_bytes() == data
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@given(st.binary(), st.booleans(), st.booleans())
|
|
27
|
+
def test_base64_round_trip(data, urlsafe, padding):
|
|
28
|
+
text = hx.from_bytes(data).to_base64(urlsafe=urlsafe, padding=padding)
|
|
29
|
+
assert hx.from_base64(text, urlsafe=urlsafe, padding=padding).to_bytes() == data
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@given(st.binary(), st.booleans())
|
|
33
|
+
def test_base32_round_trip(data, padding):
|
|
34
|
+
text = hx.from_bytes(data).to_base32(padding=padding)
|
|
35
|
+
assert hx.from_base32(text, padding=padding).to_bytes() == data
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@given(st.binary())
|
|
39
|
+
def test_base85_round_trip(data):
|
|
40
|
+
assert hx.from_base85(hx.from_bytes(data).to_base85()).to_bytes() == data
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@given(st.binary(), st.booleans())
|
|
44
|
+
def test_ascii85_round_trip(data, adobe):
|
|
45
|
+
text = hx.from_bytes(data).to_ascii85(adobe=adobe)
|
|
46
|
+
assert hx.from_ascii85(text, adobe=adobe).to_bytes() == data
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@given(st.binary(), st.sampled_from(["msb", "lsb"]))
|
|
50
|
+
def test_binary_round_trip(data, bit_order):
|
|
51
|
+
text = hx.from_bytes(data).to_binary(bit_order=bit_order)
|
|
52
|
+
assert hx.from_binary(text, bit_order=bit_order).to_bytes() == data
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@given(st.binary(), st.sampled_from(["msb", "lsb"]))
|
|
56
|
+
def test_bits_round_trip(data, bit_order):
|
|
57
|
+
bits = hx.from_bytes(data).to_bits(bit_order=bit_order)
|
|
58
|
+
assert hx.from_bits(bits, bit_order=bit_order).to_bytes() == data
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@given(st.binary())
|
|
62
|
+
def test_escaped_round_trip(data):
|
|
63
|
+
assert hx.from_escaped(hx.from_bytes(data).to_escaped()).to_bytes() == data
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@given(st.binary())
|
|
67
|
+
def test_bytes_string_round_trip(data):
|
|
68
|
+
text = hx.from_bytes(data).to_bytes_string()
|
|
69
|
+
assert hx.from_bytes_string(text).to_bytes() == data
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@given(st.binary())
|
|
73
|
+
def test_bytes_array_round_trip(data):
|
|
74
|
+
assert hx.from_bytes_array(hx.from_bytes(data).to_bytes_array()).to_bytes() == data
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@given(st.binary(min_size=1))
|
|
78
|
+
def test_hexdump_round_trip(data):
|
|
79
|
+
dump = hx.from_bytes(data).to_hexdump()
|
|
80
|
+
assert hx.from_hexdump(dump).to_bytes() == data
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@given(st.integers(min_value=0))
|
|
84
|
+
def test_unsigned_int_round_trip(n):
|
|
85
|
+
assert hx.from_int(n).to_int() == n
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@given(st.integers())
|
|
89
|
+
def test_signed_int_round_trip(n):
|
|
90
|
+
assert hx.from_int(n, signed=True).to_int(signed=True) == n
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@given(
|
|
94
|
+
st.lists(st.integers(min_value=0, max_value=0xFFFF)),
|
|
95
|
+
st.sampled_from(["big", "little"]),
|
|
96
|
+
)
|
|
97
|
+
def test_int_array_width2_round_trip(values, endian):
|
|
98
|
+
packed = hx.from_int_array(values, width=2, endian=endian)
|
|
99
|
+
assert packed.to_int_array(width=2, endian=endian) == values
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|