Source code for bitmath

# -*- coding: utf-8 -*-
# SPDX-License-Identifier: MIT
# The MIT License (MIT)
#
# SPDX-FileCopyrightText: 2014-2026 Tim Case <bitmath@lnx.cx>
# SPDX-FileCopyrightText: See GitHub Contributors Graph for more information
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sub-license, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# pylint: disable=line-too-long

"""Reference material:
The bitmath homepage is located at:
* https://bitmath.readthedocs.io/en/latest/

Prefixes for binary multiples:
https://physics.nist.gov/cuu/Units/binary.html

decimal and binary prefixes:
man 7 units (from the Linux Documentation Project 'man-pages' package)
"""

from __future__ import annotations

import argparse
import contextlib
import fnmatch
import math
import numbers
import os
import os.path
import pathlib
import platform
import re
import shutil
import struct
import sys
import threading
import warnings

from collections.abc import Generator, Iterable, Iterator
from typing import IO, Any, NamedTuple, Union

# For device capacity reading in query_device_capacity().
if os.name == 'posix':
    import stat
    import fcntl
elif os.name == 'nt':  # pragma: no cover
    import ctypes
    import ctypes.wintypes
    import msvcrt  # pylint: disable=import-error

#: Platforms where :func:`query_device_capacity` is supported.
#: Corresponds to possible values of :data:`os.name`. macOS (Darwin)
#: is not supported due to SIP restrictions on raw block device access.
SUPPORTED_PLATFORMS = frozenset({'posix', 'nt'})

__all__ = ['Bit', 'Byte', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB',
           'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB', 'Kib',
           'Mib', 'Gib', 'Tib', 'Pib', 'Eib', 'Zib', 'Yib', 'kb', 'Mb', 'Gb', 'Tb',
           'Pb', 'Eb', 'Zb', 'Yb', 'getsize', 'listdir', 'format',
           'format_string', 'format_plural', 'parse_string', 'parse_string_unsafe',
           'sum', 'ALL_UNIT_TYPES', 'NIST', 'NIST_PREFIXES', 'NIST_STEPS',
           'SI', 'SI_PREFIXES', 'SI_STEPS', 'Capacity', 'query_capacity',
           'query_device_capacity']

#: A list of all the valid prefix unit types. Mostly for reference,
#: also used by the CLI tool as valid types
ALL_UNIT_TYPES = ['Bit', 'Byte', 'kb', 'kB', 'Mb', 'MB', 'Gb', 'GB', 'Tb',
                  'TB', 'Pb', 'PB', 'Eb', 'EB', 'Zb', 'ZB', 'Yb',
                  'YB', 'Kib', 'KiB', 'Mib', 'MiB', 'Gib', 'GiB',
                  'Tib', 'TiB', 'Pib', 'PiB', 'Eib', 'EiB', 'Zib', 'ZiB',
                  'Yib', 'YiB']

# #####################################################################
# Set up our module variables/constants

###################################
# Internal:

# Console repr(), ex: MiB(13.37), or kB(42.0)
_FORMAT_REPR = '{unit_singular}({value})'

# ##################################
# Exposed:

#: Constants for referring to NIST prefix system
NIST = int(2)

#: Constants for referring to SI prefix system
SI = int(10)

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

#: All of the SI prefixes
SI_PREFIXES = ['k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']

#: Byte values represented by each SI prefix unit
SI_STEPS = {
    'Bit': 1 / 8,
    'Byte': 1,
    'k': 1000,
    'M': 1000000,
    'G': 1000000000,
    'T': 1000000000000,
    'P': 1000000000000000,
    'E': 1000000000000000000,
    'Z': 1000000000000000000000,
    'Y': 1000000000000000000000000
}


#: All of the NIST prefixes
NIST_PREFIXES = ['Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi']

#: Byte values represented by each NIST prefix unit
NIST_STEPS = {
    'Bit': 1 / 8,
    'Byte': 1,
    'Ki': 1024,
    'Mi': 1048576,
    'Gi': 1073741824,
    'Ti': 1099511627776,
    'Pi': 1125899906842624,
    'Ei': 1152921504606846976,
    'Zi': 1180591620717411303424,
    'Yi': 1208925819614629174706176
}

#: String representation, ex: ``13.37 MiB``, or ``42.0 kB``
format_string = "{value} {unit}"

#: Pluralization behavior
format_plural = False

# Thread-local storage for context manager overrides. When a thread is inside
# a bitmath.format() context, these shadow the module globals above for that
# thread only — other threads are unaffected.
_thread_local = threading.local()
_FMT_SENTINEL = object()  # distinguishes "not set" from any real value


def _get_format_string():
    return getattr(_thread_local, 'format_string', format_string)


def _get_format_plural():
    return getattr(_thread_local, 'format_plural', format_plural)


def _get_bestprefix():
    return getattr(_thread_local, 'bestprefix', False)


def capitalize_first(s: str) -> str:
    """Capitalize ONLY the first letter of the input `s`

* returns a copy of input `s` with the first letter capitalized
    """
    pfx = s[0].upper()
    _s = s[1:]
    return pfx + _s


