diff --git a/.github/workflows/bandit.yml b/.github/workflows/bandit.yml new file mode 100644 index 0000000..58d698c --- /dev/null +++ b/.github/workflows/bandit.yml @@ -0,0 +1,27 @@ +--- +name: Bandit Security Scan + +on: + push: + branches: ["master"] + pull_request: + branches: ["master"] + schedule: + - cron: "0 0 * * 0" + workflow_dispatch: + +jobs: + analyze: + runs-on: ubuntu-latest + permissions: + security-events: write + env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + steps: + - name: Checkout code + uses: actions/checkout@v6.0.2 + + - name: Perform Bandit Analysis + uses: PyCQA/bandit-action@v1 + with: + targets: "bitmath/ tests/" diff --git a/.gitignore b/.gitignore index 0229c8e..0f762d2 100644 --- a/.gitignore +++ b/.gitignore @@ -53,3 +53,4 @@ docsite/build/doctrees bitmathenv3 bitmathenv2 bitmath2 +.vscode diff --git a/CLAUDE.md b/CLAUDE.md index 6830ee3..8abd675 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -27,11 +27,14 @@ Phases 1 (maintenance 1.4.0) and 2 (bitmath 2.0.0) are complete. The project: ## Common Commands ```bash -# Run the full test suite with coverage (creates venv, runs pytest + linting) +# Run the full test suite with coverage (creates venv, runs pytest + linting + bandit) make ci +# Run security scan only +make ci-bandit + # Run linting only -ruff check bitmath/ tests/ +make ci-pylint # Build a wheel make build @@ -66,6 +69,14 @@ All unit values are normalized to bits internally; conversion between units happ **Constants:** `NIST`, `SI`, `NIST_PREFIXES`, `SI_PREFIXES`, `ALL_UNIT_TYPES` +## Versioning + +The single source of truth for the version is the `VERSION` file. `pyproject.toml` reads it dynamically via `[tool.hatch.version]` — do not edit the version in `pyproject.toml` directly. The `Makefile` also reads `VERSION` for docs, man pages, and RPM builds. To bump the version, edit `VERSION` only. + +## Security Scanning + +Bandit runs as part of `make ci` via the `ci-bandit` target, scanning both `bitmath/` and `tests/`. It also runs as a GitHub Actions workflow (`.github/workflows/bandit.yml`) on push/PR to master and weekly. No issues were present as of 2.0.2. + ## Testing Notes - Test runner: `pytest` diff --git a/Makefile b/Makefile index 43e53de..698f730 100644 --- a/Makefile +++ b/Makefile @@ -216,5 +216,12 @@ ci-pylint: @echo "#################################################" . $(NAME)env3/bin/activate && pylint bitmath/__init__.py -ci: clean uniquetestnames virtualenv ci-list-deps ci-pycodestyle ci-pylint ci-unittests +ci-bandit: + @echo "" + @echo "#############################################" + @echo "# Running Bandit Security Scan in virtualenv" + @echo "#############################################" + . $(NAME)env3/bin/activate && bandit -r -v bitmath/ tests/ + +ci: clean uniquetestnames virtualenv ci-list-deps ci-pycodestyle ci-pylint ci-bandit ci-unittests : diff --git a/NEWS.rst b/NEWS.rst index 4cd1e99..9f62319 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -5,6 +5,80 @@ NEWS :depth: 1 :local: +.. _bitmath-2.1.0: + +bitmath-2.1.0 +************* + +*Unreleased* + +bitmath 2.1.0 is a focused follow-up to the 2.0.0 modernization. It +finishes the last of the Python 2 cleanup, tightens the project's +quality tooling, and retires one piece of legacy API surface. + + +Breaking Changes +================ + +**Internal representation is uniformly floating-point** + Every bitmath instance now stores its size as a 64-bit float, no + matter which constructor created it. Previously the ``bytes=`` and + ``bits=`` keyword constructors, along with the bit-family value + constructors such as ``Kib(N)``, leaked Python ``int`` values + through the ``.bytes`` and ``.bits`` properties. Those properties + now always return ``float``, matching the long-documented + floating-point measurement design described in the :ref:`Rules for + Math ` appendix. Equality, ordering, ``repr()``, and + arithmetic results are unchanged; only code that inspected + ``type(instance.bytes)`` or ``type(instance.bits)`` directly will + observe the difference. + +**listdir() is deprecated** + :func:`bitmath.listdir` now emits a :exc:`DeprecationWarning` on + every call and will be removed in a future release. Iterate with + :py:func:`os.walk` and call :func:`bitmath.getsize` directly + instead. Closes `issue #27 + `_. + + +Library Improvements +==================== + +**pathlib support** + :func:`bitmath.getsize` and :func:`bitmath.listdir` now accept + :class:`pathlib.Path` objects, not just strings, for their path + and ``search_base`` arguments. + + +Project Infrastructure +====================== + +**Linting moved to pylint** + pylint replaces flake8/pyflakes across the CI workflow and the + local toolchain, and the library is held at a 10.00/10 score. + pycodestyle is retained for the PEP 8 whitespace checks pylint + does not cover. + +**Security scanning with bandit** + bandit runs as part of ``make ci`` and as a dedicated GitHub + Actions workflow that fires on every push, every pull request, and + on a weekly schedule, scanning both ``bitmath/`` and ``tests/``. + +**100% test coverage** + The remaining coverage gaps were closed, including the + platform-specific :func:`bitmath.query_device_capacity` branches, + bringing the suite to 100% measured coverage on every supported + platform. + +**SPDX license headers** + Every source and test file now carries ``SPDX-License-Identifier`` + and ``SPDX-FileCopyrightText`` headers. + +**Single-sourced version** + The package version is read dynamically from the ``VERSION`` file + by hatchling, so bumping that one file propagates everywhere. + + .. _bitmath-2.0.0: bitmath-2.0.0 diff --git a/README.rst b/README.rst index 27b2ff6..f9d4b86 100644 --- a/README.rst +++ b/README.rst @@ -1,42 +1,44 @@ -.. image:: https://readthedocs.org/projects/bitmath/badge/?version=latest - :target: http://bitmath.rtfd.org/ - :align: right - :height: 19 - :width: 77 - .. image:: https://github.com/timlnx/bitmath/actions/workflows/python.yml/badge.svg :target: https://github.com/timlnx/bitmath/actions/workflows/python.yml +.. image:: https://img.shields.io/pypi/v/bitmath.svg + :target: https://pypi.org/project/bitmath/ + :alt: Latest Version +.. image:: https://img.shields.io/pypi/dm/bitmath?style=flat-square + :target: https://pypistats.org/packages/bitmath + :alt: PyPI - Package Downloads +.. image:: https://img.shields.io/pypi/implementation/bitmath?style=flat-square + :alt: PyPI - Implementation +.. image:: https://img.shields.io/pypi/pyversions/bitmath?style=flat-square + :alt: PyPI - Python Version +.. image:: https://readthedocs.org/projects/bitmath/badge/?version=latest + :target: http://bitmath.rtfd.org/ +.. image:: https://github.com/timlnx/bitmath/actions/workflows/bandit.yml/badge.svg + :target: https://github.com/timlnx/bitmath/actions/workflows/bandit.yml + :alt: Bandit Security Scan .. image:: https://img.shields.io/github/issues/timlnx/bitmath?style=flat-square :target: https://github.com/timlnx/bitmath/issues :alt: Open issues - .. image:: https://img.shields.io/github/issues-pr/timlnx/bitmath?style=flat-square :target: https://github.com/timlnx/bitmath/pulls :alt: Open pull requests - -.. image:: https://img.shields.io/pypi/dm/bitmath?style=flat-square - :target: https://pypistats.org/packages/bitmath - :alt: PyPI - Package Downloads - .. image:: https://img.shields.io/github/stars/timlnx/bitmath?style=flat-square - :target: https://pypistats.org/packages/bitmath + :target: https://github.com/timlnx/bitmath :alt: GitHub Project Popularity - -.. image:: https://img.shields.io/pypi/l/bitmath?style=flat-square - :target: https://opensource.org/licenses/MIT - :alt: PyPI - License - -.. image:: https://img.shields.io/pypi/implementation/bitmath?style=flat-square - :alt: PyPI - Implementation - -.. image:: https://img.shields.io/pypi/pyversions/bitmath?style=flat-square - :alt: PyPI - Python Version +.. image:: https://img.shields.io/badge/license-MIT-blue.svg + :target: https://github.com/timlnx/bitmath/blob/master/LICENSE + :alt: License bitmath ======= +* Free software: MIT License +* Documentation: https://bitmath.readthedocs.io/en/latest/ +* Source: https://github.com/timlnx/bitmath +* Bugs: https://github.com/timlnx/bitmath/issues +* Contributing: https://bitmath.readthedocs.io/en/latest/contributing.html + `bitmath `_ simplifies many facets of interacting with file sizes in various units. Originally focusing on file size unit conversion, functionality now includes: @@ -88,44 +90,33 @@ issues. Installation ============ -The easiest way to install bitmath is via ``dnf`` (or ``yum``) if -you're on a Fedora/RHEL based distribution. bitmath is available in -the main Fedora repositories, as well as EPEL Repositories. As of 2023 -bitmath is only developed, tested, and supported for `currently -supported `_ Python releases. - - -.. code-block:: bash - - $ sudo dnf install python3-bitmath +.. admonition:: Seeking a Debian Maintainer + bitmath is not currently packaged for Debian or Ubuntu. If you're + interested in maintaining the package for those distributions, please + see `issue #117 `_. -**PyPI**: +Requires Python 3.9 or newer. No runtime dependencies outside the +standard library. -You could also install bitmath from `PyPI -`_ if you like: +**PyPI** (the typical path): .. code-block:: bash - $ pip install --user bitmath - - - -**Source**: + pip install bitmath -To install from source, clone the repository and use pip: +**Fedora and EPEL** .. code-block:: bash - $ git clone https://github.com/timlnx/bitmath.git - $ cd bitmath - $ pip install . + sudo dnf install python3-bitmath -To also install the ``bitmath`` manpage: +**From source** .. code-block:: bash - $ sudo make install + git clone https://github.com/timlnx/bitmath.git + pip install ./bitmath Documentation diff --git a/VERSION b/VERSION index 38f77a6..7ec1d6d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.0.1 +2.1.0 diff --git a/bitmath.1 b/bitmath.1 index 3bbd11f..39d58ba 100644 --- a/bitmath.1 +++ b/bitmath.1 @@ -2,12 +2,12 @@ .\" Title: bitmath .\" Author: [see the "AUTHOR" section] .\" Generator: DocBook XSL Stylesheets vsnapshot -.\" Date: 05/04/2026 +.\" Date: 05/15/2026 .\" Manual: python-bitmath -.\" Source: bitmath 2.0.1 +.\" Source: bitmath 2.1.0 .\" Language: English .\" -.TH "BITMATH" "1" "05/04/2026" "bitmath 2\&.0\&.1" "python\-bitmath" +.TH "BITMATH" "1" "05/15/2026" "bitmath 2\&.1\&.0" "python\-bitmath" .\" ----------------------------------------------------------------- .\" * Define some portability stuff .\" ----------------------------------------------------------------- diff --git a/bitmath/__init__.py b/bitmath/__init__.py index b686bfe..a0ca26e 100644 --- a/bitmath/__init__.py +++ b/bitmath/__init__.py @@ -47,6 +47,7 @@ import numbers import os import os.path +import pathlib import platform import re import shutil @@ -114,7 +115,7 @@ #: Byte values represented by each SI prefix unit SI_STEPS = { - 'Bit': 1 / 8.0, + 'Bit': 1 / 8, 'Byte': 1, 'k': 1000, 'M': 1000000, @@ -132,7 +133,7 @@ #: Byte values represented by each NIST prefix unit NIST_STEPS = { - 'Bit': 1 / 8.0, + 'Bit': 1 / 8, 'Byte': 1, 'Ki': 1024, 'Mi': 1048576, @@ -219,14 +220,14 @@ def __init__(self, value=0, bytes=None, bits=None): # pylint: disable=redefined if bytes: # We were provided with the fundamental base unit, no need # to normalize - self._byte_value = bytes + 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.0 - self._bit_value = bits + 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 @@ -242,7 +243,7 @@ def _set_prefix_value(self) -> None: 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 / float(self._unit_value) + return value / self._unit_value def _setup(self) -> tuple: raise NotImplementedError("The base 'bitmath.Bitmath' class can not be used directly") @@ -552,7 +553,7 @@ def to_Bit(self): def to_Byte(self): """Convert to Byte.""" - return Byte(self._byte_value / float(NIST_STEPS['Byte'])) + return Byte(self._byte_value / NIST_STEPS['Byte']) # Properties Bit = property(lambda s: s.to_Bit()) @@ -844,7 +845,7 @@ def __truediv__(self, other): result = self._byte_value / other return (type(self))(bytes=result) # bm1 / bm2 - return self._byte_value / float(other.bytes) + return self._byte_value / other.bytes def __floordiv__(self, other): """Floor division: Supported operations with result types: @@ -898,7 +899,7 @@ def __rmul__(self, other): def __rtruediv__(self, other): # num / bm = num - return other / float(self.value) + 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: @@ -1195,8 +1196,8 @@ def _setup(self): def _norm(self, value): """Normalize the input value into the fundamental unit for this prefix type""" - self._bit_value = value * self._unit_value - self._byte_value = self._bit_value / 8.0 + self._bit_value = float(value) * self._unit_value + self._byte_value = self._bit_value / 8 ###################################################################### @@ -1560,12 +1561,14 @@ def query_capacity(path: Union[str, os.PathLike], bestprefix: bool = True, return Capacity(total, used, free) -def getsize(path: str, bestprefix: bool = True, system: int = NIST) -> Bitmath: +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. """ @@ -1577,7 +1580,7 @@ def getsize(path: str, bestprefix: bool = True, system: int = NIST) -> Bitmath: def listdir( # pylint: disable=too-many-arguments,too-many-positional-arguments - search_base: str, + search_base: str | pathlib.Path, followlinks: bool = False, glob: str = '*', relpath: bool = False, @@ -1591,7 +1594,8 @@ def listdir( # pylint: disable=too-many-arguments,too-many-positional-arguments * 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. + - `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) @@ -1602,11 +1606,19 @@ def listdir( # pylint: disable=too-many-arguments,too-many-positional-arguments - `system` - Provide a preferred unit system by setting `system` to either ``bitmath.NIST`` (default) or ``bitmath.SI``. -.. note:: This function does NOT return tuples for directory entities. - .. 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 " diff --git a/docsite/source/appendices/mixed_math.rst b/docsite/source/appendices/mixed_math.rst index c3cf66e..ed484ce 100644 --- a/docsite/source/appendices/mixed_math.rst +++ b/docsite/source/appendices/mixed_math.rst @@ -298,6 +298,9 @@ Design Philosophy: Floating-Point Measurements bitmath represents sizes as **floating-point measurements**, not as discrete counts of hardware bits. This is an intentional design choice. +Every constructor (by unit value, by ``bytes=``, or by ``bits=``) +normalizes its input to a float, so the ``bytes`` and ``bits`` +properties always return floating-point values. A file reported as ``1.7 GiB`` is a *measurement* — the same way ``2.3 miles`` or ``1.7 liters`` are measurements. Physical storage is diff --git a/docsite/source/appendices/on_units.rst b/docsite/source/appendices/on_units.rst index 8234268..cd0865c 100644 --- a/docsite/source/appendices/on_units.rst +++ b/docsite/source/appendices/on_units.rst @@ -21,7 +21,7 @@ a percent one "unit" of SI is to one "unit" of NIST. In [16]: one_kibi = 1 * 2**10 - In [17]: round(one_kilo / float(one_kibi), 2) + In [17]: round(one_kilo / one_kibi, 2) Out[17]: 0.98 @@ -29,7 +29,7 @@ a percent one "unit" of SI is to one "unit" of NIST. In [19]: one_tebi = 1 * 2**40 - In [20]: round(one_tera / float(one_tebi), 2) + In [20]: round(one_tera / one_tebi, 2) Out[20]: 0.91 @@ -37,7 +37,7 @@ a percent one "unit" of SI is to one "unit" of NIST. In [22]: one_exbi = 1 * 2**60 - In [23]: round(one_exa / float(one_exbi), 2) + In [23]: round(one_exa / one_exbi, 2) Out[23]: 0.87 diff --git a/docsite/source/classes.rst b/docsite/source/classes.rst index e58aebd..1389652 100644 --- a/docsite/source/classes.rst +++ b/docsite/source/classes.rst @@ -15,8 +15,8 @@ Available Classes There are two **fundamental** classes available, the :class:`Bit` and the :class:`Byte`. -There are **24** other classes available, representing all the prefix -units from **k** through **e** (*kilo/kibi* through *exa/exbi*). +There are **32** other classes available, representing all the prefix +units from **k** through **Y** (*kilo/kibi* through *yotta/yobi*). Classes with **'i'** in their names are **NIST** type classes. They were defined by the `National Institute of Standards and Technology @@ -64,6 +64,14 @@ more apparent: +---------------+--------------+ | ``TiB(Byte)`` | ``TB(Byte)`` | +---------------+--------------+ +| ``Zib(Bit)`` | ``Zb(Bit)`` | ++---------------+--------------+ +| ``ZiB(Byte)`` | ``ZB(Byte)`` | ++---------------+--------------+ +| ``Yib(Bit)`` | ``Yb(Bit)`` | ++---------------+--------------+ +| ``YiB(Byte)`` | ``YB(Byte)`` | ++---------------+--------------+ .. note:: As per SI definition, the ``kB`` and ``kb`` classes begins with a *lower-case* **k** character. @@ -107,8 +115,12 @@ Initializing .. class:: Tib([value=0[, bytes=None[, bits=None]]]) .. class:: YB([value=0[, bytes=None[, bits=None]]]) .. class:: Yb([value=0[, bytes=None[, bits=None]]]) +.. class:: YiB([value=0[, bytes=None[, bits=None]]]) +.. class:: Yib([value=0[, bytes=None[, bits=None]]]) .. class:: ZB([value=0[, bytes=None[, bits=None]]]) .. class:: Zb([value=0[, bytes=None[, bits=None]]]) +.. class:: ZiB([value=0[, bytes=None[, bits=None]]]) +.. class:: Zib([value=0[, bytes=None[, bits=None]]]) .. class:: Bitmath([value=0[, bytes=None[, bits=None]]]) diff --git a/docsite/source/index.rst b/docsite/source/index.rst index 82cd57a..167199f 100644 --- a/docsite/source/index.rst +++ b/docsite/source/index.rst @@ -31,6 +31,12 @@ bitmath ####### +* Free software: MIT License +* Documentation: https://bitmath.readthedocs.io/en/latest/ +* Source: https://github.com/timlnx/bitmath +* Bugs: https://github.com/timlnx/bitmath/issues +* Contributing: https://bitmath.readthedocs.io/en/latest/contributing.html + `bitmath `_ simplifies many facets of interacting with file sizes in various units. Originally focusing on file size unit conversion, functionality now includes: @@ -77,35 +83,34 @@ yourself `_. Installation ############ -bitmath is available in Fedora and EPEL repositories, as well as -directly available via `PyPI -`_. As of 2023 bitmath is only -developed, tested, and supported for `currently supported -`_ Python releases. +.. admonition:: Seeking a Debian Maintainer -**Package Managers** + bitmath is not currently packaged for Debian or Ubuntu. If you're + interested in maintaining the package for those distributions, please + see `issue #117 `_. -.. code-block:: bash +Requires Python 3.9 or newer. No runtime dependencies outside the +standard library. - $ sudo dnf install python3-bitmath - $ pip install --user bitmath +**PyPI** (the typical path): +.. code-block:: bash -**Source** + pip install bitmath -To install from source, clone the repository and use pip: +**Fedora and EPEL** .. code-block:: bash - $ git clone https://github.com/timlnx/bitmath.git - $ cd bitmath - $ pip install . + sudo dnf install python3-bitmath -To also install the ``bitmath`` manpage: +**From source** .. code-block:: bash - $ sudo make install + git clone https://github.com/timlnx/bitmath.git + pip install ./bitmath + Contents diff --git a/docsite/source/index.rst.in b/docsite/source/index.rst.in index 89e5c0d..97eea85 100644 --- a/docsite/source/index.rst.in +++ b/docsite/source/index.rst.in @@ -31,6 +31,12 @@ bitmath ####### +* Free software: MIT License +* Documentation: https://bitmath.readthedocs.io/en/latest/ +* Source: https://github.com/timlnx/bitmath +* Bugs: https://github.com/timlnx/bitmath/issues +* Contributing: https://bitmath.readthedocs.io/en/latest/contributing.html + `bitmath `_ simplifies many facets of interacting with file sizes in various units. Originally focusing on file size unit conversion, functionality now includes: @@ -77,35 +83,34 @@ yourself `_. Installation ############ -bitmath is available in Fedora and EPEL repositories, as well as -directly available via `PyPI -`_. As of 2023 bitmath is only -developed, tested, and supported for `currently supported -`_ Python releases. +.. admonition:: Seeking a Debian Maintainer -**Package Managers** + bitmath is not currently packaged for Debian or Ubuntu. If you're + interested in maintaining the package for those distributions, please + see `issue #117 `_. -.. code-block:: bash +Requires Python 3.9 or newer. No runtime dependencies outside the +standard library. - $ sudo dnf install python3-bitmath - $ pip install --user bitmath +**PyPI** (the typical path): +.. code-block:: bash -**Source** + pip install bitmath -To install from source, clone the repository and use pip: +**Fedora and EPEL** .. code-block:: bash - $ git clone https://github.com/timlnx/bitmath.git - $ cd bitmath - $ pip install . + sudo dnf install python3-bitmath -To also install the ``bitmath`` manpage: +**From source** .. code-block:: bash - $ sudo make install + git clone https://github.com/timlnx/bitmath.git + pip install ./bitmath + Contents diff --git a/docsite/source/instances.rst b/docsite/source/instances.rst index 803add6..6123ec3 100644 --- a/docsite/source/instances.rst +++ b/docsite/source/instances.rst @@ -175,7 +175,7 @@ bitmath objects come with a few basic methods: :py:meth:`to_THING`, to_THING() ========== -Like the :ref:`available classes `, there are 24 +Like the :ref:`available classes `, there are 32 ``to_THING()`` methods available. ``THING`` is any of the bitmath classes. You can even ``to_THING()`` an instance into itself again: @@ -516,7 +516,7 @@ Instance Properties THING Properties ================ -Like the :ref:`available classes `, there are 24 +Like the :ref:`available classes `, there are 32 ``THING`` properties available. ``THING`` is any of the bitmath classes. Under the covers these properties call ``to_THING``. diff --git a/docsite/source/module.rst b/docsite/source/module.rst index c24c97b..611d7c8 100644 --- a/docsite/source/module.rst +++ b/docsite/source/module.rst @@ -26,7 +26,9 @@ bitmath.getsize() Return a bitmath instance representing the size of a file at any given path. - :param string path: The path of a file to read the size of + :param path: The path of a file to read the size of. Accepts a + plain string or a :class:`pathlib.Path` object. + :type path: str or pathlib.Path :param bool bestprefix: **Default:** ``True``, the returned instance will be in the best human-readable prefix unit. If set to ``False`` the result @@ -37,46 +39,43 @@ bitmath.getsize() Internally :py:func:`bitmath.getsize` calls :py:func:`os.path.realpath` before calling - :py:func:`os.path.getsize` on any paths. + :py:func:`os.path.getsize` on any paths. Both functions accept + :class:`pathlib.Path` objects natively, so no conversion is needed. - Here's an example of where we'll run :py:func:`bitmath.getsize` on - the bitmath source code using the defaults for the ``bestprefix`` - and ``system`` parameters: + The traditional approach uses :func:`os.stat`, which gives raw bytes + and leaves unit conversion to you: .. code-block:: python - >>> import bitmath - >>> print(bitmath.getsize('./bitmath/__init__.py')) - 33.3583984375 KiB + >>> import os + >>> stat = os.stat('./bitmath/__init__.py') + >>> stat.st_size # raw bytes — no unit context + 34159 + >>> f"{stat.st_size / 1024:.4f} KiB" # manual math, locked to one unit + '33.3584 KiB' - Let's say we want to see the results in bytes. We can do this by - setting ``bestprefix`` to ``False``: + :py:func:`bitmath.getsize` returns a :class:`.Bitmath` object you can + format, convert, and do arithmetic with — no manual math required: .. code-block:: python - >>> import bitmath - >>> print(bitmath.getsize('./bitmath/__init__.py', bestprefix=False)) - 34159.0 B - - Recall, the default for representation is with the best - human-readable prefix. We can control the prefix system used by - setting ``system`` to either :py:data:`bitmath.NIST` (the default) - or :py:data:`bitmath.SI`: - - .. code-block:: python - :linenos: - :emphasize-lines: 1-4 + >>> import bitmath, pathlib + # NIST default - picks the most human-readable prefix automatically >>> print(bitmath.getsize('./bitmath/__init__.py')) 33.3583984375 KiB - >>> print(bitmath.getsize('./bitmath/__init__.py', system=bitmath.NIST)) + + # pathlib.Path works directly, no conversion needed + >>> print(bitmath.getsize(pathlib.Path('./bitmath/__init__.py'))) 33.3583984375 KiB + + # SI units instead of NIST >>> print(bitmath.getsize('./bitmath/__init__.py', system=bitmath.SI)) 34.159 kB - We can see in lines **1** → **4** that the same result is returned - when ``system`` is not set and when ``system`` is set to - :py:data:`bitmath.NIST` (the default). + # skip prefix selection — get the value as a plain Byte instance + >>> print(bitmath.getsize('./bitmath/__init__.py', bestprefix=False)) + 34159.0 B .. versionadded:: 1.0.7 @@ -92,7 +91,9 @@ bitmath.listdir() * The absolute/relative path to a discovered file * A bitmath instance representing the *apparent size* of the file - :param string search_base: The directory to begin walking down + :param search_base: The directory to begin walking down. May be a + plain string or a :class:`pathlib.Path` object. + :type search_base: str or pathlib.Path :param bool followlinks: **Default:** ``False``, do not follow links. Whether or not to follow symbolic links to directories. Setting to ``True`` @@ -118,123 +119,53 @@ bitmath.listdir() ``True`` :type system: One of :py:data:`bitmath.NIST` or :py:data:`bitmath.SI` - .. note:: - - * This function does **not** return tuples for directory - entities. Including directories in results is `scheduled for - introduction `_ - in an upcoming release. - * Symlinks to **files** are followed automatically + .. note:: Symlinks to **files** are followed automatically + .. deprecated:: 2.1.0 - When interpreting the results from this function it is *crucial* to - understand exactly which items are being taken into account, what - decisions were made to select those items, and how their sizes are - measured. + :func:`bitmath.listdir` is deprecated and will be removed in a future + release. Use :func:`os.walk` with :func:`bitmath.getsize` directly. - Results from this function may seem invalid when directly compared - to the results from common command line utilities, such as ``du``, - or ``tree``. - Let's pretend we have a directory structure like the following:: + Given this directory tree:: some_files/ ├── deeper_files/ - │   └── second_file - └── first_file - - Where ``some_files/`` is a directory, and so is - ``some_files/deeper_files/``. There are two regular files in this - tree: - - * ``somefiles/first_file`` - 1337 Bytes - * ``some_files/deeper_files/second_file`` - 13370 Bytes - - The **total** size of the files in this tree is **1337 + 13370 = - 14707** bytes. - - .. versionadded:: 2.0.0 - - By far the simplest way to sum all of the results is using the built-in - :py:func:`sum` function, or :py:func:`bitmath.sum` for additional control - (complete docs on that following this section). + │ └── second_file (13370 bytes) + └── first_file (1337 bytes) .. code-block:: python - >>> discovered_files = [f[1] for f in bitmath.listdir('./some_files')] - >>> print(discovered_files) - [Byte(1337.0), Byte(13370.0)] - >>> print(sum(discovered_files)) - 14707.0 B - >>> print(sum(discovered_files).best_prefix()) - 14.3623046875 KiB - - - - Let's call :py:func:`bitmath.listdir` on the ``some_files/`` - directory and see what the results look like. First we'll use all - the default parameters, then we'll set ``relpath`` to ``True``: - - .. code-block:: python - :linenos: - :emphasize-lines: 5-6,10-11 - >>> import bitmath - >>> for f in bitmath.listdir('./some_files'): - ... print(f) - ... - ('/tmp/tmp.P5lqtyqwPh/some_files/first_file', Byte(1337.0)) - ('/tmp/tmp.P5lqtyqwPh/some_files/deeper_files/second_file', Byte(13370.0)) - >>> for f in bitmath.listdir('./some_files', relpath=True): - ... print(f) - ... - ('some_files/first_file', Byte(1337.0)) - ('some_files/deeper_files/second_file', Byte(13370.0)) - - On lines **5** and **6** the results print the full path, whereas - on lines **10** and **11** the path is relative to the present - working directory. - - Let's play with the ``glob`` parameter now. Let's say we only - want to include results for files whose name begins with "second": - - .. code-block:: python - - >>> for f in bitmath.listdir('./some_files', glob='second*'): - ... print(f) - ... - ('/tmp/tmp.P5lqtyqwPh/some_files/deeper_files/second_file', Byte(13370.0)) - - - If we wish to avoid having to write for-loops, we can collect the - results into a list rather simply: - - .. code-block:: python - - >>> files = list(bitmath.listdir('./some_files')) - >>> print(files) - [('/tmp/tmp.P5lqtyqwPh/some_files/first_file', Byte(1337.0)), ('/tmp/tmp.P5lqtyqwPh/some_files/deeper_files/second_file', Byte(13370.0))] - Here's a more advanced example where we will sum the size of all - the returned results and then play around with the possible - formatting. Recall that a bitmath instance representing the size of - the discovered file is the second item in each returned tuple. - - .. code-block:: python - - >>> discovered_files = [f[1] for f in bitmath.listdir('./some_files')] - >>> print(discovered_files) - [Byte(1337.0), Byte(13370.0)] - >>> print(reduce(lambda x,y: x+y, discovered_files)) - 14707.0 B - >>> print(reduce(lambda x,y: x+y, discovered_files).best_prefix()) + >>> # Default: absolute paths, Byte instances + >>> for path, size in bitmath.listdir('./some_files'): + ... print(path, size) + /tmp/some_files/first_file Byte(1337.0) + /tmp/some_files/deeper_files/second_file Byte(13370.0) + + >>> # Relative paths + >>> for path, size in bitmath.listdir('./some_files', relpath=True): + ... print(path, size) + some_files/first_file Byte(1337.0) + some_files/deeper_files/second_file Byte(13370.0) + + >>> # Filter by glob + >>> for path, size in bitmath.listdir('./some_files', glob='second*'): + ... print(path, size) + /tmp/some_files/deeper_files/second_file Byte(13370.0) + + >>> # Sum all results + >>> sizes = [size for _, size in bitmath.listdir('./some_files')] + >>> print(sum(sizes).best_prefix()) 14.3623046875 KiB - >>> print(reduce(lambda x,y: x+y, discovered_files).best_prefix().format("{value:.3f} {unit}")) - 14.362 KiB - .. versionadded:: 1.0.7 + .. versionchanged:: 2.0.0 + The ``filter`` parameter was renamed to ``glob``. Passing ``filter`` + still works but emits a :exc:`DeprecationWarning`. + .. _bitmath_sum: diff --git a/pyproject.toml b/pyproject.toml index 2749282..c729a41 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "bitmath" -version = "2.0.1" +dynamic = ["version"] description = "Pythonic module for representing and manipulating file sizes with different prefix notations (file size unit conversion)" readme = "README.rst" requires-python = ">=3.9" @@ -78,6 +78,10 @@ exclude = [ [tool.hatch.build.targets.wheel] packages = ["bitmath"] +[tool.hatch.version] +path = "VERSION" +pattern = '(?P.+)' + [tool.hatch.publish.index] disable = true diff --git a/requirements.txt b/requirements.txt index 8e4bceb..e5acdd0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +bandit pycodestyle pylint pytest diff --git a/tests/test_file_size.py b/tests/test_file_size.py index ff08fb1..4dacaa0 100644 --- a/tests/test_file_size.py +++ b/tests/test_file_size.py @@ -271,10 +271,9 @@ def test_listdir_filter_kwarg_emits_deprecation_warning(self): with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") list(bitmath.listdir('./tests/listdir_nosymlinks/', filter='*')) - self.assertEqual(len(w), 1) - self.assertTrue(issubclass(w[0].category, DeprecationWarning)) - self.assertIn("filter", str(w[0].message)) - self.assertIn("glob", str(w[0].message)) + messages = [str(warning.message) for warning in w] + self.assertTrue(all(issubclass(warning.category, DeprecationWarning) for warning in w)) + self.assertTrue(any("filter" in m and "glob" in m for m in messages)) def test_listdir_filter_kwarg_still_works(self): """listdir() with deprecated filter= kwarg returns correct results""" diff --git a/tests/test_properties.py b/tests/test_properties.py index 056d021..2ba0de3 100644 --- a/tests/test_properties.py +++ b/tests/test_properties.py @@ -82,3 +82,25 @@ def test_system_property_invalid_base_raises(self): obj._base = 7 with self.assertRaises(ValueError): _ = obj.system + + +class TestPropertyTypesAlwaysFloat(TestCase): + """The .bytes and .bits properties are float for every construction + path. The internal representation is uniformly floating-point (see + the 'Floating-Point Measurements' design philosophy appendix).""" + + def test_bytes_property_float_from_value_constructor(self): + """.bytes is float when built via the unit-value constructor""" + self.assertIs(type(bitmath.KiB(1).bytes), float) + + def test_bytes_property_float_from_bytes_kwarg(self): + """.bytes is float when built via the bytes= keyword""" + self.assertIs(type(bitmath.Byte(bytes=1).bytes), float) + + def test_bits_property_float_from_bits_kwarg(self): + """.bits is float when built via the bits= keyword""" + self.assertIs(type(bitmath.Byte(bits=1).bits), float) + + def test_bits_property_float_from_bit_value_constructor(self): + """.bits is float when a Bit-family unit is built by value""" + self.assertIs(type(bitmath.Kib(1).bits), float)