hexconv 0.2.7__tar.gz → 0.3.1__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.1}/PKG-INFO +22 -2
- {hexconv-0.2.7 → hexconv-0.3.1}/README.md +18 -1
- {hexconv-0.2.7 → hexconv-0.3.1}/pyproject.toml +22 -1
- {hexconv-0.2.7 → hexconv-0.3.1}/src/hexconv/__init__.py +65 -53
- {hexconv-0.2.7 → hexconv-0.3.1}/src/hexconv/_core.py +87 -60
- {hexconv-0.2.7 → hexconv-0.3.1/src/hexconv.egg-info}/PKG-INFO +22 -2
- {hexconv-0.2.7 → hexconv-0.3.1}/src/hexconv.egg-info/SOURCES.txt +2 -1
- {hexconv-0.2.7 → hexconv-0.3.1}/src/hexconv.egg-info/requires.txt +3 -0
- {hexconv-0.2.7 → hexconv-0.3.1}/tests/test_hexconv.py +153 -56
- hexconv-0.3.1/tests/test_properties.py +99 -0
- {hexconv-0.2.7 → hexconv-0.3.1}/LICENSE +0 -0
- {hexconv-0.2.7 → hexconv-0.3.1}/MANIFEST.in +0 -0
- {hexconv-0.2.7 → hexconv-0.3.1}/setup.cfg +0 -0
- {hexconv-0.2.7 → hexconv-0.3.1}/src/hexconv/py.typed +0 -0
- {hexconv-0.2.7 → hexconv-0.3.1}/src/hexconv.egg-info/dependency_links.txt +0 -0
- {hexconv-0.2.7 → hexconv-0.3.1}/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.1
|
|
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,7 +23,10 @@ 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"
|
|
27
|
+
Requires-Dist: mypy; extra == "dev"
|
|
26
28
|
Requires-Dist: pytest; extra == "dev"
|
|
29
|
+
Requires-Dist: ruff; extra == "dev"
|
|
27
30
|
Requires-Dist: twine; extra == "dev"
|
|
28
31
|
Dynamic: license-file
|
|
29
32
|
|
|
@@ -319,6 +322,23 @@ Available format specs all work in their simplest bare form:
|
|
|
319
322
|
- `Bits()`
|
|
320
323
|
- `Auto()`
|
|
321
324
|
|
|
325
|
+
The short names are ergonomic aliases of the explicit long-form specs — pick
|
|
326
|
+
whichever reads better, they resolve to the same format:
|
|
327
|
+
|
|
328
|
+
| Short | Long form |
|
|
329
|
+
| --- | --- |
|
|
330
|
+
| `Hex` | `HexString` |
|
|
331
|
+
| `Binary` | `BinaryString` |
|
|
332
|
+
| `Base64` / `Base32` / `Base85` / `Ascii85` | `Base64String` / `Base32String` / `Base85String` / `Ascii85String` |
|
|
333
|
+
| `Escaped` | `EscapedString` |
|
|
334
|
+
| `Hexdump` | `HexDump` |
|
|
335
|
+
| `Struct` | `StructFormat` |
|
|
336
|
+
| `Bits` | `BitArray` |
|
|
337
|
+
| `DecimalInt` / `DecimalIntArray` | `Int` / `IntArray` |
|
|
338
|
+
| `LargeHexNumber` | `HexInt` |
|
|
339
|
+
| `ASCIIText` / `RawString` | `Text` |
|
|
340
|
+
| `Group` (transform) | `Chunk` |
|
|
341
|
+
|
|
322
342
|
Add options only when you need control over formatting or parsing:
|
|
323
343
|
|
|
324
344
|
- Display: `Hex(sep=" ", prefix=True, uppercase=True)`, `HexArray(width=2)`.
|
|
@@ -363,7 +383,7 @@ hx.from_hex("dead beef").bytes
|
|
|
363
383
|
hx.from_text("data").hex
|
|
364
384
|
# '64617461'
|
|
365
385
|
|
|
366
|
-
hx.convert("data",
|
|
386
|
+
hx.convert("data", hx.Text, hx.HexString)
|
|
367
387
|
# '64617461'
|
|
368
388
|
```
|
|
369
389
|
|
|
@@ -290,6 +290,23 @@ Available format specs all work in their simplest bare form:
|
|
|
290
290
|
- `Bits()`
|
|
291
291
|
- `Auto()`
|
|
292
292
|
|
|
293
|
+
The short names are ergonomic aliases of the explicit long-form specs — pick
|
|
294
|
+
whichever reads better, they resolve to the same format:
|
|
295
|
+
|
|
296
|
+
| Short | Long form |
|
|
297
|
+
| --- | --- |
|
|
298
|
+
| `Hex` | `HexString` |
|
|
299
|
+
| `Binary` | `BinaryString` |
|
|
300
|
+
| `Base64` / `Base32` / `Base85` / `Ascii85` | `Base64String` / `Base32String` / `Base85String` / `Ascii85String` |
|
|
301
|
+
| `Escaped` | `EscapedString` |
|
|
302
|
+
| `Hexdump` | `HexDump` |
|
|
303
|
+
| `Struct` | `StructFormat` |
|
|
304
|
+
| `Bits` | `BitArray` |
|
|
305
|
+
| `DecimalInt` / `DecimalIntArray` | `Int` / `IntArray` |
|
|
306
|
+
| `LargeHexNumber` | `HexInt` |
|
|
307
|
+
| `ASCIIText` / `RawString` | `Text` |
|
|
308
|
+
| `Group` (transform) | `Chunk` |
|
|
309
|
+
|
|
293
310
|
Add options only when you need control over formatting or parsing:
|
|
294
311
|
|
|
295
312
|
- Display: `Hex(sep=" ", prefix=True, uppercase=True)`, `HexArray(width=2)`.
|
|
@@ -334,7 +351,7 @@ hx.from_hex("dead beef").bytes
|
|
|
334
351
|
hx.from_text("data").hex
|
|
335
352
|
# '64617461'
|
|
336
353
|
|
|
337
|
-
hx.convert("data",
|
|
354
|
+
hx.convert("data", hx.Text, hx.HexString)
|
|
338
355
|
# '64617461'
|
|
339
356
|
```
|
|
340
357
|
|
|
@@ -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.1"
|
|
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,7 +31,10 @@ classifiers = [
|
|
|
31
31
|
[project.optional-dependencies]
|
|
32
32
|
dev = [
|
|
33
33
|
"build",
|
|
34
|
+
"hypothesis",
|
|
35
|
+
"mypy",
|
|
34
36
|
"pytest",
|
|
37
|
+
"ruff",
|
|
35
38
|
"twine",
|
|
36
39
|
]
|
|
37
40
|
|
|
@@ -44,3 +47,21 @@ hexconv = ["py.typed"]
|
|
|
44
47
|
[tool.pytest.ini_options]
|
|
45
48
|
testpaths = ["tests"]
|
|
46
49
|
pythonpath = ["src"]
|
|
50
|
+
|
|
51
|
+
[tool.ruff]
|
|
52
|
+
line-length = 100
|
|
53
|
+
target-version = "py310"
|
|
54
|
+
|
|
55
|
+
[tool.ruff.lint]
|
|
56
|
+
select = ["E", "F", "I", "B", "UP"]
|
|
57
|
+
|
|
58
|
+
[tool.ruff.lint.per-file-ignores]
|
|
59
|
+
# Public re-exports are grouped by concept rather than sorted alphabetically.
|
|
60
|
+
"src/hexconv/__init__.py" = ["I001"]
|
|
61
|
+
|
|
62
|
+
[tool.mypy]
|
|
63
|
+
python_version = "3.10"
|
|
64
|
+
no_implicit_optional = true
|
|
65
|
+
warn_redundant_casts = true
|
|
66
|
+
warn_unused_ignores = true
|
|
67
|
+
strict_equality = true
|
|
@@ -1,52 +1,60 @@
|
|
|
1
1
|
"""Conversion toolkit for bytes, hex, integers, arrays, binary, base64, and text."""
|
|
2
2
|
|
|
3
3
|
from ._core import (
|
|
4
|
-
|
|
5
|
-
Auto,
|
|
4
|
+
# Core types and errors
|
|
6
5
|
AutoResult,
|
|
7
|
-
|
|
6
|
+
Converter,
|
|
7
|
+
HexConvError,
|
|
8
|
+
Pipeline,
|
|
9
|
+
Transform,
|
|
10
|
+
Value,
|
|
11
|
+
# Format specifications (explicit long-form names)
|
|
8
12
|
Ascii85String,
|
|
9
|
-
|
|
10
|
-
Base64,
|
|
11
|
-
Base32,
|
|
13
|
+
Auto,
|
|
12
14
|
Base32String,
|
|
13
|
-
|
|
15
|
+
Base64String,
|
|
14
16
|
Base85String,
|
|
15
17
|
BinaryString,
|
|
16
|
-
Binary,
|
|
17
18
|
BitArray,
|
|
18
|
-
Bits,
|
|
19
19
|
Bytes,
|
|
20
20
|
BytesArray,
|
|
21
21
|
BytesString,
|
|
22
|
-
Chunk,
|
|
23
|
-
Converter,
|
|
24
|
-
DecimalInt,
|
|
25
|
-
DecimalIntArray,
|
|
26
|
-
Escaped,
|
|
27
22
|
EscapedString,
|
|
28
|
-
Group,
|
|
29
|
-
Hex,
|
|
30
23
|
HexArray,
|
|
31
|
-
HexConvError,
|
|
32
24
|
HexDump,
|
|
33
|
-
Hexdump,
|
|
34
25
|
HexInt,
|
|
35
26
|
HexNumbersArray,
|
|
36
27
|
HexString,
|
|
37
28
|
Int,
|
|
38
29
|
IntArray,
|
|
30
|
+
StructFormat,
|
|
31
|
+
Text,
|
|
32
|
+
# Short, ergonomic aliases for the format specifications above
|
|
33
|
+
ASCIIText,
|
|
34
|
+
Ascii85,
|
|
35
|
+
Base32,
|
|
36
|
+
Base64,
|
|
37
|
+
Base85,
|
|
38
|
+
Binary,
|
|
39
|
+
Bits,
|
|
40
|
+
DecimalInt,
|
|
41
|
+
DecimalIntArray,
|
|
42
|
+
Escaped,
|
|
43
|
+
Hex,
|
|
44
|
+
Hexdump,
|
|
39
45
|
LargeHexNumber,
|
|
40
|
-
Pad,
|
|
41
|
-
Pipeline,
|
|
42
46
|
RawString,
|
|
43
|
-
Reverse,
|
|
44
47
|
Struct,
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
# Pipeline transforms
|
|
49
|
+
Chunk,
|
|
50
|
+
Group,
|
|
51
|
+
Pad,
|
|
52
|
+
Reverse,
|
|
53
|
+
# One-shot conversion and pipeline builders
|
|
49
54
|
convert,
|
|
55
|
+
infer,
|
|
56
|
+
pipeline,
|
|
57
|
+
# Source helpers
|
|
50
58
|
from_ascii85,
|
|
51
59
|
from_auto,
|
|
52
60
|
from_base32,
|
|
@@ -66,57 +74,63 @@ from ._core import (
|
|
|
66
74
|
from_int_array,
|
|
67
75
|
from_struct,
|
|
68
76
|
from_text,
|
|
69
|
-
infer,
|
|
70
|
-
pipeline,
|
|
71
77
|
)
|
|
72
78
|
|
|
73
79
|
__all__ = [
|
|
74
|
-
|
|
75
|
-
"Auto",
|
|
80
|
+
# Core types and errors
|
|
76
81
|
"AutoResult",
|
|
77
|
-
"
|
|
82
|
+
"Converter",
|
|
83
|
+
"HexConvError",
|
|
84
|
+
"Pipeline",
|
|
85
|
+
"Transform",
|
|
86
|
+
"Value",
|
|
87
|
+
# Format specifications (explicit long-form names)
|
|
78
88
|
"Ascii85String",
|
|
79
|
-
"
|
|
89
|
+
"Auto",
|
|
80
90
|
"Base32String",
|
|
81
91
|
"Base64String",
|
|
82
|
-
"Base64",
|
|
83
|
-
"Base85",
|
|
84
92
|
"Base85String",
|
|
85
|
-
"Binary",
|
|
86
93
|
"BinaryString",
|
|
87
94
|
"BitArray",
|
|
88
|
-
"Bits",
|
|
89
95
|
"Bytes",
|
|
90
96
|
"BytesArray",
|
|
91
97
|
"BytesString",
|
|
92
|
-
"Chunk",
|
|
93
|
-
"Converter",
|
|
94
|
-
"DecimalInt",
|
|
95
|
-
"DecimalIntArray",
|
|
96
|
-
"Escaped",
|
|
97
98
|
"EscapedString",
|
|
98
|
-
"Group",
|
|
99
|
-
"Hex",
|
|
100
99
|
"HexArray",
|
|
101
|
-
"HexConvError",
|
|
102
100
|
"HexDump",
|
|
103
|
-
"Hexdump",
|
|
104
101
|
"HexInt",
|
|
105
102
|
"HexNumbersArray",
|
|
106
103
|
"HexString",
|
|
107
104
|
"Int",
|
|
108
105
|
"IntArray",
|
|
106
|
+
"StructFormat",
|
|
107
|
+
"Text",
|
|
108
|
+
# Short, ergonomic aliases for the format specifications above
|
|
109
|
+
"ASCIIText",
|
|
110
|
+
"Ascii85",
|
|
111
|
+
"Base32",
|
|
112
|
+
"Base64",
|
|
113
|
+
"Base85",
|
|
114
|
+
"Binary",
|
|
115
|
+
"Bits",
|
|
116
|
+
"DecimalInt",
|
|
117
|
+
"DecimalIntArray",
|
|
118
|
+
"Escaped",
|
|
119
|
+
"Hex",
|
|
120
|
+
"Hexdump",
|
|
109
121
|
"LargeHexNumber",
|
|
110
|
-
"Pad",
|
|
111
|
-
"Pipeline",
|
|
112
122
|
"RawString",
|
|
113
|
-
"Reverse",
|
|
114
123
|
"Struct",
|
|
115
|
-
|
|
116
|
-
"
|
|
117
|
-
"
|
|
118
|
-
"
|
|
124
|
+
# Pipeline transforms
|
|
125
|
+
"Chunk",
|
|
126
|
+
"Group",
|
|
127
|
+
"Pad",
|
|
128
|
+
"Reverse",
|
|
129
|
+
# One-shot conversion and pipeline builders
|
|
119
130
|
"convert",
|
|
131
|
+
"infer",
|
|
132
|
+
"pipeline",
|
|
133
|
+
# Source helpers
|
|
120
134
|
"from_ascii85",
|
|
121
135
|
"from_auto",
|
|
122
136
|
"from_base32",
|
|
@@ -136,6 +150,4 @@ __all__ = [
|
|
|
136
150
|
"from_int_array",
|
|
137
151
|
"from_struct",
|
|
138
152
|
"from_text",
|
|
139
|
-
"infer",
|
|
140
|
-
"pipeline",
|
|
141
153
|
]
|
|
@@ -3,12 +3,13 @@ from __future__ import annotations
|
|
|
3
3
|
import ast
|
|
4
4
|
import base64
|
|
5
5
|
import binascii
|
|
6
|
+
import builtins
|
|
6
7
|
import codecs
|
|
7
8
|
import re
|
|
8
9
|
import struct
|
|
9
|
-
from collections.abc import Iterable
|
|
10
|
+
from collections.abc import Callable, Iterable
|
|
10
11
|
from dataclasses import dataclass, field
|
|
11
|
-
from typing import Any,
|
|
12
|
+
from typing import Any, Literal, TypeAlias, overload
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
class HexConvError(ValueError):
|
|
@@ -18,12 +19,12 @@ class HexConvError(ValueError):
|
|
|
18
19
|
class _Format:
|
|
19
20
|
"""Base class for public format marker classes."""
|
|
20
21
|
|
|
21
|
-
def __call__(self, value: Any) ->
|
|
22
|
+
def __call__(self, value: Any) -> Value:
|
|
22
23
|
"""Parse a value using this configured format."""
|
|
23
24
|
|
|
24
25
|
return _parse(value, self)
|
|
25
26
|
|
|
26
|
-
def __rshift__(self, other: Any) ->
|
|
27
|
+
def __rshift__(self, other: Any) -> Pipeline:
|
|
27
28
|
"""Compose this format with a transform, target format, or pipeline."""
|
|
28
29
|
|
|
29
30
|
return Pipeline(source=self) >> other
|
|
@@ -87,7 +88,7 @@ class HexArray(_Format):
|
|
|
87
88
|
width: int | None = None,
|
|
88
89
|
prefix: bool = False,
|
|
89
90
|
uppercase: bool = False,
|
|
90
|
-
endian:
|
|
91
|
+
endian: Endian = "big",
|
|
91
92
|
pad_odd: bool = True,
|
|
92
93
|
) -> None:
|
|
93
94
|
self.width = width
|
|
@@ -118,7 +119,7 @@ class HexNumbersArray(_Format):
|
|
|
118
119
|
self,
|
|
119
120
|
*,
|
|
120
121
|
width: int | None = 1,
|
|
121
|
-
endian:
|
|
122
|
+
endian: Endian = "big",
|
|
122
123
|
signed: bool = False,
|
|
123
124
|
) -> None:
|
|
124
125
|
self.width = width
|
|
@@ -140,7 +141,7 @@ class HexInt(_Format):
|
|
|
140
141
|
*,
|
|
141
142
|
min_width: int = 1,
|
|
142
143
|
width: int | None = None,
|
|
143
|
-
endian:
|
|
144
|
+
endian: Endian = "big",
|
|
144
145
|
signed: bool = False,
|
|
145
146
|
pad_odd: bool = True,
|
|
146
147
|
) -> None:
|
|
@@ -174,7 +175,7 @@ class Int(_Format):
|
|
|
174
175
|
*,
|
|
175
176
|
min_width: int = 1,
|
|
176
177
|
width: int | None = None,
|
|
177
|
-
endian:
|
|
178
|
+
endian: Endian = "big",
|
|
178
179
|
signed: bool = False,
|
|
179
180
|
) -> None:
|
|
180
181
|
self.min_width = min_width
|
|
@@ -205,7 +206,7 @@ class IntArray(_Format):
|
|
|
205
206
|
self,
|
|
206
207
|
*,
|
|
207
208
|
width: int | None = None,
|
|
208
|
-
endian:
|
|
209
|
+
endian: Endian = "big",
|
|
209
210
|
signed: bool = False,
|
|
210
211
|
) -> None:
|
|
211
212
|
self.width = width
|
|
@@ -258,7 +259,7 @@ class BinaryString(_Format):
|
|
|
258
259
|
sep: str = "",
|
|
259
260
|
prefix: bool = False,
|
|
260
261
|
pad_odd: bool = True,
|
|
261
|
-
bit_order:
|
|
262
|
+
bit_order: BitOrder = "msb",
|
|
262
263
|
) -> None:
|
|
263
264
|
self.sep = sep
|
|
264
265
|
self.prefix = prefix
|
|
@@ -414,9 +415,9 @@ class BitArray(_Format):
|
|
|
414
415
|
def __init__(
|
|
415
416
|
self,
|
|
416
417
|
*,
|
|
417
|
-
bit_order:
|
|
418
|
+
bit_order: BitOrder = "msb",
|
|
418
419
|
pad: bool = True,
|
|
419
|
-
pad_side:
|
|
420
|
+
pad_side: PadSide = "left",
|
|
420
421
|
) -> None:
|
|
421
422
|
self.bit_order = bit_order
|
|
422
423
|
self.pad = pad
|
|
@@ -435,9 +436,9 @@ class BitArray(_Format):
|
|
|
435
436
|
|
|
436
437
|
Format: TypeAlias = type[_Format]
|
|
437
438
|
FormatLike: TypeAlias = type[_Format] | _Format
|
|
438
|
-
Endian: TypeAlias =
|
|
439
|
-
BitOrder: TypeAlias =
|
|
440
|
-
PadSide: TypeAlias =
|
|
439
|
+
Endian: TypeAlias = Literal["big", "little"]
|
|
440
|
+
BitOrder: TypeAlias = Literal["msb", "lsb"]
|
|
441
|
+
PadSide: TypeAlias = Literal["left", "right"]
|
|
441
442
|
|
|
442
443
|
|
|
443
444
|
Hex = HexString
|
|
@@ -468,12 +469,12 @@ class Value:
|
|
|
468
469
|
source: str | None = None
|
|
469
470
|
meta: dict[str, Any] = field(default_factory=dict)
|
|
470
471
|
|
|
471
|
-
def with_data(self, data: bytes, *, source: str | None = None) ->
|
|
472
|
+
def with_data(self, data: bytes, *, source: str | None = None) -> Value:
|
|
472
473
|
"""Return a copy with different bytes and preserved metadata."""
|
|
473
474
|
|
|
474
475
|
return Value(bytes(data), source=source or self.source, meta=dict(self.meta))
|
|
475
476
|
|
|
476
|
-
def with_meta(self, **meta: Any) ->
|
|
477
|
+
def with_meta(self, **meta: Any) -> Value:
|
|
477
478
|
"""Return a copy with additional metadata used by composable transforms."""
|
|
478
479
|
|
|
479
480
|
merged = dict(self.meta)
|
|
@@ -484,6 +485,12 @@ class Value:
|
|
|
484
485
|
"""Emit this value using a public format marker such as HexString."""
|
|
485
486
|
|
|
486
487
|
format_type = _format_type(format_)
|
|
488
|
+
if "chunk_width" in self.meta and format_type not in _CHUNK_AWARE_OUTPUTS:
|
|
489
|
+
raise HexConvError(
|
|
490
|
+
f"chunking has no effect on {format_type.__name__} output; emit to a "
|
|
491
|
+
"chunk-aware format such as HexArray, HexNumbersArray, IntArray, or "
|
|
492
|
+
"DecimalIntArray, or drop the Chunk/Group step"
|
|
493
|
+
)
|
|
487
494
|
output_options = _format_options(format_, "output")
|
|
488
495
|
output_options.update(options)
|
|
489
496
|
try:
|
|
@@ -602,18 +609,6 @@ class Value:
|
|
|
602
609
|
_validate_endian(endian)
|
|
603
610
|
return int.from_bytes(self._data, endian, signed=signed)
|
|
604
611
|
|
|
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
612
|
def to_int_array(
|
|
618
613
|
self,
|
|
619
614
|
*,
|
|
@@ -779,6 +774,22 @@ class Value:
|
|
|
779
774
|
raise HexConvError("chunk width cannot be None")
|
|
780
775
|
return chunk_width
|
|
781
776
|
|
|
777
|
+
# ``int``/``integer`` are convenience shortcuts whose names shadow the builtin
|
|
778
|
+
# ``int``. They are declared after every method above so those methods keep
|
|
779
|
+
# annotating with the builtin, and they spell their own return type as
|
|
780
|
+
# ``builtins.int`` so their order here does not matter.
|
|
781
|
+
@property
|
|
782
|
+
def int(self) -> builtins.int:
|
|
783
|
+
"""Shortcut for to_int()."""
|
|
784
|
+
|
|
785
|
+
return self.to_int()
|
|
786
|
+
|
|
787
|
+
@property
|
|
788
|
+
def integer(self) -> builtins.int:
|
|
789
|
+
"""Shortcut for to_int()."""
|
|
790
|
+
|
|
791
|
+
return self.to_int()
|
|
792
|
+
|
|
782
793
|
|
|
783
794
|
@dataclass(frozen=True)
|
|
784
795
|
class AutoResult:
|
|
@@ -796,7 +807,7 @@ class Transform:
|
|
|
796
807
|
def __call__(self, value: Any) -> Value:
|
|
797
808
|
return self.apply(_ensure_value(value))
|
|
798
809
|
|
|
799
|
-
def __rshift__(self, other: Any) ->
|
|
810
|
+
def __rshift__(self, other: Any) -> Pipeline:
|
|
800
811
|
return Pipeline(transforms=[self]) >> other
|
|
801
812
|
|
|
802
813
|
def apply(self, value: Value) -> Value:
|
|
@@ -852,7 +863,7 @@ class Pad(Transform):
|
|
|
852
863
|
width: int | None = None,
|
|
853
864
|
block_size: int | None = None,
|
|
854
865
|
byte: int = 0,
|
|
855
|
-
) ->
|
|
866
|
+
) -> Pad:
|
|
856
867
|
return cls(width=width, block_size=block_size, byte=byte, side="left")
|
|
857
868
|
|
|
858
869
|
@classmethod
|
|
@@ -862,7 +873,7 @@ class Pad(Transform):
|
|
|
862
873
|
width: int | None = None,
|
|
863
874
|
block_size: int | None = None,
|
|
864
875
|
byte: int = 0,
|
|
865
|
-
) ->
|
|
876
|
+
) -> Pad:
|
|
866
877
|
return cls(width=width, block_size=block_size, byte=byte, side="right")
|
|
867
878
|
|
|
868
879
|
def apply(self, value: Value) -> Value:
|
|
@@ -899,23 +910,23 @@ class Pipeline:
|
|
|
899
910
|
self.transforms = list(transforms or [])
|
|
900
911
|
self.target = target
|
|
901
912
|
|
|
902
|
-
def from_(self, source: FormatLike) ->
|
|
913
|
+
def from_(self, source: FormatLike) -> Pipeline:
|
|
903
914
|
return Pipeline(source=source, transforms=self.transforms, target=self.target)
|
|
904
915
|
|
|
905
|
-
def to(self, target: FormatLike) ->
|
|
916
|
+
def to(self, target: FormatLike) -> Pipeline:
|
|
906
917
|
return Pipeline(source=self.source, transforms=self.transforms, target=target)
|
|
907
918
|
|
|
908
|
-
def then(self, transform: Transform) ->
|
|
919
|
+
def then(self, transform: Transform) -> Pipeline:
|
|
909
920
|
return Pipeline(
|
|
910
921
|
source=self.source,
|
|
911
922
|
transforms=[*self.transforms, transform],
|
|
912
923
|
target=self.target,
|
|
913
924
|
)
|
|
914
925
|
|
|
915
|
-
def chunk(self, width: int, *, strict: bool = True) ->
|
|
926
|
+
def chunk(self, width: int, *, strict: bool = True) -> Pipeline:
|
|
916
927
|
return self.then(Chunk(width, strict=strict))
|
|
917
928
|
|
|
918
|
-
def group(self, width: int, *, strict: bool = True) ->
|
|
929
|
+
def group(self, width: int, *, strict: bool = True) -> Pipeline:
|
|
919
930
|
return self.then(Group(width, strict=strict))
|
|
920
931
|
|
|
921
932
|
def pad(
|
|
@@ -925,13 +936,13 @@ class Pipeline:
|
|
|
925
936
|
block_size: int | None = None,
|
|
926
937
|
byte: int = 0,
|
|
927
938
|
side: PadSide = "left",
|
|
928
|
-
) ->
|
|
939
|
+
) -> Pipeline:
|
|
929
940
|
return self.then(Pad(width=width, block_size=block_size, byte=byte, side=side))
|
|
930
941
|
|
|
931
|
-
def reverse(self) ->
|
|
942
|
+
def reverse(self) -> Pipeline:
|
|
932
943
|
return self.then(Reverse())
|
|
933
944
|
|
|
934
|
-
def __rshift__(self, other: Any) ->
|
|
945
|
+
def __rshift__(self, other: Any) -> Pipeline:
|
|
935
946
|
if isinstance(other, Pipeline):
|
|
936
947
|
source = self.source if self.source is not None else other.source
|
|
937
948
|
return Pipeline(
|
|
@@ -1002,31 +1013,22 @@ def convert(
|
|
|
1002
1013
|
from_format: FormatLike | None = None,
|
|
1003
1014
|
to_format: FormatLike | None = None,
|
|
1004
1015
|
*,
|
|
1005
|
-
from_: FormatLike | None = None,
|
|
1006
|
-
to: FormatLike | None = None,
|
|
1007
1016
|
input_options: dict[str, Any] | None = None,
|
|
1008
1017
|
output_options: dict[str, Any] | None = None,
|
|
1009
1018
|
) -> Any:
|
|
1010
1019
|
"""One-shot conversion between explicit formats.
|
|
1011
1020
|
|
|
1012
|
-
|
|
1021
|
+
The source and target formats are positional:
|
|
1022
|
+
|
|
1013
1023
|
convert(value, Hex(), Bytes())
|
|
1014
1024
|
|
|
1015
|
-
|
|
1016
|
-
|
|
1025
|
+
Omit ``from_format`` to infer the source with ``Auto``; omit ``to_format``
|
|
1026
|
+
to emit raw ``Bytes``.
|
|
1017
1027
|
"""
|
|
1018
1028
|
|
|
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
1029
|
return Converter(
|
|
1028
|
-
Auto if
|
|
1029
|
-
Bytes if
|
|
1030
|
+
Auto if from_format is None else from_format,
|
|
1031
|
+
Bytes if to_format is None else to_format,
|
|
1030
1032
|
input_options=input_options,
|
|
1031
1033
|
output_options=output_options,
|
|
1032
1034
|
)(value)
|
|
@@ -1082,6 +1084,18 @@ def infer(value: Any) -> AutoResult:
|
|
|
1082
1084
|
raise HexConvError(f"cannot infer source format for {type(value).__name__}")
|
|
1083
1085
|
|
|
1084
1086
|
|
|
1087
|
+
@overload
|
|
1088
|
+
def from_auto(value: Any, *, explain: Literal[False] = ...) -> Value: ...
|
|
1089
|
+
|
|
1090
|
+
|
|
1091
|
+
@overload
|
|
1092
|
+
def from_auto(value: Any, *, explain: Literal[True]) -> AutoResult: ...
|
|
1093
|
+
|
|
1094
|
+
|
|
1095
|
+
@overload
|
|
1096
|
+
def from_auto(value: Any, *, explain: bool) -> Value | AutoResult: ...
|
|
1097
|
+
|
|
1098
|
+
|
|
1085
1099
|
def from_auto(value: Any, *, explain: bool = False) -> Value | AutoResult:
|
|
1086
1100
|
"""Parse a value using convenience heuristics.
|
|
1087
1101
|
|
|
@@ -1100,6 +1114,12 @@ def from_auto(value: Any, *, explain: bool = False) -> Value | AutoResult:
|
|
|
1100
1114
|
return result if explain else result.value
|
|
1101
1115
|
|
|
1102
1116
|
|
|
1117
|
+
def _from_auto_value(value: Any) -> Value:
|
|
1118
|
+
"""Auto-format parser that always returns a Value (never AutoResult)."""
|
|
1119
|
+
|
|
1120
|
+
return from_auto(value)
|
|
1121
|
+
|
|
1122
|
+
|
|
1103
1123
|
def from_bytes(value: bytes | bytearray | memoryview | Iterable[int]) -> Value:
|
|
1104
1124
|
"""Parse a bytes-like object or a byte-sized integer sequence."""
|
|
1105
1125
|
|
|
@@ -1549,7 +1569,7 @@ def _emit_bits(value: Value, **options: Any) -> list[int]:
|
|
|
1549
1569
|
|
|
1550
1570
|
|
|
1551
1571
|
_INPUTS: dict[Format, Callable[..., Value]] = {
|
|
1552
|
-
Auto:
|
|
1572
|
+
Auto: _from_auto_value,
|
|
1553
1573
|
Bytes: from_bytes,
|
|
1554
1574
|
BytesArray: from_bytes_array,
|
|
1555
1575
|
BytesString: from_bytes_string,
|
|
@@ -1604,6 +1624,14 @@ _OUTPUTS: dict[Format, Callable[..., Any]] = {
|
|
|
1604
1624
|
}
|
|
1605
1625
|
|
|
1606
1626
|
|
|
1627
|
+
# Output formats whose emitters honor a Chunk/Group width stored on Value.meta.
|
|
1628
|
+
# Every other target silently ignores chunking, so emitting to one after a chunk
|
|
1629
|
+
# is treated as a mistake (see Value.to).
|
|
1630
|
+
_CHUNK_AWARE_OUTPUTS: frozenset[Format] = frozenset(
|
|
1631
|
+
{HexArray, HexNumbersArray, IntArray, DecimalIntArray}
|
|
1632
|
+
)
|
|
1633
|
+
|
|
1634
|
+
|
|
1607
1635
|
def _is_format_like(value: Any) -> bool:
|
|
1608
1636
|
return isinstance(value, _Format) or (
|
|
1609
1637
|
isinstance(value, type) and issubclass(value, _Format)
|
|
@@ -1631,10 +1659,7 @@ def _format_options(format_: FormatLike, direction: str) -> dict[str, Any]:
|
|
|
1631
1659
|
def _ensure_value(value: Any) -> Value:
|
|
1632
1660
|
if isinstance(value, Value):
|
|
1633
1661
|
return value
|
|
1634
|
-
|
|
1635
|
-
if isinstance(parsed, AutoResult):
|
|
1636
|
-
return parsed.value
|
|
1637
|
-
return parsed
|
|
1662
|
+
return from_auto(value)
|
|
1638
1663
|
|
|
1639
1664
|
|
|
1640
1665
|
def _validate_endian(endian: Endian) -> None:
|
|
@@ -1824,6 +1849,8 @@ def _int_to_bytes(
|
|
|
1824
1849
|
|
|
1825
1850
|
|
|
1826
1851
|
def _signed_int_to_minimal_bytes(value: int, *, min_width: int, endian: Endian) -> bytes:
|
|
1852
|
+
# Two's complement needs a bit beyond the magnitude for the sign, so start
|
|
1853
|
+
# one byte wider than the magnitude and widen further until the value fits.
|
|
1827
1854
|
candidate = max(min_width, 1, (abs(value).bit_length() + 8) // 8)
|
|
1828
1855
|
for width in range(candidate, candidate + 2):
|
|
1829
1856
|
try:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hexconv
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
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,7 +23,10 @@ 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"
|
|
27
|
+
Requires-Dist: mypy; extra == "dev"
|
|
26
28
|
Requires-Dist: pytest; extra == "dev"
|
|
29
|
+
Requires-Dist: ruff; extra == "dev"
|
|
27
30
|
Requires-Dist: twine; extra == "dev"
|
|
28
31
|
Dynamic: license-file
|
|
29
32
|
|
|
@@ -319,6 +322,23 @@ Available format specs all work in their simplest bare form:
|
|
|
319
322
|
- `Bits()`
|
|
320
323
|
- `Auto()`
|
|
321
324
|
|
|
325
|
+
The short names are ergonomic aliases of the explicit long-form specs — pick
|
|
326
|
+
whichever reads better, they resolve to the same format:
|
|
327
|
+
|
|
328
|
+
| Short | Long form |
|
|
329
|
+
| --- | --- |
|
|
330
|
+
| `Hex` | `HexString` |
|
|
331
|
+
| `Binary` | `BinaryString` |
|
|
332
|
+
| `Base64` / `Base32` / `Base85` / `Ascii85` | `Base64String` / `Base32String` / `Base85String` / `Ascii85String` |
|
|
333
|
+
| `Escaped` | `EscapedString` |
|
|
334
|
+
| `Hexdump` | `HexDump` |
|
|
335
|
+
| `Struct` | `StructFormat` |
|
|
336
|
+
| `Bits` | `BitArray` |
|
|
337
|
+
| `DecimalInt` / `DecimalIntArray` | `Int` / `IntArray` |
|
|
338
|
+
| `LargeHexNumber` | `HexInt` |
|
|
339
|
+
| `ASCIIText` / `RawString` | `Text` |
|
|
340
|
+
| `Group` (transform) | `Chunk` |
|
|
341
|
+
|
|
322
342
|
Add options only when you need control over formatting or parsing:
|
|
323
343
|
|
|
324
344
|
- Display: `Hex(sep=" ", prefix=True, uppercase=True)`, `HexArray(width=2)`.
|
|
@@ -363,7 +383,7 @@ hx.from_hex("dead beef").bytes
|
|
|
363
383
|
hx.from_text("data").hex
|
|
364
384
|
# '64617461'
|
|
365
385
|
|
|
366
|
-
hx.convert("data",
|
|
386
|
+
hx.convert("data", hx.Text, hx.HexString)
|
|
367
387
|
# '64617461'
|
|
368
388
|
```
|
|
369
389
|
|
|
@@ -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
|