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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hexconv
3
- Version: 0.2.7
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", from_=hx.Text, to=hx.HexString)
367
+ hx.convert("data", hx.Text, hx.HexString)
367
368
  # '64617461'
368
369
  ```
369
370
 
@@ -334,7 +334,7 @@ hx.from_hex("dead beef").bytes
334
334
  hx.from_text("data").hex
335
335
  # '64617461'
336
336
 
337
- hx.convert("data", from_=hx.Text, to=hx.HexString)
337
+ hx.convert("data", hx.Text, hx.HexString)
338
338
  # '64617461'
339
339
  ```
340
340
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "hexconv"
7
- version = "0.2.7"
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 = str
439
- BitOrder: TypeAlias = str
440
- PadSide: TypeAlias = str
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
- Preferred:
1019
+ The source and target formats are positional:
1020
+
1013
1021
  convert(value, Hex(), Bytes())
1014
1022
 
1015
- Backward-compatible:
1016
- convert(value, from_=Hex(), to=Bytes())
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 input_format is None else input_format,
1029
- Bytes if output_format is None else output_format,
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: from_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.2.7
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", from_=hx.Text, to=hx.HexString)
367
+ hx.convert("data", hx.Text, hx.HexString)
367
368
  # '64617461'
368
369
  ```
369
370
 
@@ -10,4 +10,5 @@ src/hexconv.egg-info/SOURCES.txt
10
10
  src/hexconv.egg-info/dependency_links.txt
11
11
  src/hexconv.egg-info/requires.txt
12
12
  src/hexconv.egg-info/top_level.txt
13
- tests/test_hexconv.py
13
+ tests/test_hexconv.py
14
+ tests/test_properties.py
@@ -1,5 +1,6 @@
1
1
 
2
2
  [dev]
3
3
  build
4
+ hypothesis
4
5
  pytest
5
6
  twine
@@ -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", from_=hx.Text, to=hx.HexString) == "64617461"
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
- def test_documented_format_options_quick_tour():
54
- assert hx.from_hex("de ad:be-ef").bytes == b"\xde\xad\xbe\xef"
55
- assert hx.from_bytes(b"\xde\xad\xbe\xef").to(hx.Hex(sep=":", uppercase=True)) == "DE:AD:BE:EF"
56
- assert hx.from_bytes_array([0xDE, 0xAD, 0xBE, 0xEF]).to(
57
- hx.HexArray(width=2, prefix=True, uppercase=True)
58
- ) == ["0xDEAD", "0xBEEF"]
59
- assert hx.from_text("π", encoding="utf-8").to(hx.Hex(prefix=True)) == "0xcf80"
60
- assert hx.from_int(0x12345678, endian="little").hex == "78563412"
61
- assert hx.from_int_array([0x1234, 0x5678], width=2).to(hx.HexArray(width=2)) == [
62
- "1234",
63
- "5678",
64
- ]
65
- assert hx.from_binary("01100100 01100001 01110100 01100001").text == "data"
66
- assert hx.from_base64("-_8", urlsafe=True, padding=False).hex == "fbff"
67
- assert hx.from_base32("MRQXIYI", padding=False).text == "data"
68
- assert hx.from_escaped(r"\xDE\xAD").hex == "dead"
69
- assert hx.from_struct((0x1234, 0x5678), fmt=">HH").hex == "12345678"
70
- assert hx.from_bits("101", pad_side="right").hex == "a0"
71
-
72
- assert hx.convert("de ad be ef", hx.Hex(), hx.Bytes()) == b"\xde\xad\xbe\xef"
73
- assert (
74
- hx.convert(
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
- == "0xDE AD BE EF"
80
- )
81
- assert hx.convert(["0xDEAD", "0xBEEF"], hx.HexArray(width=2), hx.IntArray(width=2)) == [
82
- 57005,
83
- 48879,
84
- ]
85
- assert hx.convert("78563412", hx.Hex(), hx.Int(endian="little")) == 305419896
86
- assert hx.convert("0b10100000", hx.Binary(bit_order="lsb"), hx.Hex()) == "05"
87
- assert hx.from_text("data").to(hx.Base64(urlsafe=True, padding=False)) == "ZGF0YQ"
88
- assert hx.from_text("data").to(hx.Base85()) == "WMOn+"
89
- assert hx.from_text("data").to(hx.Ascii85(adobe=True)) == "<~A79Rg~>"
90
- assert hx.from_hex("deadbeef").to(hx.Hexdump(width=2, offset=False, ascii=False)) == "de ad\nbe ef"
91
- assert hx.from_hex("dead").to(hx.Escaped(uppercase=True)) == r"\xDE\xAD"
92
- assert hx.from_hex("0506").to(hx.Binary(sep=" ", prefix=True, bit_order="lsb")) == (
93
- "0b10100000 0b01100000"
94
- )
95
- assert hx.from_hex("05").to(hx.Bits(bit_order="lsb")) == [1, 0, 1, 0, 0, 0, 0, 0]
96
-
97
-
98
- def test_convert_rejects_mixed_positional_and_keyword_formats():
99
- with pytest.raises(hx.HexConvError, match="from_"):
100
- hx.convert("data", hx.Text(), from_=hx.Text(), to=hx.Hex())
101
- with pytest.raises(hx.HexConvError, match="to"):
102
- hx.convert("data", hx.Text(), hx.Hex(), to=hx.Hex())
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", from_=hx.Bytes, to=hx.BytesString) == r"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("π", from_=hx.Text(encoding="utf-8"), to=hx.Hex()) == "cf80"
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", from_=hx.Base64(urlsafe=True, padding=False), to=hx.Hex()) == "fbff"
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", from_=hx.Base32(padding=False), to=hx.Text()) == "data"
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, from_=hx.Base85(), to=hx.Text()) == "data"
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, from_=hx.Ascii85(), to=hx.Text()) == "data"
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, from_=hx.Hexdump(), to=hx.Hex()) == "deadbeef"
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), from_=hx.Struct(">HH"), to=hx.Hex()) == "12345678"
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