Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
uses: actions/checkout@v6.0.2

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
uses: actions/setup-python@v6.2.0
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
Expand Down
6 changes: 3 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

## Project Overview

**bitmath** is a pure-Python library (no external runtime dependencies) for representing and converting file sizes across SI (decimal) and NIST (binary) unit systems. It supports arithmetic, rich comparisons, bitwise ops, parsing, formatting, and f-string/format() support.
**bitmath** is a pure-Python library (no external runtime dependencies) for representing and converting file sizes across SI (decimal) and NIST (binary) unit systems. It supports arithmetic (including floor division, modulo, and `divmod` for capacity math), rich comparisons, bitwise ops, parsing, formatting, and f-string/format() support.

## Project Direction
bitmath has been around for almost 12 years, and over that lifetime it promised to deliver backwards compatibility. It delivered on that promise and gathered a strong supporting of people and eventual "critical infrastructure" project status on the PyPI.org website.
Expand All @@ -20,7 +20,7 @@ Phases 1 (maintenance 1.4.0) and 2 (bitmath 2.0.0) are complete. The project:

- Supports **Python 3.9 and newer only** (`requires-python = ">=3.9"` in `pyproject.toml`)
- Uses `hatchling` as the build backend (replaces `setup.py`)
- Uses `pytest` as the test runner (292 tests, 99% coverage — one branch in `system` property intentionally uncovered)
- Uses `pytest` as the test runner (303 tests). Coverage is high but platform-sensitive: the `query_device_capacity` branches for the *other* OS are naturally uncovered on any single run.
- Is published on PyPI as version 2.0.0
- Drop-in compatible with the 1.x public API

Expand Down Expand Up @@ -71,5 +71,5 @@ All unit values are normalized to bits internally; conversion between units happ
- Test runner: `pytest`
- All tests are in `tests/` as `test_*.py` files
- Test case names must be unique across the suite — enforced by `tests/test_unique_testcase_names.sh`
- Coverage: 99% (one branch in `system` property intentionally uncovered)
- Coverage is platform-sensitive: Windows and POSIX `query_device_capacity` paths only run on their respective OS
- `unittest.mock` (stdlib) is used for patching in integration tests
19 changes: 19 additions & 0 deletions NEWS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -142,13 +142,32 @@ still works exactly the same way. What 2.0.0 adds on top of that:
NIST is the tiebreaker. Closes `issue #54
<https://github.com/timlnx/bitmath/issues/54>`_.

