# 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

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

    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

    class KiB(Byte):
        """Kibibyte — 2^10 (1,024) bytes."""

        def _setup(self):
            return (2, 10, 'KiB', 'KiBs')

    Kio = KiB

    class MiB(Byte):
        """Mebibyte — 2^20 (1,048,576) bytes."""

        def _setup(self):
            return (2, 20, 'MiB', 'MiBs')

    Mio = MiB

    class GiB(Byte):
        """Gibibyte — 2^30 (1,073,741,824) bytes."""

        def _setup(self):
            return (2, 30, 'GiB', 'GiBs')

    Gio = GiB

    class TiB(Byte):
        """Tebibyte — 2^40 bytes."""

        def _setup(self):
            return (2, 40, 'TiB', 'TiBs')

    Tio = TiB

    class PiB(Byte):
        """Pebibyte — 2^50 bytes."""

        def _setup(self):
            return (2, 50, 'PiB', 'PiBs')

    Pio = PiB

    class EiB(Byte):
        """Exbibyte — 2^60 bytes."""

        def _setup(self):
            return (2, 60, 'EiB', 'EiBs')

    Eio = EiB

    class ZiB(Byte):
        """Zebibyte — 2^70 bytes."""

        def _setup(self):
            return (2, 70, 'ZiB', 'ZiBs')

    Zio = ZiB

    class YiB(Byte):
        """Yobibyte — 2^80 bytes."""

        def _setup(self):
            return (2, 80, 'YiB', 'YiBs')

    Yio = YiB

    ######################################################################
    # SI Prefixes for Byte based types

    class kB(Byte):
        """Kilobyte — 10^3 (1,000) bytes."""

        def _setup(self):
            return (10, 3, 'kB', 'kBs')

    ko = kB

    class MB(Byte):
        """Megabyte — 10^6 (1,000,000) bytes."""

        def _setup(self):
            return (10, 6, 'MB', 'MBs')

    Mo = MB

    class GB(Byte):
        """Gigabyte — 10^9 (1,000,000,000) bytes."""

        def _setup(self):
            return (10, 9, 'GB', 'GBs')

    Go = GB

    class TB(Byte):
        """Terabyte — 10^12 bytes."""

        def _setup(self):
            return (10, 12, 'TB', 'TBs')

    To = TB

    class PB(Byte):
        """Petabyte — 10^15 bytes."""

        def _setup(self):
            return (10, 15, 'PB', 'PBs')

    Po = PB

    class EB(Byte):
        """Exabyte — 10^18 bytes."""

        def _setup(self):
            return (10, 18, 'EB', 'EBs')

    Eo = EB

    class ZB(Byte):
        """Zettabyte — 10^21 bytes."""

        def _setup(self):
            return (10, 21, 'ZB', 'ZBs')

    Zo = ZB

    class YB(Byte):
        """Yottabyte — 10^24 bytes."""

        def _setup(self):
            return (10, 24, 'YB', 'YBs')

    Yo = YB

    ######################################################################
    # And now the bit types

    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

    class Kib(Bit):
        """Kibibit — 2^10 (1,024) bits."""

        def _setup(self):
            return (2, 10, 'Kib', 'Kibs')

    class Mib(Bit):
        """Mebibit — 2^20 (1,048,576) bits."""

        def _setup(self):
            return (2, 20, 'Mib', 'Mibs')

    class Gib(Bit):
        """Gibibit — 2^30 (1,073,741,824) bits."""

        def _setup(self):
            return (2, 30, 'Gib', 'Gibs')

    class Tib(Bit):
        """Tebibit — 2^40 bits."""

        def _setup(self):
            return (2, 40, 'Tib', 'Tibs')

    class Pib(Bit):
        """Pebibit — 2^50 bits."""

        def _setup(self):
            return (2, 50, 'Pib', 'Pibs')

    class Eib(Bit):
        """Exbibit — 2^60 bits."""

        def _setup(self):
            return (2, 60, 'Eib', 'Eibs')

    class Zib(Bit):
        """Zebibit — 2^70 bits."""

        def _setup(self):
            return (2, 70, 'Zib', 'Zibs')

    class Yib(Bit):
        """Yobibit — 2^80 bits."""

        def _setup(self):
            return (2, 80, 'Yib', 'Yibs')

    ######################################################################
    # SI Prefixes for Bit based types

    class kb(Bit):
        """Kilobit — 10^3 (1,000) bits."""

        def _setup(self):
            return (10, 3, 'kb', 'kbs')

    class Mb(Bit):
        """Megabit — 10^6 (1,000,000) bits."""

        def _setup(self):
            return (10, 6, 'Mb', 'Mbs')

    class Gb(Bit):
        """Gigabit — 10^9 (1,000,000,000) bits."""

        def _setup(self):
            return (10, 9, 'Gb', 'Gbs')

    class Tb(Bit):
        """Terabit — 10^12 bits."""

        def _setup(self):
            return (10, 12, 'Tb', 'Tbs')

    class Pb(Bit):
        """Petabit — 10^15 bits."""

        def _setup(self):
            return (10, 15, 'Pb', 'Pbs')

    class Eb(Bit):
        """Exabit — 10^18 bits."""

        def _setup(self):
            return (10, 18, 'Eb', 'Ebs')

    class Zb(Bit):
        """Zettabit — 10^21 bits."""

        def _setup(self):
            return (10, 21, 'Zb', 'Zbs')

    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

    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]:?$')

    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)

    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)

    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))

    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)

    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)

    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

    @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()