######################################################################
# Base class for everything else
[docs] class Bitmath: # pylint: disable=too-many-public-methods,too-many-instance-attributes """The base class for all the other prefix classes""" # All the allowed input types valid_types: tuple[type, ...] = (int, float) def __init__(self, value=0, bytes=None, bits=None): # pylint: disable=redefined-builtin """Instantiate with `value` by the unit, in plain bytes, or bits. Don't supply more than one keyword. default behavior: initialize with value of 0 only setting value: assert bytes is None and bits is None only setting bytes: assert value == 0 and bits is None only setting bits: assert value == 0 and bytes is None """ _raise = False if (value == 0) and (bytes is None) and (bits is None): pass # Setting by bytes elif bytes is not None: if (value == 0) and (bits is None): pass else: _raise = True # setting by bits elif bits is not None: if (value == 0) and (bytes is None): pass else: _raise = True if _raise: raise ValueError("Only one parameter of: value, bytes, or bits is allowed") self._do_setup() if bytes: # We were provided with the fundamental base unit, no need # to normalize self._byte_value = float(bytes) self._bit_value = bytes * 8.0 elif bits: # We were *ALMOST* given the fundamental base # unit. Translate it into the fundamental unit then # normalize. self._byte_value = bits / 8 self._bit_value = float(bits) else: # We were given a value representative of this *prefix # unit*. We need to normalize it into the number of bytes # it represents. self._norm(value) # We have the fundamental unit figured out. Set the 'pretty' unit self._set_prefix_value() def _set_prefix_value(self) -> None: self.prefix_value = self._to_prefix_value(self._byte_value) def _to_prefix_value(self, value: float) -> float: """Return the number of bits/bytes as they would look like if we converted *to* this unit""" return value / self._unit_value def _setup(self) -> tuple: raise NotImplementedError("The base 'bitmath.Bitmath' class can not be used directly") def _do_setup(self) -> None: """Setup basic parameters for this class. `base` is the numeric base which when raised to `power` is equivalent to 1 unit of the corresponding prefix. I.e., base=2, power=10 represents 2^10, which is the NIST Binary Prefix for 1 Kibibyte. Likewise, for the SI prefix classes `base` will be 10, and the `power` for the Kilobyte is 3. """ (self._base, self._power, self._name_singular, self._name_plural) = self._setup() self._unit_value = self._base ** self._power def _norm(self, value: int | float) -> None: """Normalize the input value into the fundamental unit for this prefix type. :param number value: The input value to be normalized :raises ValueError: if the input value is not a type of real number """ if isinstance(value, self.valid_types): self._byte_value = float(value) * self._unit_value self._bit_value = self._byte_value * 8.0 else: raise ValueError( f"Initialization value '{value}' is of an invalid type: {type(value)}. " f"Must be one of {', '.join(str(x) for x in self.valid_types)}" ) ################################################################## # Properties #: The mathematical base of an instance base = property(lambda s: s._base, doc="The mathematical base of the unit of the instance (this will be 2 or 10)") binary = property(lambda s: bin(int(s.bits)), doc="""The binary representation of an instance in binary 1s and 0s. Note that for very large numbers this will mean a lot of 1s and 0s. For example, GiB(100) would be represented in Python as:: 0b1100100000000000000000000000000000000000 That leading ``0b`` is normal. That's how Python represents binary. """) #: Alias for :attr:`binary` bin = property(lambda s: s.binary, doc="Alias for the 'binary' property") #: The number of bits in an instance bits = property(lambda s: s._bit_value, doc="The number of bits in an instance") #: The number of bytes in an instance bytes = property(lambda s: s._byte_value, doc="The number of bytes in an instance") #: The mathematical power of an instance power = property(lambda s: s._power, doc="The mathematical power of an instance") @property def system(self): """The system of units used to measure an instance""" if self._base == 2: return "NIST" if self._base == 10: return "SI" # I don't expect to ever encounter this logic branch, but # hey, it's better to have extra test coverage than # insufficient test coverage. raise ValueError(f"Instances mathematical base is an unsupported value: {self._base}") @property def unit(self): """The string that is this instances prefix unit name in agreement with this instance value (singular or plural). Following the convention that only 1 is singular. This will always be the singular form when :attr:`bitmath.format_plural` is ``False`` (default value). For example: >>> KiB(1).unit == 'KiB' >>> Byte(0).unit == 'Bytes' >>> Byte(1).unit == 'Byte' >>> Byte(1.1).unit == 'Bytes' >>> Gb(2).unit == 'Gbs' """ if self.prefix_value == 1: # If it's a '1', return it singular, no matter what return self._name_singular if _get_format_plural(): # Pluralization requested return self._name_plural # Pluralization NOT requested, and the value is not 1 return self._name_singular @property def unit_plural(self): """The string that is an instances prefix unit name in the plural form. For example: >>> KiB(1).unit_plural == 'KiB' >>> Byte(1024).unit_plural == 'Bytes' >>> Gb(1).unit_plural == 'Gb' """ return self._name_plural @property def unit_singular(self): """The string that is an instances prefix unit name in the singular form. For example: >>> KiB(1).unit_singular == 'KiB' >>> Byte(1024).unit == 'B' >>> Gb(1).unit_singular == 'Gb' """ return self._name_singular #: The "prefix" value of an instance value = property(lambda s: s.prefix_value) @classmethod def from_other(cls, item): """Factory function to return instances of `item` converted into a new instance of ``cls``. Because this is a class method, it may be called from any bitmath class object without the need to explicitly instantiate the class ahead of time. *Implicit Parameter:* * ``cls`` A bitmath class, implicitly set to the class of the instance object it is called on *User Supplied Parameter:* * ``item`` A :class:`bitmath.Bitmath` subclass instance *Example:* >>> import bitmath >>> kib = bitmath.KiB.from_other(bitmath.MiB(1)) >>> print(kib) KiB(1024.0) """ if isinstance(item, Bitmath): return cls(bits=item.bits) raise ValueError(f"The provided items must be a valid bitmath class: {item.__class__}") ###################################################################### # The following implement the Python datamodel customization methods # # Reference: https://docs.python.org/3/reference/datamodel.html#basic-customization def __repr__(self) -> str: """Representation of this object as you would expect to see in an interpreter""" return self.format(_FORMAT_REPR) def __str__(self) -> str: """String representation of this object""" if _get_bestprefix(): return self.best_prefix().format(_get_format_string()) return self.format(_get_format_string()) def __format__(self, fmt_spec: str) -> str: """Support Python's string formatting protocol. When *fmt_spec* is empty, returns ``str(self)`` — the same as the default string representation (e.g. ``"1.0 KiB"``). When *fmt_spec* is a standard numeric format spec (e.g. ``".2f"``, ``">10.1f"``), it is applied to ``self.value`` only, returning the formatted number without a unit suffix. The caller controls the surrounding string:: size = bitmath.MiB(2.847598437) f'size: {size:.1f} {size.unit}' # -> 'size: 2.8 MiB' f'size: {size}' # -> 'size: 2.847598437 MiB' """ if fmt_spec == '': return str(self) return self.value.__format__(fmt_spec) def format(self, fmt: str) -> str: """Return a representation of this instance formatted with user supplied syntax""" _fmt_params = { 'base': self.base, 'bin': self.bin, 'binary': self.binary, 'bits': self.bits, 'bytes': self.bytes, 'power': self.power, 'system': self.system, 'unit': self.unit, 'unit_plural': self.unit_plural, 'unit_singular': self.unit_singular, 'value': self.value } return fmt.format(**_fmt_params) ################################################################## # Guess the best human-readable prefix unit for representation ################################################################## def best_prefix(self, system=None): """Optional parameter, `system`, allows you to prefer NIST or SI in the results. By default, the current system is used (Bit/Byte default to NIST). Logic discussion/notes: Base-case, does it need converting? If the instance is less than one Byte, return the instance as a Bit instance. Else, begin by recording the unit system the instance is defined by. This determines which steps (NIST_STEPS/SI_STEPS) we iterate over. If the instance is not already a ``Byte`` instance, convert it to one for the purpose of the log calculation. NIST units step up by powers of 1024, SI units step up by powers of 1000. Take integer value of the log(base=STEP_POWER) of the instance's byte value. E.g.: >>> int(math.log(Gb(100).bytes, 1000)) 3 This will return a value >= 0. The following determines the 'best prefix unit' for representation: * result == 0, best represented as a Byte (or Bit for Bit-family inputs) * result >= len(SYSTEM_STEPS), best represented as an Exbi/Exabyte * 0 < result < len(SYSTEM_STEPS), best represented as SYSTEM_PREFIXES[result-1] Unit family is preserved: Bit-family instances (Bit, Kib, Mib, kb, Mb, etc.) always return a Bit-family result. Byte-family instances always return a Byte-family result. .. versionchanged:: 2.0.0 Bit-family instances now return Bit-family results. Previously, ``best_prefix()`` always returned a Byte-family unit regardless of the input type (e.g. ``Bit(30950093).best_prefix()`` returned ``MiB`` instead of ``Mib``). See GitHub issue #95. """ def _resolve_prefix_table(pref): """Return (prefixes, base) for the given system preference.""" if pref is None: if self.system == 'NIST': return NIST_PREFIXES, 1024 return SI_PREFIXES, 1000 if pref == NIST: return NIST_PREFIXES, 1024 if pref == SI: return SI_PREFIXES, 1000 raise ValueError("Invalid value given for 'system' parameter. Must be one of NIST or SI") # Use absolute value so we don't return Bit's for *everything* # less than Byte(1). From github issue #55 if abs(self) < Byte(1): return Bit.from_other(self) _inst = self if isinstance(self, Byte) else Byte.from_other(self) _steps, _base = _resolve_prefix_table(system) # Index of the string of the best prefix in the STEPS list _index = int(math.log(abs(_inst.bytes), _base)) # Recall that the log() function returns >= 0. This doesn't # map to the STEPS list 1:1. That is to say, 0 is handled with # special care. So if the _index is 1, we actually want item 0 # in the list. if _index == 0: # Below the first prefix threshold. Bit-family inputs return as # Bit to preserve family; Byte-family inputs return as Byte. if isinstance(self, Bit): return Bit.from_other(self) return _inst # Default to the largest prefix; override if a more fitting one exists. _best_prefix = _steps[-1] if 0 < _index < len(_steps): _best_prefix = _steps[_index - 1] # Preserve unit family: Bit-family -> 'to_Xib'/'to_Xb', # Byte-family -> 'to_XiB'/'to_XB'. suffix = 'b' if isinstance(self, Bit) else 'B' return getattr(self, f'to_{_best_prefix}{suffix}')() ################################################################## def to_Bit(self): """Convert to Bit.""" return Bit(self._bit_value) def to_Byte(self): """Convert to Byte.""" return Byte(self._byte_value / NIST_STEPS['Byte']) # Properties Bit = property(lambda s: s.to_Bit()) Byte = property(lambda s: s.to_Byte()) ################################################################## def to_KiB(self): """Convert to KiB.""" return KiB(bits=self._bit_value) def to_Kib(self): """Convert to Kib.""" return Kib(bits=self._bit_value) def to_kB(self): """Convert to kB.""" return kB(bits=self._bit_value) def to_kb(self): """Convert to kb.""" return kb(bits=self._bit_value) # Properties KiB = property(lambda s: s.to_KiB()) Kib = property(lambda s: s.to_Kib()) kB = property(lambda s: s.to_kB()) kb = property(lambda s: s.to_kb()) ################################################################## def to_MiB(self): """Convert to MiB.""" return MiB(bits=self._bit_value) def to_Mib(self): """Convert to Mib.""" return Mib(bits=self._bit_value) def to_MB(self): """Convert to MB.""" return MB(bits=self._bit_value) def to_Mb(self): """Convert to Mb.""" return Mb(bits=self._bit_value) # Properties MiB = property(lambda s: s.to_MiB()) Mib = property(lambda s: s.to_Mib()) MB = property(lambda s: s.to_MB()) Mb = property(lambda s: s.to_Mb()) ################################################################## def to_GiB(self): """Convert to GiB.""" return GiB(bits=self._bit_value) def to_Gib(self): """Convert to Gib.""" return Gib(bits=self._bit_value) def to_GB(self): """Convert to GB.""" return GB(bits=self._bit_value) def to_Gb(self): """Convert to Gb.""" return Gb(bits=self._bit_value) # Properties GiB = property(lambda s: s.to_GiB()) Gib = property(lambda s: s.to_Gib()) GB = property(lambda s: s.to_GB()) Gb = property(lambda s: s.to_Gb()) ################################################################## def to_TiB(self): """Convert to TiB.""" return TiB(bits=self._bit_value) def to_Tib(self): """Convert to Tib.""" return Tib(bits=self._bit_value) def to_TB(self): """Convert to TB.""" return TB(bits=self._bit_value) def to_Tb(self): """Convert to Tb.""" return Tb(bits=self._bit_value) # Properties TiB = property(lambda s: s.to_TiB()) Tib = property(lambda s: s.to_Tib()) TB = property(lambda s: s.to_TB()) Tb = property(lambda s: s.to_Tb()) ################################################################## def to_PiB(self): """Convert to PiB.""" return PiB(bits=self._bit_value) def to_Pib(self): """Convert to Pib.""" return Pib(bits=self._bit_value) def to_PB(self): """Convert to PB.""" return PB(bits=self._bit_value) def to_Pb(self): """Convert to Pb.""" return Pb(bits=self._bit_value) # Properties PiB = property(lambda s: s.to_PiB()) Pib = property(lambda s: s.to_Pib()) PB = property(lambda s: s.to_PB()) Pb = property(lambda s: s.to_Pb()) ################################################################## def to_EiB(self): """Convert to EiB.""" return EiB(bits=self._bit_value) def to_Eib(self): """Convert to Eib.""" return Eib(bits=self._bit_value) def to_EB(self): """Convert to EB.""" return EB(bits=self._bit_value) def to_Eb(self): """Convert to Eb.""" return Eb(bits=self._bit_value) # Properties EiB = property(lambda s: s.to_EiB()) Eib = property(lambda s: s.to_Eib()) EB = property(lambda s: s.to_EB()) Eb = property(lambda s: s.to_Eb()) ################################################################## def to_ZiB(self): """Convert to ZiB.""" return ZiB(bits=self._bit_value) def to_Zib(self): """Convert to Zib.""" return Zib(bits=self._bit_value) def to_ZB(self): """Convert to ZB.""" return ZB(bits=self._bit_value) def to_Zb(self): """Convert to Zb.""" return Zb(bits=self._bit_value) ZiB = property(lambda s: s.to_ZiB()) Zib = property(lambda s: s.to_Zib()) ZB = property(lambda s: s.to_ZB()) Zb = property(lambda s: s.to_Zb()) ################################################################## def to_YiB(self): """Convert to YiB.""" return YiB(bits=self._bit_value) def to_Yib(self): """Convert to Yib.""" return Yib(bits=self._bit_value) def to_YB(self): """Convert to YB.""" return YB(bits=self._bit_value) def to_Yb(self): """Convert to Yb.""" return Yb(bits=self._bit_value) YiB = property(lambda s: s.to_YiB()) Yib = property(lambda s: s.to_Yib()) YB = property(lambda s: s.to_YB()) Yb = property(lambda s: s.to_Yb()) ################################################################## # Rich comparison operations ################################################################## def __lt__(self, other): if isinstance(other, numbers.Number): return self.prefix_value < other return self._byte_value < other.bytes def __le__(self, other): if isinstance(other, numbers.Number): return self.prefix_value <= other return self._byte_value <= other.bytes def __eq__(self, other): if isinstance(other, numbers.Number): return self.prefix_value == other return self._byte_value == other.bytes def __ne__(self, other): if isinstance(other, numbers.Number): return self.prefix_value != other return self._byte_value != other.bytes def __gt__(self, other): if isinstance(other, numbers.Number): return self.prefix_value > other return self._byte_value > other.bytes def __ge__(self, other): if isinstance(other, numbers.Number): return self.prefix_value >= other return self._byte_value >= other.bytes ################################################################## # Basic math operations ################################################################## # Reference: https://docs.python.org/3/reference/datamodel.html#emulating-numeric-types def __add__(self, other): """Supported operations with result types: - bm + bm = bm - bm + num = num - num + bm = num (see radd) """ if isinstance(other, numbers.Number): # bm + num return other + self.value # bm + bm total_bytes = self._byte_value + other.bytes return (type(self))(bytes=total_bytes) def __sub__(self, other): """Subtraction: Supported operations with result types: - bm - bm = bm - bm - num = num - num - bm = num (see rsub) """ if isinstance(other, numbers.Number): # bm - num return self.value - other # bm - bm total_bytes = self._byte_value - other.bytes return (type(self))(bytes=total_bytes) def __mul__(self, other): """Multiplication: Supported operations with result types: - bm1 * bm2 = bm1 - bm * num = bm - num * bm = bm (see rmul) """ if isinstance(other, numbers.Number): # bm * num result = self._byte_value * other return (type(self))(bytes=result) # bm1 * bm2 _other = other.value * other.base ** other.power _self = self.prefix_value * self._base ** self._power return (type(self))(bytes=_other * _self) def __truediv__(self, other): """Division: Supported operations with result types: - bm1 / bm2 = num - bm / num = bm - num / bm = num (see rtruediv) """ if isinstance(other, numbers.Number): # bm / num result = self._byte_value / other return (type(self))(bytes=result) # bm1 / bm2 return self._byte_value / other.bytes def __floordiv__(self, other): """Floor division: Supported operations with result types: - 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) # bm1 // bm2 return int(self._byte_value // other.bytes) def __mod__(self, other): """Modulo (remainder): Supported operations with result types: - 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) # 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)) ################################################################## def __radd__(self, other): # Special case: 0 + bm = bm (identity element, enables built-in sum()) if other == 0: return self # num + bm = num return other + self.value def __rsub__(self, other): # num - bm = num return other - self.value def __rmul__(self, other): # num * bm = bm return self * other def __rtruediv__(self, other): # num / bm = num return other / self.value # Called to implement the built-in functions complex(), int(), and # float(). These return the int/float equivalent of the prefix value: # - int(KiB(3.336)) -> 3 # - float(KiB(3.336)) -> 3.336 def __int__(self) -> int: """Return this instances prefix unit as an integer""" return int(self.prefix_value) def __float__(self) -> float: """Return this instances prefix unit as a floating point number""" return float(self.prefix_value) # floor/ceil/round operate on the prefix value and return the same unit # type. Explicit opt-in for integer prefix values. See the Rules for Math # appendix in the docs for the floating-point design rationale. def __floor__(self): """Return the largest integer prefix value <= this instance as the same type. Rounds the prefix value down. math.floor(MiB(1.9)) -> MiB(1). """ return (type(self))(math.floor(self.prefix_value)) def __ceil__(self): """Return the smallest integer prefix value >= this instance as the same type. Rounds the prefix value up. math.ceil(MiB(1.1)) -> MiB(2). """ return (type(self))(math.ceil(self.prefix_value)) def __round__(self, ndigits=None): """Return this instance rounded to ndigits precision as the same type. round(MiB(1.75)) -> MiB(2); round(KiB(1.555), 2) -> KiB(1.56). Rounds the prefix value using Python's built-in round(). When ndigits is omitted the result has an integer prefix value. Only round at the final output step; rounding intermediate results loses precision. """ if ndigits is None: return (type(self))(round(self.prefix_value)) return (type(self))(round(self.prefix_value, ndigits)) ################################################################## # Bitwise operations ################################################################## def __lshift__(self, other): """Left shift, ex: 100 << 2 A left shift by n bits is equivalent to multiplication by pow(2, n). A long integer is returned if the result exceeds the range of plain integers.""" shifted = int(self.bits) << other return type(self)(bits=shifted) def __rshift__(self, other): """Right shift, ex: 100 >> 2 A right shift by n bits is equivalent to division by pow(2, n).""" shifted = int(self.bits) >> other return type(self)(bits=shifted) def __and__(self, other): """"Bitwise and, ex: 100 & 2 bitwise and". Each bit of the output is 1 if the corresponding bit of x AND of y is 1, otherwise it's 0.""" andd = int(self.bits) & other return type(self)(bits=andd) def __xor__(self, other): """Bitwise xor, ex: 100 ^ 2 Does a "bitwise exclusive or". Each bit of the output is the same as the corresponding bit in x if that bit in y is 0, and it's the complement of the bit in x if that bit in y is 1.""" xord = int(self.bits) ^ other return type(self)(bits=xord) def __or__(self, other): """Bitwise or, ex: 100 | 2 Does a "bitwise or". Each bit of the output is 0 if the corresponding bit of x AND of y is 0, otherwise it's 1.""" result = int(self.bits) | other return type(self)(bits=result) ################################################################## def __neg__(self): """The negative version of this instance""" return (type(self))(-abs(self.prefix_value)) def __pos__(self): return (type(self))(abs(self.prefix_value)) def __abs__(self): return (type(self))(abs(self.prefix_value))
# def __invert__(self): # """Called to implement the unary arithmetic operations (-, +, abs() # and ~).""" # return NotImplemented ###################################################################### # First, the bytes...
[docs] class Byte(Bitmath): """Byte based types fundamentally operate on self._bit_value""" def _setup(self): return (2, 0, 'B', 'B')
###################################################################### # NIST Prefixes for Byte based types
[docs] class KiB(Byte): """Kibibyte — 2^10 (1,024) bytes.""" def _setup(self): return (2, 10, 'KiB', 'KiBs')
Kio = KiB
[docs] class MiB(Byte): """Mebibyte — 2^20 (1,048,576) bytes.""" def _setup(self): return (2, 20, 'MiB', 'MiBs')
Mio = MiB
[docs] class GiB(Byte): """Gibibyte — 2^30 (1,073,741,824) bytes.""" def _setup(self): return (2, 30, 'GiB', 'GiBs')
Gio = GiB
[docs] class TiB(Byte): """Tebibyte — 2^40 bytes.""" def _setup(self): return (2, 40, 'TiB', 'TiBs')
Tio = TiB
[docs] class PiB(Byte): """Pebibyte — 2^50 bytes.""" def _setup(self): return (2, 50, 'PiB', 'PiBs')
Pio = PiB
[docs] class EiB(Byte): """Exbibyte — 2^60 bytes.""" def _setup(self): return (2, 60, 'EiB', 'EiBs')
Eio = EiB
[docs] class ZiB(Byte): """Zebibyte — 2^70 bytes.""" def _setup(self): return (2, 70, 'ZiB', 'ZiBs')
Zio = ZiB
[docs] class YiB(Byte): """Yobibyte — 2^80 bytes.""" def _setup(self): return (2, 80, 'YiB', 'YiBs')
Yio = YiB ###################################################################### # SI Prefixes for Byte based types
[docs] class kB(Byte): """Kilobyte — 10^3 (1,000) bytes.""" def _setup(self): return (10, 3, 'kB', 'kBs')
ko = kB
[docs] class MB(Byte): """Megabyte — 10^6 (1,000,000) bytes.""" def _setup(self): return (10, 6, 'MB', 'MBs')
Mo = MB
[docs] class GB(Byte): """Gigabyte — 10^9 (1,000,000,000) bytes.""" def _setup(self): return (10, 9, 'GB', 'GBs')
Go = GB
[docs] class TB(Byte): """Terabyte — 10^12 bytes.""" def _setup(self): return (10, 12, 'TB', 'TBs')
To = TB
[docs] class PB(Byte): """Petabyte — 10^15 bytes.""" def _setup(self): return (10, 15, 'PB', 'PBs')
Po = PB
[docs] class EB(Byte): """Exabyte — 10^18 bytes.""" def _setup(self): return (10, 18, 'EB', 'EBs')
Eo = EB
[docs] class ZB(Byte): """Zettabyte — 10^21 bytes.""" def _setup(self): return (10, 21, 'ZB', 'ZBs')
Zo = ZB
[docs] class YB(Byte): """Yottabyte — 10^24 bytes.""" def _setup(self): return (10, 24, 'YB', 'YBs')
Yo = YB ###################################################################### # And now the bit types
[docs] class Bit(Bitmath): """Bit based types fundamentally operate on self._bit_value""" def _set_prefix_value(self): self.prefix_value = self._to_prefix_value(self._bit_value) def _setup(self): return (2, 0, 'b', 'b') def _norm(self, value): """Normalize the input value into the fundamental unit for this prefix type""" self._bit_value = float(value) * self._unit_value self._byte_value = self._bit_value / 8
###################################################################### # NIST Prefixes for Bit based types
[docs] class Kib(Bit): """Kibibit — 2^10 (1,024) bits.""" def _setup(self): return (2, 10, 'Kib', 'Kibs')
[docs] class Mib(Bit): """Mebibit — 2^20 (1,048,576) bits.""" def _setup(self): return (2, 20, 'Mib', 'Mibs')
[docs] class Gib(Bit): """Gibibit — 2^30 (1,073,741,824) bits.""" def _setup(self): return (2, 30, 'Gib', 'Gibs')
[docs] class Tib(Bit): """Tebibit — 2^40 bits.""" def _setup(self): return (2, 40, 'Tib', 'Tibs')
[docs] class Pib(Bit): """Pebibit — 2^50 bits.""" def _setup(self): return (2, 50, 'Pib', 'Pibs')
[docs] class Eib(Bit): """Exbibit — 2^60 bits.""" def _setup(self): return (2, 60, 'Eib', 'Eibs')
[docs] class Zib(Bit): """Zebibit — 2^70 bits.""" def _setup(self): return (2, 70, 'Zib', 'Zibs')
[docs] class Yib(Bit): """Yobibit — 2^80 bits.""" def _setup(self): return (2, 80, 'Yib', 'Yibs')
###################################################################### # SI Prefixes for Bit based types
[docs] class kb(Bit): """Kilobit — 10^3 (1,000) bits.""" def _setup(self): return (10, 3, 'kb', 'kbs')
[docs] class Mb(Bit): """Megabit — 10^6 (1,000,000) bits.""" def _setup(self): return (10, 6, 'Mb', 'Mbs')
[docs] class Gb(Bit): """Gigabit — 10^9 (1,000,000,000) bits.""" def _setup(self): return (10, 9, 'Gb', 'Gbs')
[docs] class Tb(Bit): """Terabit — 10^12 bits.""" def _setup(self): return (10, 12, 'Tb', 'Tbs')
[docs] class Pb(Bit): """Petabit — 10^15 bits.""" def _setup(self): return (10, 15, 'Pb', 'Pbs')
[docs] class Eb(Bit): """Exabit — 10^18 bits.""" def _setup(self): return (10, 18, 'Eb', 'Ebs')
[docs] class Zb(Bit): """Zettabit — 10^21 bits.""" def _setup(self): return (10, 21, 'Zb', 'Zbs')
[docs] class Yb(Bit): """Yottabit — 10^24 bits.""" def _setup(self): return (10, 24, 'Yb', 'Ybs')
###################################################################### # Utility functions def best_prefix(bytes: Bitmath | int | float, system: int = NIST) -> Bitmath: # pylint: disable=redefined-builtin """Return a bitmath instance representing the best human-readable representation of the number of bytes given by ``bytes``. In addition to a numeric type, the ``bytes`` parameter may also be a bitmath type. Optionally select a preferred unit system by specifying the ``system`` keyword. Choices for ``system`` are ``bitmath.NIST`` (default) and ``bitmath.SI``. Basically a shortcut for: >>> import bitmath >>> b = bitmath.Byte(12345) >>> best = b.best_prefix() Or: >>> import bitmath >>> best = (bitmath.KiB(12345) * 4201).best_prefix() """ if isinstance(bytes, Bitmath): value = bytes.bytes else: value = bytes return Byte(value).best_prefix(system=system) def _query_device_capacity_windows(device_fd: IO[Any]) -> int: """Return device capacity in bytes on Windows via DeviceIoControl. Windows physical disk paths look like ``\\\\.\\PhysicalDrive0``. Raises :class:`ValueError` if the file descriptor is not a physical device. Raises :class:`OSError` if the DeviceIoControl call fails. """ if not device_fd.name.startswith('\\\\.\\'): raise ValueError("The file descriptor provided is not of a device type") # pylint: disable=used-before-assignment # ctypes/msvcrt: platform-guarded; only reached on os.name == 'nt' IOCTL_DISK_GET_DRIVE_GEOMETRY_EX = 0x000700A0 class DISK_GEOMETRY(ctypes.Structure): # pylint: disable=too-few-public-methods """Windows API DISK_GEOMETRY structure (DeviceIoControl layout).""" _fields_ = [ ('Cylinders', ctypes.c_longlong), ('MediaType', ctypes.c_uint), ('TracksPerCylinder', ctypes.c_ulong), ('SectorsPerTrack', ctypes.c_ulong), ('BytesPerSector', ctypes.c_ulong), ] class DISK_GEOMETRY_EX(ctypes.Structure): # pylint: disable=too-few-public-methods """Extended disk geometry including total disk size.""" _fields_ = [ ('Geometry', DISK_GEOMETRY), ('DiskSize', ctypes.c_longlong), ('Data', ctypes.c_byte * 1), ] geometry = DISK_GEOMETRY_EX() bytes_returned = ctypes.wintypes.DWORD(0) handle = msvcrt.get_osfhandle(device_fd.fileno()) result = ctypes.windll.kernel32.DeviceIoControl( handle, IOCTL_DISK_GET_DRIVE_GEOMETRY_EX, None, 0, ctypes.byref(geometry), ctypes.sizeof(geometry), ctypes.byref(bytes_returned), None, ) if not result: error_code = ctypes.windll.kernel32.GetLastError() raise OSError(f"DeviceIoControl failed with error code: {error_code}") return geometry.DiskSize
[docs] def query_device_capacity(device_fd: IO[Any]) -> Byte: """Query the raw physical capacity of a block device. Most users should prefer :func:`query_capacity`. This function is for callers who need raw physical device capacity (e.g. disk imaging tools). Requires root on Linux and administrator on Windows. Not supported on macOS (SIP restriction). :param file device_fd: A ``file`` object of the device to query. On Linux: ``open("/dev/sda", "rb")`` (requires root). On Windows: ``open(r'\\\\.\\PhysicalDrive0', 'rb')`` (requires administrator). :return: a bitmath :class:`bitmath.Byte` instance equivalent to the capacity of the target device in bytes. :raises NotImplementedError: on macOS or any other unsupported platform. :raises ValueError: if the file descriptor is not a block device. """ if os.name not in SUPPORTED_PLATFORMS: raise NotImplementedError(f"'bitmath.query_device_capacity' is not supported on this platform: {os.name}") if os.name == 'nt': return Byte(_query_device_capacity_windows(device_fd)) if platform.system() == 'Darwin': raise NotImplementedError( "query_device_capacity is not supported on macOS; " "SIP blocks raw block device access. Use query_capacity() instead." ) s = os.stat(device_fd.name).st_mode if not stat.S_ISBLK(s): # pylint: disable=possibly-used-before-assignment raise ValueError("The file descriptor provided is not of a device type") # The keys of the ``ioctl_map`` dictionary correlate to possible # values from the ``platform.system`` function. ioctl_map = { # ioctls for the "Linux" platform "Linux": { "request_params": [ # A list of parameters to calculate the block size. # # ( PARAM_NAME , FORMAT_CHAR , REQUEST_CODE ) ("BLKGETSIZE64", "L", 0x80081272) # Per <linux/fs.h>, the BLKGETSIZE64 request returns a # 'u64' sized value. This is an unsigned 64 bit # integer C type. This means to correctly "buffer" the # result we need 64 bits, or 8 bytes, of memory. # # The struct module documentation include a reference # chart relating formatting characters to native C # Types. In this case, using the "native size", the # table tells us: # # * Character 'L' - Unsigned Long C Type (u64) - Loads into a Python int type # # Confirm this character is right by running (on Linux): # # >>> import struct # >>> print(8 == struct.calcsize('L')) # # The result should be true as long as your kernel # headers define BLKGETSIZE64 as a u64 type (please # file a bug report at # https://github.com/timlnx/bitmath/issues/new if # this does *not* work for you) ], # func is how the final result is decided. Because the # Linux BLKGETSIZE64 call returns the block device # capacity in bytes as an integer value, no extra # calculations are required. Simply return the value of # BLKGETSIZE64. "func": lambda x: x["BLKGETSIZE64"] }, } platform_params = ioctl_map[platform.system()] results = {} for req_name, fmt, request_code in platform_params['request_params']: # Read the systems native size (in bytes) of this format type. buffer_size = struct.calcsize(fmt) # This code has been ran on only a few test systems. If it's # appropriate, maybe in the future we'll add try/except # conditions for some possible errors. Really only for cases # where it would add value to override the default exception # message string. buffer = fcntl.ioctl( # pylint: disable=possibly-used-before-assignment device_fd.fileno(), request_code, b'\x00' * buffer_size ) # Unpack the raw result from the ioctl call into a familiar # python data type according to the ``fmt`` rules. result = struct.unpack(fmt, buffer)[0] # Add the new result to our collection results[req_name] = result return Byte(platform_params['func'](results))
class Capacity(NamedTuple): """Capacity of a filesystem volume returned by :func:`query_capacity`.""" total: 'Bitmath' used: 'Bitmath' free: 'Bitmath' # Matches a bare drive letter: "C", "c", "C:", "c:" — nothing else. _DRIVE_LETTER_RE = re.compile(r'^[A-Za-z]:?$')
[docs] def query_capacity(path: Union[str, os.PathLike], bestprefix: bool = True, system: int = NIST) -> Capacity: """Return the total, used, and free capacity of the volume at ``path``. This is the recommended API for querying volume or mount-point size. It works cross-platform without elevated privileges. :param path: A path on the filesystem volume to query. On Windows, a bare drive letter (``"C"``, ``"C:"``) is normalized to ``"C:\\"``. :param bool bestprefix: When ``True`` (default), each field of the returned :class:`Capacity` is already normalized via :meth:`~bitmath.Bitmath.best_prefix` for human-readable output. When ``False``, each field is a raw :class:`bitmath.Byte`. :param int system: Unit system to use when ``bestprefix`` is ``True``. Either :data:`bitmath.NIST` (default, binary prefixes like ``GiB``) or :data:`bitmath.SI` (decimal prefixes like ``GB``). Ignored when ``bestprefix`` is ``False``. :return: A :class:`Capacity` NamedTuple with ``total``, ``used``, and ``free`` fields, each a :class:`bitmath.Bitmath` instance. :raises FileNotFoundError: if ``path`` does not exist. :raises PermissionError: if the process lacks access to query ``path``. Example — attribute access (human-readable by default):: cap = bitmath.query_capacity("/") print(cap.total) # e.g. 465.762 GiB Example — tuple unpacking:: total, used, free = bitmath.query_capacity("/") Example — raw bytes and SI prefixes:: cap_raw = bitmath.query_capacity("/", bestprefix=False) cap_si = bitmath.query_capacity("/", system=bitmath.SI) """ normalized: Union[str, os.PathLike] = path if os.name == 'nt': s = str(path).upper() if _DRIVE_LETTER_RE.match(s): normalized = s.rstrip(':') + ':\\' usage = shutil.disk_usage(normalized) total, used, free = Byte(usage.total), Byte(usage.used), Byte(usage.free) if bestprefix: return Capacity( total.best_prefix(system=system), used.best_prefix(system=system), free.best_prefix(system=system), ) return Capacity(total, used, free)
[docs] def getsize(path: str | pathlib.Path, bestprefix: bool = True, system: int = NIST) -> Bitmath: """Return a bitmath instance in the best human-readable representation of the file size at `path`. Optionally, provide a preferred unit system by setting `system` to either `bitmath.NIST` (default) or `bitmath.SI`. `path` may be a plain string or a :class:`pathlib.Path` object. Optionally, set ``bestprefix`` to ``False`` to get ``bitmath.Byte`` instances back. """ _path = os.path.realpath(path) size_bytes = os.path.getsize(_path) if bestprefix: return Byte(size_bytes).best_prefix(system=system) return Byte(size_bytes)
[docs] def listdir( # pylint: disable=too-many-arguments,too-many-positional-arguments search_base: str | pathlib.Path, followlinks: bool = False, glob: str = '*', relpath: bool = False, bestprefix: bool = False, system: int = NIST, **kwargs, ) -> Iterator[tuple[str, Bitmath]]: """This is a generator which recurses the directory tree `search_base`, yielding 2-tuples of: * The absolute/relative path to a discovered file * A bitmath instance representing the "apparent size" of the file. - `search_base` - The directory to begin walking down. May be a plain string or a :class:`pathlib.Path` object. - `followlinks` - Whether or not to follow symbolic links to directories - `glob` - A glob (see :py:mod:`fnmatch`) to filter results with (default: ``*``, everything) - `relpath` - ``True`` to return the relative path from `pwd` or ``False`` (default) to return the fully qualified path - ``bestprefix`` - set to ``False`` to get ``bitmath.Byte`` instances back instead. - `system` - Provide a preferred unit system by setting `system` to either ``bitmath.NIST`` (default) or ``bitmath.SI``. .. note:: Symlinks to **files** are followed automatically .. deprecated:: 2.1.0 :func:`bitmath.listdir` is deprecated and will be removed in a future release. Use :func:`os.walk` with :func:`bitmath.getsize` directly. """ warnings.warn( "bitmath.listdir() is deprecated and will be removed in a future release. " "Use os.walk() with bitmath.getsize() directly.", DeprecationWarning, stacklevel=2, ) if 'filter' in kwargs: warnings.warn( "The 'filter' parameter of listdir() is deprecated as of 2.0.0 and will be " "removed in a future release. Use 'glob' instead.", DeprecationWarning, stacklevel=2, ) glob = kwargs.pop('filter') if kwargs: raise TypeError(f"listdir() got unexpected keyword arguments: {list(kwargs)}") for root, _, files in os.walk(search_base, followlinks=followlinks): for name in fnmatch.filter(files, glob): _path = os.path.join(root, name) if relpath: # RELATIVE path _return_path = os.path.relpath(_path, '.') else: # REAL path _return_path = os.path.realpath(_path) if followlinks: yield (_return_path, getsize(_path, bestprefix=bestprefix, system=system)) else: if os.path.isdir(_path) or os.path.islink(_path): pass # pragma: no cover else: yield (_return_path, getsize(_path, bestprefix=bestprefix, system=system))
def _parse_string_strict(s: str) -> 'Bitmath': if not isinstance(s, str): raise ValueError(f"parse_string only accepts string inputs but a {type(s)} was given") # get the index of the first alphabetic character try: index = next(i for i, c in enumerate(s) if c.isalpha()) except StopIteration: raise ValueError(f"No unit detected, can not parse string '{s}' into a bitmath object") from None # split the string into the value and the unit val, unit = s[:index], s[index:] # see if the unit exists as a type in our namespace if unit == "b": unit_class = Bit elif unit == "B": unit_class = Byte else: if not (hasattr(sys.modules[__name__], unit) and isinstance(getattr(sys.modules[__name__], unit), type)): raise ValueError(f"The unit {unit} is not a valid bitmath unit") unit_class = globals()[unit] val = float(val) return unit_class(val) def _parse_string_unsafe(s: str | numbers.Number, system: int) -> 'Bitmath': # pylint: disable=too-many-branches if not isinstance(s, str) and not isinstance(s, numbers.Number): raise ValueError(f"parse_string only accepts string/number inputs but a {type(s)} was given") # Test case: raw number input (easy!) if isinstance(s, numbers.Number): return Byte(s) # Test case: a number pretending to be a string if isinstance(s, str): try: return Byte(float(s)) except ValueError: pass # At this point the input is a string with a unit component. # Separate the number and the unit. try: index = next(i for i, c in enumerate(s) if c.isalpha()) except StopIteration: # pragma: no cover raise ValueError(f"No unit detected, can not parse string '{s}' into a bitmath object") from None val, unit = s[:index], s[index:] # Explicit base-unit and word-form checks: handle B, b, bit(s), # 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)) 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') unit += 'B' unit_class = None if len(unit) == 2: if system == NIST: unit = capitalize_first(unit) _unit = list(unit) _unit.insert(1, 'i') unit = ''.join(_unit) if unit in globals(): unit_class = globals()[unit] else: if unit.startswith('K'): unit = unit.replace('K', 'k') elif not unit.startswith('k'): unit = capitalize_first(unit) if unit[0] in SI_PREFIXES: unit_class = globals()[unit] elif len(unit) == 3: unit = capitalize_first(unit) if unit[:2] in NIST_PREFIXES: unit_class = globals()[unit] else: raise ValueError(f"The unit {unit} is not a valid bitmath unit") if unit_class is None: raise ValueError(f"The unit {unit} is not a valid bitmath unit") return unit_class(float(val))
[docs] def parse_string(s: str | numbers.Number, system: int = NIST, strict: bool = True) -> Bitmath: """Parse a string with units and return a bitmath instance. String inputs may include whitespace characters between the value and the unit. :param s: The string to parse. :param system: Unit system to use when ``strict=False``. Ignored when ``strict=True`` (the default). Set to ``bitmath.NIST`` (default) or ``bitmath.SI``. :param strict: When ``True`` (default), the unit must be an exact bitmath type name (e.g. ``"KiB"``, ``"MB"``). When ``False``, accepts ambiguous input such as plain numbers, numeric strings, and case-insensitive single-letter units (e.g. ``"4k"``, ``"2.7M"``); see caveats below. When ``strict=False`` the following rules apply: * All inputs are assumed to be byte-based (not bit-based) * Plain numbers and numeric strings are assumed to be bytes * Single-letter units (``k``, ``M``, ``G``, etc.) are assumed NIST unless ``system=bitmath.SI`` * Inputs with an ``i`` after the leading letter (``Ki``, ``Mi``) are treated as NIST units * Capitalisation does not matter The result is returned in the parsed unit system. To coerce the result into a preferred unit system call ``.best_prefix(system=system)`` on the return value:: parse_string("4k", strict=False).best_prefix(system=bitmath.SI) .. versionchanged:: 2.0.0 Added ``strict`` and ``system`` parameters. When ``strict=True`` (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``. """ if strict: return _parse_string_strict(s) return _parse_string_unsafe(s, system)
[docs] def parse_string_unsafe(s: str | numbers.Number, system: int = NIST) -> Bitmath: """Deprecated wrapper for ``parse_string(s, strict=False, system=system)``. .. deprecated:: 2.0.0 ``parse_string_unsafe`` is deprecated and will be removed in a future release. Use ``parse_string(s, strict=False, system=system)`` instead. To suppress this warning:: import warnings warnings.filterwarnings('ignore', category=DeprecationWarning, module='bitmath') """ warnings.warn( "parse_string_unsafe is deprecated as of 2.0.0 and will be removed " "in a future release. Use parse_string(s, strict=False, system=system) " "instead. To suppress: " "warnings.filterwarnings('ignore', category=DeprecationWarning, module='bitmath')", DeprecationWarning, stacklevel=2, ) return parse_string(s, system=system, strict=False)
[docs] def sum(iterable: Iterable[Bitmath], start: Bitmath | None = None) -> Bitmath: # pylint: disable=redefined-builtin """Sum an iterable of bitmath instances, returning a Byte by default. The built-in sum() also works with bitmath objects: the __radd__ identity (0 + bm = bm) means sum() preserves the type of the first element. Use bitmath.sum() instead when you need the result normalised to a specific unit regardless of input types — it accumulates into Byte(0) by default, or into the provided start instance. - bitmath.sum([MiB(1), GiB(1)]) -> Byte(1074790400.0) - bitmath.sum([KiB(1), KiB(2)], start=MiB(0)) -> MiB(0.0029296875) """ result = Byte(0) if start is None else start for item in iterable: result = result + item return result
###################################################################### # Context Managers
[docs] @contextlib.contextmanager def format(fmt_str: str | None = None, plural: bool = False, bestprefix: bool = False) -> Generator[None, None, None]: # pylint: disable=redefined-builtin """Thread-safe context manager for printing bitmath instances. ``fmt_str`` - a formatting mini-language compatible string. See the @properties (above) for a list of available items. ``plural`` - True enables printing instances with 's' if they're plural. False (default) prints them as singular (no trailing 's'). ``bestprefix`` - True converts instances to their best human-readable prefix unit before formatting. False (default) formats the instance as its current prefix unit. All settings are thread-local: concurrent contexts in different threads are fully isolated from one another. Nested contexts within the same thread correctly save and restore the enclosing context's settings. """ prev_fmt = getattr(_thread_local, 'format_string', _FMT_SENTINEL) prev_plural = getattr(_thread_local, 'format_plural', _FMT_SENTINEL) prev_bestprefix = getattr(_thread_local, 'bestprefix', _FMT_SENTINEL) _thread_local.format_string = fmt_str if fmt_str is not None else format_string _thread_local.format_plural = plural _thread_local.bestprefix = bestprefix try: yield finally: if prev_fmt is _FMT_SENTINEL: del _thread_local.format_string else: _thread_local.format_string = prev_fmt if prev_plural is _FMT_SENTINEL: del _thread_local.format_plural else: _thread_local.format_plural = prev_plural if prev_bestprefix is _FMT_SENTINEL: del _thread_local.bestprefix else: _thread_local.bestprefix = prev_bestprefix
def cli_script_main(cli_args): """ A command line interface to basic bitmath operations. """ choices = ALL_UNIT_TYPES parser = argparse.ArgumentParser( description='Converts from one type of size to another.') parser.add_argument('--from-stdin', default=False, action='store_true', help='Reads number from stdin rather than the cli') parser.add_argument( '-f', '--from', choices=choices, nargs=1, type=str, dest='fromunit', default=['Byte'], help='Input type you are converting from. Defaultes to Byte.') parser.add_argument( '-t', '--to', choices=choices, required=False, nargs=1, type=str, help=('Input type you are converting to. ' 'Attempts to detect best result if omitted.'), dest='tounit') parser.add_argument( 'size', nargs='*', type=float, help='The number to convert.') args = parser.parse_args(cli_args) # Not sure how to cover this with tests, or if the functionality # will remain in this form long enough for it to make writing a # test worth the effort. if args.from_stdin: # pragma: no cover args.size = [float(sys.stdin.readline()[:-1])] results = [] for size in args.size: instance = getattr(__import__( 'bitmath', fromlist=['True']), args.fromunit[0])(size) # If we have a unit provided then use it if args.tounit: result = getattr(instance, args.tounit[0]) # Otherwise use the best_prefix call else: result = instance.best_prefix() results.append(result) return results def cli_script(): # pragma: no cover """Entry point for the bitmath CLI; wraps cli_script_main for testability.""" for result in cli_script_main(sys.argv[1:]): print(result) if __name__ == '__main__': cli_script()