7. Integration Examples

The following are self-contained, copy-paste examples showing how to use bitmath with popular third-party libraries. These libraries are not installed by bitmath — install them separately before use.

7.1. argparse

The argparse module (part of the Python standard library) accepts command-line arguments as strings by default. The type parameter of add_argument() lets you supply a callable that converts a raw string into whatever type your application needs.

The snippet below defines a BitmathType callable and registers it as the type for a --block-size option so that users can write values like --block-size 10MiB and receive a bitmath.MiB object directly.

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():
    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()
    print(f"Block size: {args.block_size}")
    print(f"In KiB:     {args.block_size.to_KiB():.2f}")


if __name__ == "__main__":
    main()

Example run:

$ python script.py --block-size 10MiB
Block size: 10.0 MiB
In KiB:     10240.00 KiB

$ python script.py --block-size bad
error: argument --block-size: 'bad' is not a recognized bitmath unit string (examples: 10MiB, 1.5GiB, 500kB)

7.1.1. 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 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.

 1import argparse
 2import bitmath
 3
 4
 5def BitmathType(value):
 6    """Convert a command-line string such as '10MiB' into a bitmath object."""
 7    try:
 8        return bitmath.parse_string(value)
 9    except ValueError:
10        raise argparse.ArgumentTypeError(
11            f"{value!r} is not a recognized bitmath unit string "
12            "(examples: 10MiB, 1.5GiB, 500kB)"
13        )
14
15
16def main():
17    max_block_size = bitmath.GiB(1)
18    parser = argparse.ArgumentParser(
19        description="Example script using a bitmath argument type"
20    )
21    parser.add_argument(
22        "--block-size",
23        type=BitmathType,
24        required=True,
25        help="Block size with unit, e.g. 10MiB",
26    )
27    args = parser.parse_args()
28
29    if args.block_size > bitmath.GiB(1):
30        raise ValueError(f"Provided block size {args.block_size} exceeds maximum {max_block_size}")
31
32    print(f"Block size: {args.block_size}")
33    print(f"In KiB:     {args.block_size.to_KiB():.2f}")
34
35
36if __name__ == "__main__":
37    main()

Example run:

$ python script.py --block-size 42GiB
Traceback (most recent call last):
File "script.py", line 37, in <module>
    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

7.2. click

click is a popular command-line interface toolkit. Custom parameter types are implemented by subclassing click.ParamType and overriding convert().

Install click before use:

pip install click
import click
import bitmath


class BitmathParamType(click.ParamType):
    """A click parameter type that accepts bitmath unit strings."""

    name = "SIZE"

    def convert(self, value, param, ctx):
        if isinstance(value, bitmath.Bitmath):
            return value
        try:
            return bitmath.parse_string(value)
        except ValueError:
            self.fail(
                f"{value!r} is not a recognized bitmath unit string "
                "(examples: 10MiB, 1.5GiB, 500kB)",
                param,
                ctx,
            )


BITMATH = BitmathParamType()


@click.command()
@click.option(
    "--block-size",
    type=BITMATH,
    required=True,
    help="Block size with unit, e.g. 10MiB",
)
def main(block_size):
    """Example command using a bitmath click parameter type."""
    click.echo(f"Block size: {block_size}")
    click.echo(f"In KiB:     {block_size.to_KiB():.2f}")


if __name__ == "__main__":
    main()

Example run:

$ python script.py --block-size 10MiB
Block size: 10.0 MiB
In KiB:     10240.00 KiB

$ python script.py --block-size bad
Error: Invalid value for '--block-size': 'bad' is not a recognized bitmath unit string (examples: 10MiB, 1.5GiB, 500kB)

7.3. progressbar2

progressbar2 is a flexible terminal progress-bar library. The example below defines a custom widget that displays a data-transfer speed (bytes per second) in a human-readable bitmath unit, and demonstrates it with a simulated file download.

Install progressbar2 before use:

pip install progressbar2
import time
import progressbar
import bitmath


class DataTransferSpeed(progressbar.widgets.FormatWidgetMixin,
                        progressbar.widgets.TimeSensitiveMixin):
    """Display transfer speed as a human-readable bitmath value per second."""

    def __call__(self, progress, data, **kwargs):
        elapsed = data.get("seconds_elapsed") or 0
        if elapsed <= 0 or data.get("value") is None:
            return "?? B/s"
        bytes_done = data["value"]
        speed = bitmath.Byte(bytes_done / elapsed).best_prefix()
        return f"{speed:.2f}/s"


def simulate_download(total_bytes):
    widgets = [
        "Downloading: ",
        progressbar.Bar(),
        " ",
        progressbar.Percentage(),
        "  ",
        DataTransferSpeed(),
        " ",
        progressbar.ETA(),
    ]
    with progressbar.ProgressBar(
        max_value=total_bytes, widgets=widgets
    ) as bar:
        received = 0
        chunk = total_bytes // 50
        while received < total_bytes:
            time.sleep(0.05)
            received = min(received + chunk, total_bytes)
            bar.update(received)


if __name__ == "__main__":
    # Simulate a 100 MiB download
    simulate_download(int(bitmath.MiB(100).to_Byte()))

Example run:

Downloading: |####################| 100%  18.32 MiB/s ETA:  0:00:00