From 79a323f1b100909ed44ea8dfa086ed4671b2db76 Mon Sep 17 00:00:00 2001 From: Tim Case Date: Sat, 18 Apr 2026 00:03:53 -0500 Subject: [PATCH 1/2] parse_string strict=False: explicit B/b/bit(s)/byte(s) handling Closes #106. In the strict=False path, add explicit checks before the prefix-normalisation step so that lone 'B' -> Byte, lone 'b' -> Bit, and the word forms bit/bits/byte/bytes (case-insensitive) resolve to the correct base type instead of raising ValueError. --- bitmath/__init__.py | 8 ++++++++ tests/test_parse.py | 26 ++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/bitmath/__init__.py b/bitmath/__init__.py index e6c5431..8690804 100644 --- a/bitmath/__init__.py +++ b/bitmath/__init__.py @@ -1583,6 +1583,14 @@ def parse_string(s, system=NIST, strict=True): val, unit = s[:index], s[index:] + # Explicit base-unit and word-form checks: handle B, b, bit(s), + # byte(s) before the prefix-normalisation logic below. + _unit_lower = unit.lower() + if unit == 'B' or _unit_lower in ('byte', 'bytes'): + return Byte(float(val)) + if unit == 'b' or _unit_lower in ('bit', 'bits'): + return Bit(float(val)) + # Normalise: strip trailing b/B and append 'B' so we always # work with byte-family units regardless of what was supplied. unit = unit.rstrip('Bb') diff --git a/tests/test_parse.py b/tests/test_parse.py index ddce52c..5de0ed1 100644 --- a/tests/test_parse.py +++ b/tests/test_parse.py @@ -299,6 +299,32 @@ def test_parse_non_strict_github_issue_60(self): bitmath.parse_string('4.7M', strict=False, system=bitmath.SI), bitmath.MB(4.7)) + def test_parse_non_strict_capital_B_is_Byte(self): + """parse_string strict=False: lone 'B' parses as Byte""" + self.assertIs(type(bitmath.parse_string("1B", strict=False)), bitmath.Byte) + self.assertEqual(bitmath.parse_string("1 B", strict=False), bitmath.Byte(1)) + + def test_parse_non_strict_lowercase_b_is_Bit(self): + """parse_string strict=False: lone 'b' parses as Bit""" + self.assertIs(type(bitmath.parse_string("1b", strict=False)), bitmath.Bit) + self.assertEqual(bitmath.parse_string("1 b", strict=False), bitmath.Bit(1)) + + def test_parse_non_strict_bit_word_forms(self): + """parse_string strict=False: bit/bits/Bit/Bits/BIT all parse as Bit""" + expected = bitmath.Bit(42) + for unit in ('bit', 'bits', 'Bit', 'Bits', 'BIT', 'BITS'): + result = bitmath.parse_string(f"42 {unit}", strict=False) + self.assertEqual(result, expected, msg=f"Failed for unit '{unit}'") + self.assertIs(type(result), bitmath.Bit, msg=f"Wrong type for unit '{unit}'") + + def test_parse_non_strict_byte_word_forms(self): + """parse_string strict=False: byte/bytes/Byte/Bytes/BYTE all parse as Byte""" + expected = bitmath.Byte(42) + for unit in ('byte', 'bytes', 'Byte', 'Bytes', 'BYTE', 'BYTES'): + result = bitmath.parse_string(f"42 {unit}", strict=False) + self.assertEqual(result, expected, msg=f"Failed for unit '{unit}'") + self.assertIs(type(result), bitmath.Byte, msg=f"Wrong type for unit '{unit}'") + def test_parse_string_unsafe_deprecation_warning(self): """parse_string_unsafe emits DeprecationWarning as of 2.0.0""" import warnings From 3472fb971b10a1ecc10f9eadd968f0cccd13a475 Mon Sep 17 00:00:00 2001 From: Tim Case Date: Sat, 18 Apr 2026 00:13:02 -0500 Subject: [PATCH 2/2] Byte and Bit display as 'B' and 'b' (closes #93) The base unit display names were the only ones that didn't use abbreviated form. Byte('Byte'/'Bytes') -> 'B' and Bit('Bit'/'Bits') -> 'b', consistent with KiB, MiB, kB, etc. Breaking change noted in NEWS.rst. Five tests updated to expect the new strings. update Byte/Bit display string examples to B/b --- NEWS.rst | 8 +++++ bitmath/__init__.py | 6 ++-- docsite/source/_static/.gitkeep | 0 docsite/source/instances.rst | 38 +++++++++++------------ docsite/source/module.rst | 20 ++++++------ tests/test_context_manager.py | 6 ++-- tests/test_context_manager_thread_safe.py | 8 ++--- tests/test_representation.py | 2 +- 8 files changed, 48 insertions(+), 40 deletions(-) create mode 100644 docsite/source/_static/.gitkeep diff --git a/NEWS.rst b/NEWS.rst index 843998b..dbd1d38 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -49,6 +49,14 @@ Breaking Changes ``setup.py`` and ``setup.py.in`` are gone. Installation is ``pip install bitmath``. Source builds use ``python -m build``. +**Byte and Bit display names** + ``Byte`` and ``Bit`` now display as ``B`` and ``b`` respectively, + matching the abbreviated style of every other unit. Code that + compares formatted strings (e.g. ``"{unit}"`` in a format template + or the output of ``str()`` / ``repr()``) against the literal words + ``"Byte"`` or ``"Bit"`` will need to be updated. The class names + themselves are unchanged. + Library Improvements ==================== diff --git a/bitmath/__init__.py b/bitmath/__init__.py index 8690804..303b9c7 100644 --- a/bitmath/__init__.py +++ b/bitmath/__init__.py @@ -1022,7 +1022,7 @@ def __abs__(self): class Byte(Bitmath): """Byte based types fundamentally operate on self._bit_value""" def _setup(self): - return (2, 0, 'Byte', 'Bytes') + return (2, 0, 'B', 'B') ###################################################################### # NIST Prefixes for Byte based types @@ -1167,7 +1167,7 @@ def _set_prefix_value(self): self.prefix_value = self._to_prefix_value(self._bit_value) def _setup(self): - return (2, 0, 'Bit', 'Bits') + return (2, 0, 'b', 'b') def _norm(self, value): """Normalize the input value into the fundamental unit for this prefix @@ -1584,7 +1584,7 @@ def parse_string(s, system=NIST, strict=True): val, unit = s[:index], s[index:] # Explicit base-unit and word-form checks: handle B, b, bit(s), - # byte(s) before the prefix-normalisation logic below. + # byte(s) before the prefix-normalization logic below. _unit_lower = unit.lower() if unit == 'B' or _unit_lower in ('byte', 'bytes'): return Byte(float(val)) diff --git a/docsite/source/_static/.gitkeep b/docsite/source/_static/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docsite/source/instances.rst b/docsite/source/instances.rst index ce97f2c..803add6 100644 --- a/docsite/source/instances.rst +++ b/docsite/source/instances.rst @@ -109,7 +109,7 @@ bitmath objects have several instance attributes: >>> b = bitmath.Byte(1337) >>> print(b.unit) - Byte + B .. py:attribute:: BitMathInstance.unit_plural @@ -119,7 +119,7 @@ bitmath objects have several instance attributes: >>> b = bitmath.Byte(1337) >>> print(b.unit_plural) - Bytes + B .. py:attribute:: BitMathInstance.unit_singular @@ -130,7 +130,7 @@ bitmath objects have several instance attributes: >>> b = bitmath.Byte(1337) >>> print(b.unit_singular) - Byte + B **Notes:** @@ -197,7 +197,7 @@ classes. You can even ``to_THING()`` an instance into itself again: >>> six_TB = TB(6) >>> six_TB_in_bits = six_TB.to_Bit() >>> print(six_TB, six_TB_in_bits) - 6.0 TB 4.8e+13 Bit + 6.0 TB 4.8e+13 b >>> six_TB == six_TB_in_bits True @@ -252,16 +252,16 @@ even easier to read. ... print("Rate: %s/second" % Bit(_rate)) ... time.sleep(1) - Rate: 100.0 Bit/sec - Rate: 24000.0 Bit/sec - Rate: 1024.0 Bit/sec - Rate: 60151.0 Bit/sec - Rate: 33.0 Bit/sec - Rate: 9999.0 Bit/sec - Rate: 9238742.0 Bit/sec - Rate: 2.09895849555e+13 Bit/sec - Rate: 934098021.0 Bit/sec - Rate: 934894.0 Bit/sec + Rate: 100.0 b/sec + Rate: 24000.0 b/sec + Rate: 1024.0 b/sec + Rate: 60151.0 b/sec + Rate: 33.0 b/sec + Rate: 9999.0 b/sec + Rate: 9238742.0 b/sec + Rate: 2.09895849555e+13 b/sec + Rate: 934098021.0 b/sec + Rate: 934894.0 b/sec And now using a custom formatting definition: @@ -271,11 +271,11 @@ And now using a custom formatting definition: ... print(Bit(_rate).best_prefix().format("Rate: {value:.3f} {unit}/sec")) ... time.sleep(1) - Rate: 12.500 Byte/sec + Rate: 12.500 B/sec Rate: 2.930 KiB/sec - Rate: 128.000 Byte/sec + Rate: 128.000 B/sec Rate: 7.343 KiB/sec - Rate: 4.125 Byte/sec + Rate: 4.125 B/sec Rate: 1.221 KiB/sec Rate: 1.101 MiB/sec Rate: 2.386 TiB/sec @@ -304,7 +304,7 @@ bitmath instances come with a verbose built-in string representation: >>> leet_bits = Bit(1337) >>> print(leet_bits) - 1337.0 Bit + 1337.0 b However, for instances which aren't whole numbers (as in ``MiB(1/3.0) == 0.333333333333 MiB``, etc), their representation can be undesirable. @@ -535,7 +535,7 @@ classes. Under the covers these properties call ``to_THING``. >>> six_TB = TB(6) >>> print(six_TB, six_TB.Bit) - 6.0 TB 4.8e+13 Bit + 6.0 TB 4.8e+13 b >>> six_TB == six_TB.Bit True diff --git a/docsite/source/module.rst b/docsite/source/module.rst index d4062fd..f5fe8a9 100644 --- a/docsite/source/module.rst +++ b/docsite/source/module.rst @@ -56,7 +56,7 @@ bitmath.getsize() >>> import bitmath >>> print(bitmath.getsize('./bitmath/__init__.py', bestprefix=False)) - 34159.0 Byte + 34159.0 B Recall, the default for representation is with the best human-readable prefix. We can control the prefix system used by @@ -208,7 +208,7 @@ bitmath.listdir() >>> print(discovered_files) [Byte(1337.0), Byte(13370.0)] >>> print(reduce(lambda x,y: x+y, discovered_files)) - 14707.0 Byte + 14707.0 B >>> print(reduce(lambda x,y: x+y, discovered_files).best_prefix()) 14.3623046875 KiB >>> print(reduce(lambda x,y: x+y, discovered_files).best_prefix().format("{value:.3f} {unit}")) @@ -721,9 +721,9 @@ bitmath.format() None of the following will be pluralized, because that feature is turned off - One unit of 'Bit': 1.0 Bit + One unit of 'Bit': 1.0 b - 0 of a unit is typically said pluralized in US English: 0.0 Byte + 0 of a unit is typically said pluralized in US English: 0.0 B several items of a unit will always be pluralized in normal US English speech: 42.0 kb @@ -733,9 +733,9 @@ bitmath.format() Now, we'll use the bitmath.format() context manager to print the same test string, but with pluralization enabled. - One unit of 'Bit': 1.0 Bit + One unit of 'Bit': 1.0 b - 0 of a unit is typically said pluralized in US English: 0.0 Bytes + 0 of a unit is typically said pluralized in US English: 0.0 B several items of a unit will always be pluralized in normal US English speech: 42.0 kbs @@ -754,13 +754,13 @@ bitmath.format() >>> import bitmath >>> print("Some instances: %s, %s" % (bitmath.KiB(1 / 3.0), bitmath.Bit(512))) - Some instances: 0.333333333333 KiB, 512.0 Bit + Some instances: 0.333333333333 KiB, 512.0 b >>> with bitmath.format("{value:e}-{unit}"): ... print("Some instances: %s, %s" % (bitmath.KiB(1 / 3.0), bitmath.Bit(512))) ... - Some instances: 3.333333e-01-KiB, 5.120000e+02-Bit + Some instances: 3.333333e-01-KiB, 5.120000e+02-b >>> print("Some instances: %s, %s" % (bitmath.KiB(1 / 3.0), bitmath.Bit(512))) - Some instances: 0.333333333333 KiB, 512.0 Bit + Some instances: 0.333333333333 KiB, 512.0 b .. versionadded:: 1.0.8 @@ -805,7 +805,7 @@ behavior. >>> from bitmath import * >>> print(MiB(1337), kb(0.1234567), Byte(0)) - 1337.0 MiB 0.1234567 kb 0.0 Byte + 1337.0 MiB 0.1234567 kb 0.0 B We can make these instances print however we want to. Let's wrap each one in square brackets (``[``, ``]``), replace the separating diff --git a/tests/test_context_manager.py b/tests/test_context_manager.py index c59bcb0..1c71a9d 100644 --- a/tests/test_context_manager.py +++ b/tests/test_context_manager.py @@ -47,7 +47,7 @@ def test_with_format(self): ] str_reps = [ - "101.00-Byte", + "101.00-B", "202.00-KiB", "303.00-MB", "404.00-GiB", @@ -68,7 +68,7 @@ def test_with_format(self): def test_print_byte_plural(self): """Byte(3.0) prints out units in plural form""" - expected_result = "3Bytes" + expected_result = "3B" fmt_str = "{value:.1g}{unit}" three_Bytes = bitmath.Byte(3.0) @@ -78,7 +78,7 @@ def test_print_byte_plural(self): def test_print_byte_plural_fmt_in_mgr(self): """Byte(3.0) prints out units in plural form, setting the fmt str in the mgr""" - expected_result = "3Bytes" + expected_result = "3B" with bitmath.format(fmt_str="{value:.1g}{unit}", plural=True): three_Bytes = bitmath.Byte(3.0) diff --git a/tests/test_context_manager_thread_safe.py b/tests/test_context_manager_thread_safe.py index cfad417..c6481b5 100644 --- a/tests/test_context_manager_thread_safe.py +++ b/tests/test_context_manager_thread_safe.py @@ -93,12 +93,12 @@ def plural_worker(expect_plural): with bitmath.format(plural=expect_plural): barrier.wait() result = str(bitmath.Byte(3.0)) - if expect_plural and result != "3.0 Bytes": + if expect_plural and result != "3.0 B": errors.put(AssertionError( - "plural thread: expected '3.0 Bytes', got %r" % result)) - elif not expect_plural and result != "3.0 Byte": + "plural thread: expected '3.0 B', got %r" % result)) + elif not expect_plural and result != "3.0 B": errors.put(AssertionError( - "singular thread: expected '3.0 Byte', got %r" % result)) + "singular thread: expected '3.0 B', got %r" % result)) except Exception as exc: errors.put(exc) diff --git a/tests/test_representation.py b/tests/test_representation.py index d4ea7b1..86a4dcb 100644 --- a/tests/test_representation.py +++ b/tests/test_representation.py @@ -142,7 +142,7 @@ def test_change_format_string(self): def test_print_byte_singular(self): """Byte(1.0) prints out units in singular form""" - expected_result = "1Byte" + expected_result = "1B" fmt_str = "{value:.2g}{unit}" one_Byte = bitmath.Byte(1.0) actual_result = one_Byte.format(fmt_str)