**Floor division, modulo, and divmod for capacity math**
bitmath objects now implement ``__floordiv__`` (``//``),
``__mod__`` (``%``), and ``__divmod__`` — useful for
chunk-and-remainder capacity planning (*"how many N-sized chunks
fit into this device, and how much is left over?"*).
``bm1 // bm2`` returns an ``int`` (count of whole divisions),
mirroring how ``bm1 / bm2`` returns a unitless ratio.
``bm1 % bm2`` and ``divmod(bm1, bm2)`` return remainders as
bitmath objects of the **left-hand operand's type**, consistent
with every other bitmath arithmetic operator. The identity
``(a // b) * b + (a % b) == a`` holds. See :ref:`capacity_math`
for worked examples including ``best_prefix()`` coercion and
``bitmath.format`` context-manager integration.


Project Infrastructure
======================

The project infrastructure has been rebuilt to reflect how Python
projects are actually maintained in 2026:

**Project Security**
GitHub now has branch protection enabled. Releases are signed with
the maintainers `GPG key
<https://keys.openpgp.org/search?q=bitmath%40lnx.cx>`_.

**Packaging**
``pyproject.toml`` with a hatchling backend replaces the old
``setup.py``/``setup.py.in`` template system. The package is
Expand Down
48 changes: 48 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ focusing on file size unit conversion, functionality now includes:
* Full NIST unit coverage including **ZiB**, **YiB**, **Zib**, and **Yib**
* Automatic human-readable prefix selection (like in `hurry.filesize <https://pypi.python.org/pypi/hurry.filesize>`_)
* Basic arithmetic operations (subtracting 42KiB from 50GiB)
* Capacity math with floor division, modulo, and ``divmod`` (``GiB(1) // MiB(300)``, ``GiB(1) % MiB(300)``)
* Rich comparison operations (``1024 Bytes == 1KiB``)
* Bitwise operations (``<<``, ``>>``, ``&``, ``|``, ``^``)
* Rounding via ``math.floor``, ``math.ceil``, and ``round``
Expand Down Expand Up @@ -207,6 +208,53 @@ Arithmetic
2457.6


Capacity Planning
-----------------

Floor division (``//``), modulo (``%``), and ``divmod()`` are handy for
chunk-and-remainder capacity math. ``bm1 // bm2`` returns an ``int``
(how many whole chunks fit); ``bm1 % bm2`` returns a ``bitmath`` of the
**left-hand operand's type** (the leftover).

.. code-block:: python

>>> from bitmath import GiB, MiB, TiB
>>> disk = GiB(1)
>>> chunk = MiB(300)

>>> disk // chunk # how many whole 300 MiB chunks fit?
3
>>> disk % chunk # leftover, typed as the LHS (GiB)
GiB(0.12109375)
>>> divmod(disk, chunk) # both at once
(3, GiB(0.12109375))

Re-express the remainder in a human-readable unit with
``best_prefix()`` (or coerce directly with ``to_MiB()``, etc.):

.. code-block:: python

>>> (GiB(1) % MiB(300)).best_prefix()
MiB(124.0)

Pair with the ``bitmath.format`` context manager for clean reporting
across a block of capacity calculations:

.. code-block:: python

>>> import bitmath
>>> volume = TiB(1)
>>> block = GiB(7)
>>> with bitmath.format(fmt_str="{value:.2f} {unit}", bestprefix=True):
... whole, leftover = divmod(volume, block)
... print(f"{whole} whole blocks of {block} fit in {volume}")
... print(f"leftover: {leftover}")
146 whole blocks of 7.00 GiB fit in 1.00 TiB
leftover: 2.00 GiB

The identity ``(a // b) * b + (a % b) == a`` holds, so ``divmod`` round-trips.


Convert Units
-------------

Expand Down
39 changes: 31 additions & 8 deletions bitmath/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -858,17 +858,40 @@ def __truediv__(self, other):
# bm1 / bm2
return self._byte_value / float(other.bytes)

# def __floordiv__(self, other):
# return NotImplemented
def __floordiv__(self, other):
"""Floor division: Supported operations with result types:

# def __mod__(self, other):
# return NotImplemented
- bm1 // bm2 = int (whole divisions, unitless — mirrors bm1 / bm2 returning a ratio)
- bm // num = bm (LHS type)
"""
if isinstance(other, numbers.Number):
# bm // num
result = self._byte_value // other
return (type(self))(bytes=result)
else:
# bm1 // bm2
return int(self._byte_value // other.bytes)

# def __divmod__(self, other):
# return NotImplemented
def __mod__(self, other):
"""Modulo (remainder): Supported operations with result types:

# def __pow__(self, other, modulo=None):
# return NotImplemented
- bm1 % bm2 = bm (LHS type) — remainder after floor-dividing bm1 by bm2
- bm % num = bm (LHS type)
"""
if isinstance(other, numbers.Number):
# bm % num
result = self._byte_value % other
return (type(self))(bytes=result)
else:
# bm1 % bm2
return (type(self))(bytes=self._byte_value % other.bytes)

def __divmod__(self, other):
"""divmod(bm, other) == (bm // other, bm % other).

Result types match __floordiv__ and __mod__.
"""
return (self.__floordiv__(other), self.__mod__(other))

##################################################################

Expand Down
48 changes: 48 additions & 0 deletions docsite/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ focusing on file size unit conversion, functionality now includes:
* Full NIST unit coverage including **ZiB**, **YiB**, **Zib**, and **Yib**
* Automatic human-readable prefix selection (like in `hurry.filesize <https://pypi.python.org/pypi/hurry.filesize>`_)
* Basic arithmetic operations (subtracting 42KiB from 50GiB)
* Capacity math with floor division, modulo, and ``divmod`` (``GiB(1) // MiB(300)``, ``GiB(1) % MiB(300)``)
* Rich comparison operations (``1024 Bytes == 1KiB``)
* Bitwise operations (``<<``, ``>>``, ``&``, ``|``, ``^``)
* Rounding via :py:func:`math.floor`, :py:func:`math.ceil`, and :py:func:`round`
Expand Down Expand Up @@ -152,6 +153,53 @@ Arithmetic
2457.6


Capacity Planning
-----------------

Floor division (``//``), modulo (``%``), and ``divmod()`` are handy for
chunk-and-remainder capacity math. ``bm1 // bm2`` returns an ``int``
(how many whole chunks fit); ``bm1 % bm2`` returns a ``bitmath`` of the
**left-hand operand's type** (the leftover).

.. code-block:: python

>>> from bitmath import GiB, MiB, TiB
>>> disk = GiB(1)
>>> chunk = MiB(300)

>>> disk // chunk # how many whole 300 MiB chunks fit?
3
>>> disk % chunk # leftover, typed as the LHS (GiB)
GiB(0.12109375)
>>> divmod(disk, chunk) # both at once
(3, GiB(0.12109375))

Re-express the remainder in a human-readable unit with
``best_prefix()`` (or coerce directly with ``to_MiB()``, etc.):

.. code-block:: python

>>> (GiB(1) % MiB(300)).best_prefix()
MiB(124.0)

Pair with the ``bitmath.format`` context manager for clean reporting
across a block of capacity calculations:

.. code-block:: python

>>> import bitmath
>>> volume = TiB(1)
>>> block = GiB(7)
>>> with bitmath.format(fmt_str="{value:.2f} {unit}", bestprefix=True):
... whole, leftover = divmod(volume, block)
... print(f"{whole} whole blocks of {block} fit in {volume}")
... print(f"leftover: {leftover}")
146 whole blocks of 7.00 GiB fit in 1.00 TiB
leftover: 2.00 GiB

The identity ``(a // b) * b + (a % b) == a`` holds, so ``divmod`` round-trips.


Convert Units
-------------

Expand Down
1 change: 1 addition & 0 deletions docsite/source/index.rst.in
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ focusing on file size unit conversion, functionality now includes:
* Full NIST unit coverage including **ZiB**, **YiB**, **Zib**, and **Yib**
* Automatic human-readable prefix selection (like in `hurry.filesize <https://pypi.python.org/pypi/hurry.filesize>`_)
* Basic arithmetic operations (subtracting 42KiB from 50GiB)
* Capacity math with floor division, modulo, and ``divmod`` (``GiB(1) // MiB(300)``, ``GiB(1) % MiB(300)``)
* Rich comparison operations (``1024 Bytes == 1KiB``)
* Bitwise operations (``<<``, ``>>``, ``&``, ``|``, ``^``)
* Rounding via :py:func:`math.floor`, :py:func:`math.ceil`, and :py:func:`round`
Expand Down
Loading
Loading