Skip to content

Commit 80fdaca

Browse files
committed
Normalize internal representation to uniform floating-point
Finishes the Python 2 to 3 cleanup of the numeric internals. - Remove dead Py2-era float() casts from division expressions, where Python 3 true division already yields a float - Force float on the three constructor paths (bytes=, bits=, and the Bit value constructor) that previously leaked int, so .bytes and .bits return float regardless of how an instance was built - Add regression tests pinning the float invariant - Document the uniform float behavior in the Rules for Math appendix - Add the bitmath-2.1.0 NEWS section and bump VERSION to 2.1.0
1 parent 3aa1f8b commit 80fdaca

7 files changed

Lines changed: 114 additions & 15 deletions

File tree

NEWS.rst

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,80 @@ NEWS
55
:depth: 1
66
:local:
77

8+
.. _bitmath-2.1.0:
9+
10+
bitmath-2.1.0
11+
*************
12+
13+
*Unreleased*
14+
15+
bitmath 2.1.0 is a focused follow-up to the 2.0.0 modernization. It
16+
finishes the last of the Python 2 cleanup, tightens the project's
17+
quality tooling, and retires one piece of legacy API surface.
18+
19+
20+
Breaking Changes
21+
================
22+
23+
**Internal representation is uniformly floating-point**
24+
Every bitmath instance now stores its size as a 64-bit float, no
25+
matter which constructor created it. Previously the ``bytes=`` and
26+
``bits=`` keyword constructors, along with the bit-family value
27+
constructors such as ``Kib(N)``, leaked Python ``int`` values
28+
through the ``.bytes`` and ``.bits`` properties. Those properties
29+
now always return ``float``, matching the long-documented
30+
floating-point measurement design described in the :ref:`Rules for
31+
Math <appendix_math>` appendix. Equality, ordering, ``repr()``, and
32+
arithmetic results are unchanged; only code that inspected
33+
``type(instance.bytes)`` or ``type(instance.bits)`` directly will
34+
observe the difference.
35+
36+
**listdir() is deprecated**
37+
:func:`bitmath.listdir` now emits a :exc:`DeprecationWarning` on
38+
every call and will be removed in a future release. Iterate with
39+
:py:func:`os.walk` and call :func:`bitmath.getsize` directly
40+
instead. Closes `issue #27
41+
<https://github.com/timlnx/bitmath/issues/27>`_.
42+
43+
44+
Library Improvements
45+
====================
46+
47+
**pathlib support**
48+
:func:`bitmath.getsize` and :func:`bitmath.listdir` now accept
49+
:class:`pathlib.Path` objects, not just strings, for their path
50+
and ``search_base`` arguments.
51+
52+
53+
Project Infrastructure
54+
======================
55+
56+
**Linting moved to pylint**
57+
pylint replaces flake8/pyflakes across the CI workflow and the
58+
local toolchain, and the library is held at a 10.00/10 score.
59+
pycodestyle is retained for the PEP 8 whitespace checks pylint
60+
does not cover.
61+
62+
**Security scanning with bandit**
63+
bandit runs as part of ``make ci`` and as a dedicated GitHub
64+
Actions workflow that fires on every push, every pull request, and
65+
on a weekly schedule, scanning both ``bitmath/`` and ``tests/``.
66+
67+
**100% test coverage**
68+
The remaining coverage gaps were closed, including the
69+
platform-specific :func:`bitmath.query_device_capacity` branches,
70+
bringing the suite to 100% measured coverage on every supported
71+
platform.
72+
73+
**SPDX license headers**
74+
Every source and test file now carries ``SPDX-License-Identifier``
75+
and ``SPDX-FileCopyrightText`` headers.
76+
77+
**Single-sourced version**
78+
The package version is read dynamically from the ``VERSION`` file
79+
by hatchling, so bumping that one file propagates everywhere.
80+
81+
882
.. _bitmath-2.0.0:
983

1084
bitmath-2.0.0

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2.0.2
1+
2.1.0

bitmath.1

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
.\" Title: bitmath
33
.\" Author: [see the "AUTHOR" section]
44
.\" Generator: DocBook XSL Stylesheets vsnapshot <http://docbook.sf.net/>
5-
.\" Date: 05/05/2026
5+
.\" Date: 05/15/2026
66
.\" Manual: python-bitmath
7-
.\" Source: bitmath 2.0.2
7+
.\" Source: bitmath 2.1.0
88
.\" Language: English
99
.\"
10-
.TH "BITMATH" "1" "05/05/2026" "bitmath 2\&.0\&.2" "python\-bitmath"
10+
.TH "BITMATH" "1" "05/15/2026" "bitmath 2\&.1\&.0" "python\-bitmath"
1111
.\" -----------------------------------------------------------------
1212
.\" * Define some portability stuff
1313
.\" -----------------------------------------------------------------

