From c72f297fa0b694302f1a6c66726cec7b7253347e Mon Sep 17 00:00:00 2001 From: Tim Case Date: Tue, 21 Apr 2026 19:34:19 -0500 Subject: [PATCH] Refreshing docs --- README.rst | 2 +- bitmath/__init__.py | 4 +- docsite/source/appendices/mixed_math.rst | 78 +++++++++++++++++------ docsite/source/index.rst | 2 +- docsite/source/integration_examples.rst | 81 ++++++++++++++++++++++-- docsite/source/module.rst | 24 ++++++- docsite/source/simple_examples.rst | 46 ++++++++++++++ 7 files changed, 206 insertions(+), 31 deletions(-) diff --git a/README.rst b/README.rst index a5776b2..14f74bb 100644 --- a/README.rst +++ b/README.rst @@ -433,7 +433,7 @@ chapter of the documentation. return bitmath.parse_string(value) except ValueError: raise argparse.ArgumentTypeError( - f"{value!r} is not a recognised bitmath unit string" + f"{value!r} is not a recognized bitmath unit string" ) parser = argparse.ArgumentParser() diff --git a/bitmath/__init__.py b/bitmath/__init__.py index 303b9c7..a2edaed 100644 --- a/bitmath/__init__.py +++ b/bitmath/__init__.py @@ -1516,8 +1516,8 @@ def parse_string(s, system=NIST, strict=True): .. versionchanged:: 2.0.0 Added ``strict`` and ``system`` parameters. When ``strict=True`` - (default) behaviour is identical to the original function. - When ``strict=False`` the behaviour of the former + (default) behavior is identical to the original function. + When ``strict=False`` the behavior of the former ``parse_string_unsafe`` is applied. The ``system`` parameter defaults to ``bitmath.NIST`` and is ignored when ``strict=True``. """ diff --git a/docsite/source/appendices/mixed_math.rst b/docsite/source/appendices/mixed_math.rst index 6cf9eb2..c3cf66e 100644 --- a/docsite/source/appendices/mixed_math.rst +++ b/docsite/source/appendices/mixed_math.rst @@ -69,37 +69,57 @@ operation. The result will be of the type of the LHS. *Multiplication* - Supported, but yields strange results. + Supported, but yields results which may not be intuitive. The math is + performed at the byte-level (ignoring prefix units). The result is in the + unit of the LHS. Technically speaking, if the LHS of the equation is a byte + unit, then the result should be squared. You are advised to call the + :py:meth:`best_prefix` method on the result to get a useful value back. .. code-block:: python - :linenos: - :emphasize-lines: 6,9 - In [10]: first = MiB(5) + >>> bitmath.kB(3).bytes, bitmath.MiB(5).bytes + (3000.0, 5242880.0) + >>> bitmath.best_prefix(3000 * 5242880) + GiB(14.6484375) - In [11]: second = kB(2) + >>> bitmath.Byte(3000) * bitmath.MiB(5) + B(15728640000.0) - In [12]: first * second - Out[12]: MiB(10000.0) + >>> (bitmath.Byte(3000) * bitmath.MiB(5)).best_prefix() + GiB(14.6484375) - In [13]: (first * second).best_prefix() - Out[13]: GiB(9.765625) +The final result represents how many “GiB-sized” chunks of bytes are in that +total. It's weird, but it works. -As we can see on lines **6** and **9**, multiplying even two -relatively small quantities together (``MiB(5)`` and ``kB(2)``) yields -quite large results. +If the LHS is larger than a byte prefix unit then you get a result matching the +unit of the LHS of the equation. -Internally, this is implemented as: - -.. math:: +.. code-block:: python - (5 \cdot 2^{20}) \cdot (2 \cdot 10^{3}) = 10,485,760,000 B + >>> bitmath.MiB(5) * bitmath.kB(3) + MiB(15000.0) + # LHS was MiB, result is MiB - 10,485,760,000 B \cdot \dfrac{1 MiB}{1,048,576 B} = 10,000 MiB + # And if we wrap it with best_prefix() we get the earlier result back + >>> (bitmath.MiB(5) * bitmath.kB(3)).best_prefix() + GiB(14.6484375) *Division* The result will be a number type due to unit cancellation. +.. code-block:: python + + >>> bitmath.kB(3) / bitmath.MiB(5) + 0.00057220458984375 + + >>> bitmath.kB(3).bytes / bitmath.MiB(5).bytes + 0.00057220458984375 + +Above you can see that the math is performed on the bytes of each operand. As +noted above, the units cancel out. This is the opposite of the multiplication +case where the units square together if you do not coerce them into a larger +prefix unit. + .. _appendix_math_mixed_types: Mixed Types: Addition and Subtraction @@ -123,7 +143,7 @@ function works correctly with iterables of bitmath objects, since >>> sum([bitmath.Byte(1), bitmath.MiB(1), bitmath.GiB(1)]) Byte(1074790401.0) -For all non-zero numeric operands the behaviour (returning a number) +For all non-zero numeric operands the behavior (returning a number) applies. **Discussion:** Why do ``100 - KiB(90)`` and ``KiB(100) - 90`` both @@ -251,7 +271,25 @@ yourself what you would expect to get if you did this: .. math:: - \dfrac{100}{kB(33)} = x + \dfrac{100}{kB(33)} + +Unless you're representing rates that doesn't mean much at all. The units of +operands when expressing rates is going to be context sensitive and can be very +non-intuitive without additional knowledge. For example: + +.. code-block:: python + + >>> 100/bitmath.kB(33) + 3.0303030303030303 + +This is functionally equivalent to writing: + +.. math:: + + \dfrac{1}{kB(33)} \cdot 100 + +This might mean something to you, but we can't express that as a prefix unit. We +let you do it, but it is up to you to determine the significance of the result. @@ -326,6 +364,6 @@ Footnotes `_ .. [#datamodel] `Python Datamodel Customization Methods - `_ + `_ .. [#significance] https://en.wikipedia.org/wiki/Significance_arithmetic diff --git a/docsite/source/index.rst b/docsite/source/index.rst index 6d04939..957e8d4 100644 --- a/docsite/source/index.rst +++ b/docsite/source/index.rst @@ -378,7 +378,7 @@ chapter of the documentation. return bitmath.parse_string(value) except ValueError: raise argparse.ArgumentTypeError( - f"{value!r} is not a recognised bitmath unit string" + f"{value!r} is not a recognized bitmath unit string" ) parser = argparse.ArgumentParser() diff --git a/docsite/source/integration_examples.rst b/docsite/source/integration_examples.rst index 6dcaa0a..8caf1ba 100644 --- a/docsite/source/integration_examples.rst +++ b/docsite/source/integration_examples.rst @@ -40,7 +40,7 @@ directly. return bitmath.parse_string(value) except ValueError: raise argparse.ArgumentTypeError( - f"{value!r} is not a recognised bitmath unit string " + f"{value!r} is not a recognized bitmath unit string " "(examples: 10MiB, 1.5GiB, 500kB)" ) @@ -72,7 +72,80 @@ Example run: In KiB: 10240.00 KiB $ python script.py --block-size bad - error: argument --block-size: 'bad' is not a recognised bitmath unit string (examples: 10MiB, 1.5GiB, 500kB) + error: argument --block-size: 'bad' is not a recognized bitmath unit string (examples: 10MiB, 1.5GiB, 500kB) + +argparse validation +=================== + +Now say you want to perform some additional validation on this custom +``BitmathType`` unit. This is best done **after** the parsing is complete, not +as part of the ``BitmathType`` implementation. This follows the advice outlined +in the upstream :mod:`argparse` documentation. + + In general, the ``type`` keyword is a convenience that should only be used for + simple conversions that can only raise one of the three supported exceptions. + Anything with more interesting error-handling or resource management should be + done downstream after the arguments are parsed. + +First, parse the unit, allow ``BitmathType`` to handle the parsing validation. +Second, perform your own context-aware validation. For example, you might set +minimum or maximums and need to compare the parsed argument against them. + +.. code-block:: python + :linenos: + :emphasize-lines: 17,29-30 + + import argparse + import bitmath + + + def BitmathType(value): + """Convert a command-line string such as '10MiB' into a bitmath object.""" + try: + return bitmath.parse_string(value) + except ValueError: + raise argparse.ArgumentTypeError( + f"{value!r} is not a recognized bitmath unit string " + "(examples: 10MiB, 1.5GiB, 500kB)" + ) + + + def main(): + max_block_size = bitmath.GiB(1) + parser = argparse.ArgumentParser( + description="Example script using a bitmath argument type" + ) + parser.add_argument( + "--block-size", + type=BitmathType, + required=True, + help="Block size with unit, e.g. 10MiB", + ) + args = parser.parse_args() + + if args.block_size > bitmath.GiB(1): + raise ValueError(f"Provided block size {args.block_size} exceeds maximum {max_block_size}") + + print(f"Block size: {args.block_size}") + print(f"In KiB: {args.block_size.to_KiB():.2f}") + + + if __name__ == "__main__": + main() + +Example run: + +.. code-block:: bash + + $ python script.py --block-size 42GiB + Traceback (most recent call last): + File "script.py", line 37, in + main() + ~~~~^^ + File "script.py", line 30, in main + raise ValueError(f"Provided block size {args.block_size} exceeds maximum {max_block_size}") + ValueError: Provided block size 42.0 GiB exceeds maximum 1.0 GiB + .. _integration_examples_click: @@ -108,7 +181,7 @@ Install click before use: return bitmath.parse_string(value) except ValueError: self.fail( - f"{value!r} is not a recognised bitmath unit string " + f"{value!r} is not a recognized bitmath unit string " "(examples: 10MiB, 1.5GiB, 500kB)", param, ctx, @@ -143,7 +216,7 @@ Example run: In KiB: 10240.00 KiB $ python script.py --block-size bad - Error: Invalid value for '--block-size': 'bad' is not a recognised bitmath unit string (examples: 10MiB, 1.5GiB, 500kB) + Error: Invalid value for '--block-size': 'bad' is not a recognized bitmath unit string (examples: 10MiB, 1.5GiB, 500kB) .. _integration_examples_progressbar2: diff --git a/docsite/source/module.rst b/docsite/source/module.rst index f5fe8a9..97d9b0e 100644 --- a/docsite/source/module.rst +++ b/docsite/source/module.rst @@ -153,6 +153,24 @@ bitmath.listdir() The **total** size of the files in this tree is **1337 + 13370 = 14707** bytes. + .. versionadded:: 2.0.0 + + By far the simplest way to sum all of the results is using the built-in + :py:func:`sum` function, or :py:func:`bitmath.sum` for additional control + (complete docs on that following this section). + + .. code-block:: python + + >>> discovered_files = [f[1] for f in bitmath.listdir('./some_files')] + >>> print(discovered_files) + [Byte(1337.0), Byte(13370.0)] + >>> print(sum(discovered_files)) + 14707.0 B + >>> print(sum(discovered_files).best_prefix()) + 14.3623046875 KiB + + + Let's call :py:func:`bitmath.listdir` on the ``some_files/`` directory and see what the results look like. First we'll use all the default parameters, then we'll set ``relpath`` to ``True``: @@ -391,7 +409,7 @@ bitmath.parse_string() .. versionchanged:: 2.0.0 Added ``strict`` and ``system`` parameters. The default - ``strict=True`` behaviour is identical to earlier versions. + ``strict=True`` behavior is identical to earlier versions. ``system`` defaults to :py:data:`bitmath.NIST` and is only consulted when ``strict=False``. @@ -406,7 +424,7 @@ parse_string with ``strict=False`` When ``strict=False`` the parser accepts ambiguous input that does not conform to exact bitmath type names — for example, the single-letter units produced by tools like ``ls -h``, ``df``, and ``qemu-img``. This -is the behaviour previously provided by the now-deprecated +is the behavior previously provided by the now-deprecated :py:func:`bitmath.parse_string_unsafe`. All inputs are treated as **byte-based**. Bit-based units are not @@ -524,7 +542,7 @@ bitmath.parse_string_unsafe() .. function:: parse_string_unsafe(repr[, system=bitmath.NIST]) A deprecated thin wrapper around - ``parse_string(repr, strict=False, system=system)``. All behaviour, + ``parse_string(repr, strict=False, system=system)``. All behavior, parameters, and caveats are identical to :ref:`parse_string with strict=False `. diff --git a/docsite/source/simple_examples.rst b/docsite/source/simple_examples.rst index 14e8d15..5a421c9 100644 --- a/docsite/source/simple_examples.rst +++ b/docsite/source/simple_examples.rst @@ -141,6 +141,52 @@ Unit Conversion >>> fourty_two_mib.KiB KiB(43008.0) +Let's convert a unit and show the differences. What happens if we convert ``2048 +MB`` into GiBs? Let's look the MB unit in closer detail: + +.. code-block:: python + + >>> from bitmath import * + >>> MB(2048).base, MB(2048).power, MB(2048).bytes + (10, 6, 2048000000.0) + +An MB (megabyte) is an SI unit in the base-10 number system. A single megabyte +is 10 raised to the power of 6. When you raise 10 to the power of 6 and multiply +the result by 2048 you get the number of bytes in ``MB(2048)``. We can check +this by creating a byte object with that many bytes as the value and ask for the +``MB`` equivalent: + +.. code-block:: python + + >>> bitmath.Byte((10**6)*2048).MB + MB(2048.0) + +What about that conversion though, how about we convert this into GiBs? Those +are NIST units, it is a base-2 number system. Let's convert the MB to a GiB and +look at those instance attributes again: + +.. code-block:: python + + >>> convert_demo = bitmath.MB(2048).GiB + >>> convert_demo + GiB(1.9073486328125) + >>> convert_demo.base, convert_demo.power, convert_demo.bytes + (2, 30, 2048000000.0) + +We can see that the entire calculation has changed but the number of bytes has +remained the same. Now we have 2 raised to the power of 6, times +1.9073486328125. + +A cleaner looking conversion is possible if we convert this within the same unit +system. If we look at the GB equivalent we get: + +.. code-block:: python + + >>> convert_demo.GB + GB(2.048) + +Because we aren't shifting between different base number systems. + Rich Comparison ***************