bitmath/__init__.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -220,14 +220,14 @@ def __init__(self, value=0, bytes=None, bits=None): # pylint: disable=redefined
220220
if bytes:
221221
# We were provided with the fundamental base unit, no need
222222
# to normalize
223-
self._byte_value = bytes
223+
self._byte_value = float(bytes)
224224
self._bit_value = bytes * 8.0
225225
elif bits:
226226
# We were *ALMOST* given the fundamental base
227227
# unit. Translate it into the fundamental unit then
228228
# normalize.
229-
self._byte_value = bits / 8.0
230-
self._bit_value = bits
229+
self._byte_value = bits / 8
230+
self._bit_value = float(bits)
231231
else:
232232
# We were given a value representative of this *prefix
233233
# unit*. We need to normalize it into the number of bytes
@@ -243,7 +243,7 @@ def _set_prefix_value(self) -> None:
243243
def _to_prefix_value(self, value: float) -> float:
244244
"""Return the number of bits/bytes as they would look like if we
245245
converted *to* this unit"""
246-
return value / float(self._unit_value)
246+
return value / self._unit_value
247247

248248
def _setup(self) -> tuple:
249249
raise NotImplementedError("The base 'bitmath.Bitmath' class can not be used directly")
@@ -845,7 +845,7 @@ def __truediv__(self, other):
845845
result = self._byte_value / other
846846
return (type(self))(bytes=result)
847847
# bm1 / bm2
848-
return self._byte_value / float(other.bytes)
848+
return self._byte_value / other.bytes
849849

850850
def __floordiv__(self, other):
851851
"""Floor division: Supported operations with result types:
@@ -899,7 +899,7 @@ def __rmul__(self, other):
899899

900900
def __rtruediv__(self, other):
901901
# num / bm = num
902-
return other / float(self.value)
902+
return other / self.value
903903

904904
# Called to implement the built-in functions complex(), int(), and
905905
# float(). These return the int/float equivalent of the prefix value:
@@ -1196,8 +1196,8 @@ def _setup(self):
11961196
def _norm(self, value):
11971197
"""Normalize the input value into the fundamental unit for this prefix
11981198
type"""
1199-
self._bit_value = value * self._unit_value
1200-
self._byte_value = self._bit_value / 8.0
1199+
self._bit_value = float(value) * self._unit_value
1200+
self._byte_value = self._bit_value / 8
12011201

12021202

12031203
######################################################################

docsite/source/appendices/mixed_math.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,9 @@ Design Philosophy: Floating-Point Measurements
298298

299299
bitmath represents sizes as **floating-point measurements**, not as
300300
discrete counts of hardware bits. This is an intentional design choice.
301+
Every constructor (by unit value, by ``bytes=``, or by ``bits=``)
302+
normalizes its input to a float, so the ``bytes`` and ``bits``
303+
properties always return floating-point values.
301304

302305
A file reported as ``1.7 GiB`` is a *measurement* — the same way
303306
``2.3 miles`` or ``1.7 liters`` are measurements. Physical storage is

docsite/source/appendices/on_units.rst

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,23 +21,23 @@ a percent one "unit" of SI is to one "unit" of NIST.
2121
2222
In [16]: one_kibi = 1 * 2**10
2323
24-
In [17]: round(one_kilo / float(one_kibi), 2)
24+
In [17]: round(one_kilo / one_kibi, 2)
2525
2626
Out[17]: 0.98
2727
2828
In [18]: one_tera = 1 * 10**12
2929
3030
In [19]: one_tebi = 1 * 2**40
3131
32-
In [20]: round(one_tera / float(one_tebi), 2)
32+
In [20]: round(one_tera / one_tebi, 2)
3333
3434
Out[20]: 0.91
3535
3636
In [21]: one_exa = 1 * 10**18
3737
3838
In [22]: one_exbi = 1 * 2**60
3939
40-
In [23]: round(one_exa / float(one_exbi), 2)
40+
In [23]: round(one_exa / one_exbi, 2)
4141
4242
Out[23]: 0.87
4343

tests/test_properties.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,25 @@ def test_system_property_invalid_base_raises(self):
8282
obj._base = 7
8383
with self.assertRaises(ValueError):
8484
_ = obj.system
85+
86+
87+
class TestPropertyTypesAlwaysFloat(TestCase):
88+
"""The .bytes and .bits properties are float for every construction
89+
path. The internal representation is uniformly floating-point (see
90+
the 'Floating-Point Measurements' design philosophy appendix)."""
91+
92+
def test_bytes_property_float_from_value_constructor(self):
93+
""".bytes is float when built via the unit-value constructor"""
94+
self.assertIs(type(bitmath.KiB(1).bytes), float)
95+
96+
def test_bytes_property_float_from_bytes_kwarg(self):
97+
""".bytes is float when built via the bytes= keyword"""
98+
self.assertIs(type(bitmath.Byte(bytes=1).bytes), float)
99+
100+
def test_bits_property_float_from_bits_kwarg(self):
101+
""".bits is float when built via the bits= keyword"""
102+
self.assertIs(type(bitmath.Byte(bits=1).bits), float)
103+
104+
def test_bits_property_float_from_bit_value_constructor(self):
105+
""".bits is float when a Bit-family unit is built by value"""
106+
self.assertIs(type(bitmath.Kib(1).bits), float)

0 commit comments

Comments
 (0)