pax_global_header00006660000000000000000000000064152050462610014513gustar00rootroot0000000000000052 comment=8aafd7ad9559d5f167e5ed3c7d85336b89e763b7 timlnx-bitmath-8354f39/000077500000000000000000000000001520504626100147435ustar00rootroot00000000000000timlnx-bitmath-8354f39/.coveragerc000066400000000000000000000013351520504626100170660ustar00rootroot00000000000000# -*- mode: conf -*- [run] [report] exclude_lines = # These should be impossible to trigger. raise ValueError."Invalid value given for 'system' parameter." raise ValueError."Instances mathematical base is an unsupported value raise ValueError."Can't parse string %s into a bitmath object # Don't test the __main__ entry point __main__ # Explicitly label a line as 'do not cover' pragma: no cover # Review: https://coverage.readthedocs.io/en/stable/config.html#syntax if you need a refresher # # BTW, the exports happen in the Makefile on lines like (for the 3.x tests): # # . $(NAME)env3/bin/activate && export PYVER=PY3X && nosetests-3.3 -v .... pragma: ${PYVER} no cover timlnx-bitmath-8354f39/.github/000077500000000000000000000000001520504626100163035ustar00rootroot00000000000000timlnx-bitmath-8354f39/.github/CONTRIBUTING.md000066400000000000000000000001671520504626100205400ustar00rootroot00000000000000Please see the [Contributing Guidelines](https://bitmath.readthedocs.io/en/latest/contributing.html) on Read The Docs. timlnx-bitmath-8354f39/.github/ISSUE_TEMPLATE/000077500000000000000000000000001520504626100204665ustar00rootroot00000000000000timlnx-bitmath-8354f39/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000014751520504626100231670ustar00rootroot00000000000000> Please put all multi-line output into the ``` blocks. Feel free to > delete any part that does not apply to your issue. For *enhancement* > requests you may delete everything. DESCRIPTION of the issue. Please include full error messages. Include links to relevant documentation if there is a disparity between what you observed and what you expected: ``` Put stack traces or error messages here if you have any, delete this if you don't. ``` How to REPRODUCE the issue: 1. meow 2. bark 3. nyan 4. woof How REPRODUCIBLE (every time? intermittently? only in certain environments?): What you EXPECTED to happen: What ACTUALLY happened: VERSION of bitmath effected (git hashes are OK). Did you install from RPM, PyPi, source? * Version: * Install Source: Your OPERATING SYSTEM and the affected PYTHON VERSION: timlnx-bitmath-8354f39/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000011241520504626100242110ustar00rootroot00000000000000--- name: Feature request about: Suggest an idea for this project title: '' labels: '' assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. timlnx-bitmath-8354f39/.github/dependabot.yml000066400000000000000000000007241520504626100211360ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" open-pull-requests-limit: 5 groups: actions-minor-patch: update-types: - "minor" - "patch" - package-ecosystem: "pip" directory: "/" schedule: interval: "weekly" open-pull-requests-limit: 5 groups: pip-minor-patch: update-types: - "minor" - "patch" timlnx-bitmath-8354f39/.github/pull_request_template.md000066400000000000000000000011621520504626100232440ustar00rootroot00000000000000Short description: Adds or changes $FEATURE/DOCUMENTATION Longer description, this may help improve our documentation, please be as verbose as you wish: Longer paragraph that describes what this is intended for. Include reasons why this is a useful feature. For example: what use-cases would be enhanced by your proposed change, why you are submitting this proposal, etc. Include links to any relevant external documentation or apropos libraries this may interact with. If this is a documentation change, please explain what inaccuracy you are fixing and be sure to include a link to the current inaccurate documentation. timlnx-bitmath-8354f39/.github/workflows/000077500000000000000000000000001520504626100203405ustar00rootroot00000000000000timlnx-bitmath-8354f39/.github/workflows/bandit.yml000066400000000000000000000011671520504626100223310ustar00rootroot00000000000000--- name: Bandit Security Scan on: push: branches: ["master"] pull_request: branches: ["master"] schedule: - cron: "0 0 * * 0" workflow_dispatch: permissions: read-all jobs: analyze: runs-on: ubuntu-latest permissions: security-events: write env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true steps: - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Perform Bandit Analysis uses: PyCQA/bandit-action@8a1b30610f61f3f792fe7556e888c9d7dffa52de # v1.0.1 with: targets: "bitmath/ tests/" timlnx-bitmath-8354f39/.github/workflows/codeql.yml000066400000000000000000000017361520504626100223410ustar00rootroot00000000000000name: CodeQL Security Scan on: push: branches: ["master", "bitmath2"] pull_request: branches: ["master"] schedule: - cron: "0 0 * * 0" permissions: read-all jobs: analyze: name: Analyze Python runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Initialize CodeQL uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 with: languages: python - name: Autobuild uses: github/codeql-action/autobuild@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 with: category: "/language:python" timlnx-bitmath-8354f39/.github/workflows/publish.yml000066400000000000000000000064161520504626100225400ustar00rootroot00000000000000name: Publish to PyPI on: release: types: [published] permissions: read-all jobs: build: runs-on: ubuntu-latest env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Verify tag matches VERSION file run: | TAG="${GITHUB_REF#refs/tags/}" TAG_VERSION="${TAG#v}" FILE_VERSION="$(cat VERSION)" if [ "$TAG_VERSION" != "$FILE_VERSION" ]; then echo "::error::Release tag does not match VERSION file." echo " Tag (raw): $TAG" echo " Tag (normalized): $TAG_VERSION" echo " VERSION file: $FILE_VERSION" echo "" echo "This almost always means VERSION was not bumped before the tag was cut." echo "Fix: bump VERSION on master, delete the bad tag and release, re-tag, recreate the release." exit 1 fi echo "OK: tag $TAG matches VERSION $FILE_VERSION" - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.12" - name: Build package run: pip install build && python -m build - name: Generate CycloneDX SBOM run: | pip install cyclonedx-bom VERSION=$(cat VERSION) mkdir -p sbom python -m venv /tmp/sbom-env /tmp/sbom-env/bin/pip install --upgrade pip /tmp/sbom-env/bin/pip install --no-deps dist/*.whl cyclonedx-py environment /tmp/sbom-env/bin/python \ --output-format JSON \ --output-file "sbom/bitmath-${VERSION}.cdx.json" - name: Upload dist artifacts uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: dist path: dist/ - name: Upload SBOM artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: sbom path: sbom/ publish: needs: build runs-on: ubuntu-latest environment: pypi permissions: id-token: write contents: write steps: - name: Download dist artifacts uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: dist path: dist/ - name: Download SBOM artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: sbom path: sbom/ - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 with: packages-dir: dist/ skip-existing: false - name: Attach release artifacts to GitHub Release # The .publish.attestation files are what Scorecard's Signed-Releases # check looks for; it inspects GitHub release assets, not PyPI. env: GH_TOKEN: ${{ github.token }} run: | TAG="${GITHUB_REF#refs/tags/}" gh release upload "$TAG" \ dist/bitmath-*.whl \ dist/bitmath-*.tar.gz \ dist/bitmath-*.publish.attestation \ sbom/bitmath-*.cdx.json \ --clobber --repo "$GITHUB_REPOSITORY" timlnx-bitmath-8354f39/.github/workflows/python.yml000066400000000000000000000026471520504626100224150ustar00rootroot00000000000000name: Python CI on: [push, pull_request] permissions: read-all jobs: build: env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true strategy: matrix: python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] os: ["macos-latest", "ubuntu-latest", "windows-latest"] runs-on: ${{ matrix.os }} steps: - name: "GitHub Checks it out :sunglasses-face:" uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: ${{ matrix.python-version }} cache: 'pip' - name: Verify Test Case Names Unique if: runner.os != 'Windows' run: | ./tests/test_unique_testcase_names.sh - name: install dependencies run: | python -m pip install --upgrade pip pip install -r requirements.txt - name: Pre-Tests code smell validation run: | pycodestyle -v --ignore=E501 bitmath/__init__.py tests/ - name: Lint with pylint if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12' run: | pylint bitmath/__init__.py - name: Run Unit Tests run: | pytest -v --cov=bitmath --cov-report term-missing --cov-report term:skip-covered --cov-report xml:coverage.xml tests timlnx-bitmath-8354f39/.github/workflows/sca.yml000066400000000000000000000013661520504626100216370ustar00rootroot00000000000000--- name: SCA Security Scan on: push: branches: ["master"] pull_request: branches: ["master"] schedule: - cron: "0 0 * * 0" workflow_dispatch: permissions: read-all jobs: audit: runs-on: ubuntu-latest env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true steps: - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Python uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.12" - name: Audit dev dependencies with pip-audit uses: pypa/gh-action-pip-audit@1220774d901786e6f652ae159f7b6bc8fea6d266 # v1.1.0 with: inputs: requirements.txt timlnx-bitmath-8354f39/.github/workflows/scorecard.yml000066400000000000000000000022241520504626100230300ustar00rootroot00000000000000name: OSSF Scorecard Security Scan on: branch_protection_rule: push: branches: ["master"] schedule: - cron: "30 1 * * 6" workflow_dispatch: permissions: read-all jobs: analysis: name: Scorecard analysis runs-on: ubuntu-latest permissions: security-events: write id-token: write contents: read actions: read steps: - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Run analysis uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 with: results_file: results.sarif results_format: sarif publish_results: true - name: Upload artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: SARIF file path: results.sarif retention-days: 5 - name: Upload to code-scanning uses: github/codeql-action/upload-sarif@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 with: sarif_file: results.sarif timlnx-bitmath-8354f39/.gitignore000066400000000000000000000010111520504626100167240ustar00rootroot00000000000000# Emacs backup garbage *~ # setup/distutils MANIFEST build dist rpm-build # python *.py[cod] # C extensions *.so # Packages *.egg *.egg-info dist build eggs parts bin var sdist develop-eggs .installed.cfg lib lib64 __pycache__ # Installer logs pip-log.txt # Unit test / coverage reports .coverage coverage.xml .tox nosetests.xml # Translations *.mo # Mr Developer .mr.developer.cfg .project .pydevproject htmlcov cover bitmathenv docsite/build/html docsite/build/doctrees bitmathenv3 bitmathenv2 bitmath2 .vscode timlnx-bitmath-8354f39/.packit.yaml000066400000000000000000000026411520504626100171630ustar00rootroot00000000000000upstream_package_name: bitmath downstream_package_name: python-bitmath specfile_path: python-bitmath.spec upstream_tag_template: "v{version}" jobs: # Copr build on every pull request - job: copr_build trigger: pull_request version_suffix: "" owner: tbielawa project: bitmath-ci targets: - fedora-rawhide-x86_64 - fedora-42-x86_64 - epel-9-x86_64 - epel-10-x86_64 # Copr build on RC tags (v*-rc* pattern) - job: copr_build trigger: release upstream_tag_include: "v.*-rc.*" owner: tbielawa project: bitmath-ci targets: - fedora-rawhide-x86_64 - fedora-42-x86_64 - epel-9-x86_64 - epel-10-x86_64 # propose_downstream on production tags (clean vX.Y.Z, no RC suffix) - job: propose_downstream trigger: release upstream_tag_exclude: ".*-rc.*" dist_git_branches: - rawhide - f42 - epel9 - epel10 # Koji build triggers after dist-git PR is merged - job: koji_build trigger: commit dist_git_branches: - rawhide - f42 - epel9 - epel10 # Bodhi update after successful Koji build (stable branches only, not rawhide) - job: bodhi_update trigger: commit dist_git_branches: - f42 - epel9 - epel10 bodhi_extra_params: update_type: enhancement notes: "New upstream release. See https://github.com/timlnx/bitmath/releases for details." timlnx-bitmath-8354f39/.readthedocs.yaml000066400000000000000000000003201520504626100201650ustar00rootroot00000000000000version: 2 build: os: ubuntu-24.04 tools: python: "3.11" sphinx: configuration: docsite/source/conf.py python: install: - requirements: doc-requirements.txt - method: pip path: . timlnx-bitmath-8354f39/ARCHITECTURE.md000066400000000000000000000462571520504626100171650ustar00rootroot00000000000000# bitmath Architecture > This document exists to satisfy the OSPS Baseline Level 2 control > [OSPS-SA-01.01](https://www.bestpractices.dev/en/projects/12749/baseline-2) > (design documentation showing the actions and actors within the > system), in addition to being useful for downstream consumers and > contributors who want to understand how bitmath fits together. ## Contents - [Why This Document Exists](#why-this-document-exists) - [Runtime Architecture](#runtime-architecture) - [Build and Release Pipeline](#build-and-release-pipeline) - [CI and Quality Gates](#ci-and-quality-gates) - [Trust Boundaries](#trust-boundaries) - [Documentation Pipeline](#documentation-pipeline) - [Maintenance](#maintenance) ## Why This Document Exists This is the architecture map for bitmath. It exists so downstream consumers and future contributors can see, in one place, what the moving parts are and how they fit together. It covers two things: the runtime library (what runs in your process when you `import bitmath`), and the build and release pipeline (how that code gets from this repository onto your machine). It does not cover everything in the repository. Other concerns have their own homes: * [MAINTAINERS.md](MAINTAINERS.md): who has access to what. * [SECURITY.md](SECURITY.md): how to report a vulnerability. * [SECURITY_ASSESSMENT.md](SECURITY_ASSESSMENT.md): the standing threat model. * [bitmath on Read The Docs](https://bitmath.readthedocs.io/): API reference and tutorials. If you are wondering whether to update this file: yes, if you are adding a new public function, restructuring the class hierarchy, changing the build backend, or changing how releases get published. No, if you are fixing a bug or adding a test. ## Runtime Architecture ### Component Inventory The runtime library is a single Python module, `bitmath`, deliberately kept small. Almost all the logic lives in `bitmath/__init__.py`. The actors that show up at runtime: | Actor | Lives at | What it does | |-------|----------|--------------| | User code | The consumer's process | Imports bitmath, calls module-level functions, instantiates unit classes, does arithmetic | | Public module API | `bitmath.*` | Module-level entry points: `parse_string`, `parse_string_unsafe`, `best_prefix`, `getsize`, `listdir` (deprecated in 2.1.0), `query_capacity`, `query_device_capacity` | | `Bitmath` base class | `bitmath.Bitmath` | The arithmetic, comparison, bitwise, and formatting engine. Every concrete unit class inherits from it | | `Byte` family | `bitmath.Byte`, `KiB`, `MiB`, `GiB`, `TiB`, `PiB`, `EiB`, `kB`, `MB`, `GB`, `TB`, `PB`, `EB`, `ZB`, `YB` | Byte-based concrete unit classes (NIST binary and SI decimal) | | `Bit` family | `bitmath.Bit`, `Kib`, `Mib`, `Gib`, `Tib`, `Pib`, `Eib`, `kb`, `Mb`, `Gb`, `Tb`, `Pb`, `Eb`, `Zb`, `Yb` | Bit-based concrete unit classes (NIST binary and SI decimal) | | Constants | `bitmath.NIST`, `bitmath.SI`, `bitmath.NIST_PREFIXES`, `bitmath.SI_PREFIXES`, `bitmath.ALL_UNIT_TYPES` | Module-level lookup tables and system identifiers | | OS abstraction layer | Standard library: `shutil`, `os`, `fcntl` (Linux), `ctypes` (Windows) | Platform-specific calls used by the capacity-query functions | ### Layered View ``` +--------------------------------------------------------------+ | User code | | import bitmath; KiB(1024).MiB | +--------------------------------------------------------------+ | v +--------------------------------------------------------------+ | Public module API (bitmath.*) | | parse_string, best_prefix, getsize, query_capacity, etc. | +--------------------------------------------------------------+ | v +--------------------------------------------------------------+ | Library core | | Bitmath base class | | Byte family (KiB, MiB, kB, MB, ...) | | Bit family (Kib, Mib, kb, Mb, ...) | | _norm_value: normalize every value to bits internally | +--------------------------------------------------------------+ | v (only for getsize / listdir / | query_capacity / | query_device_capacity) +--------------------------------------------------------------+ | OS abstraction layer | | shutil.disk_usage -- cross-platform capacity | | fcntl.ioctl -- Linux block device capacity | | ctypes / Win32 API -- Windows block device capacity | | macOS: NotImplementedError (System Integrity Protection) | +--------------------------------------------------------------+ ``` ### Actions The things that happen at runtime, in roughly the order a typical consumer triggers them: 1. **Construction.** A user creates a bitmath instance (`KiB(1024)`, `Byte(bits=8192)`, `parse_string("4.5 GiB")`). The constructor passes the value through `_norm_value`, which converts everything to an internal bits representation using the class-level `_base_value` and `_unit_value` constants. After construction, the instance carries one canonical value. 2. **Conversion.** A user requests a different unit (`KiB(1024).MiB`). Each unit class exposes properties that compute their own value from the internal bits representation. No conversion is cached; every property access does the math fresh. 3. **Arithmetic.** Operators (`+`, `-`, `*`, `/`, `//`, `%`, `divmod`) operate on the internal bits representation and return a new instance. Mixed-unit arithmetic (`KiB(1) + MiB(1)`) resolves through the bits representation, so the result is unit-correct. 4. **Comparison and bitwise.** Comparisons (`<`, `>`, `==`) and bitwise operations (`&`, `|`, `^`, `~`, `<<`, `>>`) work on the internal bits representation. 5. **Formatting.** `str()`, `repr()`, and `format()` (including f-string usage) produce human-readable output. The `best_prefix` function picks the most readable unit for a given byte value. 6. **Parsing.** `parse_string` and `parse_string_unsafe` accept a string like `"4.5 GiB"` and return a bitmath instance. The strict form raises on ambiguous unit; the unsafe form picks a default system (SI by default) when the unit is ambiguous. 7. **Filesystem inspection.** `getsize` accepts a path or `pathlib.Path` and returns a bitmath instance for the file size. `listdir` (deprecated in 2.1.0) walks a tree and yields sizes. 8. **Capacity querying.** `query_capacity(path)` returns a `Capacity(total, used, free)` named tuple of `Byte` instances. This uses `shutil.disk_usage` and works on every supported platform without elevated privileges. `query_device_capacity(fd)` returns the raw physical block-device capacity and requires elevated privileges on Linux (root) and Windows (administrator); macOS raises `NotImplementedError`. ### Data Flow Untrusted input enters the library at exactly three runtime places: the string parsers (`parse_string`, `parse_string_unsafe`), the filesystem helpers (`getsize`, `listdir`), and the capacity functions (`query_capacity`, `query_device_capacity`). For everything else the input is a Python number or another bitmath instance, both of which are inherently trusted. The trust boundaries section below covers what bitmath does and does not validate at each entry point. See [SECURITY_ASSESSMENT.md](SECURITY_ASSESSMENT.md) for the full threat model. ## Build and Release Pipeline ### Component Inventory | Actor | Lives at | What it does | |-------|----------|--------------| | Source repository | github.com/timlnx/bitmath | Single source of truth for all code, docs, and pipeline configuration | | `VERSION` file | Repository root | Single source of the project version; read by Hatchling and the Makefile | | `pyproject.toml` | Repository root | PEP 517/518/621 project metadata; declares Hatchling as the build backend | | Hatchling | Build-time only | PyPA-blessed build backend; produces the wheel and sdist artifacts | | `Makefile` | Repository root | Local automation; targets like `make ci`, `make build`, `make docs` | | GitHub Actions runners | `.github/workflows/*.yml` | Hosted CI execution environment | | `publish.yml` workflow | `.github/workflows/publish.yml` | Two-job workflow: build artifacts, then publish via OIDC | | PyPI Trusted Publishing | PyPI's OIDC verifier | Validates the OIDC token from the `publish.yml` workflow and authorizes the upload | | PyPI | pypi.org | The distribution channel; serves wheels and sdists over HTTPS to `pip` | | Read The Docs | bitmath.readthedocs.io | Hosts the rendered documentation; rebuilds on push to master | | `.packit.yaml` | Repository root | Packit configuration; declares which release events trigger Copr scratch builds, dist-git PRs, Koji builds, and Bodhi updates | | `python-bitmath.spec` | Repository root | RPM packaging spec used by Packit to build Fedora and EPEL RPMs | | Packit | packit.dev | Hosted automation that connects upstream releases to Fedora dist-git, Copr, Koji, and Bodhi based on `.packit.yaml` | | Copr | copr.fedorainfracloud.org/coprs/tbielawa/bitmath-ci | Fedora's build-on-demand farm; used for PR scratch builds and release-candidate builds | | Fedora dist-git | src.fedoraproject.org/rpms/python-bitmath | Where the official RPM source lives for each Fedora/EPEL branch | | Koji | koji.fedoraproject.org | Fedora's official build system; produces the RPMs that ship to users | | Bodhi | bodhi.fedoraproject.org | Fedora's update system; pushes Koji-built RPMs to stable for f42, epel9, and epel10 | ### Pipeline View ``` +------------------------+ | Developer | | (see MAINTAINERS.md) | +-----------+------------+ | git push, git tag v +------------------------+ | GitHub repository | | timlnx/bitmath | | branch protection on | | master (17 required | | checks, enforce | | admins) | +-----------+------------+ | release: published v +------------------------+ | publish.yml workflow | | | | Job 1: build | | pip install build | | python -m build | | -> dist/ artifacts | | | | Job 2: publish | | id-token: write | | PyPI OIDC exchange | +-----------+------------+ | OIDC-authenticated upload v +------------------------+ | PyPI | | pypi.org/project | | /bitmath/ | | wheel + sdist | | + PEP 740 | | attestations | +-----------+------------+ | pip install bitmath v +------------------------+ | Consumer's process | +------------------------+ ``` ### RPM Distribution Path (Fedora and EPEL via Packit) The same `release: published` event that triggers `publish.yml` also fires the Packit pipeline. The two paths run in parallel and are independent of each other; PyPI consumers get the wheel, Fedora and EPEL consumers get an RPM. Production releases (clean `vX.Y.Z` tags) flow through `propose_downstream` to dist-git, Koji, and Bodhi. Release-candidate tags (`vX.Y.Z-rcN`) take a shorter path that only produces Copr builds for testing. ``` +------------------------+ | release: published | | (same event that fires | | the publish.yml flow | | above) | +-----------+------------+ | reads .packit.yaml v + python-bitmath.spec +------------------------+ | Packit | | | | clean vX.Y.Z tag: | | propose_downstream | | opens dist-git PRs | | for rawhide, f42, | | epel9, epel10 | | | | vX.Y.Z-rcN tag: | | copr_build only | +-----------+------------+ | dist-git PR merged v +------------------------+ | Fedora dist-git | | src.fedoraproject | | .org/rpms/ | | python-bitmath | +-----------+------------+ | commit trigger v +------------------------+ | Koji | | builds RPMs from | | each dist-git branch | +-----------+------------+ | tagged build v +------------------------+ | Bodhi | | update_type: | | enhancement; | | stable push for | | f42, epel9, epel10 | | (rawhide skipped) | +-----------+------------+ | dnf install python-bitmath v +------------------------+ | Fedora / EPEL consumer | +------------------------+ ``` The Copr scratch-build job that fires on every pull request is not shown above; it is a CI-style validation, not part of the release pipeline. It uses the same `.packit.yaml` and spec file to confirm the RPM still builds across the targeted Fedora and EPEL branches before a change merges. **Known limitation**: the target matrix in `.packit.yaml` today is `fedora-rawhide`, `fedora-42`, `epel-9`, and `epel-10`. As new Fedora and EPEL releases land, that list needs to be refreshed. That work is tracked outside this document. ### Actions 1. **Version bump.** I edit `VERSION` to the new SemVer string. `pyproject.toml` reads this dynamically via `[tool.hatch.version]`; the Makefile reads it for docs and man pages. There is no other place to update. 2. **Changelog.** I add the section for the new version to `NEWS.rst`. 3. **Tag and push.** A signed tag is pushed to GitHub. 4. **Create a GitHub Release.** Releasing from the tag fires the `release: published` event. The `publish.yml` workflow has no other trigger; it cannot fire on pull requests or direct pushes. 5. **Build job.** Runs on a clean ubuntu-latest runner with Python 3.12. Installs `build`, runs `python -m build`, uploads the `dist/` directory as a workflow artifact. 6. **Publish job.** Depends on the build job. Downloads the artifact. Requests an OIDC token (`id-token: write`) from GitHub Actions and exchanges it with PyPI for short-lived upload credentials via Trusted Publishing. Uploads the wheel and sdist. No long-lived PyPI API token exists anywhere in the project. 7. **Consumer install.** A consumer runs `pip install bitmath`. `pip` fetches the wheel over HTTPS from PyPI, verifies the package against the published SHA-256 hash, and installs into their environment. ## CI and Quality Gates CI runs continuously on `master` and on every pull request. These actors influence what is merged but do not produce released artifacts. They are summarized here for completeness; the source of truth for each is the corresponding workflow file in `.github/workflows/`. | Actor | Workflow | What it gates | |-------|----------|---------------| | pytest matrix | `python.yml` | Full test suite across Python 3.9–3.13 on macOS, Ubuntu, Windows | | Bandit | `bandit.yml` | Python security smell analysis across `bitmath/` and `tests/` | | CodeQL | `codeql.yml` | Semantic analysis for Python vulnerabilities | | OSSF Scorecard | `scorecard.yml` | Weekly repository security posture report | | Dependabot | `.github/dependabot.yml` | Automated dependency update pull requests | The first three are required status checks on the `master` branch. Merges are blocked until they pass. `enforce_admins: true` so I cannot bypass them. ## Trust Boundaries bitmath validates input only at the places untrusted data enters the library. The ingress points are: * **String parsing** (`parse_string`, `parse_string_unsafe`). Regex-driven, no `eval`, raises `ValueError` on anything malformed. Hypothesis-based fuzzing tests exercise this path. * **Filesystem paths** (`getsize`, `listdir`). bitmath does no path sanitization. The consumer is responsible for validating paths before passing them in. This is documented behavior; sanitization rules depend on the consumer's threat model in ways the library cannot anticipate. * **File descriptors** (`query_device_capacity`). Operates only on descriptors the consumer already opened. The ioctl arguments are fixed constants; no untrusted input flows into the kernel call. * **The build pipeline.** `publish.yml` cannot be triggered by a pull request, only by a release event. Untrusted contributor code never touches PyPI credentials. Branch protection prevents direct pushes to master from bypassing CI. See [SECURITY_ASSESSMENT.md](SECURITY_ASSESSMENT.md) for the full threat model and the specific mitigations at each boundary. ## Documentation Pipeline The documentation lives in `docsite/source/`. Sphinx builds it. `index.rst.in` is the source template; `index.rst` is generated from `index.rst.in` plus `README.rst` by `make docs`. Editing `index.rst` directly is wrong because the next `make docs` will overwrite it. Read The Docs runs the same build automatically on push to master and publishes the result at https://bitmath.readthedocs.io/. ## Maintenance This document is reviewed at every minor release. The contract: * New public API → update the Runtime architecture component inventory and the actions list. * New CI workflow or build pipeline change → update Build and release pipeline and/or CI and quality gates. * New trust boundary → update Trust boundaries here and the threat model in [SECURITY_ASSESSMENT.md](SECURITY_ASSESSMENT.md). * Build-backend or package-tooling change → update Build and release pipeline. If this document drifts out of date, that is a defect worth [opening an issue](https://github.com/timlnx/bitmath/issues/new) or a PR. timlnx-bitmath-8354f39/CLAUDE.md000066400000000000000000000104771520504626100162330ustar00rootroot00000000000000# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Project Overview **bitmath** is a pure-Python library (no external runtime dependencies) for representing and converting file sizes across SI (decimal) and NIST (binary) unit systems. It supports arithmetic (including floor division, modulo, and `divmod` for capacity math), rich comparisons, bitwise ops, parsing, formatting, and f-string/format() support. ## Project Direction bitmath has been around for almost 12 years, and over that lifetime it promised to deliver backwards compatibility. It delivered on that promise and gathered a strong supporting of people and eventual "critical infrastructure" project status on the PyPI.org website. As of January 2023 the project maintainer (me) has stated in this issue https://github.com/timlnx/bitmath/issues/99 that the project is still alive and the next release will be python 3 support only. Much of that porting work has already happened in the `2023-01-26-no-more-py2` branch https://github.com/timlnx/bitmath/tree/2023-01-26-no-more-py2 ### Current State (as of 2.0.0) Phases 1 (maintenance 1.4.0) and 2 (bitmath 2.0.0) are complete. The project: - Supports **Python 3.9 and newer only** (`requires-python = ">=3.9"` in `pyproject.toml`) - Uses `hatchling` as the build backend (replaces `setup.py`) - Uses `pytest` as the test runner (303 tests). Coverage is high but platform-sensitive: the `query_device_capacity` branches for the *other* OS are naturally uncovered on any single run. - Is published on PyPI as version 2.0.0 - Drop-in compatible with the 1.x public API ## Common Commands ```bash # 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 make ci-pylint # Build a wheel make build # Install in development mode pip install -e . ``` Run a single test file: ```bash python -m pytest tests/test_arithmetic.py -v ``` ## Architecture Almost all logic lives in a single file: `bitmath/__init__.py` (~1650 lines). **Class hierarchy:** - `Bitmath` — base class with all arithmetic, comparison, bitwise, formatting, and conversion logic - `Byte` — byte-based units; subclasses: `KiB MiB GiB TiB PiB EiB` (NIST/base-2) and `kB MB GB TB PB EB ZB YB` (SI/base-10) - `Bit` — bit-based units; subclasses: `Kib Mib Gib Tib Pib Eib` (NIST) and `kb Mb Gb Tb Pb Eb Zb Yb` (SI) All unit values are normalized to bits internally; conversion between units happens at construction time via `_norm_value` and class-level `_base_value` / `_unit_value` constants. **Key module-level functions:** - `best_prefix(value, system=NIST)` — pick the most human-readable unit for a raw byte value - `getsize(path, ...)` — file size with automatic prefix selection - `listdir(search_base, ...)` — recursive directory listing with sizes - `parse_string(s)` / `parse_string_unsafe(s, system=SI)` — string → bitmath object - `query_capacity(path)` — volume/mount-point capacity as a `Capacity(total, used, free)` NamedTuple of `Byte` instances; cross-platform, no elevated privileges required - `query_device_capacity(device_fd)` — raw physical block device capacity (Linux: requires root; Windows: requires administrator; macOS: raises `NotImplementedError` due to SIP) **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` - All tests are in `tests/` as `test_*.py` files - Test case names must be unique across the suite — enforced by `tests/test_unique_testcase_names.sh` - Coverage is platform-sensitive: Windows and POSIX `query_device_capacity` paths only run on their respective OS - `unittest.mock` (stdlib) is used for patching in integration tests timlnx-bitmath-8354f39/CODE_OF_CONDUCT.md000066400000000000000000000062131520504626100175440ustar00rootroot00000000000000# Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at bitmath@lnx.cx. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ timlnx-bitmath-8354f39/LICENSE000066400000000000000000000021301520504626100157440ustar00rootroot00000000000000The MIT License (MIT) Copyright © 2014-2026 Tim Case (fmr. tbielawa) 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, sublicense, 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. timlnx-bitmath-8354f39/LICENSES/000077500000000000000000000000001520504626100161505ustar00rootroot00000000000000timlnx-bitmath-8354f39/LICENSES/MIT.txt000066400000000000000000000021301520504626100173360ustar00rootroot00000000000000The MIT License (MIT) Copyright © 2014-2026 Tim Case (fmr. tbielawa) 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, sublicense, 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. timlnx-bitmath-8354f39/MAINTAINERS.md000066400000000000000000000100201520504626100170300ustar00rootroot00000000000000# Maintainers bitmath is a one-person project, and has been since 2014. This file exists so downstream consumers (and any future contributors) can see exactly who has the keys to the kingdom and what those keys unlock. ## Current Maintainer **Tim Case** (GitHub: [@timlnx](https://github.com/timlnx)) * Role: Project lead, sole maintainer * Contact: `bitmath@lnx.cx` * Bluesky: [@lnx.cx](https://bsky.app/profile/lnx.cx) * Homepage: [Technitribe](https://blog.lnx.cx) * Formerly: Tim Bielawa (`tbielawa`) ## Access to Sensitive Resources I have admin access to the following resources. Nobody else does. * The [timlnx/bitmath](https://github.com/timlnx/bitmath) GitHub repository (admin role, including the ability to change branch protection rules, manage secrets, and modify security settings). * The `bitmath` project on [PyPI](https://pypi.org/project/bitmath/). Publishing happens through PyPI Trusted Publishing (OIDC), so there is no long-lived API token to leak. The trust relationship is configured in PyPI's web UI and is scoped to the `.github/workflows/publish.yml` workflow on the `master` branch. PyPI account has 2fa enabled. * The [bitmath on Read The Docs](https://bitmath.readthedocs.io/) documentation project. * The `bitmath@lnx.cx` email address used for private vulnerability reports (see [SECURITY.md](SECURITY.md)). If you ever see a release on PyPI, a tag pushed to master, or a security advisory published that did not originate from me, treat it as a compromise and reach out immediately through any of the contact channels in [SECURITY.md](SECURITY.md). ## Responsibilities As the sole maintainer I am responsible for: * Reviewing and merging pull requests. * Cutting releases (version bumps, `NEWS.rst` entries, tags, PyPI publication via the OIDC workflow). * Triaging security reports per the timelines in [SECURITY.md](SECURITY.md). * Enforcing the [Code of Conduct](CODE_OF_CONDUCT.md). Note that I am both the sole enforcer and the sole maintainer, which means if a Code of Conduct concern involves me directly, the project itself cannot offer you a neutral venue. Your remedies in that case are the usual ones available against any open source maintainer: public comment on the issue tracker, a fork, or moving to a different library. I would rather be honest about that limitation than pretend the project has a governance structure it does not have. * Keeping the dependency surface honest. bitmath has zero runtime dependencies on purpose; development dependencies are listed in `requirements.txt`. ## Decision Making Decisions are made by me, with input solicited from the issue and PR discussions when the question is non-obvious or has visible downstream impact. I try to write up the reasoning when something might surprise people (see commit messages and `NEWS.rst` for the audit trail). ## Becoming a Maintainer bitmath has not historically had additional maintainers, but the project has been around for over a decade and I am not immortal. If you are interested in helping carry this forward, open an issue or reach out by email. Co-maintainership would start with regularly landing reviewed contributions and graduate to write access after we have built up some trust. ### Open Maintainer Request: Debian / Ubuntu Packaging There is a standing ask tracked in [issue #117](https://github.com/timlnx/bitmath/issues/117) ("Deb Maintainer Wanted") and echoed in the [contributing guide](https://bitmath.readthedocs.io/en/latest/contributing.html#supported-python-versions): bitmath currently tracks the Python versions shipped in the current and previous major RHEL releases via EPEL, because that is the ecosystem I personally pay attention to. There is no official Debian or Ubuntu release channel right now and I am looking for somebody to take over `.deb` patching and the related distribution coverage. This is a real way to get involved without having to take on the entire project. Comment on #117, email me, or open a fresh issue if you want to claim the role. ## Inactive Maintainers None at present. timlnx-bitmath-8354f39/Makefile000066400000000000000000000205301520504626100164030ustar00rootroot00000000000000######################################################## # Makefile for bitmath # # useful targets: # make build ---------------- build sdist + wheel (output in dist/) # make pypitest ------------- upload to TestPyPI (requires: pip install twine) # make pypi ----------------- upload to PyPI (requires: pip install twine) # make rpm ----------------- produce RPMs # make docs ----------------- rebuild the docs # make ci ----------------- run full test suite in virtualenv ######################################################## # > VARIABLE = value # # Normal setting of a variable - values within it are recursively # expanded when the variable is USED, not when it's declared. # # > VARIABLE := value # # Setting of a variable with simple expansion of the values inside - # values within it are expanded at DECLARATION time. ######################################################## NAME := bitmath PKGNAME := python-$(NAME) # VERSION file provides one place to update the software version. VERSION := $(shell cat VERSION) RPMRELEASE := 1 RPMSPECDIR := . RPMSPEC := $(RPMSPECDIR)/$(PKGNAME).spec # This doesn't evaluate until it's called. The -D argument is the # directory of the target file ($@), kinda like `dirname`. ASCII2MAN = a2x -D $(dir $@) -d manpage -f manpage $< ASCII2HTMLMAN = a2x -D docs/html/man/ -d manpage -f xhtml MANPAGES := bitmath.1 ###################################################################### # Begin make targets ###################################################################### # Documentation. YAY!!!! DOCSVENV := bitmath2 docs-venv: @if [ ! -d "$(DOCSVENV)" ]; then \ echo "Creating docs virtualenv '$(DOCSVENV)' with Python 3.12..."; \ python3.12 -m venv $(DOCSVENV); \ fi . $(DOCSVENV)/bin/activate && pip install -q -r doc-requirements.txt docs: docs-venv $(MANPAGES) docsite/source/index.rst . $(DOCSVENV)/bin/activate && cd docsite && make html # Scan every external URL in the docsite (plus NEWS.rst via include) and # report broken links. Output lands in docsite/build/linkcheck/output.txt. # Configured via linkcheck_* settings in docsite/source/conf.py. linkcheck: docs-venv docsite/source/index.rst . $(DOCSVENV)/bin/activate && cd docsite && make linkcheck # Add examples to the RTD docs by taking it from the README docsite/source/index.rst: docsite/source/index.rst.in README.rst VERSION @echo "#############################################" @echo "# Building $@ Now" @echo "#############################################" awk 'BEGIN{P=0} /^Examples/ { P=1} { if (P == 1) print $$0 }' README.rst | cat $< - > $@ # Regenerate %.1.asciidoc if %.1.asciidoc.in has been modified more # recently than %.1.asciidoc. %.1.asciidoc: %.1.asciidoc.in VERSION sed "s/%VERSION%/$(VERSION)/" $< > $@ # Regenerate %.1 if %.1.asciidoc or VERSION has been modified more # recently than %.1. (Implicitly runs the %.1.asciidoc recipe) %.1: %.1.asciidoc @echo "#############################################" @echo "# Building $@ NOW" @echo "#############################################" $(ASCII2MAN) viewdocs: docs @if [ "$$(uname)" = "Darwin" ]; then \ open docsite/build/html/index.html; \ elif echo "$$(uname)" | grep -qi "mingw\|cygwin\|msys"; then \ start docsite/build/html/index.html; \ else \ xdg-open docsite/build/html/index.html; \ fi viewcover: ci-unittests @if [ "$$(uname)" = "Darwin" ]; then \ open htmlcov/index.html; \ else \ xdg-open htmlcov/index.html; \ fi build: clean virtualenv @echo "#############################################" @echo "# Building sdist + wheel" @echo "#############################################" . $(NAME)env3/bin/activate && pip install build && python -m build pypi: build @echo "#############################################" @echo "# Uploading to PyPI" @echo "#############################################" . $(NAME)env3/bin/activate && pip install twine && twine upload dist/* pypitest: build @echo "#############################################" @echo "# Uploading to TestPyPI" @echo "#############################################" . $(NAME)env3/bin/activate && pip install twine && twine upload --repository testpypi dist/* # usage example: make tag TAG=v1.1.0-1 tag: @if [ -z "$(TAG)" ]; then echo "ERROR: TAG is required. Example: make tag TAG=v2.0.0"; exit 1; fi @case "$(TAG)" in v*) ;; *) echo "ERROR: TAG must start with 'v'. Got: '$(TAG)'. Example: make tag TAG=v2.0.0"; exit 1 ;; esac git tag -s -m $(TAG) $(TAG) clean: @find . -type f -regex ".*\.py[co]$$" -delete @find . -type f \( -name "*~" -or -name "#*" \) -delete @rm -fR build cover dist rpm-build MANIFEST htmlcov .coverage bitmathenv3 docsite/build/html/ docsite/build/doctrees/ bitmath.egg-info uniquetestnames: @echo "#############################################" @echo "# Running Unique TestCase checker" @echo "#############################################" ./tests/test_unique_testcase_names.sh install: pip install . mkdir -p /usr/share/man/man1/ gzip -9 -c bitmath.1 > /usr/share/man/man1/bitmath.1.gz sdist: clean virtualenv @echo "#############################################" @echo "# Creating SDIST" @echo "#############################################" . $(NAME)env3/bin/activate && pip install build && python -m build --sdist rpmcommon: sdist @echo "#############################################" @echo "# Building (S)RPM Now" @echo "#############################################" @mkdir -p rpm-build @cp dist/$(NAME)-$(VERSION).tar.gz rpm-build/$(VERSION).tar.gz srpm: rpmcommon rpmbuild --define "_topdir %(pwd)/rpm-build" \ --define "_builddir %{_topdir}" \ --define "_rpmdir %{_topdir}" \ --define "_srcrpmdir %{_topdir}" \ --define "_specdir $(RPMSPECDIR)" \ --define "_sourcedir %{_topdir}" \ --define "_pkgversion $(VERSION)" \ --define "_pkgrelease $(RPMRELEASE)" \ -bs $(RPMSPEC) @echo "#############################################" @echo "$(PKGNAME) SRPM is built:" @find rpm-build -maxdepth 2 -name '$(PKGNAME)*src.rpm' | awk '{print " " $$1}' @echo "#############################################" rpm: rpmcommon rpmbuild --define "_topdir %(pwd)/rpm-build" \ --define "_builddir %{_topdir}" \ --define "_rpmdir %{_topdir}" \ --define "_srcrpmdir %{_topdir}" \ --define "_specdir $(RPMSPECDIR)" \ --define "_sourcedir %{_topdir}" \ --define "_pkgversion $(VERSION)" \ --define "_pkgrelease $(RPMRELEASE)" \ -ba $(RPMSPEC) @echo "#############################################" @echo "$(PKGNAME) RPMs are built:" @find rpm-build -maxdepth 2 -name '$(PKGNAME)*.rpm' | awk '{print " " $$1}' @echo "#############################################" virtualenv: @echo "" @echo "#############################################" @echo "# Creating a virtualenv" @echo "#############################################" @if [ ! -d "$(NAME)env3" ]; then \ python3 -m venv $(NAME)env3; \ fi . $(NAME)env3/bin/activate && python -m pip install --upgrade pip && pip install -r requirements.txt ci-unittests: virtualenv @echo "" @echo "#############################################" @echo "# Running Unit Tests in virtualenv" @echo "# Using python: $(shell ./$(NAME)env3/bin/python --version 2>&1)" @echo "#############################################" . $(NAME)env3/bin/activate && pytest -v --cov=bitmath --cov-report term-missing --cov-report term:skip-covered --cov-report xml:coverage.xml --cov-report html:htmlcov tests ci-list-deps: @echo "" @echo "#############################################" @echo "# Listing all pip deps" @echo "#############################################" . $(NAME)env3/bin/activate && pip freeze ci-pycodestyle: @echo "" @echo "#############################################" @echo "# Running PEP8 Compliance Tests in virtualenv" @echo "#############################################" . $(NAME)env3/bin/activate && pycodestyle -v --ignore=E501 bitmath/__init__.py tests/*.py ci-pylint: @echo "" @echo "#################################################" @echo "# Running pylint in virtualenv" @echo "#################################################" . $(NAME)env3/bin/activate && pylint bitmath/__init__.py 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 : timlnx-bitmath-8354f39/NEWS.rst000066400000000000000000001000611520504626100162470ustar00rootroot00000000000000NEWS #### .. contents:: :depth: 1 :local: .. _bitmath-2.1.0: bitmath-2.1.0 ************* *Released: May 24, 2026* 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, retires one piece of legacy API surface, and works through OpenSSF's full menu of supply-chain checks (Scorecard, Best Practices, and the OSPS Baseline) to put a credible security posture on the record. The public API is essentially unchanged from 2.0; the two breaking changes below are narrow ones that affect internal-type inspection and one deprecated helper. 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` now accepts :class:`pathlib.Path` objects, not just strings, for its ``path`` argument. 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 is a static analyzer from PyCQA that scans Python source for common security smells (hardcoded secrets, ``eval``, ``subprocess`` calls with ``shell=True``, insecure ``yaml`` and ``pickle`` deserialization, weak crypto primitives). It 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. **Property-based testing with Hypothesis** Four ``@given`` properties cover :func:`bitmath.parse_string` roundtrips, lossless unit conversion across every ``(src, dst)`` pair in :data:`bitmath.ALL_UNIT_TYPES`, and the contract that :func:`bitmath.parse_string` raises only :exc:`ValueError` on bad input. The roundtrip property already turned up a real parser bug around scientific-notation float formatting, which is filed as a follow-up. See `PR #144 `_. **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. **Signed releases (landing with 2.1.0)** The 2.1.0 release closes out the open OSSF Scorecard Signed-Releases finding ("Determines if the project cryptographically signs release artifacts," currently flagged at high severity). Scorecard should reflect the improvement on its next weekly run after the release lands. Security and Supply Chain ========================= .. |scorecard| image:: https://api.securityscorecards.dev/projects/github.com/timlnx/bitmath/badge :target: https://securityscorecards.dev/viewer/?uri=github.com/timlnx/bitmath :alt: OSSF Scorecard .. |bestpractices| image:: https://www.bestpractices.dev/projects/12749/badge :target: https://www.bestpractices.dev/en/projects/12749 :alt: OpenSSF Best Practices .. |baseline| image:: https://www.bestpractices.dev/projects/12749/baseline :target: https://www.bestpractices.dev/projects/12749 :alt: OpenSSF Baseline |scorecard| |bestpractices| |baseline| Most of the work in this release happened here. OpenSSF's supply-chain story has matured enough to be worth pursuing in earnest even on a small library like bitmath, and the project now carries the badges and the automation to back them up. **OpenSSF Best Practices passing badge** bitmath earned the `OpenSSF Best Practices passing badge `_. The full self-assessment is on the linked profile; the badge appears on the README and the docsite landing page alongside the OSSF Scorecard and OSPS Baseline badges. **OSPS Baseline Levels 1 and 2 (Level 3 mostly)** `OSPS Baseline `_ Level 1: 23 Met, 2 Not Applicable, 0 Unmet. Level 2: 19 Met, 0 Unmet. Level 3: 18 Met, 2 Not Applicable, 1 Unmet. The single Unmet at Level 3 is OSPS-QA-07.01 (non-author code review), which is structurally blocked by this being a single-maintainer project. Closing it would require either onboarding a second maintainer or changing the project governance model, neither of which is happening in 2.1.0. The control responses backing each Met assessment live in the docsite under :ref:`Project Architecture ` and the policy documents listed below. **OSSF Scorecard** ``.github/workflows/scorecard.yml`` runs on every push to ``master``, on branch-protection-rule changes, and on a weekly cron. Current score and history are published at https://securityscorecards.dev/viewer/?uri=github.com/timlnx/bitmath. **GitHub Actions pinned to commit SHAs** Every GitHub Actions reference in ``.github/workflows/`` is now pinned to a full-length commit SHA with the version as a trailing ``# vX.Y.Z`` comment. Floating tags (the old ``@v4``) and floating branch refs (the now-removed ``MishaKav/pytest-coverage-comment@main`` third-party step) are gone. Dependabot keeps the SHAs fresh. **Dependabot** ``.github/dependabot.yml`` watches the ``github-actions`` and ``pip`` ecosystems on a weekly cadence. Minor and patch bumps are grouped per ecosystem to keep PR noise bounded; major bumps land as standalone PRs so breaking-change review stays honest. **Hardened workflow token permissions** Every workflow declares an explicit ``permissions: read-all`` at the top level, with ``write`` scopes applied only to the jobs that need them (``security-events: write`` for SARIF upload, ``id-token: write`` for PyPI OIDC Trusted Publishing, and so on). The default-write ``GITHUB_TOKEN`` posture is no longer inherited anywhere. **SCA via pip-audit** ``.github/workflows/sca.yml`` runs `pip-audit `_ on every push, every pull request, and on a weekly cron. bitmath has no runtime dependencies, so this scans the test-only deps in ``requirements.txt``. Findings are resolved on the release-blocking timetable specified in ``SECURITY_POLICIES.md``. **CycloneDX SBOM attached to every release** The publish workflow now installs the built wheel into an isolated venv, snapshots it with `cyclonedx-py `_, and attaches the resulting ``bitmath-.cdx.json`` to the GitHub release alongside the wheel and sdist. Downstream consumers who SBOM-scan everything they ingest can now pull bitmath's SBOM from the release page directly instead of generating their own. Project Documentation and Governance ==================================== The documentation set picked up five new top-level files this release. These exist to answer standing security, governance, and verification questions for downstream consumers without requiring a real-time email exchange, and several of them are direct deliverables for OSPS Baseline controls. `MAINTAINERS.md `_ New top-level file naming the sole current maintainer, the sensitive-resource access that comes with the role (PyPI account, GitHub admin, GPG key, ReadTheDocs admin), maintainer responsibilities, and a standing open call for a Debian/Ubuntu co-maintainer (see `issue #117 `_ if you're interested). `ARCHITECTURE.md `_ New top-level design document covering runtime architecture, the build and release pipeline (including the Packit downstream RPM flow into Fedora and EPEL), CI gates, trust boundaries, and the documentation pipeline. Lives at the repo root so the OpenSSF scanner finds it by filename, and so anyone exploring the repo for the first time can get the design picture before reading code. `SECURITY_ASSESSMENT.md `_ Standing threat model describing the attack surfaces (supply chain, PyPI account, source repository, build chain, distribution chain), the mitigations in place for each, and the automated security tooling stack with what each tool catches. `SECURITY_POLICIES.md `_ The SAST and SCA remediation thresholds and release-blocking criteria in one place. Bandit and pip-audit findings, severity ladder, the rule for what blocks a release versus what gets a follow-up, and the timetable for acting on each. `VERIFICATION.md `_ Downstream-consumer guide for verifying a bitmath release end to end: integrity via the SHA-256 sums published alongside the GitHub release, authenticity via `PEP 740 `_ attestations and `pypi-attestations `_, and provenance via the CycloneDX SBOM. If your organization's policy requires verifying every dependency before installation, this is the playbook. `SECURITY.md `_ tightened up Coordinated Vulnerability Disclosure now states explicit response timeframes (72 hours to acknowledge a report, 7 days to triage), and the document picked up new Scope of Support, Duration of Support, and Secrets and Credentials Policy sections. **HTTPS for all project doc URLs** Every ``http://`` reference to project-owned documentation (``bitmath.readthedocs.io``, the docsite, README badge targets, NEWS historical links, CONTRIBUTING) has been switched to ``https://`` to clear the bestpractices.dev ``sites_https`` check. ReadTheDocs was already serving TLS; only the scheme needed flipping. See `PR #146 `_. .. _bitmath-2.0.0: bitmath-2.0.0 ************* *Released: April 25, 2026* Nearly eight years after 1.3.3 shipped in 2018, bitmath is back with a major release. Version 2.0.0 is a thorough modernization: the Python 2 era is officially over, the library picks up several long-requested features, and the entire project infrastructure has been rebuilt from scratch. If you've been running bitmath on Python 3.9 or later and quietly wishing it felt more modern — this release is for you. Check over on `my blog `_ for a personal recap of this release, how much it means to me, and what it took to get here. Breaking Changes ================ **Python support** Python 3.9+ only. Python 2 and Python < 3.9 are no longer supported or tested. **parse_string() default system** The default unit system when ``strict=False`` is now **NIST** (base-2). Previously it defaulted to SI (base-10). Code that relied on the old default for ambiguous strings such as ``"1g"`` could get a different result. See :ref:`parse-string-non-strict` for full details. All bitmath now consistently defaults to the NIST system. **parse_string_unsafe() deprecated** Use :func:`bitmath.parse_string` with ``strict=False`` instead. The old name still works but emits a :exc:`DeprecationWarning`. **bitmath.integrations removed** The ``argparse``, ``click``, and ``progressbar`` integrations have been removed from the package. Copy-paste replacements are provided in the new :ref:`Integration Examples ` documentation chapter. No changes to calling code are required — just a local copy of the relevant snippet. **Byte and Bit display names** ``Byte`` and ``Bit`` now display as ``B`` and ``b`` respectively, matching the abbreviated style of every other unit. Code that compares formatted strings (e.g. ``"{unit}"`` in a format template or the output of ``str()`` / ``repr()``) against the literal words ``"Byte"`` or ``"Bit"`` will need to be updated. The class names themselves are unchanged. **query_device_capacity() on macOS** :func:`bitmath.query_device_capacity` now raises :exc:`NotImplementedError` on macOS. System Integrity Protection (SIP) blocks raw block device access even for root, making the previous ioctl path unreliable. Use :func:`bitmath.query_capacity` instead. **Build and install** ``setup.py`` and ``setup.py.in`` are gone. Installation is ``pip install bitmath``. Source builds use ``python -m build``. Library Improvements ==================== The core API is unchanged — every bitmath object you created before still works exactly the same way. What 2.0.0 adds on top of that: **Full NIST unit coverage** The four largest NIST prefix units — :class:`~bitmath.ZiB`, :class:`~bitmath.YiB`, :class:`~bitmath.Zib`, and :class:`~bitmath.Yib` — are now first-class bitmath types. All constants (:ref:`NIST_PREFIXES, NIST_STEPS, ALL_UNIT_TYPES `) reflect reality. **f-string and format() support** bitmath objects now implement the Python format protocol (``__format__``, `PEP 3101 `_). They can be used directly in f-strings with format specs — ``f"{some_size:.2f}"`` just works. See :ref:`instances_dunder_format` for the full reference. Credit to `Jonathan Eunice `_ for the original concept in `PR #76 `_. **bitmath.sum() and built-in sum()** A new :func:`bitmath.sum` function returns a unit-normalized result when summing mixed-type iterables. For uniform collections, the built-in :py:func:`sum` now works directly on bitmath sequences without a ``start=`` argument. **Thread-safe context manager** The :func:`bitmath.format` context manager previously mutated module-level globals, making it unsafe under concurrent access. It now uses ``threading.local`` with proper save/restore semantics, including correct nesting behavior. Closes `issue #83 `_. **best_prefix() bit-family fix** :func:`bitmath.best_prefix` incorrectly converted bit-family units (e.g. ``Kib``) into byte-family units (e.g. ``KiB``). The unit family is now preserved. Closes `issue #95 `_. **query_capacity() — recommended volume size API** New :func:`bitmath.query_capacity` returns a :class:`bitmath.Capacity` NamedTuple of ``(total, used, free)`` :class:`bitmath.Bitmath` instances for any path or mount point. Works cross-platform without elevated privileges. This is the recommended API for "how big is this volume?" queries. Accepts ``bestprefix=True`` (default) to get already human-readable units (e.g. ``GiB``) and ``system=bitmath.SI`` to opt into decimal prefixes instead of the default NIST binary prefixes. Set ``bestprefix=False`` to receive raw :class:`bitmath.Byte` instances. **Windows device capacity** :func:`bitmath.query_device_capacity` now works on Windows via ``DeviceIoControl``. Open the device as ``open(r'\\.\PhysicalDrive0', 'rb')`` (administrator privileges required). Unsupported platforms raise :exc:`NotImplementedError`. The new :data:`bitmath.SUPPORTED_PLATFORMS` constant lists all platforms where the function is available. Closes `issue #52 `_. **query_device_capacity() Linux buffer fix** :func:`bitmath.query_device_capacity` on Linux was passing an integer where an ioctl buffer was required, causing ``OSError: [Errno 14] Bad address``. The call now correctly allocates a zero-filled buffer of the proper size via ``b'\\x00' * struct.calcsize(fmt)``. **Flexible string parsing** :func:`bitmath.parse_string` with ``strict=False`` accepts ambiguous input such as ``"1g"`` or ``"1GB"`` and resolves it to the most likely unit. When the system cannot be reliably determined, NIST is the tiebreaker. Closes `issue #54 `_. **Floor division, modulo, and divmod for capacity math** bitmath objects now implement ``__floordiv__`` (``//``), ``__mod__`` (``%``), and ``__divmod__`` — useful for chunk-and-remainder capacity planning (*"how many N-sized chunks fit into this device, and how much is left over?"*). ``bm1 // bm2`` returns an ``int`` (count of whole divisions), mirroring how ``bm1 / bm2`` returns a unitless ratio. ``bm1 % bm2`` and ``divmod(bm1, bm2)`` return remainders as bitmath objects of the **left-hand operand's type**, consistent with every other bitmath arithmetic operator. The identity ``(a // b) * b + (a % b) == a`` holds. See :ref:`capacity_math` for worked examples including ``best_prefix()`` coercion and ``bitmath.format`` context-manager integration. Project Infrastructure ====================== The project infrastructure has been rebuilt to reflect how Python projects are actually maintained in 2026: **Project Security** GitHub now has branch protection enabled. Releases are signed with the maintainers `GPG key `_. **Packaging** ``pyproject.toml`` with a hatchling backend replaces the old ``setup.py``/``setup.py.in`` template system. The package is `PEP 517 `_/`PEP 518 `_ compliant. ``MANIFEST.in`` is gone; sdist content is declared explicitly in ``pyproject.toml``. **GitHub Actions** CI now runs against Python 3.9 through 3.13 on Ubuntu, macOS, and Windows, with actions pinned to current versions (``checkout@v6``, ``setup-python@v5``). Tests run on every pull request, not just pushes. **Security scanning** CodeQL analysis runs on every push to master, every pull request, and on a weekly schedule. **ReadTheDocs** ``.readthedocs.yaml`` is now present and explicit. The RTD build uses Python 3.11 and installs ``sphinx_rtd_theme`` directly. **Development workflow** ``make ci`` is the single command for a full local build: unique test name check, pycodestyle, flake8, and pytest with coverage. ``make build``, ``make pypitest``, and ``make pypi`` replace the old ``make sdist upload`` pattern. **Fedora/EPEL** Expect to see fresh builds in Fedora/EPEL over the coming weeks. Hopefully I can sneak this into EPEL 10, EPEL 9 might be picky since this is a major version change. Closing Thoughts ================ bitmath started as a small passion project of mine. A utility for thinking about and clearly expressing file sizes, and that's still exactly what it is. This 2.0.0 release doesn't change what the library does. What I've done is change the very foundation that it's built on. The test suite measures in at 311 tests now, basically 100% coverage when you add all the platform-specific checks together. The documentation has been comprehensively reviewed and updated. The packaging is modernized. It really is a remarkable milestone in project history. I have to give the warmest thanks to all of the users and fans who have written bug reports and submitted pull requests. Especially in the least active years of the project. Most of those PRs and Issues have been integrated into this massive 2.0.0 release. **Thanks for your patience and your participation.** If you've been holding off on adopting bitmath because the last release predated your Python version — yeah I totally get it. This place was a dumpster for the last 8 years. Go on, give it a shot. It really is better than ever. .. _bitmath-1.4.0-1: bitmath-1.4.0-1 *************** bitmath-1.4.0 was published on 2026-04-16. Project ======= Version 1.4.0 is a maintenance release — the final release to support Python 2 era packaging infrastructure. It addresses two long-standing issues and makes no breaking changes. Changes ======= **Bug Fixes** * Fixed packaging: ``bitmath.integrations`` subpackage was missing from the ``packages`` list in ``setup.py.in``, causing the argparse, click, and progressbar integrations to be absent from installed distributions. Reported in `PR #107 `_. * Modernized mock imports in the test suite: ``tests/test_progressbar.py`` and ``tests/test_query_device_capacity.py`` now prefer ``unittest.mock`` (stdlib, Python 3.3+) and fall back to the third-party ``mock`` package. Based on `PR #105 `_. .. _bitmath-1.3.3-1: bitmath-1.3.3-1 *************** `bitmath-1.3.3-1 `__ was published on 2018-08-23. Project ======= Version 1.3.3 is a minor update primarily released to synchronize versions across platforms. Additionally there are small packaging updates to keep up with changing standards. Minor bug fixes and documentation tweaks are included as well. The project now has an official `Code of Conduct `_, as well as issue and pull request templates. What happened to bitmath 1.3.2? It only ever existed as an idea in source control. Changes ======= **Bug Fixes** `Alexander Kapshuna `_ has submitted `several fixes `_ since the last release. Thanks! * Packaging requirements fixes * Python 3 compatibility * Subclassing and Type checking fixes/improvements `Marcus Kazmierczak `_ submitted `a fix `_ for some broken documentation links. And `Dawid Gosławski `_ make sure our `documentation `_ is accurate. Thanks to all the bitmath contributors over the years! .. _bitmath-1.3.1-1: bitmath-1.3.1-1 *************** `bitmath-1.3.1-1 `__ was published on 2016-07-17. Changes ======= **Added Functionality** * New function: :func:`bitmath.parse_string_unsafe`, a less strict version of :func:`bitmath.parse_string`. Accepts inputs using *non-standard* prefix units (such as single-letter, or mis-capitalized units). * Inspired by `@darkblaze69 `_'s request in `#60 "Problems in parse_string" `_. Project ======= **Ubuntu** * Bitmath is now available for installation via Ubuntu Xenial, Wily, Vivid, Trusty, and Precise PPAs. * Ubuntu builds inspired by `@hkraal `_ reporting an `installation issue `_ on Ubuntu systems. **Documentation** * `Cleaned up a lot `_ of broken or re-directing links using output from the Sphinx ``make linkcheck`` command. .. _bitmath-1.3.0-1: bitmath-1.3.0-1 *************** `bitmath-1.3.0-1 `__ was published on 2016-01-08. Changes ======= **Bug Fixes** * Closed `GitHub Issue #55 `_ "best_prefix for negative values". Now :func:`bitmath.best_prefix` returns correct prefix units for negative values. Thanks `mbdm `_! .. _bitmath-1.2.4-1: bitmath-1.2.4-1 *************** `bitmath-1.2.4-1 `__ was published on 2015-11-30. Changes ======= **Added Functionality** * New bitmath module function: :func:`bitmath.query_device_capacity`. Create :class:`bitmath.Byte` instances representing the capacity of a block device. Support is presently limited to Linux and Mac. * The :func:`bitmath.parse_string` function now can parse 'octet' based units. Enhancement requested in `#53 parse french unit names `_ by `walidsa3d `_. **Bug Fixes** * `#49 `_ - Fix handling unicode input in the `bitmath.parse_string `__ function. Thanks `drewbrew `_! * `#50 `_ - Update the ``setup.py`` script to be python3.x compat. Thanks `ssut `_! Documentation ============= * The project documentation is now installed along with the bitmath library in RPM packages. Project ======= **Fedora/EPEL** Look for separate python3.x and python2.x packages coming soon to `Fedora `_ and `EPEL `_. This is happening because of the `initiative `_ to update the base Python implementation on Fedora to Python 3.x * `BZ1282560 `_ .. _bitmath-1.2.3-1: bitmath-1.2.3-1 *************** `bitmath-1.2.3-1 `__ was published on 2015-01-03. Changes ======= **Added Functionality** * New utility: ``progressbar`` integration: `bitmath.integrations.BitmathFileTransferSpeed `_. A more functional file transfer speed widget. Documentation ============= * The command-line ``bitmath`` tool now has `online documentation `_ * A full demo of the ``argparse`` and ``progressbar`` integrations has been written. Additionally, it includes a comprehensive demonstration of the full capabilities of the bitmath library. View it in the *Real Life Demos* `Creating Download Progress Bars `_ example. Project ======= **Tests** * Travis-CI had some issues with installing dependencies for the 3.x build unittests. These were fixed and the build status has returned back to normal. .. _bitmath-1.2.0-1: bitmath-1.2.0-1 *************** `bitmath-1.2.0-1 `__ was published on 2014-12-29. Changes ======= **Added Functionality** * New utility: ``argparse`` integration: `bitmath.BitmathType `_. Allows you to specify arguments as bitmath types. Documentation ============= * The command-line ``bitmath`` tool now has a `proper manpage `_ Project ======= **Tests** * The command-line ``bitmath`` tool is now properly unittested. Code coverage back to ~100%. .. _bitmath-1.1.0-0: bitmath-1.1.0-1 *************** `bitmath-1.1.0-1 `_ was published on 2014-12-20. * `GitHub Milestone Tracker for 1.1.0 `_ Changes ======= **Added Functionality** * New ``bitmath`` `command-line tool `_ added. Provides CLI access to basic unit conversion functions * New utility function `bitmath.parse_string `_ for parsing a human-readable string into a bitmath object. `Patch submitted `_ by new contributor `tonycpsu `_. .. _bitmath-1.0.8-1: bitmath-1.0.5-1 through 1.0.8-1 ******************************* `bitmath-1.0.8-1 `__ was published on 2014-08-14. * `GitHub Milestone Tracker for 1.0.8 `_ Major Updates ============= * bitmath has a proper documentation website up now on Read the Docs, check it out: `bitmath.readthedocs.io `_ * bitmath is now Python 3.x compatible * bitmath is now included in the `Extra Packages for Enterprise Linux `_ EPEL6 and EPEL7 repositories (`pkg info `_) * merged 6 `pull requests `_ from 3 `contributors `_ Bug Fixes ========= * fixed some math implementation bugs * `commutative multiplication `_ * `true division `_ Changes ======= **Added Functionality** * `best-prefix `_ guessing: automatic best human-readable unit selection * support for `bitwise operations `_ * `formatting customization `_ methods (including plural/singular selection) * exposed many more `instance attributes `_ (all instance attributes are usable in custom formatting) * a `context manager `_ for applying formatting to an entire block of code * utility functions for sizing `files `_ and `directories `_ * add `instance properties `_ equivalent to ``instance.to_THING()`` methods Project ======= **Tests** * Test suite is now implemented using `Python virtualenv's `_ for consistency across across platforms * Test suite now contains 150 unit tests. This is **110** more tests than the previous major release (`1.0.4-1 `__) * Test suite now runs on EPEL6 and EPEL7 * `Code coverage `_ is stable around 95-100% .. _bitmath-1.0.4-1: bitmath-1.0.4-1 *************** This is the first release of **bitmath**. `bitmath-1.0.4-1 `__ was published on 2014-03-20. Project ======= Available via: * `PyPi `_ * Fedora 19 * Fedora 20 bitmath had been under development for 12 days when the 1.0.4-1 release was made available. Debut Functionality =================== * Converting between **SI** and **NIST** prefix units (``GiB`` to ``kB``) * Converting between units of the same type (SI to SI, or NIST to NIST) * Basic arithmetic operations (subtracting 42KiB from 50GiB) * Rich comparison operations (``1024 Bytes == 1KiB``) * Sorting * Useful *console* and *print* representations timlnx-bitmath-8354f39/README.rst000066400000000000000000000343531520504626100164420ustar00rootroot00000000000000.. image:: https://api.securityscorecards.dev/projects/github.com/timlnx/bitmath/badge :target: https://securityscorecards.dev/viewer/?uri=github.com/timlnx/bitmath :alt: OSSF Scorecard .. image:: https://www.bestpractices.dev/projects/12749/badge :target: https://www.bestpractices.dev/en/projects/12749 :alt: OpenSSF Best Practices .. image:: https://www.bestpractices.dev/projects/12749/baseline :target: https://www.bestpractices.dev/projects/12749 :alt: OpenSSF Baseline .. 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: https://bitmath.readthedocs.io/ .. 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/github/stars/timlnx/bitmath?style=flat-square :target: https://github.com/timlnx/bitmath :alt: GitHub Project Popularity .. 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: * Converting between **SI** and **NIST** prefix units (``kB`` to ``GiB``) * Converting between units of the same type (SI to SI, or NIST to NIST) * Full NIST unit coverage including **ZiB**, **YiB**, **Zib**, and **Yib** * Automatic human-readable prefix selection (like in `hurry.filesize `_) * Basic arithmetic operations (subtracting 42KiB from 50GiB) * Capacity math with floor division, modulo, and ``divmod`` (``GiB(1) // MiB(300)``, ``GiB(1) % MiB(300)``) * Rich comparison operations (``1024 Bytes == 1KiB``) * Bitwise operations (``<<``, ``>>``, ``&``, ``|``, ``^``) * Rounding via ``math.floor``, ``math.ceil``, and ``round`` * Reading a device's storage capacity (Linux/macOS support only) * String parsing, including flexible non-strict parsing of ambiguous input * Sorting * Summing iterables via built-in ``sum`` or ``bitmath.sum`` for unit-normalised results * f-string and ``format`` support via the standard Python formatting protocol * `argparse `_ integration as a custom type In addition to the conversion and math operations, `bitmath` provides human readable representations of values which are suitable for use in interactive shells as well as larger scripts and applications. The format produced for these representations is customizable via the functionality included in stdlibs `string.format `_. In discussion we will refer to the NIST units primarily. I.e., instead of "megabyte" we will refer to "mebibyte". The former is ``10^3 = 1,000,000`` bytes, whereas the second is ``2^20 = 1,048,576`` bytes. When you see file sizes or transfer rates in your web browser, most of the time what you're really seeing are the base-2 sizes/rates. **Don't Forget!** The source for bitmath `is available on GitHub `_. And did we mention there are nearly 300 unit tests? `Check them out for yourself `_. Running the tests should be as simple as calling the ``ci`` target in the Makefile: ``make ci``. Please file a bug report if you run into issues. Installation ============ .. 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 `_. Requires Python 3.9 or newer. No runtime dependencies outside the standard library. **PyPI** (the typical path): .. code-block:: bash pip install bitmath **Fedora and EPEL** .. code-block:: bash sudo dnf install python3-bitmath **From source** .. code-block:: bash git clone https://github.com/timlnx/bitmath.git pip install ./bitmath Documentation ============= The main documentation lives at `https://bitmath.readthedocs.io/en/latest/ `_. Topics include: * The ``bitmath`` Module * Utility Functions * Context Managers * Module Variables * ``argparse`` integration * The ``bitmath`` command-line Tool * Classes * Initializing * Available Classes * Class Methods * Instances * Instance Attributes * Instance Methods * Instance Properties * The Formatting Mini-Language * Getting Started * Tables of Supported Operations * Basic Math * Unit Conversion * Rich Comparison * Sorting * Real Life Examples * Download Speeds * Calculating how many files fit on a device * Printing Human-Readable File Sizes in Python * Calculating Linux BDP and TCP Window Scaling * Contributing to bitmath * Appendices * Rules for Math * On Units * Who uses Bitmath * Related Projects * NEWS * Copyright Examples ======== Arithmetic ---------- .. code-block:: python >>> import bitmath >>> log_size = bitmath.kB(137.4) >>> log_zipped_size = bitmath.Byte(987) >>> print("Compression saved %s space" % (log_size - log_zipped_size)) Compression saved 136.413kB space >>> thumb_drive = bitmath.GiB(12) >>> song_size = bitmath.MiB(5) >>> songs_per_drive = thumb_drive / song_size >>> print(songs_per_drive) 2457.6 Capacity Planning ----------------- Floor division (``//``), modulo (``%``), and ``divmod()`` are handy for chunk-and-remainder capacity math. ``bm1 // bm2`` returns an ``int`` (how many whole chunks fit); ``bm1 % bm2`` returns a ``bitmath`` of the **left-hand operand's type** (the leftover). .. code-block:: python >>> from bitmath import GiB, MiB, TiB >>> disk = GiB(1) >>> chunk = MiB(300) >>> disk // chunk # how many whole 300 MiB chunks fit? 3 >>> disk % chunk # leftover, typed as the LHS (GiB) GiB(0.12109375) >>> divmod(disk, chunk) # both at once (3, GiB(0.12109375)) Re-express the remainder in a human-readable unit with ``best_prefix()`` (or coerce directly with ``to_MiB()``, etc.): .. code-block:: python >>> (GiB(1) % MiB(300)).best_prefix() MiB(124.0) Pair with the ``bitmath.format`` context manager for clean reporting across a block of capacity calculations: .. code-block:: python >>> import bitmath >>> volume = TiB(1) >>> block = GiB(7) >>> with bitmath.format(fmt_str="{value:.2f} {unit}", bestprefix=True): ... whole, leftover = divmod(volume, block) ... print(f"{whole} whole blocks of {block} fit in {volume}") ... print(f"leftover: {leftover}") 146 whole blocks of 7.00 GiB fit in 1.00 TiB leftover: 2.00 GiB The identity ``(a // b) * b + (a % b) == a`` holds, so ``divmod`` round-trips. Convert Units ------------- File size unit conversion: .. code-block:: python >>> from bitmath import * >>> dvd_size = GiB(4.7) >>> print("DVD Size in MiB: %s" % dvd_size.to_MiB()) DVD Size in MiB: 4812.8 MiB Select a human-readable unit ---------------------------- .. code-block:: python >>> small_number = kB(100) >>> ugly_number = small_number.to_TiB() >>> print(ugly_number) 9.09494701773e-08 TiB >>> print(ugly_number.best_prefix()) 97.65625 KiB Rich Comparison --------------- .. code-block:: python >>> cd_size = MiB(700) >>> cd_size > dvd_size False >>> cd_size < dvd_size True >>> MiB(1) == KiB(1024) True >>> MiB(1) <= KiB(1024) True Sorting ------- .. code-block:: python >>> sizes = [KiB(7337.0), KiB(1441.0), KiB(2126.0), KiB(2178.0), KiB(2326.0), KiB(4003.0), KiB(48.0), KiB(1770.0), KiB(7892.0), KiB(4190.0)] >>> print(sorted(sizes)) [KiB(48.0), KiB(1441.0), KiB(1770.0), KiB(2126.0), KiB(2178.0), KiB(2326.0), KiB(4003.0), KiB(4190.0), KiB(7337.0), KiB(7892.0)] Custom Formatting ----------------- * Use of the custom formatting system * All of the available instance properties Example: .. code-block:: python >>> longer_format = """Formatting attributes for %s ...: This instances prefix unit is {unit}, which is a {system} type unit ...: The unit value is {value} ...: This value can be truncated to just 1 digit of precision: {value:.1f} ...: In binary this looks like: {binary} ...: The prefix unit is derived from a base of {base} ...: Which is raised to the power {power} ...: There are {bytes} bytes in this instance ...: The instance is {bits} bits large ...: bytes/bits without trailing decimals: {bytes:.0f}/{bits:.0f}""" % str(ugly_number) >>> print(ugly_number.format(longer_format)) Formatting attributes for 5.96046447754 MiB This instances prefix unit is MiB, which is a NIST type unit The unit value is 5.96046447754 This value can be truncated to just 1 digit of precision: 6.0 In binary this looks like: 0b10111110101111000010000000 The prefix unit is derived from a base of 2 Which is raised to the power 20 There are 6250000.0 bytes in this instance The instance is 50000000.0 bits large bytes/bits without trailing decimals: 6250000/50000000 Utility Functions ----------------- **bitmath.getsize()** .. code-block:: python >>> print(bitmath.getsize('python-bitmath.spec')) 3.7060546875 KiB **bitmath.parse_string()** Parse a string with standard units: .. code-block:: python >>> import bitmath >>> a_dvd = bitmath.parse_string("4.7 GiB") >>> print(type(a_dvd)) >>> print(a_dvd) 4.7 GiB **bitmath.parse_string_unsafe()** Parse a string with ambiguous units: .. code-block:: python >>> import bitmath >>> a_gig = bitmath.parse_string_unsafe("1gb") >>> print(type(a_gig)) >>> a_gig == bitmath.GB(1) True >>> bitmath.parse_string_unsafe('1gb') == bitmath.parse_string_unsafe('1g') True **bitmath.query_device_capacity()** .. code-block:: python >>> import bitmath >>> with open('/dev/sda') as fp: ... root_disk = bitmath.query_device_capacity(fp) ... print(root_disk.best_prefix()) ... 238.474937439 GiB **bitmath.listdir()** .. code-block:: python >>> for i in bitmath.listdir('./tests/', followlinks=True, relpath=True, bestprefix=True): ... print(i) ... ('tests/test_file_size.py', KiB(9.2900390625)) ('tests/test_basic_math.py', KiB(7.1767578125)) ('tests/__init__.py', KiB(1.974609375)) ('tests/test_bitwise_operations.py', KiB(2.6376953125)) ('tests/test_context_manager.py', KiB(3.7744140625)) ('tests/test_representation.py', KiB(5.2568359375)) ('tests/test_properties.py', KiB(2.03125)) ('tests/test_instantiating.py', KiB(3.4580078125)) ('tests/test_future_math.py', KiB(2.2001953125)) ('tests/test_best_prefix_BASE.py', KiB(2.1044921875)) ('tests/test_rich_comparison.py', KiB(3.9423828125)) ('tests/test_best_prefix_NIST.py', KiB(5.431640625)) ('tests/test_unique_testcase_names.sh', Byte(311.0)) ('tests/.coverage', KiB(3.1708984375)) ('tests/test_best_prefix_SI.py', KiB(5.34375)) ('tests/test_to_built_in_conversion.py', KiB(1.798828125)) ('tests/test_to_Type_conversion.py', KiB(8.0185546875)) ('tests/test_sorting.py', KiB(4.2197265625)) ('tests/listdir_symlinks/10_byte_file_link', Byte(10.0)) ('tests/listdir_symlinks/depth1/depth2/10_byte_file', Byte(10.0)) ('tests/listdir_nosymlinks/depth1/depth2/10_byte_file', Byte(10.0)) ('tests/listdir_nosymlinks/depth1/depth2/1024_byte_file', KiB(1.0)) ('tests/file_sizes/kbytes.test', KiB(1.0)) ('tests/file_sizes/bytes.test', Byte(38.0)) ('tests/listdir/10_byte_file', Byte(10.0)) Formatting ---------- .. code-block:: python >>> with bitmath.format(fmt_str="[{value:.3f}@{unit}]"): ... for i in bitmath.listdir('./tests/', followlinks=True, relpath=True, bestprefix=True): ... print(i[1]) ... [9.290@KiB] [7.177@KiB] [1.975@KiB] [2.638@KiB] [3.774@KiB] [5.257@KiB] [2.031@KiB] [3.458@KiB] [2.200@KiB] [2.104@KiB] [3.942@KiB] [5.432@KiB] [311.000@Byte] [3.171@KiB] [5.344@KiB] [1.799@KiB] [8.019@KiB] [4.220@KiB] [10.000@Byte] [10.000@Byte] [10.000@Byte] [1.000@KiB] [1.000@KiB] [38.000@Byte] [10.000@Byte] ``argparse`` Integration ------------------------ A self-contained example showing how to use bitmath as an argparse argument type is available in the `Integration Examples `_ chapter of the documentation. .. code-block:: python import argparse import bitmath def BitmathType(value): try: return bitmath.parse_string(value) except ValueError: raise argparse.ArgumentTypeError( f"{value!r} is not a recognized bitmath unit string" ) parser = argparse.ArgumentParser() parser.add_argument('--block-size', type=BitmathType, required=True) args = parser.parse_args(['--block-size', '10MiB']) print(args.block_size) # 10.0 MiB timlnx-bitmath-8354f39/SECURITY.md000066400000000000000000000166171520504626100165470ustar00rootroot00000000000000# Security Policy Generally speaking, users will be encouraged to update to newer versions. Since the launch of this project in 2014 I have been committed to introducing non-breaking changes and strong forward/backward compatibility. The 2.0.0 release series is the first time in over a decade that breaking changes have been introduced. Most likely, the response will always be "update to the latest 2.x" for the latest patches. Security related bugs should be reserved for situations with actual real-life consequences. Inappropriate use of the library, "user-error", generally won't fall under this umbrella. ## Supported Versions As of the 2.0.0 re-factor only versions ≥ 2.0.0 will receive support. Versions prior to 2.0.0 are legacy and not recommended for general use. The last "update" to the pre 2.0.0 series was 1.4.0 in April of 2026, nearly 8 years after the 1.3.3 release made in 2018. | Version | Supported | | ------- | ------------------ | | `≥ 2.0.0` | :white_check_mark: | | `< 2.x.y` | :x: | ### Scope of Support * **Security fixes** are issued as patch releases on the latest minor in the 2.x series. If the current line is 2.1.x, security fixes ship as 2.1.y. * **Bug fixes** are issued on the latest minor in the 2.x series. Older minors do not receive bug-fix backports unless the bug is also a security issue covered above. * **Functional changes** ship in new minor releases (2.x → 2.(x+1)). ### Duration of Support * The **2.x series** receives security fixes for as long as it is the current major. Today that is open-ended; a future 3.x release would start a deprecation clock on 2.x with at least 12 months of overlap announced in `NEWS.rst` before 2.x support ends. * The **1.x series** (legacy) is end-of-life. No further releases will be made on 1.x. Users on 1.x should plan a migration to 2.x; the 2.x API is drop-in compatible for the documented public surface. * The **pre-1.x series** is historical and not supported in any form. This list will be updated when future releases are made in the 2-version series that require specific callouts for supportability. If you have discovered what you think is a harmful bug with the potential for exploitation in a supported version series, and this bug may lead to loss of life or data, then you have two options for reporting available to you: ## [1] Self-Reporting (GitHub) Consider using the new [Private Vulnerability Reporting](https://docs.github.com/en/code-security/how-tos/report-and-fix-vulnerabilities/privately-reporting-a-security-vulnerability) function if you want to get involved that way. * On GitHub, navigate to the main page of the repository. * Under the repository name, click the Security and quality tab. If you cannot see the " Security and quality" tab, select the dropdown menu, and then click Security and quality. * Click Report a vulnerability to open the advisory form. * Fill in the advisory details form. ...as described in the linked GitHub documentation. ## [2] Reporting a Vulnerability (Non-GitHub) You may also reach out to me at this email address: * `bitmath@lnx.cx` Please include a tag in the subject indicating the sensitivity of the issue. ## Response Timeframes I treat security reports as the top of my queue. The commitments I make to anybody who files one: * **Acknowledgement of receipt: within 72 hours.** This is just a "got it, working on it" reply so you know the report did not get lost. If you do not hear back in three days, the email may not have made it; try one of the backup channels below. * **Initial triage assessment: within 7 days.** This is where I tell you whether the report is reproducible, what the rough severity looks like, and what the disclosure timeline will be. * **Coordinated disclosure: as soon as we have a fix prepared and an impact statement we both agree on.** I aim to publish a [GitHub Security Advisory](https://github.com/timlnx/bitmath/security/advisories) and a `NEWS.rst` entry alongside the patch release that contains the fix. If the issue is severe enough that disclosure has to happen before a fix is ready, we coordinate the timing together. These are commitments, not guarantees. bitmath is maintained by one person on the side. If I am on a plane or in a hospital the clock will keep ticking and I will catch up when I land. The backup channels below exist precisely for "I tried email and got nothing back" situations. As an emergency backup you can find me on bsky or instagram and direct message me there: * [bsky - @lnx.cx](https://bsky.app/profile/lnx.cx) * [insta - @tim.lnx](https://www.instagram.com/tim.lnx/) For less serious security issues with lower potential for exploitation or damage, please open a bug on the project and apply the `security` label to it. ## Secrets and Credentials Policy bitmath is built around the principle that **no long-lived secrets should exist in the project**. The policy is short because the implementation is austere. ### What Secrets the Project Handles * **PyPI publish authority** is handled via [PyPI Trusted Publishing](https://docs.pypi.org/trusted-publishers/) (OIDC). The publish workflow exchanges a short-lived GitHub Actions OIDC token for upload credentials at the moment of publication. There is no long-lived PyPI API token stored as a GitHub secret. * **Read The Docs build credentials** are managed by Read The Docs via OAuth integration with GitHub. No credentials are stored in this repo. * **The `bitmath@lnx.cx` email address** is a forwarding alias used only for security correspondence. Compromise of that mailbox does not affect any project resource directly. ### Storage * No secret values are stored in the repository. GitHub secret scanning is enabled at the repository level and runs continuously. * No secret values are stored in `.github/workflows/*.yml`. The workflows authenticate via OIDC tokens, not stored credentials. * The list of GitHub Actions secrets configured on this repository is **empty**, verifiable with `gh api /repos/timlnx/bitmath/actions/secrets`. ### Access Control * The single maintainer (see [MAINTAINERS.md](MAINTAINERS.md)) has admin access to the repository, the PyPI project, and the Read The Docs project. * The PyPI Trusted Publishing trust relationship is scoped to the `.github/workflows/publish.yml` workflow on `master`. Any other workflow or branch attempting to authenticate to PyPI as bitmath will be rejected. * Two-factor authentication is required on both the GitHub account (authenticator app + GitHub Mobile, SMS disabled) and the PyPI account. ### Rotation * OIDC tokens are short-lived (minutes) and rotated automatically on every workflow run by GitHub Actions and PyPI. * No long-lived credentials exist, so there is no rotation schedule to maintain. * The Trusted Publisher trust relationship itself can be revoked or re-scoped via the PyPI web UI if the workflow file ever needs to change in a way that would alter the trust boundary. ### Incident Response If a project secret or credential is suspected to be compromised: 1. Revoke the Trusted Publisher relationship on PyPI immediately, before doing anything else. This breaks any in-flight publish attempt by an attacker. 2. Audit recent activity in GitHub Actions runs and PyPI release history for unexpected events. The PyPI publish journal and GitHub Actions logs are public and inspectable. 3. Reach out through the reporting channels above so the incident can be coordinated and disclosed. This policy is reviewed at every minor release. timlnx-bitmath-8354f39/SECURITY_ASSESSMENT.md000066400000000000000000000162531520504626100203100ustar00rootroot00000000000000# Security Assessment This document is bitmath's standing security assessment. The goal here is not to claim the library is bulletproof. It is to lay out, in plain language, what the realistic threats against bitmath actually look like, what is already in place to address them, and where the soft spots are. Downstream consumers and packagers should be able to read this and form their own opinion without having to reverse-engineer the project. I revisit this document at every minor release and whenever a new feature introduces a new attack surface. ## What bitmath Is (and What It Isn't) bitmath is a pure-Python library for representing and converting file sizes between SI (decimal) and NIST (binary) unit systems. It does arithmetic, comparisons, parsing, and formatting on size values. The most useful framing for the threat model is what bitmath deliberately **does not** do: * It does **not** open network sockets, make HTTP requests, or talk to any external service. * It does **not** call `eval`, `exec`, or `compile` on user input. * It does **not** shell out to subprocesses by default. The `query_capacity` and `query_device_capacity` functions call into platform APIs (`shutil.disk_usage`, `fcntl.ioctl`, `ctypes` against Win32) but not via the shell. * It has **zero runtime dependencies**. The only code that runs in a bitmath consumer's process is bitmath itself, the Python standard library, and the consumer's own application code. * It does **not** write to disk, modify the filesystem, or maintain any persistent state. This drastically narrows the realistic threat surface compared to most Python packages on PyPI. ## Attack Surfaces These are the places where bitmath consumes input that might originate from a hostile source, ranked roughly by how worried I am about them. ### 1. `parse_string()` and `parse_string_unsafe()` These take a string and return a bitmath object. If a consumer hands bitmath an attacker-controlled string (web form input, log line, config file from an untrusted source), the parser is in the hot path. **What we do about it:** * The parser is a regex-driven state machine. It does not call `eval` on any portion of the input. * Unknown unit suffixes raise `ValueError`. Malformed numeric portions raise `ValueError`. The function will not silently coerce or accept nonsense. * The hypothesis-based fuzzing tests added in 2026 (`tests/test_hypothesis.py`) exercise the parser with thousands of generated inputs per release cycle to catch crash-on-input regressions. * The known parser quirk to be aware of: scientific-notation numbers do not round-trip cleanly through `parse_string` in all cases. This is documented and tracked, and does not constitute a security vulnerability (it is a precision issue, not a code-execution surface). ### 2. `getsize()`, `listdir()`, `query_capacity()` These take a filesystem path and call into the operating system. If a consumer hands bitmath an attacker-controlled path, the standard filesystem-API risks apply ([TOCTOU](https://en.wikipedia.org/wiki/Time-of-check_to_time-of-use), symlink races, path traversal in the *consumer's* code that constructed the path). **What we do about it:** * bitmath does no path sanitization of its own. It is the consumer's responsibility to validate paths before passing them in. This is documented behavior, not negligence; sanitization rules depend on the consumer's threat model in ways the library cannot know. * `listdir()` was deprecated in 2.1.0 because the API is awkward enough that misuse was likely. Consumers should reach for `os.walk()` plus `bitmath.getsize()` directly. ### 3. `query_device_capacity()` This issues `fcntl.ioctl` calls on Linux and Win32 `DeviceIoControl` calls on Windows against a file descriptor for a block device. Both require elevated privileges (root on Linux, administrator on Windows) and operate on a descriptor the consumer has already opened. **What we do about it:** * The Linux path uses fixed `ioctl` constants from the kernel headers and writes only into bytes buffers it allocated itself. The Windows path uses fixed control codes from the Win32 API. * There is no string formatting into the ioctl argument and no untrusted-input pathway into the kernel call. * macOS raises `NotImplementedError` because the System Integrity Protection model makes the call meaningless. There is no codepath to exploit there because there is no codepath at all. ### 4. The Build and Release Pipeline The build pipeline itself is an attack surface: if the publishing workflow could be tricked into emitting a malicious wheel, every downstream pip install would be compromised. **What we do about it:** * Publishing uses [PyPI Trusted Publishing](https://docs.pypi.org/trusted-publishers/) (OIDC). No long-lived PyPI token exists to be stolen. * The publish workflow (`.github/workflows/publish.yml`) only fires on `release: published` events. It cannot be triggered by a pull request or a direct push. * All GitHub Actions are SHA-pinned, not version-pinned. Action upgrades go through Dependabot PRs that I review individually. * Branch protection on `master` requires all CI checks to pass, with `enforce_admins: true`. I cannot push a malicious commit directly even if my account were compromised, as long as the branch protection itself is not also compromised. * Two-factor authentication is required on my GitHub account (authenticator app + GitHub Mobile, SMS disabled). ## Automated Security Tooling Everything below runs on every push and pull request to `master`, plus on a weekly schedule, and posts its findings to the GitHub Security tab. | Tool | What it checks | Where it lives | |------|---------------|----------------| | [Bandit](https://bandit.readthedocs.io/) | Common Python security smells across `bitmath/` and `tests/` | `.github/workflows/bandit.yml` | | [CodeQL](https://codeql.github.com/) | Deeper semantic analysis for Python vulnerabilities | `.github/workflows/codeql.yml` | | [OSSF Scorecard](https://securityscorecards.dev/viewer/?uri=github.com/timlnx/bitmath) | Repository security posture (token permissions, pinning, signed releases, etc.) | `.github/workflows/scorecard.yml` | | [Dependabot](https://docs.github.com/en/code-security/dependabot) | Vulnerability alerts and security update PRs for dependencies | `.github/dependabot.yml` | | [GitHub Secret Scanning](https://docs.github.com/en/code-security/secret-scanning) | Prevents accidental commit of credentials | Repository setting (enabled) | Bandit currently reports zero findings against the codebase. CodeQL currently reports zero findings against the codebase. OSSF Scorecard currently rates the project at 7.8/10 and climbing as the OpenSSF Best Practices badge work lands. ## Known Vulnerabilities None at time of writing. If any are reported and confirmed, they will be published as [GitHub Security Advisories](https://github.com/timlnx/bitmath/security/advisories) with the CVE number when applicable, and called out in the relevant `NEWS.rst` section for that release. ## Reporting See [SECURITY.md](SECURITY.md) for the reporting process, response timeframes, and contact channels. ## Last Review This assessment was last reviewed against the bitmath 2.1.0 working tree. timlnx-bitmath-8354f39/SECURITY_POLICIES.md000066400000000000000000000130271520504626100200260ustar00rootroot00000000000000# Security Policies This document defines bitmath's policies for handling findings from Software Composition Analysis (SCA), Static Application Security Testing (SAST), and license compliance scanning. It exists so downstream consumers can understand the project's commitments and so future maintainers can apply the policies consistently. The threat model that motivates these policies is in [SECURITY_ASSESSMENT.md](SECURITY_ASSESSMENT.md). The vulnerability reporting process is in [SECURITY.md](SECURITY.md). The release verification process is in [VERIFICATION.md](VERIFICATION.md). The workflows that enforce these policies live in `.github/workflows/`. ## Software Composition Analysis (SCA) SCA covers known vulnerabilities and license compliance in bitmath's dependencies. Because bitmath has **zero runtime dependencies** (verifiable in `pyproject.toml`), the operational scope of SCA is the development dependency set listed in `requirements.txt`. ### Tools * **Dependabot** opens pull requests for dependency updates and surfaces known vulnerabilities. Configured in `.github/dependabot.yml`. * **pip-audit** runs on every push and pull request via `.github/workflows/sca.yml` against `requirements.txt`. The workflow is a required status check on `master`. * **GitHub secret scanning** (enabled at the repository level) catches credentials accidentally committed to the repository. ### Remediation Thresholds Severity ratings follow the CVSS score reported by the SCA tool. The remediation clock starts when the finding becomes visible in the project (Dependabot alert or `pip-audit` CI failure). | Severity | CVSS range | Action | Timeline | |----------|-----------|--------|----------| | Critical | 9.0 – 10.0 | Patch or replace the affected dependency. Block any release until remediated. | Within 7 days of disclosure | | High | 7.0 – 8.9 | Patch or replace the affected dependency. Block release if the next release is within 30 days; otherwise remediate before that release. | Within 30 days of disclosure | | Medium | 4.0 – 6.9 | Patch when the next dependency update naturally cycles through. | Within 90 days | | Low | 0.1 – 3.9 | Patch opportunistically. | Best effort | A finding may be suppressed only when it is demonstrably non-exploitable in bitmath's usage of the dependency. Suppression is documented inline in the SCA workflow configuration with a comment explaining why. ### Release Blocking A release of bitmath **MUST NOT** ship while a Critical or High SCA finding against a dev dependency is unaddressed, unless the finding has been formally suppressed as non-exploitable per the rule above. The `.github/workflows/sca.yml` status check enforces this on the merge path; the release process (see [ARCHITECTURE.md](ARCHITECTURE.md)) is gated by the same branch protection. ### License Compliance All direct dependencies must use a license listed as [OSI-approved](https://opensource.org/licenses) or [FSF-approved](https://www.gnu.org/licenses/license-list.html). Today every dev dependency uses MIT, Apache 2.0, or BSD. Adding a new dev dependency that introduces a copyleft license (GPL, AGPL, LGPL) requires explicit consideration before being merged; the PR that introduces it must document the decision in the description. ## Static Application Security Testing (SAST) SAST covers security weaknesses in bitmath's own source code. ### Tools * **Bandit** scans `bitmath/` and `tests/` for common Python security smells. Runs on every push and pull request via `.github/workflows/bandit.yml`. Required status check. * **CodeQL** performs semantic analysis for Python vulnerabilities. Runs on every push, pull request, and weekly via `.github/workflows/codeql.yml`. Required status check. * **OSSF Scorecard** performs a weekly meta-analysis of repository security posture. Reports to the GitHub Security tab. ### Remediation Thresholds | Severity | Action | Timeline | |----------|--------|----------| | High | Fix in the next patch release. Block any merge that introduces a new High finding. | Within 7 days for existing findings; immediately for new ones | | Medium | Fix in the next minor release. | Within 30 days | | Low | Address in the normal development cycle. | Best effort | | Informational | Triage and either fix or suppress with a justifying comment. | Best effort | A finding may be suppressed only when it is a true false positive. The suppression must include a `# nosec` comment with an inline justification (Bandit) or a `# lgtm[py/...]` annotation (CodeQL) explaining the rationale. The justification should be specific enough that a future maintainer can re-evaluate the suppression without re-deriving the analysis. ### Release Blocking A release of bitmath **MUST NOT** ship while any High SAST finding against the project's own code is unaddressed, unless the finding is formally suppressed as a false positive per the rule above. The Bandit and CodeQL required status checks enforce this on the merge path. ## Review and Updates This policy is reviewed at every minor release. The contract: * A new SAST or SCA tool added to CI → update the corresponding Tools section. * A change to remediation thresholds or release-blocking rules → update the corresponding table and re-state the rationale in the commit message. * A suppression added in code → also add a brief justification in the suppression comment so the rationale survives the next review. If this policy drifts out of sync with what the workflows actually enforce, that is a defect worth [opening an issue](https://github.com/timlnx/bitmath/issues/new) or a PR. timlnx-bitmath-8354f39/VERIFICATION.md000066400000000000000000000120131520504626100171440ustar00rootroot00000000000000# Verifying a bitmath Release This document explains how to verify that a bitmath release was built by this project and not tampered with in transit or at rest. It is aimed at downstream consumers who need to demonstrate due diligence (organizations subject to the EU Cyber Resilience Act, NIST SSDF, or similar frameworks) and at anyone who just wants to understand what guarantees the release process actually provides. The release pipeline that produces these artifacts is documented in [ARCHITECTURE.md](ARCHITECTURE.md). The threat model that motivates this verification stack is in [SECURITY_ASSESSMENT.md](SECURITY_ASSESSMENT.md). ## What You Can Verify For every bitmath release on PyPI, three independent properties are verifiable without trusting the maintainer: 1. **Integrity.** Each wheel and sdist has a published SHA-256 hash. `pip` checks this automatically on install. The hash is also visible on each file's page on PyPI. 2. **Authenticity.** Each release is built and published by the `publish.yml` workflow in `timlnx/bitmath` on GitHub, using GitHub Actions OIDC and PyPI Trusted Publishing. The proof of that binding is a [PEP 740](https://peps.python.org/pep-0740/) attestation attached to each release file. 3. **Provenance.** A CycloneDX Software Bill of Materials (SBOM) is attached to each GitHub Release, describing the runtime dependency closure of the wheel. bitmath has zero runtime dependencies, so this SBOM is short by design. ## Verifying Integrity `pip` verifies the SHA-256 hash on install. There is normally nothing you have to do beyond `pip install bitmath`. If you want to verify a downloaded artifact manually: ```bash # Get the expected hash from PyPI for, say, version 2.0.1 curl -s https://pypi.org/pypi/bitmath/2.0.1/json \ | jq -r '.urls[] | "\(.digests.sha256) \(.filename)"' # Compute the local hash and compare shasum -a 256 bitmath-2.0.1-py3-none-any.whl ``` The hashes must match. If they do not, do not install the file. ## Verifying Authenticity (PEP 740 Attestation) PyPI exposes attestations under each release file's API metadata. The simplest way to verify them is with the [pypi-attestations](https://pypi.org/project/pypi-attestations/) CLI: ```bash pipx install pypi-attestations # Download the wheel you want to verify pip download bitmath==2.0.1 --no-deps -d /tmp/verify-bitmath/ # Verify the attestation pypi-attestations verify pypi \ --repository timlnx/bitmath \ /tmp/verify-bitmath/bitmath-2.0.1-py3-none-any.whl ``` The expected attestation identity is: * **Source repository**: `timlnx/bitmath` * **Workflow path**: `.github/workflows/publish.yml` * **Workflow ref**: `refs/tags/` (the git tag for the release) If verification succeeds, the file was built by GitHub Actions on `timlnx/bitmath` from a workflow file whose contents you can audit in the repository at the corresponding tag. The whole signing path uses [Sigstore](https://www.sigstore.dev/) under the hood. No long-lived signing keys are involved. If verification fails, **do not install the file**. Treat it as a potential supply chain compromise and reach out via the channels in [SECURITY.md](SECURITY.md). ## Verifying the SBOM A CycloneDX SBOM is attached to each GitHub Release as `bitmath-.cdx.json`. Fetch it directly from the release page: ```bash curl -L -o bitmath-2.0.1.cdx.json \ https://github.com/timlnx/bitmath/releases/download/v2.0.1/bitmath-2.0.1.cdx.json ``` You can validate it with any CycloneDX-aware tool. For example, with [cyclonedx-cli](https://github.com/CycloneDX/cyclonedx-cli): ```bash cyclonedx validate --input-file bitmath-2.0.1.cdx.json ``` For bitmath, the SBOM should show one primary component (bitmath itself) plus the small set of components present in the install environment. The runtime dependency list under bitmath should be empty, because bitmath has no runtime dependencies by design. If the SBOM shows additional runtime dependencies under bitmath itself, that is a defect worth [opening an issue](https://github.com/timlnx/bitmath/issues/new) about. ## Why This Matters These verification steps let you confirm three things without trusting me personally: * The bytes you have are the bytes PyPI published. (Hash verification.) * The bytes PyPI published came from this repository's release workflow. (PEP 740 attestation verification.) * The dependency surface is what the project claims it is. (SBOM verification.) A compromise that bypasses all three would require simultaneously breaking the GitHub Actions OIDC infrastructure, the Sigstore signing chain, and PyPI's storage. That is the threat model the verification stack is designed against. More plausible compromises (a stolen API token, a phishing-compromised maintainer account, a tampered wheel in transit, a malicious dependency injection) are caught by one or more of these checks. ## Last Review This document is reviewed at every minor release alongside [SECURITY_ASSESSMENT.md](SECURITY_ASSESSMENT.md), [ARCHITECTURE.md](ARCHITECTURE.md), and [SECURITY_POLICIES.md](SECURITY_POLICIES.md). timlnx-bitmath-8354f39/VERSION000066400000000000000000000000061520504626100160070ustar00rootroot000000000000002.1.1 timlnx-bitmath-8354f39/bitmath.1000066400000000000000000000044561520504626100164660ustar00rootroot00000000000000'\" t .\" Title: bitmath .\" Author: [see the "AUTHOR" section] .\" Generator: DocBook XSL Stylesheets vsnapshot .\" Date: 05/15/2026 .\" Manual: python-bitmath .\" Source: bitmath 2.1.0 .\" Language: English .\" .TH "BITMATH" "1" "05/15/2026" "bitmath 2\&.1\&.0" "python\-bitmath" .\" ----------------------------------------------------------------- .\" * Define some portability stuff .\" ----------------------------------------------------------------- .\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .\" http://bugs.debian.org/507673 .\" http://lists.gnu.org/archive/html/groff/2009-02/msg00013.html .\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .ie \n(.g .ds Aq \(aq .el .ds Aq ' .\" ----------------------------------------------------------------- .\" * set default formatting .\" ----------------------------------------------------------------- .\" disable hyphenation .nh .\" disable justification (adjust text to left margin only) .ad l .\" ----------------------------------------------------------------- .\" * MAIN CONTENT STARTS HERE * .\" ----------------------------------------------------------------- .SH "NAME" bitmath \- command line utility for converting between prefix units .SH "SYNOPSIS" .sp bitmath [\-\-from\-stdin] [\-f IN_UNIT] [\-t OUT_UNIT] SIZE \&... .SH "DESCRIPTION" .sp Run it, give a number as the last parameter\&. Say what units you\(cqre giving/wanting, or not\&. .SH "OPTIONS" .PP \fB\-\-from\-stdin\fR .RS 4 Reads number from stdin rather than the cli\&. .RE .PP \fB\-f\fR \fIUNIT\fR, \fB\-\-from\fR \fIUNIT\fR .RS 4 The unit you are converting from\&. Default is \fBByte\fR\&. .RE .PP \fB\-t\fR \fIUNIT, \fR\fI\fB\-\-to\fR\fR\fI \*(AqUNIT\fR .RS 4 The unit you are converting to\&. The default is \fBauto\-selected\fR for best human\-readability\&. .RE .SH "AUTHOR" .sp Tim Case (fmr\&. tbielawa) .sp For a complete list of contributors, please visit the GitHub charts page\&. .SH "COPYRIGHT" .sp Copyright \(co 2014\-2026, Tim Case\&. .sp bitmath is released under the terms of the "MIT" License\&. .SH "SEE ALSO" .sp \fBunits\fR(7) .sp \fBThe bitmath GitHub Project\fR \(em https://github\&.com/timlnx/bitmath .sp \fBThe bitmath Documentation\fR \(em https://bitmath\&.readthedocs\&.org timlnx-bitmath-8354f39/bitmath.1.asciidoc.in000066400000000000000000000020771520504626100206450ustar00rootroot00000000000000bitmath(1) ========== :man source: bitmath :man version: %VERSION% :man manual: python-bitmath NAME ---- bitmath - command line utility for converting between prefix units SYNOPSIS -------- bitmath [--from-stdin] [-f IN_UNIT] [-t OUT_UNIT] SIZE ... DESCRIPTION ----------- Run it, give a number as the last parameter. Say what units you're giving/wanting, or not. OPTIONS ------- *--from-stdin*:: Reads number from stdin rather than the cli. *-f* 'UNIT', *--from* 'UNIT':: The unit you are converting from. Default is **Byte**. *-t* 'UNIT, *--to* 'UNIT':: The unit you are converting to. The default is **auto-selected** for best human-readability. AUTHOR ------ Tim Case (fmr. tbielawa) For a complete list of contributors, please visit the GitHub charts page. COPYRIGHT --------- Copyright © 2014-2026, Tim Case. bitmath is released under the terms of the "MIT" License. SEE ALSO -------- *units*(7) *The bitmath GitHub Project* -- *The bitmath Documentation* -- timlnx-bitmath-8354f39/bitmath/000077500000000000000000000000001520504626100163735ustar00rootroot00000000000000timlnx-bitmath-8354f39/bitmath/__init__.py000066400000000000000000001706311520504626100205140ustar00rootroot00000000000000# -*- coding: utf-8 -*- # SPDX-License-Identifier: MIT # The MIT License (MIT) # # SPDX-FileCopyrightText: 2014-2026 Tim Case # 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 , 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() timlnx-bitmath-8354f39/context7.json000066400000000000000000000001371520504626100174120ustar00rootroot00000000000000{ "url": "https://context7.com/timlnx/bitmath", "public_key": "pk_yHUkegjgthEmGSaCHS9WU" } timlnx-bitmath-8354f39/doc-requirements.txt000066400000000000000000000000211520504626100207630ustar00rootroot00000000000000sphinx_rtd_theme timlnx-bitmath-8354f39/docsite/000077500000000000000000000000001520504626100163755ustar00rootroot00000000000000timlnx-bitmath-8354f39/docsite/HEADERS000066400000000000000000000002541520504626100173740ustar00rootroot00000000000000# with overline, for parts * with overline, for chapters = for sections - for subsections ^ for subsubsections " for paragraphs | for anything smaller timlnx-bitmath-8354f39/docsite/Makefile000066400000000000000000000127671520504626100200520ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 2>&1 | grep -v 'nonlocal image URI found:' @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/bitmath.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/bitmath.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/bitmath" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/bitmath" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -N -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." timlnx-bitmath-8354f39/docsite/source/000077500000000000000000000000001520504626100176755ustar00rootroot00000000000000timlnx-bitmath-8354f39/docsite/source/NEWS.rst000066400000000000000000000000351520504626100212010ustar00rootroot00000000000000.. include:: ../../NEWS.rst timlnx-bitmath-8354f39/docsite/source/_static/000077500000000000000000000000001520504626100213235ustar00rootroot00000000000000timlnx-bitmath-8354f39/docsite/source/_static/.gitkeep000066400000000000000000000000001520504626100227420ustar00rootroot00000000000000timlnx-bitmath-8354f39/docsite/source/appendices.rst000066400000000000000000000003341520504626100225420ustar00rootroot00000000000000Appendices ########## .. contents:: :depth: 3 :local: .. include:: appendices/mixed_math.rst .. include:: appendices/on_units.rst .. include:: appendices/who_uses.rst .. include:: appendices/related_projects.rst timlnx-bitmath-8354f39/docsite/source/appendices/000077500000000000000000000000001520504626100220105ustar00rootroot00000000000000timlnx-bitmath-8354f39/docsite/source/appendices/mixed_math.rst000066400000000000000000000301701520504626100246620ustar00rootroot00000000000000.. _appendix_math: Rules for Math ************** This section describes what we need to know to effectively use bitmath for arithmetic. Because bitmath allows the use of instances as operands on either side of the operator it is especially important to understand their behavior. Just as in normal every-day math, not all operations yield the same result if the operands are switched. E.g., ``1 - 2 = -1`` whereas ``2 - 1 = 1``. This section includes discussions of the results for each supported **mixed math** operation. For mixed math operations (i.e., an operation with a bitmath instance and a number type), implicit coercion **may** happen. That is to say, a bitmath instance will be converted to a number type. When coercion happens is determined by the following conditions and rules: 1. `Precedence and Associativity of Operators `_ in Python\ [#precedence]_ 2. Situational semantics -- some operations, though mathematically valid, do not make logical sense when applied to context. Terminology =========== The definitions describes some of the terminology used throughout this section. Coercion The act of converting operands into a common type to support arithmetic operations. Somewhat similar to how adding two fractions requires coercing each operand into having a common denominator. Specific to the bitmath domain, this concept means using an instance's prefix value for mixed-math. Operand The object(s) of a mathematical operation. That is to say, given ``1 + 2``, the operands would be **1** and **2**. Operator The mathematical operation to evaluate. Given ``1 + 2``, the operation would be addition, **+**. LHS *Left-hand side*. In discussion this specifically refers to the operand on the left-hand side of the operator. RHS *Right-hand side*. In discussion this specifically refers to the operand on the right-hand side of the operator. Two bitmath operands ==================== This section describes what happens when two bitmath instances are used as operands. There are three possible results from this type of operation. *Addition and subtraction* The result will be of the type of the LHS. *Multiplication* Supported, but yields results which may not be intuitive. The math is performed at the byte-level (ignoring prefix units). The result is in the unit of the LHS. Technically speaking, if the LHS of the equation is a byte unit, then the result should be squared. You are advised to call the :py:meth:`best_prefix` method on the result to get a useful value back. .. code-block:: python >>> bitmath.kB(3).bytes, bitmath.MiB(5).bytes (3000.0, 5242880.0) >>> bitmath.best_prefix(3000 * 5242880) GiB(14.6484375) >>> bitmath.Byte(3000) * bitmath.MiB(5) B(15728640000.0) >>> (bitmath.Byte(3000) * bitmath.MiB(5)).best_prefix() GiB(14.6484375) The final result represents how many “GiB-sized” chunks of bytes are in that total. It's weird, but it works. If the LHS is larger than a byte prefix unit then you get a result matching the unit of the LHS of the equation. .. code-block:: python >>> bitmath.MiB(5) * bitmath.kB(3) MiB(15000.0) # LHS was MiB, result is MiB # And if we wrap it with best_prefix() we get the earlier result back >>> (bitmath.MiB(5) * bitmath.kB(3)).best_prefix() GiB(14.6484375) *Division* The result will be a number type due to unit cancellation. .. code-block:: python >>> bitmath.kB(3) / bitmath.MiB(5) 0.00057220458984375 >>> bitmath.kB(3).bytes / bitmath.MiB(5).bytes 0.00057220458984375 Above you can see that the math is performed on the bytes of each operand. As noted above, the units cancel out. This is the opposite of the multiplication case where the units square together if you do not coerce them into a larger prefix unit. .. _appendix_math_mixed_types: Mixed Types: Addition and Subtraction ===================================== This describes the behavior of addition and subtraction operations where one operand is a bitmath type and the other is a number type. Mixed-math addition and subtraction return a type from the :py:mod:`numbers` family (integer, float, etc...) regardless of the placement of the operands, with one exception: when the left operand is exactly ``0``, the result is the bitmath instance itself. This exception exists so that Python's built-in :py:func:`sum` function works correctly with iterables of bitmath objects, since ``sum()`` starts accumulation from ``0`` by default: .. code-block:: python >>> import bitmath >>> sum([bitmath.Byte(1), bitmath.MiB(1), bitmath.GiB(1)]) Byte(1074790401.0) For all non-zero numeric operands the behavior (returning a number) applies. **Discussion:** Why do ``100 - KiB(90)`` and ``KiB(100) - 90`` both yield a result of ``10.0`` and not another bitmath instance, such as ``KiB(10.0)``? When implementing the math part of the object datamodel customizations\ [#datamodel]_ there were two choices available: 1. Offer no support at all. Instead raise a :py:exc:`NotImplemented` exception. 2. Consistently apply coercion to the bitmath operand to produce a useful result (*useful* if you know the rules of the library). In the end it became a philosophical decision guided by scientific precedence. Put simply, bitmath uses the significance of the **least significant operand**, specifically the number-type operand because it lacks semantic significance. In application this means that we drop the semantic significance of the bitmath operand. That is to say, given an input like ``GiB(13.37)`` (equivalent to == 13.37 * 2\ :sup:`30`\ ), the only property used in calculations is the prefix value, ``13.37``. Numbers carry mathematical significance, in the form of precision, but what they lack is *semantic* (contextual) significance. A number by itself is just a measurement of an arbitrary quantity of *stuff*. In mixed-type math, bitmath effectively treats numbers as mathematical constants. A bitmath instance also has mathematical significance in that an instance is a measurement of a quantity (bits in this case) and that quantity has a measurable precision. But a bitmath instance is more than just a measurement, it is a specialized representation of a count of bits. This gives bitmath instances *semantic* significance as well. And so, in deciding how to handle mixed-type (really what we should say is mixed-significance) operations, we chose to model the behavior off of an already established set of rules. Those rules are the Rules of Significance Arithmetic\ [#significance]_. Let's look at an example of this in action: .. code-block:: python In [8]: num = 42 In [9]: bm = PiB(24) In [10]: print(num + bm) 66.0 Equivalently, divorcing the bitmath instance from it's value (this is coercion): .. code-block:: python In [12]: bm_value = bm.value In [13]: print(num + bm_value) 66.0 What it all boils down to is this: if we don't provide a unit then bitmath won't give us one back. There is no way for bitmath to guess what unit the operand was *intended* to carry. Therefore, the behavior of bitmath is **conservative**. It will meet us half way and do the math, but it will not return a unit in the result. **Keeping the result as a bitmath type** If the intent is to add or subtract a quantity of the *same unit* — for example, incrementing ``Byte(1)`` by one more byte — use an explicit bitmath operand on both sides: .. code-block:: python >>> Byte(1) + Byte(1) Byte(2.0) >>> KiB(10) - KiB(3) KiB(7.0) This makes the unit explicit rather than relying on implicit conversion, which eliminates ambiguity — ``KiB(10) - 3`` could mean "subtract 3 KiB" or "subtract the number 3 from the prefix value." bitmath does not guess; using a bitmath operand on both sides states the intent clearly. Mixed Types: Multiplication and Division ======================================== **Multiplication** has *commutative* properties. This means that the ordering of the operands is **not significant**. Because of this fact bitmath allows arbitrary placement of the operands, treating the numeric operand as a constant. Here's an example demonstrating this. .. code-block:: python In [2]: 10 * KiB(43) Out[2]: KiB(430.0) In [3]: KiB(43) * 10 Out[3]: KiB(430.0) **Division**, however, *does not* have this commutative property. I.e., the placement of the operands **is** significant. Additionally, there is a semantic difference in division. Dividing a quantity (e.g. ``MiB(100)``) by a constant (``10``) makes complete sense. Conceptually (in the domain of bitmath), the intention of ``MiB(100) / 10)`` is to separate ``MiB(10)`` into **10** equal sized parts. .. code-block:: python In [4]: KiB(43) / 10 Out[4]: KiB(4.3) The reverse operation does not maintain semantic validity. Stated differently, it does not make logical sense to divide a constant by a measured quantity of *stuff*. If you're still not clear on this, ask yourself what you would expect to get if you did this: .. math:: \dfrac{100}{kB(33)} Unless you're representing rates that doesn't mean much at all. The units of operands when expressing rates is going to be context sensitive and can be very non-intuitive without additional knowledge. For example: .. code-block:: python >>> 100/bitmath.kB(33) 3.0303030303030303 This is functionally equivalent to writing: .. math:: \dfrac{1}{kB(33)} \cdot 100 This might mean something to you, but we can't express that as a prefix unit. We let you do it, but it is up to you to determine the significance of the result. 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 discrete (you cannot store half a bit), but the *measurement* of storage is legitimately continuous. Fractional values appear naturally in division, unit conversion chains, and proportional calculations: .. code-block:: python >>> KiB(1) / 3 KiB(0.3333333333333333) >>> MiB(1).to_Bit() Bit(8388608.0) >>> KiB(1/3).to_Bit() Bit(2730.6666666666665) The last example is not a bug. The fractional bit count is the faithful representation of a fractional byte input. If you need integer results, Python's built-in :py:func:`math.floor`, :py:func:`math.ceil`, and :py:func:`round` all work on bitmath instances and return an instance of the same type: .. code-block:: python >>> import math >>> math.floor(KiB(1) / 3) KiB(0) >>> math.ceil(KiB(1) / 3) KiB(1) >>> round(MiB(1.75)) MiB(2) .. warning:: Rounding intermediate results is a lossy operation. ``math.floor(GiB(10) / 3) * 3`` yields ``GiB(9)``, not ``GiB(10)``. Only round at the **final** output step. **Floating-point accumulation:** Because bitmath uses IEEE 754 64-bit floats internally, arithmetic across many operations may accumulate small rounding errors — identical to ordinary Python float arithmetic. For the file-size domain (values up to exabyte scale), 64-bit float provides approximately 15 significant decimal digits of precision, which is sufficient for all practical purposes. If exact integer semantics are required at the byte level, use ``int(instance.bytes)`` to work in raw integers. .. seealso:: :ref:`instances_rounding` — instance methods for rounding and integer conversion. Footnotes ========= .. [#precedence] For a less technical review of precedence and associativity, see `Programiz: Precedence and Associativity of Operators in Python `_ .. [#datamodel] `Python Datamodel Customization Methods `_ .. [#significance] https://en.wikipedia.org/wiki/Significance_arithmetic timlnx-bitmath-8354f39/docsite/source/appendices/on_units.rst000066400000000000000000000113551520504626100244050ustar00rootroot00000000000000.. _appendix_on_units: On Units ******** As previously stated, in this module you will find two very similar sets of classes available. These are the **NIST** and **SI** prefixes. The **NIST** prefixes are all base 2 and have an 'i' character in the middle. The **SI** prefixes are base 10 and have no 'i' character. For smaller values, these two systems of unit prefixes are roughly equivalent. The ``round()`` operations below demonstrate how close in a percent one "unit" of SI is to one "unit" of NIST. .. code-block:: python :linenos: :emphasize-lines: 7,15,23 In [15]: one_kilo = 1 * 10**3 In [16]: one_kibi = 1 * 2**10 In [17]: round(one_kilo / one_kibi, 2) Out[17]: 0.98 In [18]: one_tera = 1 * 10**12 In [19]: one_tebi = 1 * 2**40 In [20]: round(one_tera / one_tebi, 2) Out[20]: 0.91 In [21]: one_exa = 1 * 10**18 In [22]: one_exbi = 1 * 2**60 In [23]: round(one_exa / one_exbi, 2) Out[23]: 0.87 They begin as roughly equivalent, however as you can see (lines: **7**, **15**, and **23**), they diverge significantly for higher values. Why two unit systems? Why take the time to point this difference out? Why should you care? `The Linux Documentation Project `_ comments on that: Before these binary prefixes were introduced, it was fairly common to use k=1000 and K=1024, just like b=bit, B=byte. Unfortunately, the M is capital already, and cannot be capitalized to indicate binary-ness. At first that didn't matter too much, since memory modules and disks came in sizes that were powers of two, so everyone knew that in such contexts "kilobyte" and "megabyte" meant 1024 and 1048576 bytes, respectively. What originally was a sloppy use of the prefixes "kilo" and "mega" started to become regarded as the "real true meaning" when computers were involved. But then disk technology changed, and disk sizes became arbitrary numbers. After a period of uncertainty all disk manufacturers settled on the standard, namely k=1000, M=1000k, G=1000M. The situation was messy: in the 14k4 modems, k=1000; in the 1.44MB diskettes, M=1024000; etc. In 1998 the IEC approved the standard that defines the binary prefixes given above, enabling people to be precise and unambiguous. Thus, today, MB = 1000000B and MiB = 1048576B. In the free software world programs are slowly being changed to conform. When the Linux kernel boots and says:: hda: 120064896 sectors (61473 MB) w/2048KiB Cache the MB are megabytes and the KiB are kibibytes. - Source: ``man 7 units`` - http://man7.org/linux/man-pages/man7/units.7.html Furthermore, to quote the `National Institute of Standards and Technology (NIST) `_: "Once upon a time, computer professionals noticed that 2\ :sup:`10` was very nearly equal to 1000 and started using the SI prefix "kilo" to mean 1024. That worked well enough for a decade or two because everybody who talked kilobytes knew that the term implied 1024 bytes. But, almost overnight a much more numerous "everybody" bought computers, and the trade computer professionals needed to talk to physicists and engineers and even to ordinary people, most of whom know that a kilometer is 1000 meters and a kilogram is 1000 grams. "Then data storage for gigabytes, and even terabytes, became practical, and the storage devices were not constructed on binary trees, which meant that, for many practical purposes, binary arithmetic was less convenient than decimal arithmetic. The result is that today "everybody" does not "know" what a megabyte is. When discussing computer memory, most manufacturers use megabyte to mean 2\ :sup:`20` = 1 048 576 bytes, but the manufacturers of computer storage devices usually use the term to mean 1 000 000 bytes. Some designers of local area networks have used megabit per second to mean 1 048 576 bit/s, but all telecommunications engineers use it to mean 106 bit/s. And if two definitions of the megabyte are not enough, a third megabyte of 1 024 000 bytes is the megabyte used to format the familiar 90 mm (3 1/2 inch), "1.44 MB" diskette. The confusion is real, as is the potential for incompatibility in standards and in implemented systems. "Faced with this reality, the IEEE Standards Board decided that IEEE standards will use the conventional, internationally adopted, definitions of the SI prefixes. Mega will mean 1 000 000, except that the base-two definition may be used (if such usage is explicitly pointed out on a case-by-case basis) until such time that prefixes for binary multiples are adopted by an appropriate standards body." timlnx-bitmath-8354f39/docsite/source/appendices/related_projects.rst000066400000000000000000000056051520504626100261010ustar00rootroot00000000000000.. _appendix_related_projects: Related Projects **************** Bitmath is not the first project to tackle a challenge of this nature, handling units in a sane OOP approach. Several other Python libraries exist which provide similar functionality to bitmath. It only seems fair that we should point out these other libraries in case bitmath isn't the best fit for you. Magnitude ========= *Magnitude implements efficient computation with physical quantities. It allows you to do mathematical operations with them as if they were numbers, taking care of the units behind the scenes.* **Magnitide**, from Juan Reyero, is a *very* extensible library for working with a large variety of units (e.g., ``mile`` = *one mile*), as well as derived units (e.g., ``mile/hour``). Scaling, such as indicating one **mega** byte (``1 MB``) is also programmable with Magnitude. Juan is also kind enough to include a similar `"related projects" `_ section in his documentation. * `Magnitude GitHub Project `_ * `Magnitude Docs `_ hurry.filesize ============== *hurry.filesize a simple Python library that can take a number of bytes and returns a human-readable string with the size in it, in kilobytes (K), megabytes (M), etc.* **hurry.filesize** is very limited in functionality when compared to the other alternatives. However, it is an extremely simple and lightweight module. If you're looking for a library just for turning counts of bytes into human-readable strings, then hurry.filesize will be great for you. If you need any more functionality, such as greater control over :ref:`output formatting `, or :ref:`arithmetic calculations `, then you will find hurry.filesize lacking. This project has not updated since 2009, so I would not expect to see updates any time soon. * `PyPi Homepage & Download `_ SymPy - Units ============= *This module provides around 200 predefined units that are commonly used in the sciences. Additionally, it provides the ``Unit`` class which allows you to define your own units.* The **Units** module from the `SymPy `_ library is another option. Like **Magnitude**, the Units library is very extensible and includes around 200 built-in units by default. While technically it supports handling quantities such as ``1337 PiB``, this support must be configured by the user. In contrast, the bitmath module includes classes representing the full spectrum of byte and bit based units, out of the box. No conversion or derivation code required of the user. * `Units Homepage & Docs `_ * Download available through ``pip``, or your distribution's package system timlnx-bitmath-8354f39/docsite/source/appendices/who_uses.rst000066400000000000000000000023141520504626100243760ustar00rootroot00000000000000.. _appendix_who_uses: Who uses Bitmath **************** Shout-outs to all of the bitmath adopters out there I was able to identify: * `ClusterHQ's `_ `"Flocker" `_. A data volume manager for Dockerized applications * VMware's `vsphere `_ flocker storage driver * EMC's `scaleio `_ flocker storage driver * Dell Storage's `storage center block device `_ flocker driver * TravelCRM - Free CRM for travel companies * `direscraw `_ by `Brian Mikolajczyk `_ for recovering lost files * `sizer `_ by `Calle Liljeholm `_. Calculating useable capacity in a flocker cluster timlnx-bitmath-8354f39/docsite/source/classes.rst000066400000000000000000000173121520504626100220700ustar00rootroot00000000000000.. _classes: .. currentmodule:: bitmath Classes ####### .. contents:: :depth: 3 :local: Available Classes ***************** There are two **fundamental** classes available, the :class:`Bit` and the :class:`Byte`. 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 (NIST) `_ as the 'Binary Prefix Units'. They are defined by increasing powers of 2. Classes without the **'i'** character are **SI** type classes. Though not formally defined by any standards organization, they follow the `International System of Units (SI) `_ pattern (commonly used to abbreviate base 10 values). You may hear these referred to as the "Decimal" or "SI" prefixes. Classes ending with *lower-case* **'b'** characters are **bit based**. Classes ending with upper-case **'B'** characters are **byte based**. Class inheritance is shown below in parentheses to make this more apparent: .. _classes_available: +---------------+--------------+ | NIST | SI | +===============+==============+ | ``Eib(Bit)`` | ``Eb(Bit)`` | +---------------+--------------+ | ``EiB(Byte)`` | ``EB(Byte)`` | +---------------+--------------+ | ``Gib(Bit)`` | ``Gb(Bit)`` | +---------------+--------------+ | ``GiB(Byte)`` | ``GB(Byte)`` | +---------------+--------------+ | ``Kib(Bit)`` | ``kb(Bit)`` | +---------------+--------------+ | ``KiB(Byte)`` | ``kB(Byte)`` | +---------------+--------------+ | ``Mib(Bit)`` | ``Mb(Bit)`` | +---------------+--------------+ | ``MiB(Byte)`` | ``MB(Byte)`` | +---------------+--------------+ | ``Pib(Bit)`` | ``Pb(Bit)`` | +---------------+--------------+ | ``PiB(Byte)`` | ``PB(Byte)`` | +---------------+--------------+ | ``Tib(Bit)`` | ``Tb(Bit)`` | +---------------+--------------+ | ``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. The majority of the functionality of bitmath object comes from their rich implementation of standard Python operations. You can use bitmath objects in **almost all** of the places you would normally use an integer or a float. See the :ref:`Table of Supported Operations ` and :ref:`Appendix: Rules for Math ` for more details. Initializing ************ .. class:: Bit([value=0[, bytes=None[, bits=None]]]) .. class:: Byte([value=0[, bytes=None[, bits=None]]]) .. class:: EB([value=0[, bytes=None[, bits=None]]]) .. class:: Eb([value=0[, bytes=None[, bits=None]]]) .. class:: EiB([value=0[, bytes=None[, bits=None]]]) .. class:: Eib([value=0[, bytes=None[, bits=None]]]) .. class:: GB([value=0[, bytes=None[, bits=None]]]) .. class:: Gb([value=0[, bytes=None[, bits=None]]]) .. class:: GiB([value=0[, bytes=None[, bits=None]]]) .. class:: Gib([value=0[, bytes=None[, bits=None]]]) .. class:: kB([value=0[, bytes=None[, bits=None]]]) .. class:: kb([value=0[, bytes=None[, bits=None]]]) .. class:: KiB([value=0[, bytes=None[, bits=None]]]) .. class:: Kib([value=0[, bytes=None[, bits=None]]]) .. class:: MB([value=0[, bytes=None[, bits=None]]]) .. class:: Mb([value=0[, bytes=None[, bits=None]]]) .. class:: MiB([value=0[, bytes=None[, bits=None]]]) .. class:: Mib([value=0[, bytes=None[, bits=None]]]) .. class:: PB([value=0[, bytes=None[, bits=None]]]) .. class:: Pb([value=0[, bytes=None[, bits=None]]]) .. class:: PiB([value=0[, bytes=None[, bits=None]]]) .. class:: Pib([value=0[, bytes=None[, bits=None]]]) .. class:: TB([value=0[, bytes=None[, bits=None]]]) .. class:: Tb([value=0[, bytes=None[, bits=None]]]) .. class:: TiB([value=0[, bytes=None[, bits=None]]]) .. 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]]]) The ``value``, ``bytes``, and ``bits`` parameters are **mutually exclusive**. That is to say, you cannot instantiate a bitmath class using more than **one** of the parameters. Omitting any keyword argument defaults to behaving as if ``value`` was provided. :param int value: **Default: 0**. The value of the instance in *prefix units*. For example, if we were instantiating a :class:`.KiB` object to represent 13.37 KiB, the ``value`` parameter would be **13.37**. For instance, ``k = bitmath.KiB(13.37)``. :param int bytes: The value of the instance as measured in bytes. :param int bits: The value of the instance as measured in bits. :raises ValueError: if more than one parameter is provided. The following code block demonstrates the 4 acceptable ways to instantiate a bitmath class. .. code-block:: python :linenos: :emphasize-lines: 4,7,11,15 >>> import bitmath # Omitting all keyword arguments defaults to 'value' behavior. >>> a = bitmath.KiB(1) # This is equivalent to the previous statement >>> b = bitmath.KiB(value=1) # We can also specify the initial value in bytes. # Recall, 1KiB = 1024 bytes >>> c = bitmath.KiB(bytes=1024) # Finally, we can specify exact number of bits in the # instance. Recall, 1024B = 8192b >>> d = bitmath.KiB(bits=8192) >>> a == b == c == d True Class Methods ************* Class Method: from_other() ========================== bitmath **class objects** have one public class method, :py:meth:`.from_other` which provides an alternative way to initialize a bitmath class. This method may be called on bitmath class objects directly. That is to say: you do not need to call this method on an instance of a bitmath class, however that is a valid use case. .. classmethod:: Byte.from_other(item) Instantiate any ``BitMathClass`` using another instance as reference for it's initial value. The :py:meth:`.from_other` class method has one required parameter: an instance of a bitmath class. :param BitMathInstance item: An instance of a bitmath class. :return: a bitmath instance of type ``BitMathClass`` equivalent in value to ``item`` :rtype: BitMathClass :raises TypeError: if ``item`` is not a valid :ref:`bitmath class ` In pure Python, this could also be written as: .. code-block:: python :linenos: :emphasize-lines: 3 >>> import bitmath >>> a_mebibyte = bitmath.MiB(1) >>> a_mebibyte_sized_kibibyte = bitmath.KiB(bytes=a_mebibyte.bytes) >>> a_mebibyte == a_mebibyte_sized_kibibyte True >>> print(a_mebibyte, a_mebibyte_sized_kibibyte) 1.0 MiB 1024.0 KiB Or, using the :py:meth:`.from_other` class method: .. code-block:: python :linenos: >>> a_mebibyte = bitmath.MiB(1) >>> a_big_kibibyte = bitmath.KiB.from_other(a_mebibyte) >>> a_mebibyte == a_big_kibibyte True >>> print(a_mebibyte, a_big_kibibyte) 1.0 MiB 1024.0 KiB timlnx-bitmath-8354f39/docsite/source/commandline.rst000066400000000000000000000033071520504626100227200ustar00rootroot00000000000000.. _command_line: The ``bitmath`` command-line Tool ################################# :mod:`bitmath` includes a CLI utility for easily converting units in a shell. For reference, there is also a manpage included, :manpage:`bitmath (1)`. Synopsis ******** .. code-block:: bash bitmath [--from-stdin] [-f IN_UNIT] [-t OUT_UNIT] VALUE ... Options ******* .. program:: bitmath .. option:: -f Specify the input unit to convert from. Defaults to :class:`bitmath.Byte`. .. option:: -t Specify the output unit to convert to. Defaults to the :ref:`best human-readable ` prefix unit. .. option:: --from-stdin Reads number from stdin rather than as a CLI argument. .. describe:: VALUE The value to convert. Examples ******** Convert ``1024`` into the best human-readable unit. Without specifying any ``from`` or ``to`` values this examples defaults to treating the input value as a :class:`bitmath.Byte`: .. code-block:: bash $ bitmath 1024 1.0 KiB Convert 1024 KiB into kBs: .. code-block:: bash $ bitmath -f KiB -t kb 1024 8388.608 kb Convert 1073741824 bytes into the best human-readable unit: .. code-block:: bash $ bitmath -f Byte 1073741824 1.0 GiB Use the :command:`stat` command to print the size of :file:`bitmath/__init__.py` in bytes, pipe the output into the :command:`bitmath` command, and print the result in MBs: .. code-block:: bash $ stat -c '%s' bitmath/__init__.py | bitmath --from-stdin -t MB 0.038374 MB Convert several values at once from Bytes (the default behavior) into MBs: .. code-block:: bash $ bitmath -t MB 1234567 9876543 1337 42 1.234567 MB 9.876543 MB 0.001337 MB 4.2e-05 MB timlnx-bitmath-8354f39/docsite/source/conf.py000066400000000000000000000220711520504626100211760ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # bitmath documentation build configuration file, created by # sphinx-quickstart on Sun Jul 13 20:40:01 2014. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys, os _version_file = os.path.join(os.path.dirname(__file__), '..', '..', 'VERSION') with open(_version_file) as _vf: _version = _vf.read().strip() try: import sphinx_rtd_theme except ImportError: _RTD_THEME = False pass else: _RTD_THEME = True # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath('../../bitmath')) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.viewcode', 'sphinx.ext.mathjax'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = {'.rst': 'restructuredtext'} # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'bitmath' copyright = u'2014-2026, Tim Case' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = _version # The full version, including alpha/beta/rc tags. release = _version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [ 'appendices/mixed_math.rst', 'appendices/*', 'example_block_devices.rst', 'query_device_capacity_warning.rst' ] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. if _RTD_THEME: html_theme = "sphinx_rtd_theme" else: html_theme = 'default' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. html_last_updated_fmt = '%Y-%m-%d - %H:%M:%S %Z' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. html_domain_indices = False # If false, no index is generated. html_use_index = False # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. html_show_sphinx = False # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'bitmathdoc' # -- Options for LaTeX output -------------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'bitmath.tex', u'bitmath Documentation', u'Tim Case', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'bitmath', u'bitmath Documentation', [u'Tim Case'], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'bitmath', u'bitmath Documentation', u'Tim Case', 'bitmath', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' # -- Options for linkcheck builder --------------------------------------------- # Run `make linkcheck` to scan every external URL in the docsite plus NEWS.rst # (included via `.. include::`) and report broken links to # docsite/build/linkcheck/output.txt. linkcheck_workers = 8 linkcheck_timeout = 15 linkcheck_retries = 2 linkcheck_anchors = True # Don't hang the entire run waiting on a rate-limited host (GitHub, gnu.org). # Default is 60+ seconds (honors Retry-After). Bail after 10s instead. linkcheck_rate_limit_timeout = 10.0 linkcheck_ignore = [ # The OSSF Scorecard badge endpoint serves an SVG and returns 405 to HEAD, # 302 to GET. The badge renders fine in the wild; skip the probe. r'^https://api\.securityscorecards\.dev/.*', # Wayback Machine snapshots are intentionally pinned to a specific archive # date. They occasionally return 5xx during indexing; treat as out-of-band. r'^https://web\.archive\.org/web/\d+/.*', ] timlnx-bitmath-8354f39/docsite/source/contact.rst000066400000000000000000000017041520504626100220640ustar00rootroot00000000000000.. _contact: Contact ####### Hi, I'm Tim Case, the bitmath maintainer. Would you like to get in touch? Maybe you want to peek at other stuff I'm working on? Go right ahead: **Blog** I maintain a blog which mostly covers technical problems I encounter that I think are interesting and how I solved them. Lots of rich commentary and details included! * `Technitribe `_ **Open Source** Almost every project I work on, code or not, ends up on GitHub eventually. You can see what else I've been busy with on my profile. * `GitHub: timlnx `_ **Bugs/Issues/Requests** All contributions related directly to the bitmath project, i.e. bug reports or feature requests, should be posted to the project issue tracker on GitHub. Please see the Contributing section for more information. * :ref:`Contributing ` **E-Mail** You can reach me directly at `bitmath@lnx.cx `_. timlnx-bitmath-8354f39/docsite/source/contributing.rst000066400000000000000000000225211520504626100231400ustar00rootroot00000000000000.. _contributing: Contributing to bitmath ####################### This section describes the guidelines for contributing to bitmath. .. contents:: :depth: 3 :local: .. _contributing_code_of_conduct: Code of Conduct *************** All persons submitting code or otherwise interacting with the bitmath project on GitHub must accept and abide by the terms of the `Code of Conduct `_. .. _contributing_issue_reporting: Issue Reporting *************** If you encounter an issue with the bitmath library, please use the provided template. * `Open a new issue `_ * `View open issues `_ .. _contributing_architecture: Project Architecture ******************** If you are about to make a non-trivial change, read the `ARCHITECTURE.md `_ document in the repository root first. It is the map of how bitmath is put together: the runtime library, the build and release pipeline, the CI gates, and the trust boundaries. The contract is that ``ARCHITECTURE.md`` is reviewed at every minor release and updated alongside any change to a public API, the build pipeline, or a trust boundary. If you are touching one of those things, your pull request should include the corresponding update. .. _contributing_code_style: Code Style and Formatting ************************* Two static analysis tools run on every pull request as part of the GitHub Actions CI workflow, and locally via ``make ci``: * ``pycodestyle`` — checks PEP 8 code style, with **E501** (line too long) ignored. Runs across all matrix cells. * ``pylint`` — full static analysis: naming conventions, refactoring hints, design metrics, and correctness checks. The project targets 10.00/10. Runs on Ubuntu / Python 3.12 only (linting is not platform-sensitive). A PR cannot be merged until both pass. If you want to save time you can run ``make ci`` locally to check before submitting and waiting on the GitHub runners to report back. .. _contributing_commit_messages: Commit Messages *************** Please write `intelligent commit messages `_. For example:: Short summary (50 chars or less) More detailed explanatory text, if necessary. Wrap it to about 72 characters or so. Write your commit message in the imperative: "Fix bug" and not "Fixed bug" or "Fixes bug." - Bullet points are okay, too .. _contributing_pull_requests: Pull Requests ************* When you open a pull request, GitHub Actions automatically runs the full test suite across all supported Python versions. The repository is configured to block merges until all checks pass — you don't need to trigger anything manually. If a check fails, GitHub will report the failure directly on the pull request. Review the output, push a fix, and the checks will re-run automatically. The bitmath project **welcomes all contributors**. If you're unable to fix a failing check yourself, leave a comment on the pull request explaining the situation and we'll help. .. _contributing_python_versions: Supported Python Versions ************************* bitmath supports Python versions shipping with the current and previous major RHEL release available via `EPEL `_. This means the minimum supported version tracks the oldest Python still included in a supported EPEL target: * **RHEL 10 / EPEL 10** — Python 3.12 * **RHEL 9 / EPEL 9** — Python 3.9, 3.10, 3.11 The CI matrix tests all versions in this range. When a RHEL major release reaches end-of-life and is dropped from EPEL, the corresponding Python versions may be dropped from the support matrix in the next bitmath release. If anybody wants to take over Debian/Ubuntu patching, we can add notes for which distributions are covered. .. _contributing_testing_policy: Testing Policy ************** The bitmath testing policy is short: **every major change to the library MUST be accompanied by tests in the same pull request.** A "major change" for the purposes of this policy means any of: * a new public function, method, class, or property * a change to the behavior of an existing public API * a bug fix whose root cause is reproducible by a test (which is almost all of them) * a change to the internal arithmetic, parsing, or normalization logic A "non-major change" that does not require new tests: * documentation changes * comment-only changes * CI workflow tweaks that do not affect the build matrix * changes to packaging metadata that do not change runtime behavior All bitmath code includes unit tests to verify expected functionality. New tests should: * live in ``tests/`` as a ``test_*.py`` file * have a unique test case name across the whole suite (``tests/test_unique_testcase_names.sh`` enforces this) * be written using `unittest `_ (the existing style) unless there is a specific reason to reach for ``pytest``-native features If you are unsure how to write a test for a particular change, open the pull request anyway and ask in the description. I will help you scope an appropriate test. .. _contributing_components: Components ========== The bitmath test suite depends on the following tools: * `GitHub Actions `_ — Runs the full test suite automatically on every pull request across all supported Python versions. * `unittest `_ — Python's standard unit testing framework. All bitmath tests are written using this framework. * `pytest `_ — Test runner used to execute the unittest-based test suite, collect results, and report coverage. * `pytest-cov `_ — Coverage plugin for pytest. The project aims for high coverage; reasonable exceptions can always be discussed in the pull request. * `pycodestyle `_ — Checks Python code style (PEP 8). * `pylint `_ — Full static analysis: naming, refactoring hints, design metrics, and correctness. The project targets a score of 10.00/10. * `virtualenv `_ — Creates an isolated Python environment. The ``make ci`` target manages this automatically. * `Makefile `_ — Orchestrates all build and test tasks. See :ref:`contributing_makefile_targets` below. .. _contributing_makefile_targets: Makefile Targets ================ All development tasks are driven through ``make``. The targets most relevant to contributors are: .. note:: These targets are how you test your changes locally and clean up afterwards before opening a pull request. ``make ci`` The primary target. Creates a Python virtualenv, installs all dependencies from ``requirements.txt``, runs the unique test name check, executes the full pytest suite with coverage, and runs ``pycodestyle`` and ``pylint``. Run this before opening a pull request. **This is the same check GitHub Actions runs.** ``make clean`` Removes the virtualenv, compiled ``*.pyc`` files, ``__pycache__`` directories, and build artifacts. Run ``make clean; make ci`` for a guaranteed fresh test run. ``make docs`` Builds the HTML documentation locally using Sphinx. Output is written to ``docsite/build/html/``. Run ``make viewdocs`` to open the result automatically in your default browser, or open ``docsite/build/html/index.html`` directly. .. _contributing_running_tests: Running the Tests ================= The simplest way to run the full test suite locally is: .. code-block:: console $ make ci For a guaranteed clean run (recommended before opening a PR): .. code-block:: console $ make clean; make ci The output will look something like this (dependency installation output omitted for brevity): .. code-block:: console ############################################# # Running Unique TestCase checker ############################################# ./tests/test_unique_testcase_names.sh ############################################# # Creating a virtualenv ############################################# ... (dependency installation) ... ############################################# # Running Unit Tests ############################################# ============================= test session starts ============================== tests/test_arithmetic.py::TestArithmetic::test_add_bitmath_to_bitmath PASSED [ 0%] tests/test_arithmetic.py::TestArithmetic::test_sub_bitmath_from_bitmath PASSED [ 0%] ... (hundreds more) ... ================================ tests coverage ================================ Name Stmts Miss Cover Missing ------------------------------------- TOTAL 623 0 100% ======================== NNN passed in Xs ======================== The exact test count grows as new tests are added. The definitive pass/fail verdict comes from the GitHub Actions workflow on your pull request, which runs the suite across all supported Python versions. A clean local ``make ci`` is a strong signal, but the PR checks are the final authority. timlnx-bitmath-8354f39/docsite/source/copyright.rst000066400000000000000000000021341520504626100224370ustar00rootroot00000000000000Copyright ######### The MIT License (MIT) Copyright © 2014-2026 Tim Case 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, sublicense, 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. timlnx-bitmath-8354f39/docsite/source/example_block_devices.rst000066400000000000000000000002651520504626100247410ustar00rootroot00000000000000Examples of supported devices include: * Standard Hard Drives/External Drives * Filesystem Partitions * Loop Devices * LVM Logical Volumes * Encrypted LUKS Volumes * iSCSI Devices timlnx-bitmath-8354f39/docsite/source/index.rst000066400000000000000000000330551520504626100215440ustar00rootroot00000000000000.. image:: https://api.securityscorecards.dev/projects/github.com/timlnx/bitmath/badge :target: https://securityscorecards.dev/viewer/?uri=github.com/timlnx/bitmath :alt: OSSF Scorecard .. image:: https://www.bestpractices.dev/projects/12749/badge :target: https://www.bestpractices.dev/en/projects/12749 :alt: OpenSSF Best Practices .. image:: https://www.bestpractices.dev/projects/12749/baseline :target: https://www.bestpractices.dev/projects/12749 :alt: OpenSSF Baseline .. 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://app.readthedocs.org/projects/bitmath/badge/?version=latest :target: https://bitmath.readthedocs.io/ .. 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/github/stars/timlnx/bitmath?style=flat-square :target: https://github.com/timlnx/bitmath :alt: GitHub Project Popularity .. 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: * Converting between **SI** and **NIST** prefix units (``kB`` to ``GiB``) * Converting between units of the same type (SI to SI, or NIST to NIST) * Full NIST unit coverage including **ZiB**, **YiB**, **Zib**, and **Yib** * Automatic human-readable prefix selection (like in `hurry.filesize `_) * Basic arithmetic operations (subtracting 42KiB from 50GiB) * Capacity math with floor division, modulo, and ``divmod`` (``GiB(1) // MiB(300)``, ``GiB(1) % MiB(300)``) * Rich comparison operations (``1024 Bytes == 1KiB``) * Bitwise operations (``<<``, ``>>``, ``&``, ``|``, ``^``) * Rounding via :py:func:`math.floor`, :py:func:`math.ceil`, and :py:func:`round` * Reading a device's storage capacity (Linux/macOS support only) * String parsing, including flexible non-strict parsing of ambiguous input * Sorting * Summing iterables via built-in :py:func:`sum` or :py:func:`bitmath.sum` for unit-normalised results * f-string and :py:func:`format` support via the standard Python formatting protocol * `argparse `_ integration as a custom type In addition to the conversion and math operations, `bitmath` provides human readable representations of values which are suitable for use in interactive shells as well as larger scripts and applications. The format produced for these representations is customizable via the functionality included in stdlibs `string.format `_. In discussion we will refer to the NIST units primarily. I.e., instead of "megabyte" we will refer to "mebibyte". The former is ``10^3 = 1,000,000`` bytes, whereas the second is ``2^20 = 1,048,576`` bytes. When you see file sizes or transfer rates in your web browser, most of the time what you're really seeing are the base-2 sizes/rates. **Don't Forget!** The source for bitmath `is available on GitHub `_. And did we mention there are nearly 300 unit tests? `Check them out for yourself `_. * :ref:`Examples ` after the TOC. Installation ############ .. 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 `_. Requires Python 3.9 or newer. No runtime dependencies outside the standard library. **PyPI** (the typical path): .. code-block:: bash pip install bitmath **Fedora and EPEL** .. code-block:: bash sudo dnf install python3-bitmath **From source** .. code-block:: bash git clone https://github.com/timlnx/bitmath.git pip install ./bitmath Contents ######## .. toctree:: :maxdepth: 2 :numbered: module.rst commandline.rst classes.rst instances.rst simple_examples.rst real_life_examples.rst integration_examples.rst contributing.rst appendices.rst NEWS.rst contact.rst copyright.rst .. _index_examples: Examples ======== Arithmetic ---------- .. code-block:: python >>> import bitmath >>> log_size = bitmath.kB(137.4) >>> log_zipped_size = bitmath.Byte(987) >>> print("Compression saved %s space" % (log_size - log_zipped_size)) Compression saved 136.413kB space >>> thumb_drive = bitmath.GiB(12) >>> song_size = bitmath.MiB(5) >>> songs_per_drive = thumb_drive / song_size >>> print(songs_per_drive) 2457.6 Capacity Planning ----------------- Floor division (``//``), modulo (``%``), and ``divmod()`` are handy for chunk-and-remainder capacity math. ``bm1 // bm2`` returns an ``int`` (how many whole chunks fit); ``bm1 % bm2`` returns a ``bitmath`` of the **left-hand operand's type** (the leftover). .. code-block:: python >>> from bitmath import GiB, MiB, TiB >>> disk = GiB(1) >>> chunk = MiB(300) >>> disk // chunk # how many whole 300 MiB chunks fit? 3 >>> disk % chunk # leftover, typed as the LHS (GiB) GiB(0.12109375) >>> divmod(disk, chunk) # both at once (3, GiB(0.12109375)) Re-express the remainder in a human-readable unit with ``best_prefix()`` (or coerce directly with ``to_MiB()``, etc.): .. code-block:: python >>> (GiB(1) % MiB(300)).best_prefix() MiB(124.0) Pair with the ``bitmath.format`` context manager for clean reporting across a block of capacity calculations: .. code-block:: python >>> import bitmath >>> volume = TiB(1) >>> block = GiB(7) >>> with bitmath.format(fmt_str="{value:.2f} {unit}", bestprefix=True): ... whole, leftover = divmod(volume, block) ... print(f"{whole} whole blocks of {block} fit in {volume}") ... print(f"leftover: {leftover}") 146 whole blocks of 7.00 GiB fit in 1.00 TiB leftover: 2.00 GiB The identity ``(a // b) * b + (a % b) == a`` holds, so ``divmod`` round-trips. Convert Units ------------- File size unit conversion: .. code-block:: python >>> from bitmath import * >>> dvd_size = GiB(4.7) >>> print("DVD Size in MiB: %s" % dvd_size.to_MiB()) DVD Size in MiB: 4812.8 MiB Select a human-readable unit ---------------------------- .. code-block:: python >>> small_number = kB(100) >>> ugly_number = small_number.to_TiB() >>> print(ugly_number) 9.09494701773e-08 TiB >>> print(ugly_number.best_prefix()) 97.65625 KiB Rich Comparison --------------- .. code-block:: python >>> cd_size = MiB(700) >>> cd_size > dvd_size False >>> cd_size < dvd_size True >>> MiB(1) == KiB(1024) True >>> MiB(1) <= KiB(1024) True Sorting ------- .. code-block:: python >>> sizes = [KiB(7337.0), KiB(1441.0), KiB(2126.0), KiB(2178.0), KiB(2326.0), KiB(4003.0), KiB(48.0), KiB(1770.0), KiB(7892.0), KiB(4190.0)] >>> print(sorted(sizes)) [KiB(48.0), KiB(1441.0), KiB(1770.0), KiB(2126.0), KiB(2178.0), KiB(2326.0), KiB(4003.0), KiB(4190.0), KiB(7337.0), KiB(7892.0)] Custom Formatting ----------------- * Use of the custom formatting system * All of the available instance properties Example: .. code-block:: python >>> longer_format = """Formatting attributes for %s ...: This instances prefix unit is {unit}, which is a {system} type unit ...: The unit value is {value} ...: This value can be truncated to just 1 digit of precision: {value:.1f} ...: In binary this looks like: {binary} ...: The prefix unit is derived from a base of {base} ...: Which is raised to the power {power} ...: There are {bytes} bytes in this instance ...: The instance is {bits} bits large ...: bytes/bits without trailing decimals: {bytes:.0f}/{bits:.0f}""" % str(ugly_number) >>> print(ugly_number.format(longer_format)) Formatting attributes for 5.96046447754 MiB This instances prefix unit is MiB, which is a NIST type unit The unit value is 5.96046447754 This value can be truncated to just 1 digit of precision: 6.0 In binary this looks like: 0b10111110101111000010000000 The prefix unit is derived from a base of 2 Which is raised to the power 20 There are 6250000.0 bytes in this instance The instance is 50000000.0 bits large bytes/bits without trailing decimals: 6250000/50000000 Utility Functions ----------------- **bitmath.getsize()** .. code-block:: python >>> print(bitmath.getsize('python-bitmath.spec')) 3.7060546875 KiB **bitmath.parse_string()** Parse a string with standard units: .. code-block:: python >>> import bitmath >>> a_dvd = bitmath.parse_string("4.7 GiB") >>> print(type(a_dvd)) >>> print(a_dvd) 4.7 GiB **bitmath.parse_string_unsafe()** Parse a string with ambiguous units: .. code-block:: python >>> import bitmath >>> a_gig = bitmath.parse_string_unsafe("1gb") >>> print(type(a_gig)) >>> a_gig == bitmath.GB(1) True >>> bitmath.parse_string_unsafe('1gb') == bitmath.parse_string_unsafe('1g') True **bitmath.query_device_capacity()** .. code-block:: python >>> import bitmath >>> with open('/dev/sda') as fp: ... root_disk = bitmath.query_device_capacity(fp) ... print(root_disk.best_prefix()) ... 238.474937439 GiB **bitmath.listdir()** .. code-block:: python >>> for i in bitmath.listdir('./tests/', followlinks=True, relpath=True, bestprefix=True): ... print(i) ... ('tests/test_file_size.py', KiB(9.2900390625)) ('tests/test_basic_math.py', KiB(7.1767578125)) ('tests/__init__.py', KiB(1.974609375)) ('tests/test_bitwise_operations.py', KiB(2.6376953125)) ('tests/test_context_manager.py', KiB(3.7744140625)) ('tests/test_representation.py', KiB(5.2568359375)) ('tests/test_properties.py', KiB(2.03125)) ('tests/test_instantiating.py', KiB(3.4580078125)) ('tests/test_future_math.py', KiB(2.2001953125)) ('tests/test_best_prefix_BASE.py', KiB(2.1044921875)) ('tests/test_rich_comparison.py', KiB(3.9423828125)) ('tests/test_best_prefix_NIST.py', KiB(5.431640625)) ('tests/test_unique_testcase_names.sh', Byte(311.0)) ('tests/.coverage', KiB(3.1708984375)) ('tests/test_best_prefix_SI.py', KiB(5.34375)) ('tests/test_to_built_in_conversion.py', KiB(1.798828125)) ('tests/test_to_Type_conversion.py', KiB(8.0185546875)) ('tests/test_sorting.py', KiB(4.2197265625)) ('tests/listdir_symlinks/10_byte_file_link', Byte(10.0)) ('tests/listdir_symlinks/depth1/depth2/10_byte_file', Byte(10.0)) ('tests/listdir_nosymlinks/depth1/depth2/10_byte_file', Byte(10.0)) ('tests/listdir_nosymlinks/depth1/depth2/1024_byte_file', KiB(1.0)) ('tests/file_sizes/kbytes.test', KiB(1.0)) ('tests/file_sizes/bytes.test', Byte(38.0)) ('tests/listdir/10_byte_file', Byte(10.0)) Formatting ---------- .. code-block:: python >>> with bitmath.format(fmt_str="[{value:.3f}@{unit}]"): ... for i in bitmath.listdir('./tests/', followlinks=True, relpath=True, bestprefix=True): ... print(i[1]) ... [9.290@KiB] [7.177@KiB] [1.975@KiB] [2.638@KiB] [3.774@KiB] [5.257@KiB] [2.031@KiB] [3.458@KiB] [2.200@KiB] [2.104@KiB] [3.942@KiB] [5.432@KiB] [311.000@Byte] [3.171@KiB] [5.344@KiB] [1.799@KiB] [8.019@KiB] [4.220@KiB] [10.000@Byte] [10.000@Byte] [10.000@Byte] [1.000@KiB] [1.000@KiB] [38.000@Byte] [10.000@Byte] ``argparse`` Integration ------------------------ A self-contained example showing how to use bitmath as an argparse argument type is available in the `Integration Examples `_ chapter of the documentation. .. code-block:: python import argparse import bitmath def BitmathType(value): try: return bitmath.parse_string(value) except ValueError: raise argparse.ArgumentTypeError( f"{value!r} is not a recognized bitmath unit string" ) parser = argparse.ArgumentParser() parser.add_argument('--block-size', type=BitmathType, required=True) args = parser.parse_args(['--block-size', '10MiB']) print(args.block_size) # 10.0 MiB timlnx-bitmath-8354f39/docsite/source/index.rst.in000066400000000000000000000130401520504626100221410ustar00rootroot00000000000000.. image:: https://api.securityscorecards.dev/projects/github.com/timlnx/bitmath/badge :target: https://securityscorecards.dev/viewer/?uri=github.com/timlnx/bitmath :alt: OSSF Scorecard .. image:: https://www.bestpractices.dev/projects/12749/badge :target: https://www.bestpractices.dev/en/projects/12749 :alt: OpenSSF Best Practices .. image:: https://www.bestpractices.dev/projects/12749/baseline :target: https://www.bestpractices.dev/projects/12749 :alt: OpenSSF Baseline .. 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://app.readthedocs.org/projects/bitmath/badge/?version=latest :target: https://bitmath.readthedocs.io/ .. 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/github/stars/timlnx/bitmath?style=flat-square :target: https://github.com/timlnx/bitmath :alt: GitHub Project Popularity .. 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: * Converting between **SI** and **NIST** prefix units (``kB`` to ``GiB``) * Converting between units of the same type (SI to SI, or NIST to NIST) * Full NIST unit coverage including **ZiB**, **YiB**, **Zib**, and **Yib** * Automatic human-readable prefix selection (like in `hurry.filesize `_) * Basic arithmetic operations (subtracting 42KiB from 50GiB) * Capacity math with floor division, modulo, and ``divmod`` (``GiB(1) // MiB(300)``, ``GiB(1) % MiB(300)``) * Rich comparison operations (``1024 Bytes == 1KiB``) * Bitwise operations (``<<``, ``>>``, ``&``, ``|``, ``^``) * Rounding via :py:func:`math.floor`, :py:func:`math.ceil`, and :py:func:`round` * Reading a device's storage capacity (Linux/macOS support only) * String parsing, including flexible non-strict parsing of ambiguous input * Sorting * Summing iterables via built-in :py:func:`sum` or :py:func:`bitmath.sum` for unit-normalised results * f-string and :py:func:`format` support via the standard Python formatting protocol * `argparse `_ integration as a custom type In addition to the conversion and math operations, `bitmath` provides human readable representations of values which are suitable for use in interactive shells as well as larger scripts and applications. The format produced for these representations is customizable via the functionality included in stdlibs `string.format `_. In discussion we will refer to the NIST units primarily. I.e., instead of "megabyte" we will refer to "mebibyte". The former is ``10^3 = 1,000,000`` bytes, whereas the second is ``2^20 = 1,048,576`` bytes. When you see file sizes or transfer rates in your web browser, most of the time what you're really seeing are the base-2 sizes/rates. **Don't Forget!** The source for bitmath `is available on GitHub `_. And did we mention there are nearly 300 unit tests? `Check them out for yourself `_. * :ref:`Examples ` after the TOC. Installation ############ .. 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 `_. Requires Python 3.9 or newer. No runtime dependencies outside the standard library. **PyPI** (the typical path): .. code-block:: bash pip install bitmath **Fedora and EPEL** .. code-block:: bash sudo dnf install python3-bitmath **From source** .. code-block:: bash git clone https://github.com/timlnx/bitmath.git pip install ./bitmath Contents ######## .. toctree:: :maxdepth: 2 :numbered: module.rst commandline.rst classes.rst instances.rst simple_examples.rst real_life_examples.rst integration_examples.rst contributing.rst appendices.rst NEWS.rst contact.rst copyright.rst .. _index_examples: timlnx-bitmath-8354f39/docsite/source/instances.rst000066400000000000000000000366441520504626100224330ustar00rootroot00000000000000Instances ######### .. _instance_attributes: .. contents:: :depth: 3 :local: .. _instances_attributes: Instance Attributes ******************* bitmath objects have several instance attributes: .. py:attribute:: BitMathInstance.base The mathematical base of the unit of the instance (this will be **2** or **10**) .. code-block:: python >>> print(bitmath.Byte(1337).base) 2 >>> print(bitmath.kB(1337).base) 10 .. py:attribute:: BitMathInstance.binary The `Python binary representation `_ of the instance's value (in bits) .. code-block:: python >>> b = bitmath.Byte(1337) >>> print(b.binary) 0b10100111001000 .. py:attribute:: BitMathInstance.bin This is an alias for ``binary`` .. py:attribute:: BitMathInstance.bits The number of bits in the object, as a floating-point value. .. code-block:: python >>> b = bitmath.Byte(1337) >>> print(b.bits) 10696.0 .. note:: Bit values are always floating-point. A whole-number input like ``Byte(1337)`` produces an exact float (``10696.0``), but inputs involving division or fractional bytes will produce fractional bit counts. Use ``int(instance.bits)`` or :py:func:`math.floor` to obtain an integer when needed. See :ref:`appendix_math` for the design rationale behind floating-point values. .. py:attribute:: BitMathInstance.bytes The number of bytes in the object, as a floating-point value. .. code-block:: python >>> b = bitmath.Byte(1337) >>> print(b.bytes) 1337.0 .. py:attribute:: BitMathInstance.power The mathematical power the ``base`` of the unit of the instance is raised to .. code-block:: python >>> b = bitmath.Byte(1337) >>> print(b.power) 0 .. py:attribute:: BitMathInstance.system The system of units used to measure this instance (``NIST`` or ``SI``) .. code-block:: python >>> b = bitmath.Byte(1337) >>> print(b.system) NIST .. py:attribute:: BitMathInstance.value The value of the instance in *prefix* units\ :sup:`1` .. code-block:: python >>> b = bitmath.Byte(1337) >>> print(b.value) 1337.0 .. py:attribute:: BitMathInstance.unit The string representation of this prefix unit (such as ``MiB`` or ``kb``) .. code-block:: python >>> b = bitmath.Byte(1337) >>> print(b.unit) B .. py:attribute:: BitMathInstance.unit_plural The pluralized string representation of this prefix unit. .. code-block:: python >>> b = bitmath.Byte(1337) >>> print(b.unit_plural) B .. py:attribute:: BitMathInstance.unit_singular The singular string representation of this prefix unit (such as ``MiB`` or ``kb``) .. code-block:: python >>> b = bitmath.Byte(1337) >>> print(b.unit_singular) B **Notes:** 1. Given an instance ``k``, where ``k = KiB(1.3)``, then ``k.value`` is **1.3** ---- The following is an example of how to access some of these attributes and what you can expect their printed representation to look like: .. code-block:: python :linenos: >>> dvd_capacity = GB(4.7) >>> print("Capacity in bits: %s\nbytes: %s\n" % \ (dvd_capacity.bits, dvd_capacity.bytes)) Capacity in bits: 37600000000.0 bytes: 4700000000.0 >>> dvd_capacity.value 4.7 >>> dvd_capacity.bin '0b100011000001001000100111100000000000' >>> dvd_capacity.binary '0b100011000001001000100111100000000000' Instance Methods **************** bitmath objects come with a few basic methods: :py:meth:`to_THING`, :py:meth:`format`, and :py:meth:`best_prefix`. .. _instances_to_thing: to_THING() ========== 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: .. code-block:: python :linenos: :emphasize-lines: 3,7,12 >>> from bitmath import * >>> one_mib = MiB(1) >>> one_mib_in_kb = one_mib.to_kb() >>> one_mib == one_mib_in_kb True >>> another_mib = one_mib.to_MiB() >>> print(one_mib, one_mib_in_kb, another_mib) 1.0 MiB 8388.608 kb 1.0 MiB >>> six_TB = TB(6) >>> six_TB_in_bits = six_TB.to_Bit() >>> print(six_TB, six_TB_in_bits) 6.0 TB 4.8e+13 b >>> six_TB == six_TB_in_bits True .. _instances_best_prefix: best_prefix() ============= .. py:method:: best_prefix([system=None]) Return an equivalent instance which uses the best human-readable prefix-unit to represent it. :param int system: one of :py:const:`bitmath.NIST` or :py:const:`bitmath.SI` :return: An equivalent :py:class:`bitmath` instance :rtype: :py:class:`bitmath` :raises ValueError: if an invalid unit system is given for ``system`` The :py:meth:`best_prefix` method returns the result of converting a bitmath instance into an equivalent instance using a prefix unit that better represents the original value. Another way to think of this is automatic discovery of the most sane, or *human readable*, unit to represent a given size. This functionality is especially important in the domain of interactive applications which need to report file sizes or transfer rates to users. As an analog, consider you have 923,874,434¢ in your bank account. You probably wouldn't want to read your bank statement and find your balance in pennies. Most likely, your bank statement would read a balance of $9,238,744.34. In this example, the input prefix is the *cent*: ``¢``. The *best prefix* for this is the *dollar*: ``$``. Let's, for example, say we are reporting a transfer rate in an interactive application. It's important to present this information in an easily consumable format. The library we're using to calculate the rate of transfer reports the rate in bytes per second from a :py:func:`tx_rate` function. We'll use this example twice. In the first occurrence, we will print out the transfer rate in a more easily digestible format than pure bytes/second. In the second occurrence we'll take it a step further, and use the :ref:`format ` method to make the output even easier to read. .. code-block:: python >>> for _rate in tx_rate(): ... print("Rate: %s/second" % Bit(_rate)) ... time.sleep(1) Rate: 100.0 b/sec Rate: 24000.0 b/sec Rate: 1024.0 b/sec Rate: 60151.0 b/sec Rate: 33.0 b/sec Rate: 9999.0 b/sec Rate: 9238742.0 b/sec Rate: 2.09895849555e+13 b/sec Rate: 934098021.0 b/sec Rate: 934894.0 b/sec And now using a custom formatting definition: .. code-block:: python >>> for _rate in tx_rate(): ... print(Bit(_rate).best_prefix().format("Rate: {value:.3f} {unit}/sec")) ... time.sleep(1) Rate: 12.500 B/sec Rate: 2.930 KiB/sec Rate: 128.000 B/sec Rate: 7.343 KiB/sec Rate: 4.125 B/sec Rate: 1.221 KiB/sec Rate: 1.101 MiB/sec Rate: 2.386 TiB/sec Rate: 111.353 MiB/sec Rate: 114.123 KiB/sec .. _instances_format: format() ======== .. py:method:: BitMathInstance.format(fmt_spec) Return a custom-formatted string to represent this instance. :param str fmt_spec: A valid formatting mini-language string :return: The custom formatted representation :rtype: ``string`` bitmath instances come with a verbose built-in string representation: .. code-block:: python >>> leet_bits = Bit(1337) >>> print(leet_bits) 1337.0 b However, for instances which aren't whole numbers (as in ``MiB(1/3.0) == 0.333333333333 MiB``, etc), their representation can be undesirable. The :py:meth:`format` method gives you complete control over the instance's representation. All of the :ref:`instances attributes ` are available to use when choosing a representation. The following sections describe some common use cases of the :py:meth:`format` method as well as provide a :ref:`brief tutorial ` of the greater Python formatting meta-language. Setting Decimal Precision ------------------------- By default, bitmath instances will print to a fairly long precision for values which are not whole multiples of their prefix unit. In most use cases, simply printing out the first 2 or 3 digits of precision is acceptable. The following examples will show us how to print out a bitmath instance in a more human readable way, by limiting the decimal precision to 2 digits. First, for reference, the default formatting: .. code-block:: python >>> ugly_number = MB(50).to_MiB() / 8.0 >>> print(ugly_number) 5.96046447754 MiB Now, let's use the :py:meth:`format` method to limit that to two digits of precision: .. code-block:: python >>> print(ugly_number.format("{value:.2f}{unit}")) 5.96 MiB By changing the **2** character, you increase or decrease the precision. Set it to **0** (``{value:.0f}``) and you have what effectively looks like an integer. Format All the Instance Attributes ---------------------------------- The following example prints out every instance attribute. Take note of how an attribute may be referenced multiple times. .. code-block:: python :linenos: :emphasize-lines: 4,16 >>> longer_format = """Formatting attributes for %s ...: This instances prefix unit is {unit}, which is a {system} type unit ...: The unit value is {value} ...: This value can be truncated to just 1 digit of precision: {value:.1f} ...: In binary this looks like: {binary} ...: The prefix unit is derived from a base of {base} ...: Which is raised to the power {power} ...: There are {bytes} bytes in this instance ...: The instance is {bits} bits large ...: bytes/bits without trailing decimals: {bytes:.0f}/{bits:.0f}""" % str(ugly_number) >>> print(ugly_number.format(longer_format)) Formatting attributes for 5.96046447754 MiB This instances prefix unit is MiB, which is a NIST type unit The unit value is 5.96046447754 This value can be truncated to just 1 digit of precision: 6.0 In binary this looks like: 0b10111110101111000010000000 The prefix unit is derived from a base of 2 Which is raised to the power 20 There are 6250000.0 bytes in this instance The instance is 50000000.0 bits large bytes/bits without trailing decimals: 6250000/50000000 .. note:: On line **4** we print with 1 digit of precision, on line **16** we see the value has been rounded to **6.0** .. _instances_dunder_format: Python Format Protocol (f-strings and format()) ================================================ .. py:method:: BitMathInstance.__format__(fmt_spec) Support Python's standard string formatting protocol (:pep:`3101`), enabling bitmath instances to be used directly in f-strings and :py:func:`format` calls. When *fmt_spec* is **empty**, returns ``str(self)`` — the same as the default string representation: .. code-block:: python >>> size = bitmath.MiB(2.847598437) >>> f'{size}' '2.847598437 MiB' When *fmt_spec* is a **numeric format spec**, it is applied to ``self.value`` only, returning the formatted number without a unit suffix. The caller controls the surrounding string: .. code-block:: python >>> size = bitmath.MiB(2.847598437) >>> f'{size:.1f} {size.unit}' '2.8 MiB' This makes it straightforward to build columnar output with consistent alignment across mixed unit types: .. code-block:: python >>> disk_usage = [ ... ("home", bitmath.GiB(127.3)), ... ("tmp", bitmath.MiB(843.7)), ... ("var", bitmath.GiB(2.1)), ... ] >>> for mount, size in disk_usage: ... print(f"{mount:<8} {size:>10.2f} {size.unit}") home 127.30 GiB tmp 843.70 MiB var 2.10 GiB Any standard Python numeric format spec works: ``:.2f``, ``:.3e``, ``:.0f``, ``>10.2f``, and so on. .. note:: The format spec applies to ``self.value`` — the numeric quantity in the instance's current prefix unit. To render a different unit, convert first: ``size.to_GiB()``, then format. .. versionadded:: 2.0.0 .. rubric:: Credit The original concept and implementation for this feature was contributed by `Jonathan Eunice `_ in `pull request #76 `_. .. _instances_rounding: Rounding and Integer Conversion ================================ bitmath instances support Python's standard rounding protocol. :py:func:`math.floor`, :py:func:`math.ceil`, and the built-in :py:func:`round` all return a new bitmath instance of the **same type** with the prefix value rounded accordingly. .. code-block:: python >>> import math, bitmath >>> math.floor(bitmath.MiB(1.75)) MiB(1) >>> math.ceil(bitmath.MiB(1.25)) MiB(2) >>> round(bitmath.GiB(3.7)) GiB(4) >>> round(bitmath.KiB(1.555), 2) KiB(1.56) These methods round the *prefix value*. To obtain the nearest whole **byte** count, convert first: .. code-block:: python >>> int(bitmath.KiB(1/3).bytes) 341 To obtain the nearest whole **bit** count: .. code-block:: python >>> int(bitmath.KiB(1/3).bits) 2730 .. warning:: Rounding intermediate results is lossy. ``math.floor(GiB(10) / 3) * 3`` yields ``GiB(9)``, not ``GiB(10)``. Only round at the **final** output step, not during calculation. .. seealso:: :ref:`appendix_math` — design rationale for floating-point values and guidance on when rounding is appropriate. .. _instances_properties: Instance Properties ******************* THING Properties ================ 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``. .. code-block:: python :linenos: :emphasize-lines: 3,6,10 >>> from bitmath import * >>> one_mib = MiB(1) >>> one_mib == one_mib.kb True >>> print(one_mib, one_mib.kb, one_mib.MiB) 1.0 MiB 8388.608 kb 1.0 MiB >>> six_TB = TB(6) >>> print(six_TB, six_TB.Bit) 6.0 TB 4.8e+13 b >>> six_TB == six_TB.Bit True .. _instances_mini_language: The Formatting Mini-Language **************************** That is all you begin printing numbers with custom precision. If you want to learn a little bit more about using the formatting mini-language, read on. You may be asking yourself where these ``{value:.2f}`` and ``{unit}`` strings came from. These are part of the `Format Specification Mini-Language `_ which is part of the Python standard library. To be explicitly clear about what's going on here, let's break the first specifier (``{value:.2f}``) down into it's component parts:: {value:.2f} | ||| | |||\---- The "f" says to format this as a floating point type | ||\----- The 2 indicates we want 2 digits of precision (default is 6) | |\------ The '.' character must precede the precision specifier for floats | \------- The : separates the attribute name from the formatting specification \---------- The name of the attribute to print The second specifier (``{unit}``) says to format the ``unit`` attribute as a string (string is the default type when no type is given). .. seealso:: `Python String Format Cookbook `_ `Marcus Kazmierczak’s `_ *excellent* introduction to string formatting timlnx-bitmath-8354f39/docsite/source/integration_examples.rst000066400000000000000000000177261520504626100246650ustar00rootroot00000000000000.. _integration_examples: Integration Examples #################### The following are self-contained, copy-paste examples showing how to use :mod:`bitmath` with popular third-party libraries. These libraries are **not** installed by bitmath — install them separately before use. .. contents:: :local: :depth: 1 .. _integration_examples_argparse: argparse ******** The :mod:`argparse` module (part of the Python standard library) accepts command-line arguments as strings by default. The ``type`` parameter of :py:meth:`~argparse.ArgumentParser.add_argument` lets you supply a callable that converts a raw string into whatever type your application needs. The snippet below defines a ``BitmathType`` callable and registers it as the type for a ``--block-size`` option so that users can write values like ``--block-size 10MiB`` and receive a :class:`bitmath.MiB` object directly. .. code-block:: python import argparse import bitmath def BitmathType(value): """Convert a command-line string such as '10MiB' into a bitmath object.""" try: return bitmath.parse_string(value) except ValueError: raise argparse.ArgumentTypeError( f"{value!r} is not a recognized bitmath unit string " "(examples: 10MiB, 1.5GiB, 500kB)" ) def main(): parser = argparse.ArgumentParser( description="Example script using a bitmath argument type" ) parser.add_argument( "--block-size", type=BitmathType, required=True, help="Block size with unit, e.g. 10MiB", ) args = parser.parse_args() print(f"Block size: {args.block_size}") print(f"In KiB: {args.block_size.to_KiB():.2f}") if __name__ == "__main__": main() Example run: .. code-block:: bash $ python script.py --block-size 10MiB Block size: 10.0 MiB In KiB: 10240.00 KiB $ python script.py --block-size bad error: argument --block-size: 'bad' is not a recognized bitmath unit string (examples: 10MiB, 1.5GiB, 500kB) argparse validation =================== Now say you want to perform some additional validation on this custom ``BitmathType`` unit. This is best done **after** the parsing is complete, not as part of the ``BitmathType`` implementation. This follows the advice outlined in the upstream :mod:`argparse` documentation. In general, the ``type`` keyword is a convenience that should only be used for simple conversions that can only raise one of the three supported exceptions. Anything with more interesting error-handling or resource management should be done downstream after the arguments are parsed. First, parse the unit, allow ``BitmathType`` to handle the parsing validation. Second, perform your own context-aware validation. For example, you might set minimum or maximums and need to compare the parsed argument against them. .. code-block:: python :linenos: :emphasize-lines: 17,29-30 import argparse import bitmath def BitmathType(value): """Convert a command-line string such as '10MiB' into a bitmath object.""" try: return bitmath.parse_string(value) except ValueError: raise argparse.ArgumentTypeError( f"{value!r} is not a recognized bitmath unit string " "(examples: 10MiB, 1.5GiB, 500kB)" ) def main(): max_block_size = bitmath.GiB(1) parser = argparse.ArgumentParser( description="Example script using a bitmath argument type" ) parser.add_argument( "--block-size", type=BitmathType, required=True, help="Block size with unit, e.g. 10MiB", ) args = parser.parse_args() if args.block_size > bitmath.GiB(1): raise ValueError(f"Provided block size {args.block_size} exceeds maximum {max_block_size}") print(f"Block size: {args.block_size}") print(f"In KiB: {args.block_size.to_KiB():.2f}") if __name__ == "__main__": main() Example run: .. code-block:: bash $ python script.py --block-size 42GiB Traceback (most recent call last): File "script.py", line 37, in main() ~~~~^^ File "script.py", line 30, in main raise ValueError(f"Provided block size {args.block_size} exceeds maximum {max_block_size}") ValueError: Provided block size 42.0 GiB exceeds maximum 1.0 GiB .. _integration_examples_click: click ***** `click `_ is a popular command-line interface toolkit. Custom parameter types are implemented by subclassing :class:`click.ParamType` and overriding :py:meth:`~click.ParamType.convert`. Install click before use: .. code-block:: bash pip install click .. code-block:: python import click import bitmath class BitmathParamType(click.ParamType): """A click parameter type that accepts bitmath unit strings.""" name = "SIZE" def convert(self, value, param, ctx): if isinstance(value, bitmath.Bitmath): return value try: return bitmath.parse_string(value) except ValueError: self.fail( f"{value!r} is not a recognized bitmath unit string " "(examples: 10MiB, 1.5GiB, 500kB)", param, ctx, ) BITMATH = BitmathParamType() @click.command() @click.option( "--block-size", type=BITMATH, required=True, help="Block size with unit, e.g. 10MiB", ) def main(block_size): """Example command using a bitmath click parameter type.""" click.echo(f"Block size: {block_size}") click.echo(f"In KiB: {block_size.to_KiB():.2f}") if __name__ == "__main__": main() Example run: .. code-block:: bash $ python script.py --block-size 10MiB Block size: 10.0 MiB In KiB: 10240.00 KiB $ python script.py --block-size bad Error: Invalid value for '--block-size': 'bad' is not a recognized bitmath unit string (examples: 10MiB, 1.5GiB, 500kB) .. _integration_examples_progressbar2: progressbar2 ************ `progressbar2 `_ is a flexible terminal progress-bar library. The example below defines a custom widget that displays a data-transfer speed (bytes per second) in a human-readable bitmath unit, and demonstrates it with a simulated file download. Install progressbar2 before use: .. code-block:: bash pip install progressbar2 .. code-block:: python import time import progressbar import bitmath class DataTransferSpeed(progressbar.widgets.FormatWidgetMixin, progressbar.widgets.TimeSensitiveMixin): """Display transfer speed as a human-readable bitmath value per second.""" def __call__(self, progress, data, **kwargs): elapsed = data.get("seconds_elapsed") or 0 if elapsed <= 0 or data.get("value") is None: return "?? B/s" bytes_done = data["value"] speed = bitmath.Byte(bytes_done / elapsed).best_prefix() return f"{speed:.2f}/s" def simulate_download(total_bytes): widgets = [ "Downloading: ", progressbar.Bar(), " ", progressbar.Percentage(), " ", DataTransferSpeed(), " ", progressbar.ETA(), ] with progressbar.ProgressBar( max_value=total_bytes, widgets=widgets ) as bar: received = 0 chunk = total_bytes // 50 while received < total_bytes: time.sleep(0.05) received = min(received + chunk, total_bytes) bar.update(received) if __name__ == "__main__": # Simulate a 100 MiB download simulate_download(int(bitmath.MiB(100).to_Byte())) Example run: .. code-block:: text Downloading: |####################| 100% 18.32 MiB/s ETA: 0:00:00 timlnx-bitmath-8354f39/docsite/source/module.rst000066400000000000000000000777431520504626100217360ustar00rootroot00000000000000.. _module: .. py:module:: bitmath The ``bitmath`` Module ###################### .. contents:: :depth: 3 :local: Functions ********* This section describes utility functions included in the :py:mod:`bitmath` module. .. _bitmath_getsize: bitmath.getsize() ================= .. function:: getsize(path[, bestprefix=True[, system=NIST]]) Return a bitmath instance representing the size of a file at any given path. :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 is a :class:`.Byte` instance. :param system: **Default:** :py:data:`bitmath.NIST`. The preferred system of units for the returned instance. :type system: One of :py:data:`bitmath.NIST` or :py:data:`bitmath.SI` Internally :py:func:`bitmath.getsize` calls :py:func:`os.path.realpath` before calling :py:func:`os.path.getsize` on any paths. Both functions accept :class:`pathlib.Path` objects natively, so no conversion is needed. The traditional approach uses :func:`os.stat`, which gives raw bytes and leaves unit conversion to you: .. code-block:: python >>> 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' :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, pathlib # NIST default - picks the most human-readable prefix automatically >>> print(bitmath.getsize('./bitmath/__init__.py')) 33.3583984375 KiB # 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 # 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 bitmath.listdir() ================= .. function:: listdir(search_base[, followlinks=False[, glob='*'[, relpath=False[, bestprefix=False[, system=NIST]]]]]) This is a `generator `_ which recurses a directory tree yielding 2-tuples of: * The absolute/relative path to a discovered file * A bitmath instance representing the *apparent size* of the file :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`` enables directory link following :param string glob: **Default:** ``*`` (everything). A glob to filter results with. See `fnmatch `_ for more details about *globs* :param bool relpath: **Default:** ``False``, returns the fully qualified to each discovered file. ``True`` to return the relative path from the present working directory to the discovered file. If ``relpath`` is ``False``, then :py:func:`bitmath.listdir` internally calls :py:func:`os.path.realpath` to normalize path references :param bool bestprefix: **Default:** ``False``, returns :class:`.Byte` instances. Set to ``True`` to return the best human-readable prefix unit for representation :param system: **Default:** :py:data:`bitmath.NIST`. Set a prefix preferred unit system. Requires ``bestprefix`` is ``True`` :type system: One of :py:data:`bitmath.NIST` or :py:data:`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. Given this directory tree:: some_files/ ├── deeper_files/ │ └── second_file (13370 bytes) └── first_file (1337 bytes) .. code-block:: python >>> import bitmath >>> # 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 .. 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: bitmath.sum() ============= .. function:: sum(iterable[, start=None]) Sum an iterable of bitmath instances into a single bitmath instance. :param iterable: Any iterable of bitmath objects to sum. :param start: **Default:** ``None`` (accumulates into :class:`bitmath.Byte`). Pass a bitmath instance to set both the starting value and the result type. :type start: A bitmath instance, or ``None`` :returns: A bitmath instance whose type is determined by ``start`` (or :class:`bitmath.Byte` when ``start`` is ``None``). .. note:: Python's built-in :py:func:`sum` also works with bitmath objects. Because ``0 + bm`` returns ``bm`` itself, the built-in accumulates into the type of the **first element** in the iterable. Use :py:func:`bitmath.sum` instead when you need the result normalised to a **specific unit** regardless of the input types. Sum a homogeneous list — result type matches ``start`` (``Byte`` by default): .. code-block:: python >>> import bitmath >>> bitmath.sum([bitmath.MiB(1), bitmath.GiB(1)]) Byte(1074790400.0) Pass ``start`` to choose a different accumulator unit: .. code-block:: python >>> bitmath.sum([bitmath.KiB(1), bitmath.KiB(2)], start=bitmath.MiB(0)) MiB(0.0029296875) Contrast with the built-in :py:func:`sum`, whose result type tracks the first element: .. code-block:: python >>> sum([bitmath.KiB(1), bitmath.KiB(2)]) KiB(3.0) >>> sum([bitmath.Byte(1), bitmath.MiB(1), bitmath.GiB(1)]) Byte(1074790401.0) .. seealso:: :ref:`Summing an Iterable ` in *Getting Started* Side-by-side examples of built-in :py:func:`sum` vs :py:func:`bitmath.sum`. .. versionadded:: 2.0.0 bitmath.parse_string() ====================== .. function:: parse_string(str_repr, system=bitmath.NIST, strict=True) Parse a string (or, when ``strict=False``, a string or number) into a bitmath object. :param str_repr: The value to parse. String inputs may include whitespace between the value and the unit. :param system: Unit system used when ``strict=False`` and the intended unit cannot be reliably determined from the input. Ignored when ``strict=True``. One of :py:data:`bitmath.NIST` (default) or :py:data:`bitmath.SI`. :param strict: When ``True`` (default) the unit must be an exact bitmath type name such as ``"KiB"`` or ``"MB"``. When ``False`` the parser accepts ambiguous input such as plain numbers, numeric strings, and case-insensitive single-letter units. See :ref:`parse-string-non-strict` below. :return: A bitmath object representing the input. :raises ValueError: if the input cannot be parsed. A simple usage example: .. code-block:: python >>> import bitmath >>> a_dvd = bitmath.parse_string("4.7 GiB") >>> print(type(a_dvd)) >>> print(a_dvd) 4.7 GiB .. caution:: Caution is advised when reading values from an unverified external source such as shell command output or a generated file. Many applications (even ``/usr/bin/ls``) do not produce file size strings with valid prefix units unless `specially configured `_. Use ``strict=False`` for those cases — see :ref:`parse-string-non-strict`. To protect your application from unexpected runtime errors, wrap calls in a ``try`` statement: .. code-block:: python >>> import bitmath >>> try: ... a_dvd = bitmath.parse_string("4.7 G") ... except ValueError: ... print("Error while parsing string into bitmath object") ... Error while parsing string into bitmath object Here are some more examples of valid and invalid input: .. code-block:: python >>> import bitmath >>> sizes = [ 1337, 1337.7, "1337", "1337.7", "1337 B", "1337B" ] >>> for size in sizes: ... try: ... print("Parsed size into %s" % bitmath.parse_string(size).best_prefix()) ... except ValueError: ... print("Could not parse input: %s" % size) ... Could not parse input: 1337 Could not parse input: 1337.7 Could not parse input: 1337 Could not parse input: 1337.7 Parsed size into 1.3056640625 KiB Parsed size into 1.3056640625 KiB .. versionchanged:: 1.2.4 Added support for parsing *octet* units via issue `#53 - parse french units `_. The `usage `_ of "octet" is still common in some `RFCs `_, as well as France, French Canada and Romania. See also, a table of the octet units and their values on `Wikipedia `_. Here are some simple examples of parsing *octet* based units: .. code-block:: python :linenos: :emphasize-lines: 4,5 >>> import bitmath >>> a_mebibyte = bitmath.parse_string("1 MiB") >>> a_mebioctet = bitmath.parse_string("1 Mio") >>> print(a_mebibyte, a_mebioctet) 1.0 MiB 1.0 MiB >>> print(bitmath.parse_string("1Po")) 1.0 PB >>> print(bitmath.parse_string("1337 Eio")) 1337.0 EiB Notice how on lines **4** and **5** the variable ``a_mebibyte`` from the input ``"1 MiB"`` is exactly equivalent to ``a_mebioctet`` from ``"1 Mio"``. After parsing, octet units are normalised into their standard NIST/SI equivalents automatically. .. versionchanged:: 2.0.0 Added ``strict`` and ``system`` parameters. The default ``strict=True`` behavior is identical to earlier versions. ``system`` defaults to :py:data:`bitmath.NIST` and is only consulted when ``strict=False``. .. versionadded:: 1.1.0 .. _parse-string-non-strict: parse_string with ``strict=False`` ----------------------------------- When ``strict=False`` the parser accepts ambiguous input that does not conform to exact bitmath type names — for example, the single-letter units produced by tools like ``ls -h``, ``df``, and ``qemu-img``. This is the behavior previously provided by the now-deprecated :py:func:`bitmath.parse_string_unsafe`. All inputs are treated as **byte-based**. Bit-based units are not supported in non-strict parsing mode. Capitalisation does not matter. .. _parse-string-system-hint: Understanding the ``system`` parameter in non-strict mode ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. important:: In ``strict=False`` mode, ``system`` is a **tiebreaker**, not a guarantee. It is only consulted when the parser cannot determine the unit system from the input itself. Passing ``system=bitmath.SI`` does **not** force all results to be SI units. The parser resolves the unit system in the following order of precedence: 1. **No unit present** — plain numbers and numeric strings (e.g. ``100``, ``"2048"``) are always returned as :class:`bitmath.Byte` regardless of ``system``. 2. **Unit is self-describing** — inputs whose unit already contains an ``i`` marker (e.g. ``"100 KiB"``, ``"4Gi"``) unambiguously identify a NIST unit. ``system`` is ignored and the result is always NIST. 3. **Unit is ambiguous** — single-letter units such as ``k``, ``M``, ``G`` carry no inherent system information. Only here does ``system`` act as the deciding hint: ``system=bitmath.NIST`` (the default) interprets ``"4G"`` as ``GiB(4)``; passing ``system=bitmath.SI`` interprets it as ``GB(4)``. In summary: ``system`` resolves ambiguity — it does not override evidence already present in the input string. In this example we parse the output of ``df -H / /boot /home``, whose ``Used`` column contains single-letter SI units. Because the units are ambiguous we pass ``system=bitmath.SI`` as a hint:: Filesystem Size Used Avail Use% Mounted on /dev/mapper/luks-ca8d5493-72bb-4691-afe1 107G 64G 38G 63% / /dev/sda1 500M 391M 78M 84% /boot /dev/mapper/vg_deepfryer-lv_home 129G 118G 4.7G 97% /home .. code-block:: python :linenos: :emphasize-lines: 7 >>> with open('/tmp/df-output.txt', 'r') as fp: ... _ = fp.readline() # skip header ... for line in fp.readlines(): ... cols = line.split()[0:4] ... print("""Filesystem: %s ... - Used: %s""" % (cols[0], ... bitmath.parse_string(cols[1], strict=False, system=bitmath.SI))) Filesystem: /dev/mapper/luks-ca8d5493-72bb-4691-afe1 - Used: 107.0 GB Filesystem: /dev/sda1 - Used: 500.0 MB Filesystem: /dev/mapper/vg_deepfryer-lv_home - Used: 129.0 GB If ``df`` is run with ``-h`` instead of ``-H`` it produces NIST-sized values but still prints the same single-letter units. Omit ``system`` (NIST is the default) or pass ``system=bitmath.NIST`` explicitly: .. code-block:: python :linenos: :emphasize-lines: 7 >>> with open('/tmp/df-output.txt', 'r') as fp: ... _ = fp.readline() # skip header ... for line in fp.readlines(): ... cols = line.split()[0:4] ... print("""Filesystem: %s ... - Used: %s""" % (cols[0], ... bitmath.parse_string(cols[1], strict=False, system=bitmath.NIST))) Filesystem: /dev/mapper/luks-ca8d5493-72bb-4691-afe1 - Used: 100.0 GiB Filesystem: /dev/sda1 - Used: 477.0 MiB Filesystem: /dev/mapper/vg_deepfryer-lv_home - Used: 120.0 GiB The results now use the proper NIST prefix syntax: ``GiB``. bitmath.parse_string_unsafe() ============================= .. deprecated:: 2.0.0 ``parse_string_unsafe`` is deprecated and will be removed in a future release. Use :py:func:`bitmath.parse_string` with ``strict=False`` instead: .. code-block:: python # old bitmath.parse_string_unsafe(value, system=bitmath.NIST) # new bitmath.parse_string(value, strict=False, system=bitmath.NIST) To suppress the deprecation warning in the interim: .. code-block:: python import warnings warnings.filterwarnings('ignore', category=DeprecationWarning, module='bitmath') .. function:: parse_string_unsafe(repr[, system=bitmath.NIST]) A deprecated thin wrapper around ``parse_string(repr, strict=False, system=system)``. All behavior, parameters, and caveats are identical to :ref:`parse_string with strict=False `. :param repr: The value to parse. :param system: :py:data:`bitmath.NIST` (default) or :py:data:`bitmath.SI`. :return: A bitmath object representing ``repr``. :raises ValueError: if ``repr`` cannot be parsed. .. versionchanged:: 2.0.0 Deprecated. Default ``system`` changed from ``bitmath.SI`` to ``bitmath.NIST`` for consistency with :py:func:`bitmath.parse_string`. .. versionadded:: 1.3.1 bitmath.query_capacity() ======================== .. function:: query_capacity(path, bestprefix=True, system=NIST) Return the total, used, and free capacity of the volume containing ``path`` as a :class:`Capacity` NamedTuple of :class:`bitmath.Bitmath` instances. This is the recommended API for volume size queries — it works cross-platform without elevated privileges. :param path: A path on the volume to query (``str`` or :class:`os.PathLike`). On Windows, a bare drive letter such as ``"C"`` or ``"C:"`` is automatically normalized to ``"C:\\"``. :param bool bestprefix: When ``True`` (default), each returned field is pre-normalized via :meth:`~bitmath.Bitmath.best_prefix` for human-readable output. When ``False``, raw :class:`bitmath.Byte` instances are returned. :param int system: Prefix system used when ``bestprefix`` is ``True``. :data:`bitmath.NIST` (default) gives binary prefixes (``GiB``); :data:`bitmath.SI` gives decimal prefixes (``GB``). Ignored when ``bestprefix`` is ``False``. :return: A :class:`bitmath.Capacity` with fields ``total``, ``used``, and ``free``. :raises FileNotFoundError: if ``path`` does not exist. :raises PermissionError: if the process lacks access to query ``path``. Linux examples (human-readable by default): .. code-block:: python >>> import bitmath >>> cap = bitmath.query_capacity("/") >>> print(cap.total) 465.762 GiB >>> cap = bitmath.query_capacity("/home") >>> print(cap.free) 120.5 GiB macOS examples: .. code-block:: python >>> cap = bitmath.query_capacity("/") >>> print(cap.total) 500.068 GiB >>> cap = bitmath.query_capacity("/Volumes/SomeVolume") >>> print(cap.used) 14.311 GiB Windows examples (bare drive letters are normalized automatically): .. code-block:: python >>> cap = bitmath.query_capacity("C:\\") >>> print(cap.total) 476.837 GiB >>> cap = bitmath.query_capacity("C") >>> print(cap.free) 200.0 GiB Tuple unpacking: .. code-block:: python >>> total, used, free = bitmath.query_capacity("/") >>> print(total, used, free) 465.762 GiB 186.264 GiB 279.397 GiB Opt into SI (decimal) prefixes: .. code-block:: python >>> cap = bitmath.query_capacity("/", system=bitmath.SI) >>> print(cap.total) 500.068 GB Raw :class:`bitmath.Byte` instances (no prefix normalization): .. code-block:: python >>> cap = bitmath.query_capacity("/", bestprefix=False) >>> print(cap.total) 500068036608.0 Byte .. versionadded:: 2.0.0 bitmath.query_device_capacity() ================================ .. function:: query_device_capacity(device_fd) Query the raw physical capacity of a block device. This is an advanced, privileged API intended for callers that need the true hardware capacity (e.g. disk imaging tools). Most users should use :func:`query_capacity` instead. Requires root on Linux and administrator on Windows. **Not supported on macOS** — SIP blocks raw block device access even for root; use :func:`query_capacity` there. :param file device_fd: An open file handle of the block device. :return: A :class:`bitmath.Byte` equal to the device capacity. :raises ValueError: if ``device_fd`` is not a block device. :raises NotImplementedError: on macOS or any unsupported platform. :raises OSError: if the underlying ioctl or DeviceIoControl call fails. Linux example (requires root): .. code-block:: python :linenos: >>> import bitmath >>> with open("/dev/sda", "rb") as device: ... size = bitmath.query_device_capacity(device).best_prefix() ... print(f"Device {device.name} capacity: {size}") Device /dev/sda capacity: 238.475 GiB Windows example (requires administrator privileges): .. code-block:: python :linenos: >>> import bitmath >>> drives = [r'\\.\PhysicalDrive0', r'\\.\PhysicalDrive1'] >>> for path in drives: ... with open(path, 'rb') as drive: ... size = bitmath.query_device_capacity(drive).best_prefix() ... print(f"Drive {path}: {size}") Drive \\.\PhysicalDrive0: 80.0 GiB Drive \\.\PhysicalDrive1: 14.311 TiB .. note:: **macOS**: this function raises :exc:`NotImplementedError` because System Integrity Protection (SIP) prevents raw block device access. Use :func:`query_capacity` instead. .. versionadded:: 1.2.4 .. versionchanged:: 2.0.0 Added Windows support. macOS now raises :exc:`NotImplementedError` (SIP restriction). .. _module_context_managers: Context Managers **************** This section describes all of the `context managers `_ provided by the bitmath class. .. note:: Context manager settings are thread safe. Concurrent contexts in different threads do not interfere with each other. .. note:: For a bit of background, a *context manager* (specifically, the ``with`` statement) is a feature of the Python language which is commonly used to: * Decorate, or *wrap*, an arbitrary block of code. I.e., effect a certain condition onto a specific body of code * Automatically *open* and *close* an object which is used in a specific context. I.e., handle set-up and tear-down of objects in the place they are used. .. seealso:: :pep:`343` *The "with" Statement* :pep:`318` *Decorators for Functions and Methods* .. _module_bitmath_format: bitmath.format() ================ .. function:: format([fmt_str=None[, plural=False[, bestprefix=False]]]) The :py:func:`bitmath.format` context manager allows you to specify the string representation of all bitmath instances within a specific block of code. This is effectively equivalent to applying the :ref:`format()` method to an entire region of code. :param str fmt_str: a formatting mini-language compat formatting string. See the :ref:`instance attributes ` for a list of available items. :param bool plural: ``True`` enables printing instances with trailing **s**'s if they're plural. ``False`` (default) prints them as singular (no trailing 's') :param bool bestprefix: ``True`` enables printing instances in their best human-readable representation. ``False``, the default, prints instances using their current prefix unit. Let's look at an example of toggling pluralization on and off. First we'll look over a demonstration script (below), and then we'll review the output. .. code-block:: python :linenos: :emphasize-lines: 33-34 import bitmath a_single_bit = bitmath.Bit(1) technically_plural_bytes = bitmath.Byte(0) always_plural_kbs = bitmath.kb(42) formatting_args = { 'not_plural': a_single_bit, 'technically_plural': technically_plural_bytes, 'always_plural': always_plural_kbs } print("""None of the following will be pluralized, because that feature is turned off """) test_string = """ One unit of 'Bit': {not_plural} 0 of a unit is typically said pluralized in US English: {technically_plural} several items of a unit will always be pluralized in normal US English speech: {always_plural}""" print(test_string.format(**formatting_args)) print(""" ---------------------------------------------------------------------- """) print("""Now, we'll use the bitmath.format() context manager to print the same test string, but with pluralization enabled. """) with bitmath.format(plural=True): print(test_string.format(**formatting_args)) The context manager is demonstrated in lines **33** → **34**. In these lines we use the :py:func:`bitmath.format` context manager, setting ``plural`` to ``True``, to print the original string again. By doing this we have enabled pluralized string representations (where appropriate). Running this script would have the following output:: None of the following will be pluralized, because that feature is turned off One unit of 'Bit': 1.0 b 0 of a unit is typically said pluralized in US English: 0.0 B several items of a unit will always be pluralized in normal US English speech: 42.0 kb ---------------------------------------------------------------------- Now, we'll use the bitmath.format() context manager to print the same test string, but with pluralization enabled. One unit of 'Bit': 1.0 b 0 of a unit is typically said pluralized in US English: 0.0 B several items of a unit will always be pluralized in normal US English speech: 42.0 kbs Here's a shorter example, where we'll: * Print a string containing bitmath instances using the default formatting (lines **2** → **3**) * Use the context manager to print the instances in scientific notation (lines **4** → **7**) * Print the string one last time to demonstrate how the formatting automatically returns to the default format (lines **8** → **9**) .. code-block:: python :linenos: >>> import bitmath >>> print("Some instances: %s, %s" % (bitmath.KiB(1 / 3.0), bitmath.Bit(512))) Some instances: 0.3333333333333333 KiB, 512.0 b >>> with bitmath.format("{value:e}-{unit}"): ... print("Some instances: %s, %s" % (bitmath.KiB(1 / 3.0), bitmath.Bit(512))) ... Some instances: 3.333333e-01-KiB, 5.120000e+02-b >>> print("Some instances: %s, %s" % (bitmath.KiB(1 / 3.0), bitmath.Bit(512))) Some instances: 0.3333333333333333 KiB, 512.0 b .. versionadded:: 1.0.8 .. _module_class_variables: Module Variables **************** This section describes the module-level variables. Some of which are constants and are used for reference. Some of which effect output or behavior. .. versionchanged:: 1.0.7 The formatting strings were not available for manupulate/inspection in earlier versions .. versionadded:: 1.1.1 Prior to this version :py:data:`ALL_UNIT_TYPES` was not defined .. note:: Modifying these variables will change the default representation indefinitely. Use the :py:func:`bitmath.format` context manager to limit changes to a specific block of code. .. _module_format_string: .. py:data:: format_string This is the default string representation of all bitmath instances. The default value is ``{value} {unit}`` which, when evaluated, formats an instance as a floating point number with at least one digit of precision, followed by a character of whitespace, followed by the prefix unit of the instance. For example, given bitmath instances representing the following values: **1337 MiB**, **0.1234567 kb**, and **0 B**, their printed output would look like the following: .. code-block:: python >>> from bitmath import * >>> print(MiB(1337), kb(0.1234567), Byte(0)) 1337.0 MiB 0.1234567 kb 0.0 B We can make these instances print however we want to. Let's wrap each one in square brackets (``[``, ``]``), replace the separating space character with a hyphen (``-``), and limit the precision to just 2 digits: .. code-block:: python >>> import bitmath >>> bitmath.format_string = "[{value:.2f}-{unit}]" >>> print(bitmath.MiB(1337), bitmath.kb(0.1234567), bitmath.Byte(0)) [1337.00-MiB] [0.12-kb] [0.00-Byte] .. py:data:: format_plural A boolean which controls the pluralization of instances in string representation. The default is ``False``. If we wanted to enable pluralization we could set the :py:data:`format_plural` variable to ``True``. First, let's look at some output using the default singular formatting. .. code-block:: python >>> import bitmath >>> print(bitmath.MiB(1337)) 1337.0 MiB And now we'll enable pluralization (line **2**): .. code-block:: python :linenos: :emphasize-lines: 2,5 >>> import bitmath >>> bitmath.format_plural = True >>> print(bitmath.MiB(1337)) 1337.0 MiBs >>> bitmath.format_plural = False >>> print(bitmath.MiB(1337)) 1337.0 MiB On line **5** we disable pluralization again and then see that the output has no trailing "s" character. .. py:data:: NIST Constant used as an argument to some functions to specify the **NIST** system. .. py:data:: SI Constant used as an argument to some functions to specify the **SI** system. .. py:data:: SI_PREFIXES An array of all of the SI unit prefixes (e.g., ``k``, ``M``, or ``E``) .. py:data:: SI_STEPS .. code-block:: python SI_STEPS = { 'Bit': 1 / 8.0, 'Byte': 1, 'k': 1000, 'M': 1000000, 'G': 1000000000, 'T': 1000000000000, 'P': 1000000000000000, 'E': 1000000000000000000, 'Z': 1000000000000000000000, 'Y': 1000000000000000000000000 } .. py:data:: NIST_PREFIXES An array of all of the NIST unit prefixes (e.g., ``Ki``, ``Mi``, or ``Ei``) .. py:data:: NIST_STEPS .. code-block:: python NIST_STEPS = { 'Bit': 1 / 8.0, 'Byte': 1, 'Ki': 1024, 'Mi': 1048576, 'Gi': 1073741824, 'Ti': 1099511627776, 'Pi': 1125899906842624, 'Ei': 1152921504606846976, 'Zi': 1180591620717411303424, 'Yi': 1208925819614629174706176 } .. py:data:: ALL_UNIT_TYPES An array of all combinations of known valid prefix units mixed with both bit and byte suffixes. .. code-block:: python 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'] .. _bitmath_3rd_party_module_integrations: 3rd Party Module Integrations ***************************** Self-contained, copy-paste examples for integrating :mod:`bitmath` with :mod:`argparse`, `click `_, and `progressbar2 `_ are collected in the :ref:`Integration Examples ` chapter. timlnx-bitmath-8354f39/docsite/source/query_device_capacity_warning.rst000066400000000000000000000004741520504626100265220ustar00rootroot00000000000000.. important:: Superuser (root/admin) privileges are required to allow :func:`bitmath.query_device_capacity` to make system calls to read a devices capacity. Use of this function on a device the user does not have access to will result in run-time errors. timlnx-bitmath-8354f39/docsite/source/real_life_examples.rst000066400000000000000000000307271520504626100242600ustar00rootroot00000000000000.. highlight:: python .. _real_life_examples: Real Life Examples ################## .. contents:: :depth: 3 :local: Download Speeds *************** Let's pretend that your Internet service provider (ISP) advertises your maximum downstream as **50Mbps** (50 Mega\ **bits** per second)\ :sup:`1` and you want to know how fast that is in Mega\ **bytes** per second? ``bitmath`` can do that for you easily. We can calculate this as such: .. code-block:: python :linenos: >>> import bitmath >>> downstream = bitmath.Mb(50) >>> print(downstream.to_MB()) MB(6.25) This tells us that if our ISP advertises **50Mbps** we can expect to see download rates of over **6MB/sec**. 1. *Assuming your ISP follows the common industry practice of using SI (base-10) units to describe file sizes/rates* Calculating how many files fit on a device ****************************************** In 2001 Apple® announced the iPod™. Their `headline statement `_ boasting: "... iPod stores up to 1,000 CD-quality songs on its super-thin 5 GB hard drive, ..." OK. That's pretty simple to work backwards: *capacity of disk drive* divided by *number of songs* equals the average size of a song. Which in this case is: .. code-block:: python :linenos: :emphasize-lines: 3 >>> song_size = GB(5) / 1000 >>> print(song_size) 0.005GB Or, using ``best_prefix``, (line **2**) to generate a more human-readable form: .. code-block:: python :linenos: :emphasize-lines: 2 >>> song_size = GB(5) / 1000 >>> print(song_size.best_prefix()) 5.0MB That's great, if you have normal radio-length songs. But how many of our `favorite jam-band's `_ 15-30+ minute-long songs could we fit on this iPod? Let's pretend we did the math and the average audio file worked out to be **18.6 MiB** (19.5 MB) large. .. code-block:: python :linenos: :emphasize-lines: 4 >>> ipod_capacity = GB(5) >>> bootleg_size = MB(19.5) >>> print(ipod_capacity / bootleg_size) 256.41025641 The result on line **4** tells tells us that we could fit **256** average-quality songs on our iPod. Using Real Device Capacity ========================== Rather than hard-coding a device size, we can query the actual free space on any mounted filesystem with :func:`bitmath.query_capacity` and feed that directly into the same calculation. :func:`bitmath.query_capacity` returns a :class:`Capacity` named tuple with ``total``, ``used``, and ``free`` fields. By default (``bestprefix=True``) each field is already normalized to a human-readable prefix (e.g. ``GiB``). Pass ``bestprefix=False`` to get raw :class:`bitmath.Byte` instances instead. It requires no elevated privileges and works cross-platform. .. code-block:: python >>> import bitmath >>> cap = bitmath.query_capacity("/") >>> print(cap.free) 93.08 GiB Now we can ask: *how many 4.7 GB DVD images fit in that free space?* .. code-block:: python >>> dvd = bitmath.GB(4.7) >>> count, remainder = divmod(cap.free, dvd) >>> print(int(count), "DVDs,", remainder.best_prefix(), "left over") 19 DVDs, 3.262 GiB left over Floor division (``//``) and modulo (``%``) work the same way if you only need one value: .. code-block:: python >>> print(int(cap.free // dvd), "DVDs fit") 19 DVDs fit >>> print((cap.free % dvd).best_prefix(), "remaining") 3.262 GiB remaining .. seealso:: :ref:`Capacity Math ` Details on floor division, modulo, and ``divmod`` for bitmath objects. :func:`bitmath.query_capacity` Full reference for the ``Capacity`` named tuple and cross-platform behaviour. Printing Human-Readable File Sizes in Python ******************************************** In a Python script or interpreter we may wish to print out file sizes in something other than bytes (which is what ``os.path.getsize`` returns). We can use ``bitmath`` to do that too: .. code-block:: python :linenos: :emphasize-lines: 6 >>> import os >>> from bitmath import * >>> these_files = os.listdir('.') >>> for f in these_files: ... f_size = Byte(os.path.getsize(f)) ... print("%s - %s" % (f, f_size.to_KiB())) test_basic_math.py - 3.048828125 KiB __init__.py - 0.1181640625 KiB test_representation.py - 0.744140625 KiB test_to_Type_conversion.py - 2.2119140625 KiB Alternatively, we could simplify things and use :func:`bitmath.getsize` to read the file size directly into a bitmath object: .. code-block:: python :linenos: :emphasize-lines: 5 >>> import os >>> import bitmath >>> these_files = os.listdir('.') >>> for f in these_files: ... print("%s - %s" % (f, bitmath.getsize(f))) test_basic_math.py - 3.048828125 KiB __init__.py - 0.1181640625 KiB test_representation.py - 0.744140625 KiB test_to_Type_conversion.py - 2.2119140625 KiB .. seealso:: :ref:`Instance Formatting ` How to print results in a *prettier* format Calculating Linux BDP and TCP Window Scaling ******************************************** Say we're doing some Linux Kernel TCP performance tuning. For optimum speeds we need to calculate our BDP, or Bandwidth Delay Product. For this we need to calculate certain values to set some kernel tuning parameters to. The point of this tuning is to send the most data we can during a measured round-trip-time without sending more than can be processed. To accomplish this we are resizing our kernel read/write networking/socket buffers. We will see two ways of doing this. The tedious manual way, and the way with bitmath. The Hard Way ============ **Core Networking Values** - ``net.core.rmem_max`` - **Bytes** - Single Value - Default receive buffer size - ``net.core.wmem_max`` - **Bytes** - Single Value - Default write buffer size **System-Wide Memory Limits** - ``net.ipv4.tcp_mem`` - **Pages** - Three Value Vector - The ``max`` field of the parameter is the number of **memory pages** allowed for queueing by all TCP sockets. **Per-Socket Buffers** Per-socket buffer sizes must not exceed the core networking buffer sizes. - ``net.ipv4.tcp_rmem`` - **Bytes** - Three Field Vector - The ``max`` field sets the size of the TCP receive buffer - ``net.ipv4.tcp_wmem`` - **Bytes** - Three Field Vector - As above, but for the write buffer We would normally calculate the optimal BDP and related values following this approach: #. Measure the latency, or round trip time (RTT, measured in milliseconds), between the host we're tuning and our target remote host #. Measure/identify our network transfer rate #. Calculate the BDP (multiply transfer rate by rtt) #. Obtain our current kernel settings #. Adjust settings as necessary But for the sake brevity we'll be working out of an example scenario with a pre-defined RTT and transfer rate. **Scenario** - We have an average network transfer rate of **1Gb/sec** (where ``Gb`` is the SI unit for Gigabits, not Gibibytes: ``GiB``) - Our latency (RTT) is **0.199ms** (milliseconds) **Calculate Manually** Lets calculate the BDP now. Because the kernel parameters expect values in units of bytes and pages we'll have to convert our transfer rate of 1Gb/sec into B/s (Gigabits/second to Bytes/second): - Convert 1Gb into an equivalent **byte** based unit Remember 1 Byte = 8 Bits: .. code-block:: python tx_rate_GB = 1/8 = 0.125 Our equivalent transfer rate is 0.125GB/sec. - Convert our RTT from milliseconds into seconds Remember 1ms = 10\ :sup:`-3`\ s: .. code-block:: python window_seconds = 0.199 * 10^-3 = 0.000199 Our equivalent RTT window is 0.000199s - Next we multiply the transfer rate by the length of our RTT window (in seconds) (The unit analysis for this is ``GB/s * s`` leaving us with ``GB``) .. code-block:: python BDP = rx_rate_GB * window_seconds = 0.125 * 0.000199 = 0.000024875 Our BDP is 0.000024875GB. - Convert 0.000024875GB to bytes: Remember 1GB = 10\ :sup:`9`\ B .. code-block:: python BDP_bytes = 0.000024875 * 10^9 = 24875.0 Our BDP is 24875 bytes (or about 24.3KiB) The :py:mod:`bitmath` way ========================= All of this math can be done much quicker (and with greater accuracy) using the :py:mod:`bitmath` library. Let's see how: .. code-block:: python :linenos: >>> from bitmath import GB >>> tx = 1/8 >>> rtt = 0.199 * 10**-3 >>> bdp = (GB(tx * rtt)).to_Byte() >>> print(bdp.to_KiB()) KiB(24.2919921875) We could shorten that even further: .. code-block:: python >>> print((GB((1/8) * (0.199 * 10**-3))).to_Byte()) 24875.0Byte **Get the current kernel parameters** Important to note is that the **per-socket** buffer sizes must not exceed the **core network** buffer sizes. Lets fetch our current core buffer sizes: .. code-block:: console $ sysctl net.core.rmem_max net.core.wmem_max net.core.rmem_max = 212992 net.core.wmem_max = 212992 Recall, these values are in bytes. What are they in KiB? .. code-block:: python >>> print(Byte(212992).to_KiB()) KiB(208.0) This means our core networking buffer sizes are set to 208KiB each. Now let's check our current per-socket buffer sizes: .. code-block:: console $ sysctl net.ipv4.tcp_rmem net.ipv4.tcp_wmem net.ipv4.tcp_rmem = 4096 87380 6291456 net.ipv4.tcp_wmem = 4096 16384 4194304 Let's double-check that our buffer sizes aren't already out of wack (per-socket should be <= networking core) .. code-block:: python >>> net_core_max = KiB(bytes=212992) >>> ipv4_tcp_rmem_max = KiB(bytes=6291456) >>> ipv4_tcp_rmem_max > net_core_max True It appears that my buffers aren't sized appropriately. We'll fix that when we set the tunable parameters. Finally, how large is the entire system TCP buffer? .. code-block:: console $ sysctl net.ipv4.tcp_mem net.ipv4.tcp_mem = 280632 374176 561264 Our max system TCP buffer size is set to **561264**. Recall that this parameter is measured in **memory pages**. Most of the time your page size is ``4096 bytes``, but you can check by running the command: ``getconf PAGESIZE``. To convert the system TCP buffer size (561264) into a byte-based unit, we'll multiply it by our pagesize (4096): .. code-block:: python >>> sys_pages = 561264 >>> page_size = 4096 >>> sys_buffer = Byte(sys_pages * page_size) >>> print(sys_buffer.to_MiB()) 2192.4375MiB >>> print(sys_buffer.to_GiB()) 2.14105224609GiB The system max TCP buffer size is about 2.14GiB. In review, we discovered the following: * Our **core network** buffer size is insufficient (**212992**), we'll set it higher * Our current **per-socket** buffer sizes are **6291456** and **4194304** And we calculated the following: * Our ideal **max** per-socket buffer size is **24875** bytes * Our ideal **default** per-socket buffer size (half the **max**): **12437** **Finally: Set the new kernel parameters** Set the **core-network** buffer sizes: .. code-block:: console $ sudo sysctl net.core.rmem_max=24875 net.core.wmem_max=24875 net.core.rmem_max = 24875 net.core.wmem_max = 24875 Set the **per-socket** buffer sizes: .. code-block:: console $ sudo sysctl net.ipv4.tcp_rmem="4096 12437 24875" net.ipv4.tcp_wmem="4096 12437 24875" net.ipv4.tcp_rmem = 4096 12437 24875 net.ipv4.tcp_wmem = 4096 12437 24875 And it's done! Testing this is left as an exercise for the reader. Note that in my experience this is less useful on wireless connections. .. _real_life_examples_download_progress_bars: Creating Download Progress Bars ******************************* For a self-contained, copy-paste example of a progress bar that displays transfer speed using bitmath, see :ref:`integration_examples_progressbar2` in the *Integration Examples* chapter. .. _real_life_examples_read_device_storage_capacity: Reading a Devices Storage Capacity ********************************** .. include:: query_device_capacity_warning.rst Using :func:`bitmath.query_device_capacity` we can read the size of a storage device or a partition on a device. .. include:: example_block_devices.rst Usage is fairly straight-forward. Create an open file handle of the device you want to read the capacity of and then create a bitmath object with the ``query_device_capacity`` function. Here's an example where we read the capacity of device ``sda``, the first device on the example system. .. code-block:: python >>> with open('/dev/sda', 'r') as fh: ... sda_capacity = bitmath.query_device_capacity(fh) >>> print(sda_capacity.best_prefix()) 238.474937439 GiB timlnx-bitmath-8354f39/docsite/source/simple_examples.rst000066400000000000000000000555551520504626100236350ustar00rootroot00000000000000Getting Started ############### In this section we will take a high-level look at the basic things you can do with bitmath. We'll include the following topics: .. contents:: :depth: 3 :local: .. _simple_examples_supported_operations: Tables of Supported Operations ****************************** The following legend describes the two operands used in the tables below. ======= ======================================= Operand Description ======= ======================================= ``bm`` A bitmath object is required ``num`` An integer or decimal value is required ======= ======================================= .. _getting_started_arithmetic: Arithmetic ========== Math works mostly like you expect it to, except for a few edge-cases: * Mixing bitmath types with Number types (the result varies per-operation) * Operations where two bitmath types would cancel out (such as dividing two bitmath types) * Multiplying two bitmath instances together is supported, but the results may not make much sense. .. seealso:: :ref:`Appendix: Rules for Math ` For a discussion of the behavior of bitmath and number types. .. _simple_examples_arithmetic_table: +----------------+----------------------+----------------------+------------------------------------------------------+ | Operation | Parameters | Result Type | Example | +================+======================+======================+======================================================+ | Addition | ``bm1`` + ``bm2`` | ``type(bm1)`` | ``KiB(1) + MiB(2)`` = ``2049.0KiB`` | +----------------+----------------------+----------------------+------------------------------------------------------+ | Addition | ``bm`` + ``num`` | ``type(num)`` | ``KiB(1) + 1`` = ``2.0`` | +----------------+----------------------+----------------------+------------------------------------------------------+ | Addition | ``num`` + ``bm`` | ``type(num)`` | ``1 + KiB(1)`` = ``2.0`` | +----------------+----------------------+----------------------+------------------------------------------------------+ | Subtraction | ``bm1`` - ``bm2`` | ``type(bm1)`` | ``KiB(1) - Byte(2048)`` = ``-1.0KiB`` | +----------------+----------------------+----------------------+------------------------------------------------------+ | Subtraction | ``bm`` - ``num`` | ``type(num)`` | ``KiB(4) - 1`` = ``3.0`` | +----------------+----------------------+----------------------+------------------------------------------------------+ | Subtraction | ``num`` - ``bm`` | ``type(num)`` | ``10 - KiB(1)`` = ``9.0`` | +----------------+----------------------+----------------------+------------------------------------------------------+ | Multiplication | ``bm1`` * ``bm2`` | ``type(bm1)`` | ``KiB(1) * KiB(2)`` = ``2048.0KiB`` | +----------------+----------------------+----------------------+------------------------------------------------------+ | Multiplication | ``bm`` * ``num`` | ``type(bm)`` | ``KiB(2) * 3`` = ``6.0KiB`` | +----------------+----------------------+----------------------+------------------------------------------------------+ | Multiplication | ``num`` * ``bm`` | ``type(bm)`` | ``2 * KiB(3)`` = ``6.0KiB`` | +----------------+----------------------+----------------------+------------------------------------------------------+ | Division | ``bm1`` / ``bm2`` | ``type(num)`` | ``KiB(1) / KiB(2)`` = ``0.5`` | +----------------+----------------------+----------------------+------------------------------------------------------+ | Division | ``bm`` / ``num`` | ``type(bm)`` | ``KiB(6) / 4`` = ``KiB(1.5)`` | +----------------+----------------------+----------------------+------------------------------------------------------+ | Division | ``num`` / ``bm`` | ``type(num)`` | ``3 / KiB(2)`` = ``1.5`` | +----------------+----------------------+----------------------+------------------------------------------------------+ | Floor Division | ``bm1`` // ``bm2`` | ``int`` | ``GiB(1) // MiB(300)`` = ``3`` | +----------------+----------------------+----------------------+------------------------------------------------------+ | Floor Division | ``bm`` // ``num`` | ``type(bm)`` | ``KiB(6) // 4`` = ``KiB(1.5)``\ :sup:`2` | +----------------+----------------------+----------------------+------------------------------------------------------+ | Modulo | ``bm1`` % ``bm2`` | ``type(bm1)`` | ``GiB(1) % MiB(300)`` = ``GiB(0.12...)`` | +----------------+----------------------+----------------------+------------------------------------------------------+ | Modulo | ``bm`` % ``num`` | ``type(bm)`` | ``GiB(1) % 1000`` = ``GiB(2.23e-7)``\ :sup:`2` | +----------------+----------------------+----------------------+------------------------------------------------------+ | divmod | ``divmod(bm1, bm2)`` | ``(int, type(bm1))`` | ``divmod(GiB(1), MiB(300))`` = ``(3, GiB(0.12...))`` | +----------------+----------------------+----------------------+------------------------------------------------------+ 2. For ``//`` and ``%`` with a scalar RHS, the scalar operates on the internal byte count (consistent with how ``/`` is defined for ``bm / num``). This is mostly useful for ``bm op bm`` forms; the ``bm op num`` forms are provided for completeness. Bitwise Operations ================== .. seealso:: `Bitwise Calculator `_ A free online calculator for checking your math Bitwise operations are also supported. Bitwise operations work directly on the ``bits`` attribute of a bitmath instance, not the number you see in an instances printed representation (``value``), to maintain accuracy. +----------------+-----------------------+--------------+---------------------------------------------------------+ | Operation | Parameters | Result Type | Example\ :sup:`1` | +================+=======================+==============+=========================================================+ | Left Shift | ``bm`` << ``num`` | ``type(bm)`` | ``MiB(1)`` << ``2`` = ``MiB(4.0)`` | +----------------+-----------------------+--------------+---------------------------------------------------------+ | Right Shift | ``bm`` >> ``num`` | ``type(bm)`` | ``MiB(1)`` >> ``2`` = ``MiB(0.25)`` | +----------------+-----------------------+--------------+---------------------------------------------------------+ | AND | ``bm`` & ``num`` | ``type(bm)`` | ``MiB(13.37)`` & ``1337`` = ``MiB(0.000126...)`` | +----------------+-----------------------+--------------+---------------------------------------------------------+ | OR | ``bm`` \| ``num`` | ``type(bm)`` | ``MiB(13.37)`` \| ``1337`` = ``MiB(13.3700...)`` | +----------------+-----------------------+--------------+---------------------------------------------------------+ | XOR | ``bm`` ^ ``num`` | ``type(bm)`` | ``MiB(13.37)`` ^ ``1337`` = ``MiB(13.369...)`` | +----------------+-----------------------+--------------+---------------------------------------------------------+ 1. *Give me a break here, it's not easy coming up with compelling examples for bitwise operations...* Basic Math ********** bitmath supports all arithmetic operations .. code-block:: python :linenos: >>> eighty_four_mib = fourty_two_mib + fourty_two_mib_in_kib >>> eighty_four_mib MiB(84.0) >>> eighty_four_mib == fourty_two_mib * 2 True .. _capacity_math: Capacity Math: Floor Division and Modulo **************************************** Floor division (``//``), modulo (``%``), and ``divmod()`` are useful for capacity-planning problems: *"how many N-sized chunks fit into this device, and how much is left over?"* As with the other arithmetic operators, the **left-hand operand's type is preserved** in the result. The one exception is ``bm1 // bm2``, which returns a unitless ``int`` — a count of whole divisions — mirroring how ``bm1 / bm2`` returns a unitless ratio. .. code-block:: python :linenos: >>> from bitmath import GiB, MiB >>> disk = GiB(1) >>> chunk = MiB(300) >>> disk // chunk # how many whole 300 MiB chunks fit in 1 GiB? 3 >>> disk % chunk # leftover, typed as the LHS (GiB) GiB(0.12109375) >>> divmod(disk, chunk) # both at once (3, GiB(0.12109375)) >>> # Exact fits produce a zero-valued bitmath in the LHS unit: >>> GiB(1) % MiB(1) GiB(0.0) .. note:: With a scalar RHS, the scalar operates on the internal byte count — consistent with how ``bm / num`` is defined. In practice the ``bm op bm`` forms above are what you want for capacity math; the ``bm op num`` forms are available but rarely useful. The fundamental identity holds — ``(a // b) * b + (a % b) == a``: .. code-block:: python >>> q, r = divmod(GiB(1), MiB(300)) >>> (q * MiB(300)) + r == GiB(1) True Coercing the Remainder with ``best_prefix`` =========================================== Because ``%`` preserves the LHS type, a small remainder can display with an awkwardly tiny ``value``. Use ``.best_prefix()`` to re-express the result in a more human-readable unit without changing its size: .. code-block:: python :linenos: >>> leftover = GiB(1) % MiB(300) >>> leftover GiB(0.12109375) >>> leftover.best_prefix() MiB(124.0) >>> # Force a specific unit system: >>> import bitmath >>> leftover.best_prefix(system=bitmath.SI) MB(130.02330...) >>> # Or coerce directly to a known unit: >>> leftover.to_MiB() MiB(124.0) Formatting Remainders in a ``bitmath.format`` Context ===================================================== The ``bitmath.format`` context manager pairs nicely with modulo results when you want consistent, human-readable display throughout a block of capacity calculations — including automatic best-prefix coercion: .. code-block:: python :linenos: >>> import bitmath >>> from bitmath import GiB, MiB, TiB >>> volume = TiB(1) >>> block = GiB(7) >>> with bitmath.format(fmt_str="{value:.2f} {unit}", bestprefix=True): ... whole = volume // block ... leftover = volume % block ... print(f"{whole} whole blocks of {block} fit in {volume}") ... print(f"leftover: {leftover}") 146 whole blocks of 7.00 GiB fit in 1.00 TiB leftover: 2.00 GiB >>> # divmod inside a context manager, reporting in plural form. >>> # Note: bestprefix on a zero remainder collapses to bits ("b"). >>> with bitmath.format(fmt_str="{value:.1f} {unit}", plural=True, bestprefix=True): ... q, r = divmod(GiB(10), MiB(256)) ... print(f"{q} chunks, {r} remaining") 40 chunks, 0.0 b remaining .. note:: ``bitmath.format`` is thread-safe — settings are thread-local, so concurrent contexts in different threads do not interfere. Unit Conversion *************** .. code-block:: python :linenos: >>> from bitmath import * >>> fourty_two_mib = MiB(42) >>> fourty_two_mib_in_kib = fourty_two_mib.to_KiB() >>> fourty_two_mib_in_kib KiB(43008.0) >>> fourty_two_mib MiB(42.0) >>> fourty_two_mib.KiB KiB(43008.0) Let's convert a unit and show the differences. What happens if we convert ``2048 MB`` into GiBs? Let's look the MB unit in closer detail: .. code-block:: python >>> from bitmath import * >>> MB(2048).base, MB(2048).power, MB(2048).bytes (10, 6, 2048000000.0) An MB (megabyte) is an SI unit in the base-10 number system. A single megabyte is 10 raised to the power of 6. When you raise 10 to the power of 6 and multiply the result by 2048 you get the number of bytes in ``MB(2048)``. We can check this by creating a byte object with that many bytes as the value and ask for the ``MB`` equivalent: .. code-block:: python >>> bitmath.Byte((10**6)*2048).MB MB(2048.0) What about that conversion though, how about we convert this into GiBs? Those are NIST units, it is a base-2 number system. Let's convert the MB to a GiB and look at those instance attributes again: .. code-block:: python >>> convert_demo = bitmath.MB(2048).GiB >>> convert_demo GiB(1.9073486328125) >>> convert_demo.base, convert_demo.power, convert_demo.bytes (2, 30, 2048000000.0) We can see that the entire calculation has changed but the number of bytes has remained the same. Now we have 2 raised to the power of 6, times 1.9073486328125. A cleaner looking conversion is possible if we convert this within the same unit system. If we look at the GB equivalent we get: .. code-block:: python >>> convert_demo.GB GB(2.048) Because we aren't shifting between different base number systems. Rich Comparison *************** Rich Comparison (as per the `Python Basic Customization `_ magic methods) ``<``, ``<=``, ``==``, ``!=``, ``>``, ``>=`` is fully supported: .. code-block:: python :linenos: >>> GB(1) < GiB(1) True >>> GB(1.073741824) == GiB(1) True >>> GB(1.073741824) <= GiB(1) True >>> Bit(1) == TiB(bits=1) True >>> kB(100) > EiB(bytes=1024) True >>> kB(100) >= EiB.from_other(kB(100)) True >>> kB(100) >= EiB.from_other(kB(99)) True >>> kB(100) >= EiB.from_other(kB(9999)) False >>> KiB(100) != Byte(1) True Sorting ******* bitmath natively supports sorting. Let's make a list of the size (in bytes) of all the files in the present working directory (lines **4** and **5**) and then print them out sorted by increasing magnitude (lines **10** and **11**, and **13** → **15**): .. code-block:: python :linenos: :emphasize-lines: 4,5,10,11,13,14,15 >>> from bitmath import * >>> import os >>> sizes = [] >>> for f in os.listdir('./tests/'): ... sizes.append(KiB(os.path.getsize('./tests/' + f))) >>> print(sizes) [KiB(7337.0), KiB(1441.0), KiB(2126.0), KiB(2178.0), KiB(2326.0), KiB(4003.0), KiB(48.0), KiB(1770.0), KiB(7892.0), KiB(4190.0)] >>> print(sorted(sizes)) [KiB(48.0), KiB(1441.0), KiB(1770.0), KiB(2126.0), KiB(2178.0), KiB(2326.0), KiB(4003.0), KiB(4190.0), KiB(7337.0), KiB(7892.0)] >>> human_sizes = [s.best_prefix() for s in sizes] >>> print(sorted(human_sizes)) [KiB(48.0), MiB(1.4072265625), MiB(1.728515625), MiB(2.076171875), MiB(2.126953125), MiB(2.271484375), MiB(3.9091796875), MiB(4.091796875), MiB(7.1650390625), MiB(7.70703125)] Now print them out in descending magnitude .. code-block:: python >>> print(sorted(human_sizes, reverse=True)) [MiB(7.70703125), MiB(7.1650390625), MiB(4.091796875), MiB(3.9091796875), MiB(2.271484375), MiB(2.126953125), MiB(2.076171875), MiB(1.728515625), MiB(1.4072265625), KiB(48.0)] Parsing Strings *************** :py:func:`bitmath.parse_string` converts a human-readable string into a bitmath instance. By default the unit must be an exact bitmath type name: .. code-block:: python >>> import bitmath >>> bitmath.parse_string("4.7 GiB") GiB(4.7) >>> bitmath.parse_string("1337 MB") MB(1337.0) >>> bitmath.parse_string("1 Mio") # octet alias MiB(1.0) When the input comes from a tool that produces ambiguous output (often-times single-letter units) use ``strict=False``. Pass ``system=bitmath.SI`` or ``system=bitmath.NIST`` to tell the parser which system to use if the unit can not reliably be determined automatically: .. code-block:: python >>> bitmath.parse_string("4G", strict=False) # NIST default GiB(4.0) >>> bitmath.parse_string("4G", strict=False, system=bitmath.SI) GB(4.0) >>> bitmath.parse_string("100", strict=False) # plain number → bytes Byte(100.0) >>> bitmath.parse_string("100 GiB", strict=False, system=bitmath.SI) # i-marker wins GiB(100.0) .. seealso:: :py:func:`bitmath.parse_string` — full parameter reference and caveats. .. _simple_examples_summing: Summing an Iterable ******************* The built-in :py:func:`sum` works with bitmath objects. Because ``0 + bm`` returns ``bm`` itself (the identity element), accumulation starts correctly and the result type matches the **first element** in the iterable: .. code-block:: python >>> import bitmath >>> sum([bitmath.KiB(1), bitmath.KiB(2)]) KiB(3.0) >>> sum([bitmath.Byte(1), bitmath.MiB(1), bitmath.GiB(1)]) Byte(1074790401.0) Results from mixing plain numbers and numbers with units yields a result with no units. .. code-block:: python >>> sum([bitmath.Byte(1), 0]) 1.0 >>> sum([1, bitmath.KiB(2)]) 3.0 .. seealso:: :ref:`Appendix: Rules for Math ` — for a thrilling discussion about the minute details when doing mixed-type math math. What it all boils down to is this: if we don’t provide a unit then bitmath won’t give us one back. Use :py:func:`bitmath.sum` when you need the result **normalised to a specific unit** regardless of the input types. Without a ``start`` argument it accumulates into :class:`bitmath.Byte`; pass ``start`` to choose a different accumulator (resultant unit): .. code-block:: python >>> bitmath.sum([bitmath.MiB(1), bitmath.GiB(1)]) Byte(1074790400.0) >>> bitmath.sum([bitmath.KiB(1), bitmath.KiB(2)], start=bitmath.MiB(0)) MiB(0.0029296875) >>> bitmath.sum([bitmath.MiB(100), bitmath.KiB(2000)], start=bitmath.GiB(0)) GiB(0.0995635986328125) Rounding ******** bitmath represents sizes as floating-point measurements. When an integer result is needed, Python's :py:func:`math.floor`, :py:func:`math.ceil`, and :py:func:`round` all work directly on bitmath instances and return an instance of the same type: .. code-block:: python >>> import math, bitmath >>> math.floor(bitmath.KiB(1) / 3) KiB(0) >>> math.ceil(bitmath.KiB(1) / 3) KiB(1) >>> round(bitmath.MiB(1.75)) MiB(2) >>> round(bitmath.GiB(1.23456), 2) GiB(1.23) .. warning:: Rounding intermediate results is lossy. ``math.floor(GiB(10) / 3) * 3`` yields ``GiB(9)``, not ``GiB(10)``. Only round at the final output step. .. seealso:: :ref:`Appendix: Rules for Math ` — discussion of floating-point representation and when rounding is appropriate. Choosing a Formatting Approach ****************************** bitmath offers several ways to control how instances are rendered as strings. They overlap deliberately — each suits a different situation. This section helps you pick the right one. **Quick reference** .. list-table:: :header-rows: 1 :widths: 30 35 35 * - Approach - Best for - Avoid when * - Default ``str()`` - Printing, debugging, logging - You need custom precision or layout * - ``instance.format()`` - Full control over layout using any instance attribute - You only need to format the number * - f-strings / ``format()`` - Inline formatting in modern Python; columnar output - You need unit-aware attributes beyond ``value`` * - ``bitmath.format()`` context manager - Consistent formatting across a block of code; threaded code - A one-off format on a single value * - ``bitmath.format_string`` global - Changing the default for an entire script or session - Anything other than a top-level script (mutates global state) Default ``str()`` ================= The simplest option. Just print or convert to string — no imports, no setup. Output follows the module-level ``format_string`` (default: ``"{value} {unit}"``): .. code-block:: python >>> import bitmath >>> print(bitmath.MiB(1.5)) 1.5 MiB >>> str(bitmath.GiB(10)) '10.0 GiB' **Use this when** you just need a readable value and don't care about precision or alignment. ``instance.format()`` ===================== The most expressive option. The format string has access to every instance attribute — ``{value}``, ``{unit}``, ``{bits}``, ``{bytes}``, ``{system}``, ``{base}``, ``{power}``, and more: .. code-block:: python >>> size = bitmath.MiB(1 / 3.0) >>> size.format("{value:.2f} {unit} ({bits:.0f} bits)") '0.33 MiB (2796202 bits)' **Use this when** you need the unit label, bit/byte counts, or any other instance attribute woven into the output string. .. seealso:: :ref:`Instance Formatting ` — full attribute reference. f-strings and ``format()`` =========================== Standard Python formatting. The format spec applies to ``self.value`` only; the unit is omitted unless you add it explicitly with ``{size.unit}``: .. code-block:: python >>> size = bitmath.GiB(127.3) >>> f'{size:.2f} {size.unit}' '127.30 GiB' >>> f'{size}' # no spec → same as str(size) '127.3 GiB' This shines for columnar output where alignment matters: .. code-block:: python >>> rows = [("home", bitmath.GiB(127.3)), ("tmp", bitmath.MiB(843.7))] >>> for mount, size in rows: ... print(f"{mount:<8} {size:>10.2f} {size.unit}") home 127.30 GiB tmp 843.70 MiB **Use this when** you're building formatted strings inline and only need the numeric value with a precision or alignment spec. ``bitmath.format()`` context manager ===================================== Sets ``fmt_str``, ``plural``, and ``bestprefix`` for every bitmath ``str()`` call within the block, then restores the previous state automatically — even if an exception is raised. Safe to use in threaded code: .. code-block:: python >>> sizes = [bitmath.KiB(1024), bitmath.MiB(512)] >>> with bitmath.format(fmt_str="{value:.1f} {unit}", bestprefix=True): ... for s in sizes: ... print(s) 1.0 MiB 512.0 MiB **Use this when** you want a consistent format across multiple ``print()`` or ``str()`` calls without touching each one individually, or when you're in a threaded environment. .. seealso:: :ref:`bitmath.format() ` — full parameter reference. ``bitmath.format_string`` global ================================= Sets the default representation for *all* bitmath instances for the remainder of the process. Useful at the top of a script; a poor choice inside a library or threaded code: .. code-block:: python >>> import bitmath >>> bitmath.format_string = "{value:.2f} {unit}" >>> print(bitmath.MiB(1.5)) 1.50 MiB **Use this when** you control the entire script and want a single format everywhere without wrapping everything in a context manager. Prefer the context manager for anything more targeted. .. warning:: Mutating ``bitmath.format_string`` directly affects all threads. Use the :py:func:`bitmath.format` context manager instead in concurrent code. timlnx-bitmath-8354f39/pyproject.toml000066400000000000000000000057541520504626100176720ustar00rootroot00000000000000[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "bitmath" 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" authors = [ { name = "Tim Case", email = "bitmath@lnx.cx" }, ] classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: Console", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Intended Audience :: System Administrators", "Intended Audience :: Telecommunications Industry", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Communications :: File Sharing", "Topic :: Internet", "Topic :: Scientific/Engineering", "Topic :: Scientific/Engineering :: Information Analysis", "Topic :: Scientific/Engineering :: Mathematics", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Software Development :: Testing", "Topic :: Software Development :: Testing :: Acceptance", "Topic :: Software Development :: Testing :: Unit", "Topic :: System :: Filesystems", "Topic :: System :: Systems Administration", "Topic :: Text Processing :: Filters", "Topic :: Utilities", ] [project.urls] Homepage = "https://bitmath.readthedocs.io/en/latest/index.html" "Bug Tracker" = "https://github.com/timlnx/bitmath/issues" "Git Repo" = "https://github.com/timlnx/bitmath" [project.scripts] bitmath = "bitmath:cli_script" [tool.hatch.build.targets.sdist] include = [ "bitmath/", "tests/", "docsite/source/", "README.rst", "LICENSE", "NEWS.rst", "bitmath.1", ".coveragerc", "requirements.txt", ] exclude = [ # local-only dirs hatchling sees but git does not track "bmintegrations/", "bmintegrations311/", ".claude/", "docs/", ".remember/", "*.claude", "CLAUDE.md", ] [tool.hatch.build.targets.wheel] packages = ["bitmath"] [tool.hatch.version] path = "VERSION" pattern = '(?P.+)' [tool.hatch.publish.index] disable = true [tool.pylint.main] py-version = "3.12" [tool.pylint.basic] # format_string / format_plural: public mutable module-level config (users assign these directly) good-names = ["format_string", "format_plural"] good-names-rgxs = [ "^to_[A-Za-z]+$", # unit conversion methods: to_KiB, to_kB, to_Mib, etc. "^[a-z][A-Za-z]$", # two-char unit class names that start lowercase: kB, kb, ko "^[A-Z][A-Z_]+$", # Windows API / ctypes names: DISK_GEOMETRY, IOCTL_DISK_GET_DRIVE_GEOMETRY_EX ] [tool.pylint.design] max-module-lines = 2000 timlnx-bitmath-8354f39/python-bitmath.spec000066400000000000000000000104131520504626100205650ustar00rootroot00000000000000Name: python-bitmath Summary: Work with file sizes like numbers — convert, compare, sort, and format across any unit prefix Version: 0 Release: 1%{?dist} License: MIT Source0: https://github.com/timlnx/bitmath/archive/refs/tags/v%{version}.tar.gz URL: https://github.com/timlnx/bitmath BuildArch: noarch BuildRequires: python3-devel BuildRequires: pyproject-rpm-macros BuildRequires: python3-hatchling BuildRequires: python3-pytest %description bitmath simplifies many facets of interacting with file sizes in various units. Examples include: converting between SI and NIST prefix units (GiB to kB), converting between units of the same type (SI to SI, or NIST to NIST), basic arithmetic operations (subtracting 42KiB from 50GiB), capacity math with floor division, modulo, and divmod (GiB(1) // MiB(300), GiB(1) % MiB(300)), rich comparison operations (1024 Bytes == 1KiB), bit-wise operations, sorting, automatic best human-readable prefix selection, and completely customizable formatting. In addition to the conversion and math operations, bitmath provides human readable representations of values which are suitable for use in interactive shells as well as larger scripts and applications. It can also read the capacity of system storage devices. bitmath can parse strings (like "1 KiB") into proper objects and has support for integration with the argparse module as a custom argument type. bitmath ships almost 300 unit tests with near 100% code coverage. ###################################################################### # Sub-package setup %package -n python3-bitmath Summary: Aids representing and manipulating file sizes in various prefix notations %description -n python3-bitmath bitmath simplifies many facets of interacting with file sizes in various units. Examples include: converting between SI and NIST prefix units (GiB to kB), converting between units of the same type (SI to SI, or NIST to NIST), basic arithmetic operations (subtracting 42KiB from 50GiB), capacity math with floor division, modulo, and divmod (GiB(1) // MiB(300), GiB(1) % MiB(300)), rich comparison operations (1024 Bytes == 1KiB), bit-wise operations, sorting, automatic best human-readable prefix selection, and completely customizable formatting. In addition to the conversion and math operations, bitmath provides human readable representations of values which are suitable for use in interactive shells as well as larger scripts and applications. It can also read the capacity of system storage devices. bitmath can parse strings (like "1 KiB") into proper objects and has support for integration with the argparse module as a custom argument type. bitmath ships almost 300 unit tests with near 100% code coverage. ###################################################################### %generate_buildrequires %pyproject_buildrequires ###################################################################### %prep %setup -n bitmath-%{version} -q ###################################################################### %build %pyproject_wheel ###################################################################### %install %pyproject_install %pyproject_save_files bitmath pushd %{buildroot}%{_bindir} ln -s bitmath bitmath-%{python3_version} popd mkdir -p %{buildroot}%{_mandir}/man1/ cp -v *.1 %{buildroot}%{_mandir}/man1/ ln -s bitmath.1 %{buildroot}%{_mandir}/man1/bitmath-%{python3_version}.1 mkdir -p %{buildroot}%{_docdir}/%{name}/docs cp -v -r docsite/source/* %{buildroot}%{_docdir}/%{name}/docs/ find %{buildroot}%{_docdir}/%{name}/docs -name '.gitkeep' -delete ###################################################################### %check %pytest tests/ ###################################################################### %files %license LICENSE ###################################################################### %files -n python3-bitmath -f %{pyproject_files} %license LICENSE %doc README.rst NEWS.rst %{_bindir}/bitmath %{_bindir}/bitmath-%{python3_version} %{_mandir}/man1/bitmath.1* %{_mandir}/man1/bitmath-%{python3_version}.1* %{_docdir}/%{name}/ ###################################################################### %changelog * Sat Apr 18 2026 Tim Case - 0-1 - Packit-managed changelog. See https://github.com/timlnx/bitmath/releases timlnx-bitmath-8354f39/requirements.txt000066400000000000000000000000671520504626100202320ustar00rootroot00000000000000bandit hypothesis pycodestyle pylint pytest pytest-cov timlnx-bitmath-8354f39/tests/000077500000000000000000000000001520504626100161055ustar00rootroot00000000000000timlnx-bitmath-8354f39/tests/__init__.py000066400000000000000000000035701520504626100202230ustar00rootroot00000000000000# -*- coding: utf-8 -*- # SPDX-License-Identifier: MIT # The MIT License (MIT) # # SPDX-FileCopyrightText: 2014-2026 Tim Case # # 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, sublicense, 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. import unittest class TestCase(unittest.TestCase): """ Parent TestCase to use for all tests. """ def assertListEqualUnordered(self, l1, l2, msg=None): """Assert that l1 and l2 contain the same elements regardless of order.""" self.assertEqual(len(l1), len(l2)) # OK, the lists are of the same size. Let's test that each # item in l1 is in l2. This assumes that what you provided in # l1 are what you expected to find in l2. for item in l1: if item not in l2: raise AssertionError("List 1 and list 2 are not equivalent: %s %s" % ( str(l1), str(l2))) return True timlnx-bitmath-8354f39/tests/file_sizes/000077500000000000000000000000001520504626100202415ustar00rootroot00000000000000timlnx-bitmath-8354f39/tests/file_sizes/bytes.test000066400000000000000000000000461520504626100222700ustar00rootroot00000000000000This file is only a few bytes in size.timlnx-bitmath-8354f39/tests/file_sizes/kbytes.test000066400000000000000000000020001520504626100224330ustar00rootroot00000000000000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaatimlnx-bitmath-8354f39/tests/listdir/000077500000000000000000000000001520504626100175575ustar00rootroot00000000000000timlnx-bitmath-8354f39/tests/listdir/10_byte_file000066400000000000000000000000121520504626100217350ustar00rootroot000000000000001234567890timlnx-bitmath-8354f39/tests/listdir_nosymlinks/000077500000000000000000000000001520504626100220455ustar00rootroot00000000000000timlnx-bitmath-8354f39/tests/listdir_nosymlinks/depth1/000077500000000000000000000000001520504626100232325ustar00rootroot00000000000000timlnx-bitmath-8354f39/tests/listdir_nosymlinks/depth1/depth2/000077500000000000000000000000001520504626100244205ustar00rootroot00000000000000timlnx-bitmath-8354f39/tests/listdir_nosymlinks/depth1/depth2/1024_byte_file000066400000000000000000000020001520504626100267430ustar00rootroot000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000timlnx-bitmath-8354f39/tests/listdir_nosymlinks/depth1/depth2/10_byte_file000066400000000000000000000000121520504626100265760ustar00rootroot000000000000001234567890timlnx-bitmath-8354f39/tests/listdir_symlinks/000077500000000000000000000000001520504626100215105ustar00rootroot00000000000000timlnx-bitmath-8354f39/tests/listdir_symlinks/10_byte_file_link000077700000000000000000000000001520504626100307572../listdir/10_byte_fileustar00rootroot00000000000000timlnx-bitmath-8354f39/tests/listdir_symlinks/depth1/000077500000000000000000000000001520504626100226755ustar00rootroot00000000000000timlnx-bitmath-8354f39/tests/listdir_symlinks/depth1/depth2/000077500000000000000000000000001520504626100240635ustar00rootroot00000000000000timlnx-bitmath-8354f39/tests/listdir_symlinks/depth1/depth2/10_byte_file000066400000000000000000000000121520504626100262410ustar00rootroot000000000000001234567890timlnx-bitmath-8354f39/tests/test_basic_math.py000066400000000000000000000167551520504626100216260ustar00rootroot00000000000000# -*- coding: utf-8 -*- # SPDX-License-Identifier: MIT # The MIT License (MIT) # # SPDX-FileCopyrightText: 2014-2026 Tim Case # # 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, sublicense, 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. """ Test for basic math operations """ from . import TestCase import bitmath class TestBasicMath(TestCase): def setUp(self): self.kib_in_bytes = 1024 self.kib_in_bits = 8192 def test_type_raw_int_equality(self): """A bitmath type is equal to the value it's instanted with""" kib = bitmath.KiB(1) self.assertEqual(kib, 1) def test_same_bitmath_types_equality(self): """Two same bitmath types are equal""" self.assertEqual(bitmath.KiB(1), bitmath.KiB(1)) def test_different_bitmath_types_equality(self): """Two different bitmath types are equal""" self.assertEqual(bitmath.KiB(1), bitmath.Byte(self.kib_in_bytes)) def test_add_same_type_equal_same_type(self): """Adding the same bitmath types is equal to result as the same type""" kib1 = bitmath.KiB(1) kib2 = bitmath.KiB(1) added_two_kib = kib1 + kib2 two_kib = bitmath.KiB(2) self.assertEqual(added_two_kib, two_kib) def test_add_different_types_equal_bitmath_type(self): """Adding two different bitmath types is equal to another type of the same size""" # One Kibibyte + 1024 Bytes = 2048 bytes = Byte(2048) kib1 = bitmath.KiB(1) byte1 = bitmath.Byte(1024) added_different_types = kib1 + byte1 two_kib_in_bytes = bitmath.Byte(2048) self.assertEqual(added_different_types, two_kib_in_bytes) def test_adding_with_different_base_units(self): """Adding a bit based type with a byte based type""" kib_sized_bit_from_bytes = bitmath.Bit(self.kib_in_bits) kib = bitmath.KiB(1) added = kib_sized_bit_from_bytes + kib two_kib = bitmath.KiB(2) self.assertEqual(added, two_kib) def test_subtracting_with_different_base_units(self): """Subtracting a bit based type with a byte based type""" kib_sized_bit_from_bytes = bitmath.Bit(self.kib_in_bits) kib = bitmath.KiB(1) subtracted = kib_sized_bit_from_bytes - kib zero_kib = bitmath.KiB(0) self.assertEqual(subtracted, zero_kib) def test_absolute_positive_value(self): """abs(PositiveObject) is positive""" self.assertEqual(bitmath.KiB(1), abs(bitmath.KiB(1))) def test_absolute_negative_value(self): """abs(NegativeObject) is positive""" self.assertEqual(bitmath.KiB(1), abs(bitmath.KiB(-1))) def test_inversion_to_negative(self): """Negating a positive makes a negative""" self.assertEqual(bitmath.KiB(-1), -bitmath.KiB(1)) def test_inversion_to_positive(self): """Plus'ing a negative makes a positive""" self.assertEqual(bitmath.KiB(1), +bitmath.KiB(-1)) ################################################################## # add def bitmath_add_bitmath_is_bitmath(self): """bitmath + bitmath = bitmath""" bm1 = bitmath.KiB(1) bm2 = bitmath.KiB(2) result = bm1 + bm2 self.assertEqual(result, bitmath.KiB(3)) self.assertIs(type(result), bitmath.Byte) def test_bitmath_add_number_is_number(self): """bitmath + number = number""" bm1 = bitmath.KiB(1) num1 = 2 result = bm1 + num1 self.assertEqual(result, 3.0) self.assertIs(type(result), float) def test_number_add_bitmath_is_number(self): """number + bitmath = number""" num1 = 2 bm1 = bitmath.KiB(1) result = num1 + bm1 self.assertEqual(result, 3.0) self.assertIs(type(result), float) def test_zero_add_bitmath_is_bitmath(self): """0 + bitmath = bitmath (identity element enables built-in sum())""" bm1 = bitmath.KiB(1) result = 0 + bm1 self.assertIsInstance(result, bitmath.Bitmath) self.assertEqual(result, bm1) ################################################################## # sub def test_bitmath_sub_bitmath_is_bitmath(self): """bitmath - bitmath = bitmath""" bm1 = bitmath.KiB(1) bm2 = bitmath.KiB(2) result = bm1 - bm2 self.assertEqual(result, bitmath.KiB(-1)) self.assertIs(type(result), bitmath.KiB) def test_bitmath_sub_number_is_number(self): """bitmath - number = number""" bm1 = bitmath.KiB(1) num1 = 2 result = bm1 - num1 self.assertEqual(result, -1.0) self.assertIs(type(result), float) def test_number_sub_bitmath_is_number(self): """number - bitmath = number""" num1 = 2 bm1 = bitmath.KiB(1) result = num1 - bm1 self.assertEqual(result, 1.0) self.assertIs(type(result), float) ################################################################## # mul def test_bitmath_mul_bitmath_is_bitmath(self): """bitmath * bitmath = bitmath""" bm1 = bitmath.KiB(1) bm2 = bitmath.KiB(2) result = bm1 * bm2 self.assertEqual(result, bitmath.KiB(2048.0)) self.assertIs(type(result), bitmath.KiB) def test_bitmath_mul_number_is_bitmath(self): """bitmath * number = bitmath""" bm1 = bitmath.KiB(1) num1 = 2 result = bm1 * num1 self.assertEqual(result, bitmath.KiB(2)) self.assertIs(type(result), bitmath.KiB) def test_number_mul_bitmath_is_number(self): """number * bitmath = bitmath""" num1 = 2 bm1 = bitmath.KiB(1) result = num1 * bm1 self.assertEqual(result, bitmath.KiB(2.0)) self.assertIs(type(result), bitmath.KiB) ################################################################## # div def test_bitmath_div_bitmath_is_number(self): """bitmath / bitmath = number""" bm1 = bitmath.KiB(1) bm2 = bitmath.KiB(2) result = bm1 / bm2 self.assertEqual(result, 0.5) self.assertIs(type(result), float) def test_bitmath_div_number_is_bitmath(self): """bitmath / number = bitmath""" bm1 = bitmath.KiB(1) num1 = 2 result = bm1 / num1 self.assertEqual(result, bitmath.KiB(0.5)) self.assertIs(type(result), bitmath.KiB) def test_number_div_bitmath_is_number(self): """number / bitmath = number""" num1 = 2 bm1 = bitmath.KiB(1) result = num1 / bm1 self.assertEqual(result, 2.0) self.assertIs(type(result), float) timlnx-bitmath-8354f39/tests/test_best_prefix_BASE.py000066400000000000000000000106741520504626100226320ustar00rootroot00000000000000# -*- coding: utf-8 -*- # SPDX-License-Identifier: MIT # The MIT License (MIT) # # SPDX-FileCopyrightText: 2014-2026 Tim Case # # 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, sublicense, 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. """ Test for base (Bit/Byte) prefix guessing """ from . import TestCase import bitmath class TestBestPrefixBASE(TestCase): def test_byte_round_down(self): """best_prefix_base: 4 Bits (as a Byte()) round down into a Bit()""" # Half a byte is 4 bits half_byte = bitmath.Byte(bits=4) # Byte(0.5) should round down into Bit(4) self.assertIs(type(half_byte.best_prefix()), bitmath.Bit) def test_bit_round_up(self): """best_prefix_base: 2 Bytes (as a Bit()) stays as Bit() — family preserved Bit(16) is 2 bytes, below the Kib/kb threshold. There is no sub-kibibit prefix unit, so best_prefix returns Bit to preserve the Bit family. Prior to 2.0.0 this returned Byte(2). """ # Two bytes is 16 bits two_bytes = bitmath.Bit(bytes=2) self.assertIs(type(two_bytes.best_prefix()), bitmath.Bit) def test_byte_no_rounding(self): """best_prefix_base: 1 Byte (as a Byte()) best prefix is still a Byte()""" # One whole byte one_byte = bitmath.Byte(1) # Byte(1.0) should stay the same, Byte(1.0) self.assertIs(type(one_byte.best_prefix()), bitmath.Byte) def test_best_prefix_with_bitmath_input(self): """best_prefix_base: can handle bitmath type inputs""" bm1 = bitmath.Byte(1024) expected = bitmath.KiB(1) self.assertEqual(bitmath.best_prefix(bm1), expected) # Negative Tests - reference: github issue #55 # # For instances where x in set { b | 0 <= abs(b) < 8 } where b is # number of bits in an instance: # # * bitmath.best_prefix(Byte(bits=-4)) -> Bit(-4) # * bitmath.best_prefix(Byte(bits=4)) -> Bit(4) def test_best_prefix_negative_less_than_a_byte(self): """best_prefix_base: negative values less than a byte stay as bits""" # assert that a Byte of -4 bits yields Bit(-4) bm1 = bitmath.Byte(bits=-4) expected = bitmath.Bit(-4) res = bitmath.best_prefix(bm1) # Verify that best prefix math works for negative numbers self.assertEqual(res, expected) # Verify that best prefix guessed the correct type self.assertIs(type(res), bitmath.Bit) # For instances where x in set { b | b >= 8 } where b is number of # bits in an instance: # # * bitmath.best_prefix(-10**8) -> MiB(-95.367...) # * bitmath.best_prefix(10**8) -> MiB(95.367...) def test_best_prefix_negative_huge_numbers(self): """best_prefix_base: large negative values retain their prefix unit""" positive_result = bitmath.best_prefix(10**8) negative_result = bitmath.best_prefix(-10**8) # Verify that the best prefix math works for negative and # positive numbers self.assertEqual(negative_result, -1 * positive_result) # Verify that they produce the same type self.assertIs(type(negative_result), type(positive_result)) # Verify that type is what we expect it to be self.assertIs(type(negative_result), bitmath.MiB) class TestBestPrefixInvalidSystem(TestCase): def test_best_prefix_invalid_system_raises(self): """best_prefix raises ValueError when an invalid system constant is passed""" with self.assertRaises(ValueError): bitmath.best_prefix(bitmath.MiB(1), system="bogus") timlnx-bitmath-8354f39/tests/test_best_prefix_NIST.py000066400000000000000000000204401520504626100226650ustar00rootroot00000000000000# -*- coding: utf-8 -*- # SPDX-License-Identifier: MIT # The MIT License (MIT) # # SPDX-FileCopyrightText: 2014-2026 Tim Case # # 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, sublicense, 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. """ Test for NIST prefix guessing """ from . import TestCase import bitmath class TestBestPrefixNIST(TestCase): ################################################################## # These tests verify guessing for cases where the best # representation is only one order of magnitude different. def test_simple_round_up(self): """NIST: 1 GiB (as a MiB()) rounds up into a GiB()""" # Represent a Gibibyte as a large MiB GiB_in_MiB = bitmath.MiB(1024) # This should turn into a GiB self.assertIs(type(GiB_in_MiB.best_prefix()), bitmath.GiB) def test_simple_round_down(self): """NIST: 1 MiB (as a GiB()) rounds down into a MiB()""" # Represent one MiB as a small GiB MiB_in_GiB = bitmath.GiB(bytes=1048576) # This should turn into a MiB self.assertIs(type(MiB_in_GiB.best_prefix()), bitmath.MiB) ################################################################## # These tests verify guessing for cases where the best # representation is more than one order of magnitude different. def test_multi_oom_round_up(self): """NIST: A very large Kibibyte rounds up into a Pibibyte""" large_KiB = bitmath.KiB.from_other(bitmath.PiB(1)) self.assertIs(type(large_KiB.best_prefix()), bitmath.PiB) def test_multi_oom_round_down(self): """NIST: A very small Pibibyte rounds down into a KibiByte""" small_PiB = bitmath.PiB.from_other(bitmath.KiB(1)) self.assertIs(type(small_PiB.best_prefix()), bitmath.KiB) ################################################################## # These tests mirror the multi_oom ones, except for extreme cases # where even the largest unit available results in values with # more than 4 digits left of the radix point. def test_extreme_oom_round_up(self): """NIST: 2048 EiB (as a KiB()) rounds up into an EiB()""" huge_KiB = bitmath.KiB.from_other(bitmath.EiB(1)) self.assertIs(type(huge_KiB.best_prefix()), bitmath.EiB) def test_extreme_oom_round_down(self): """NIST: 1 Bit (as a EiB()) rounds down into a Bit()""" tiny_EiB = bitmath.EiB.from_other(bitmath.Bit(1)) self.assertIs(type(tiny_EiB.best_prefix()), bitmath.Bit) ################################################################## # These tests verify that when we use the preferred prefix 'NIST' # we get a NIST type unit back. # # One test for each case. First, start with an SI unit, second, # start with a NIST unit def test_best_prefix_prefer_NIST_from_SI(self): """NIST: Best prefix honors a NIST preference when starting with an SI unit Start with an SI (kb) unit and prefer a NIST unit as the result (MiB) """ # Start with kB, an SI unit should_be_MiB = bitmath.kB(1600).best_prefix(system=bitmath.NIST) self.assertIs(type(should_be_MiB), bitmath.MiB) def test_best_prefix_prefer_NIST_from_NIST(self): """NIST: Best prefix honors a NIST preference when starting with an NIST unit Start with a NIST (GiB) unit and prefer a NIST unit as the result (MiB)""" # This should be MiB(512.0) should_be_MiB = bitmath.GiB(0.5).best_prefix(system=bitmath.NIST) self.assertIs(type(should_be_MiB), bitmath.MiB) ################################################################## def test_best_prefix_NIST_default(self): """NIST: Best prefix uses the current system if no preference set Start with a NIST unit and assert no preference. The default behavior returns a prefix from the current system family (GiB)""" # The MiB is == 1 GiB, conversion happens, and the result is a # unit from the same family (GiB) should_be_GiB = bitmath.MiB(1024).best_prefix() self.assertIs(type(should_be_GiB), bitmath.GiB) def test_best_prefix_identical_result(self): """NIST: instance.best_prefix returns the same type if nothing changes Start with a NIST unit that is already prefectly sized, and apply best_prefix() to it.""" # This is our perfectly sized unit. No change was required should_be_EiB = bitmath.EiB(1).best_prefix() self.assertIs(type(should_be_EiB), bitmath.EiB) # Let's be thorough and do that one more time self.assertIs(type(should_be_EiB.best_prefix()), bitmath.EiB) ################################################################## # Tests for the utility function bitmath.best_prefix() where # SYSTEM=NIST def test_bitmath_best_prefix_NIST(self): """bitmath.best_prefix return a Kibibyte for 1024""" result = bitmath.best_prefix(1024) self.assertIs(type(result), bitmath.KiB) def test_bitmath_best_prefix_NIST_exbi(self): """bitmath.best_prefix return an exbibyte for a huge number of bytes""" result = bitmath.best_prefix(1152921504606846977) self.assertIs(type(result), bitmath.EiB) ################################################################## # Tests for bit-family inputs (issue #95) # # best_prefix() on a Bit-family instance should return a Bit-family # result. Before the fix these all incorrectly return Byte-family # units (e.g. MiB instead of Mib). def test_bit_input_returns_bit_family(self): """NIST: best_prefix on Bit() returns a Bit-family unit, not a Byte-family unit""" # The exact value from issue #95 result = bitmath.Bit(30950093.15655963).best_prefix() self.assertIsInstance(result, bitmath.Bit) def test_bit_input_returns_mib_type(self): """NIST: Bit(30950093) best_prefix is Mib, not MiB (issue #95)""" result = bitmath.Bit(30950093.15655963).best_prefix() self.assertIs(type(result), bitmath.Mib) def test_kib_input_returns_mib_type(self): """NIST: Kib(8192).best_prefix() returns Mib, not MiB Kib(8192) = 8,388,608 bits = 1,048,576 bytes; log(1048576, 1024) = 2 -> Mib. """ result = bitmath.Kib(8192).best_prefix() self.assertIs(type(result), bitmath.Mib) def test_mib_input_returns_gib_type(self): """NIST: Mib(8192).best_prefix() returns Gib, not GiB Mib(8192) = 8,589,934,592 bits = 1,073,741,824 bytes; log(1073741824, 1024) = 3 -> Gib. """ result = bitmath.Mib(8192).best_prefix() self.assertIs(type(result), bitmath.Gib) def test_bit_multi_oom_round_up(self): """NIST: A very large Kib rounds up into a Pib Pib(8) = 2^53 bits = 2^50 bytes; log(2^50, 1024) = 5 -> Pib. """ large_Kib = bitmath.Kib.from_other(bitmath.Pib(8)) self.assertIs(type(large_Kib.best_prefix()), bitmath.Pib) def test_bit_multi_oom_round_down(self): """NIST: A very small Pib rounds down into a Kib Kib(8) = 8192 bits = 1024 bytes; log(1024, 1024) = 1 -> Kib. """ small_Pib = bitmath.Pib.from_other(bitmath.Kib(8)) self.assertIs(type(small_Pib.best_prefix()), bitmath.Kib) def test_bit_input_prefer_nist_returns_bit_family(self): """NIST: best_prefix(system=NIST) on a Bit() still returns a Bit-family unit""" result = bitmath.Bit(30950093.15655963).best_prefix(system=bitmath.NIST) self.assertIs(type(result), bitmath.Mib) timlnx-bitmath-8354f39/tests/test_best_prefix_SI.py000066400000000000000000000176711520504626100224370ustar00rootroot00000000000000# -*- coding: utf-8 -*- # SPDX-License-Identifier: MIT # The MIT License (MIT) # # SPDX-FileCopyrightText: 2014-2026 Tim Case # # 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, sublicense, 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. """ Test for SI prefix guessing """ from . import TestCase import bitmath class TestBestPrefixSI(TestCase): ################################################################## # These tests verify guessing for cases where the best # representation is only one order of magnitude different. def test_simple_round_up(self): """SI: 1 GB (as a MB()) rounds up into a GB()""" # Represent a Gigabyte as a large MB GB_in_MB = bitmath.MB(1024) # This should turn into a GB self.assertIs(type(GB_in_MB.best_prefix()), bitmath.GB) def test_simple_round_down(self): """SI: 1 MB (as a GB()) rounds down into a MB()""" # Represent one MB as a small GB MB_in_GB = bitmath.GB(bytes=1048576) # This should turn into a MB self.assertIs(type(MB_in_GB.best_prefix()), bitmath.MB) ################################################################## # These tests verify guessing for cases where the best # representation is more than one order of magnitude different. def test_multi_oom_round_up(self): """SI: A very large Kilobyte rounds up into a Petabyte""" large_kB = bitmath.kB.from_other(bitmath.PB(1)) self.assertIs(type(large_kB.best_prefix()), bitmath.PB) def test_multi_oom_round_down(self): """SI: A very small Petabyte rounds down into a Kilobyte""" small_PB = bitmath.PB.from_other(bitmath.kB(1)) self.assertIs(type(small_PB.best_prefix()), bitmath.kB) ################################################################## # These tests mirror the multi_oom ones, except for extreme cases # where even the largest unit available results in values with # more than 4 digits left of the radix point. def test_extreme_oom_round_up(self): """SI: 2048 EB (as a kB()) rounds up into an EB()""" huge_kB = bitmath.kB.from_other(bitmath.EB(1)) self.assertIs(type(huge_kB.best_prefix()), bitmath.EB) def test_extreme_oom_round_down(self): """SI: 1 Bit (as a EB()) rounds down into a Bit()""" tiny_EB = bitmath.EB.from_other(bitmath.Bit(1)) self.assertIs(type(tiny_EB.best_prefix()), bitmath.Bit) ################################################################## # These tests verify that when we use the preferred prefix 'SI' # we get a SI type unit back which required a new prefix # # One test for each case. First, start with an SI unit, second, # start with a NIST unit def test_best_prefix_prefer_SI_from_SI(self): """SI: Best prefix honors a SI preference when starting with an SI unit Start with an SI (kb) unit and prefer a SI unit as the result (MB) """ # Start with kB, a SI unit should_be_MB = bitmath.kB(1600).best_prefix(system=bitmath.SI) self.assertIs(type(should_be_MB), bitmath.MB) def test_best_prefix_prefer_SI_from_NIST(self): """SI: Best prefix honors a SI preference when starting with a NIST unit Start with a NIST (GiB) unit and prefer a SI unit as the result (MB)""" # Start with GiB, an NIST unit should_be_MB = bitmath.GiB(0.5).best_prefix(system=bitmath.SI) self.assertIs(type(should_be_MB), bitmath.MB) ################################################################## def test_best_prefix_SI_default(self): """SI: Best prefix uses the current system if no preference set Start with a SI unit and assert no preference. The default behavior returns a prefix from the current system family (GB)""" # The MB is == 1 GB, conversion happens, and the result is a # unit from the same family (GB) should_be_GB = bitmath.MB(1000).best_prefix() self.assertIs(type(should_be_GB), bitmath.GB) def test_best_prefix_identical_result(self): """SI: best_prefix returns the same type when nothing changes Start with a SI unit that is already prefectly sized, and apply best_prefix() to it. again""" # This is our perfectly sized unit. No change was required should_be_EB = bitmath.EB(1).best_prefix() self.assertIs(type(should_be_EB), bitmath.EB) # Let's be thorough and do that one more time self.assertIs(type(should_be_EB.best_prefix()), bitmath.EB) ################################################################## # Tests for the utility function bitmath.best_prefix() where # SYSTEM=SI def test_bitmath_best_prefix_SI(self): """bitmath.best_prefix return a Kilobyte for 1024""" result = bitmath.best_prefix(1024, system=bitmath.SI) self.assertIs(type(result), bitmath.kB) def test_bitmath_best_prefix_SI_yotta(self): """bitmath.best_prefix return a yottabyte for a huge number of bytes""" result = bitmath.best_prefix(1000000000000000000000001, system=bitmath.SI) self.assertIs(type(result), bitmath.YB) ################################################################## # Tests for bit-family inputs (issue #95) # # best_prefix() on a Bit-family SI instance should return a # Bit-family SI result. Before the fix these incorrectly return # Byte-family units (e.g. MB instead of Mb). def test_bit_input_returns_bit_family_si(self): """SI: best_prefix on Bit() returns a Bit-family unit, not a Byte-family unit""" result = bitmath.Bit.from_other(bitmath.Mb(1)).best_prefix(system=bitmath.SI) self.assertIsInstance(result, bitmath.Bit) def test_kb_input_returns_mb_type(self): """SI: kb(8000).best_prefix() returns Mb, not MB kb(8000) = 8,000,000 bits = 1,000,000 bytes; log(1000000, 1000) = 2 -> Mb. """ result = bitmath.kb(8000).best_prefix() self.assertIs(type(result), bitmath.Mb) def test_mb_input_returns_gb_type(self): """SI: Mb(8000).best_prefix() returns Gb, not GB Mb(8000) = 8,000,000,000 bits = 1,000,000,000 bytes; log(1000000000, 1000) = 3 -> Gb. """ result = bitmath.Mb(8000).best_prefix() self.assertIs(type(result), bitmath.Gb) def test_bit_multi_oom_round_up_si(self): """SI: A very large kb rounds up into a Pb Pb(8) = 8*10^15 bits = 10^15 bytes; log(10^15, 1000) = 5 -> Pb. """ large_kb = bitmath.kb.from_other(bitmath.Pb(8)) self.assertIs(type(large_kb.best_prefix()), bitmath.Pb) def test_bit_multi_oom_round_down_si(self): """SI: A very small Pb rounds down into a kb kb(8) = 8000 bits = 1000 bytes; log(1000, 1000) = 1 -> kb. """ small_Pb = bitmath.Pb.from_other(bitmath.kb(8)) self.assertIs(type(small_Pb.best_prefix()), bitmath.kb) def test_bit_input_prefer_si_returns_bit_family(self): """SI: best_prefix(system=SI) on a kb() still returns a Bit-family unit""" result = bitmath.kb(8000).best_prefix(system=bitmath.SI) self.assertIs(type(result), bitmath.Mb) timlnx-bitmath-8354f39/tests/test_bitwise_operations.py000066400000000000000000000052631520504626100234350ustar00rootroot00000000000000# -*- coding: utf-8 -*- # SPDX-License-Identifier: MIT # The MIT License (MIT) # # SPDX-FileCopyrightText: 2014-2026 Tim Case # # 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, sublicense, 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. """ Test for bitwise operations: <<, >>, &, ^, | """ from . import TestCase import bitmath class TestBitwiseOperations(TestCase): def setUp(self): self.leet = bitmath.Bit(1337) def test_left_shift(self): """Bits left shifted (<<) are increased""" shifted = self.leet << 3 self.assertEqual(shifted, bitmath.Bit(10696)) def test_right_shift(self): """Bits right shifted (>>) are decreased""" shifted = self.leet >> 3 self.assertEqual(shifted, bitmath.Bit(167)) def test_and(self): """Bits and'd (&) are correct""" and_fifteen_hundred = self.leet & 1500 self.assertEqual(and_fifteen_hundred, bitmath.Bit(1304)) and_orig = self.leet & 1337 self.assertEqual(and_orig, bitmath.Bit(1337)) and_thousand = self.leet & 1000 self.assertEqual(and_thousand, bitmath.Bit(296)) and_five_hundred = self.leet & 500 self.assertEqual(and_five_hundred, bitmath.Bit(304)) def test_or(self): """Bits or'd (|) are correct""" or_thousand = self.leet | 1000 self.assertEqual(or_thousand, bitmath.Bit(2041)) or_five_hundred = self.leet | 500 self.assertEqual(or_five_hundred, bitmath.Bit(1533)) def test_xor(self): """Bits xor'd (^) are correct""" xor_thousand = self.leet ^ 1000 self.assertEqual(xor_thousand, bitmath.Bit(1745)) xor_five_hundred = self.leet ^ 500 self.assertEqual(xor_five_hundred, bitmath.Bit(1229)) timlnx-bitmath-8354f39/tests/test_cli.py000066400000000000000000000054251520504626100202730ustar00rootroot00000000000000# -*- coding: utf-8 -*- # SPDX-License-Identifier: MIT # The MIT License (MIT) # # SPDX-FileCopyrightText: 2014-2026 Tim Case # # 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, sublicense, 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. """ Test the command line tool """ from . import TestCase import bitmath class TestCli(TestCase): def test_cli_script_main_no_options(self): """CLI script returns nothing if no input is given""" results = bitmath.cli_script_main([]) self.assertEqual(results, []) def test_cli_script_main_no_units(self): """CLI script works if no to/from units are provided""" args = ['100', '1024'] results = bitmath.cli_script_main(args) self.assertEqual(results[0], bitmath.Byte(100)) self.assertEqual(results[1], bitmath.KiB(1)) def test_cli_script_main_to_unit(self): """CLI script returns correct TO units""" args = ['-t', 'MiB', '1048576'] results = bitmath.cli_script_main(args) self.assertEqual(results[0], bitmath.MiB(1)) self.assertIs(type(results[0]), bitmath.MiB) def test_cli_script_main_from_unit(self): """CLI script returns correct if given FROM units""" args = ['-f', 'MiB', '0.5'] # Testing FROM 0.5 MiB TO best human readable unit (512 KiB) results = bitmath.cli_script_main(args) self.assertEqual(results[0], bitmath.KiB(512)) self.assertIs(type(results[0]), bitmath.KiB) def test_cli_script_main_from_and_to_unit(self): """CLI script returns correct if given FROM and TO units""" args = ['-f', 'MiB', '-t', 'Byte', '1'] # Testing FROM 1 MiB TO equivalent Bytes results = bitmath.cli_script_main(args) self.assertEqual(results[0], bitmath.Byte(1048576)) self.assertIs(type(results[0]), bitmath.Byte) timlnx-bitmath-8354f39/tests/test_context_manager.py000066400000000000000000000130331520504626100226740ustar00rootroot00000000000000# -*- coding: utf-8 -*- # SPDX-License-Identifier: MIT # The MIT License (MIT) # # SPDX-FileCopyrightText: 2014-2026 Tim Case # # 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, sublicense, 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. """ Tests to verify that the formatting context manager works """ from . import TestCase import bitmath class TestContextManager(TestCase): def test_with_format(self): """bitmath.format context mgr sets and restores formatting""" to_print = [ bitmath.Byte(101), bitmath.KiB(202), bitmath.MB(303), bitmath.GiB(404), bitmath.TB(505), bitmath.PiB(606), bitmath.EB(707) ] str_reps = [ "101.00-B", "202.00-KiB", "303.00-MB", "404.00-GiB", "505.00-TB", "606.00-PiB", "707.00-EB" ] # Make sure formatting looks right BEFORE the context manager self.assertEqual(str(bitmath.KiB(1.337)), "1.337 KiB") with bitmath.format("{value:.2f}-{unit}"): for (inst, inst_str) in zip(to_print, str_reps): self.assertEqual(str(inst), inst_str) # Make sure formatting looks right AFTER the context manager self.assertEqual(str(bitmath.KiB(1.337)), "1.337 KiB") def test_print_byte_plural(self): """Byte(3.0) prints out units in plural form""" expected_result = "3B" fmt_str = "{value:.1g}{unit}" three_Bytes = bitmath.Byte(3.0) with bitmath.format(plural=True): actual_result = three_Bytes.format(fmt_str) self.assertEqual(expected_result, actual_result) def test_print_byte_plural_fmt_in_mgr(self): """Byte(3.0) prints out units in plural form, setting the fmt str in the mgr""" expected_result = "3B" with bitmath.format(fmt_str="{value:.1g}{unit}", plural=True): three_Bytes = bitmath.Byte(3.0) actual_result = str(three_Bytes) self.assertEqual(expected_result, actual_result) def test_print_GiB_plural_fmt_in_mgr(self): """GiB(3.0) prints out units in plural form, setting the fmt str in the mgr""" expected_result = "3GiBs" with bitmath.format(fmt_str="{value:.1g}{unit}", plural=True): three_GiB = bitmath.GiB(3.0) actual_result = str(three_GiB) self.assertEqual(expected_result, actual_result) def test_print_GiB_singular_fmt_in_mgr(self): """TiB(1/3.0) prints out units in singular form, setting the fmt str in the mgr""" expected_result = "341.3GiB" with bitmath.format(fmt_str="{value:.1f}{unit}"): third_tibibyte = bitmath.TiB(1 / 3.0).best_prefix() actual_result = str(third_tibibyte) self.assertEqual(expected_result, actual_result) def test_bestprefix_in_context_manager(self): """bestprefix=True causes str() to render the best human-readable prefix""" with bitmath.format(bestprefix=True): result = str(bitmath.MiB(1024)) self.assertEqual(result, "1.0 GiB") def test_bestprefix_restores_after_context(self): """bestprefix is not active outside the context manager""" with bitmath.format(bestprefix=True): pass self.assertEqual(str(bitmath.MiB(1024)), "1024.0 MiB") def test_bestprefix_with_fmt_str(self): """bestprefix=True combined with fmt_str applies the format to the converted unit""" with bitmath.format(fmt_str="{value:.2f} {unit}", bestprefix=True): result = str(bitmath.KiB(2048)) self.assertEqual(result, "2.00 MiB") def test_format_restored_after_exception(self): """format_string is restored to default even when an exception is raised""" original = bitmath.format_string try: with bitmath.format(fmt_str="{value:.2f} {unit}"): raise ValueError("boom") except ValueError: pass self.assertEqual(bitmath.format_string, original) self.assertEqual(str(bitmath.KiB(1)), "1.0 KiB") def test_nested_context_managers(self): """Nested format contexts correctly save and restore enclosing context state""" with bitmath.format(fmt_str="{value:.1f} {unit}"): self.assertEqual(str(bitmath.KiB(1)), "1.0 KiB") with bitmath.format(fmt_str="{value:.3f} {unit}"): self.assertEqual(str(bitmath.KiB(1)), "1.000 KiB") self.assertEqual(str(bitmath.KiB(1)), "1.0 KiB") self.assertEqual(str(bitmath.KiB(1)), "1.0 KiB") timlnx-bitmath-8354f39/tests/test_context_manager_thread_safe.py000066400000000000000000000170021520504626100252210ustar00rootroot00000000000000# -*- coding: utf-8 -*- # SPDX-License-Identifier: MIT # The MIT License (MIT) # # SPDX-FileCopyrightText: 2026 Tim Case # # 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, sublicense, 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. """ Thread-safety tests for the bitmath.format() context manager. These tests verify that concurrent context managers in different threads are fully isolated: one thread's format_string, plural, and bestprefix settings cannot bleed into another thread's context. Regression coverage for GitHub issue #83. """ import queue import threading from . import TestCase import bitmath THREAD_COUNT = 8 class TestContextManagerThreadSafety(TestCase): def test_format_string_isolation(self): """Concurrent format contexts expose only their own format_string (issue #83) Each of THREAD_COUNT threads enters a context with a unique format string, then all wait at a barrier so they are guaranteed to be inside their respective contexts simultaneously before formatting any strings. """ errors = queue.Queue() barrier = threading.Barrier(THREAD_COUNT) def worker(thread_id): fmt = "{value}-T" + str(thread_id) expected = "1.0-T" + str(thread_id) try: with bitmath.format(fmt_str=fmt): barrier.wait() result = str(bitmath.KiB(1)) if result != expected: errors.put(AssertionError( "Thread %d: expected %r, got %r" % ( thread_id, expected, result))) except Exception as exc: errors.put(exc) threads = [ threading.Thread(target=worker, args=(i,)) for i in range(THREAD_COUNT) ] for t in threads: t.start() for t in threads: t.join() if not errors.empty(): raise errors.get() def test_plural_isolation(self): """Concurrent format contexts expose only their own plural setting""" errors = queue.Queue() barrier = threading.Barrier(THREAD_COUNT) def plural_worker(expect_plural): try: with bitmath.format(plural=expect_plural): barrier.wait() result = str(bitmath.Byte(3.0)) if expect_plural and result != "3.0 B": errors.put(AssertionError( "plural thread: expected '3.0 B', got %r" % result)) elif not expect_plural and result != "3.0 B": errors.put(AssertionError( "singular thread: expected '3.0 B', got %r" % result)) except Exception as exc: errors.put(exc) threads = [ threading.Thread(target=plural_worker, args=(i % 2 == 0,)) for i in range(THREAD_COUNT) ] for t in threads: t.start() for t in threads: t.join() if not errors.empty(): raise errors.get() def test_bestprefix_isolation(self): """Concurrent format contexts expose only their own bestprefix setting""" errors = queue.Queue() barrier = threading.Barrier(THREAD_COUNT) def bestprefix_worker(use_bestprefix): try: with bitmath.format(bestprefix=use_bestprefix): barrier.wait() result = str(bitmath.MiB(1024)) if use_bestprefix and result != "1.0 GiB": errors.put(AssertionError( "bestprefix thread: expected '1.0 GiB', got %r" % result)) elif not use_bestprefix and result != "1024.0 MiB": errors.put(AssertionError( "no-bestprefix thread: expected '1024.0 MiB', got %r" % result)) except Exception as exc: errors.put(exc) threads = [ threading.Thread(target=bestprefix_worker, args=(i % 2 == 0,)) for i in range(THREAD_COUNT) ] for t in threads: t.start() for t in threads: t.join() if not errors.empty(): raise errors.get() def test_issue_83_format_string_race(self): """Reproduces the exact race condition reported in GitHub issue #83 The module-level format_string is set to an invalid key. Each thread enters a context that supplies a valid format string. Without thread- local storage all threads would see the invalid format_string after any one of them exits its context, producing a KeyError. """ saved = bitmath.format_string bitmath.format_string = "{not_a_valid_key}" errors = queue.Queue() barrier = threading.Barrier(THREAD_COUNT) def worker(): try: with bitmath.format(fmt_str=saved): barrier.wait() str(bitmath.KiB(1)) except Exception as exc: errors.put(exc) threads = [threading.Thread(target=worker) for _ in range(THREAD_COUNT)] try: for t in threads: t.start() for t in threads: t.join() finally: bitmath.format_string = saved if not errors.empty(): raise errors.get() def test_module_global_unchanged_by_context(self): """The module-level format_string is never mutated by the context manager""" original = bitmath.format_string with bitmath.format(fmt_str="{value:.4f} {unit}"): self.assertEqual(bitmath.format_string, original) self.assertEqual(bitmath.format_string, original) def test_format_string_restored_after_exception_in_thread(self): """Thread-local state is cleaned up even when an exception escapes the with block""" errors = queue.Queue() def worker(): try: try: with bitmath.format(fmt_str="{value:.2f} {unit}"): raise ValueError("intentional") except ValueError: pass result = str(bitmath.KiB(1)) if result != "1.0 KiB": errors.put(AssertionError( "after exception: expected '1.0 KiB', got %r" % result)) except Exception as exc: errors.put(exc) t = threading.Thread(target=worker) t.start() t.join() if not errors.empty(): raise errors.get() timlnx-bitmath-8354f39/tests/test_file_size.py000066400000000000000000000267561520504626100215070ustar00rootroot00000000000000# -*- coding: utf-8 -*- # SPDX-License-Identifier: MIT # The MIT License (MIT) # # SPDX-FileCopyrightText: 2014-2026 Tim Case # # 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, sublicense, 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. """ Tests to verify that string representations are accurate """ from . import TestCase import bitmath import os import pathlib import warnings class TestFileSize(TestCase): # expected sizes are given in bytes def setUp(self): self.byte_file = './tests/file_sizes/bytes.test' self.kibibyte_file = './tests/file_sizes/kbytes.test' # ***************************************************************** # getsize # ***************************************************************** ################################################################## # NIST tests def test_getsize_byte_system_NIST(self): """NIST: getsize reports the correct type and size for byte sized files""" expected = bitmath.Byte(bytes=38) result = bitmath.getsize(self.byte_file, system=bitmath.NIST) self.assertEqual(result, expected) self.assertIs(type(result), bitmath.Byte) def test_getsize_kibibyte_system_NIST(self): """NIST: getsize reports the correct type and size for kibibyte sized files""" expected = bitmath.KiB(bytes=1024) result = bitmath.getsize(self.kibibyte_file, system=bitmath.NIST) self.assertEqual(result, expected) self.assertIs(type(result), bitmath.KiB) ################################################################## # SI tests def test_getsize_byte_system_SI(self): """SI: getsize reports the correct type and size for byte sized files""" expected = bitmath.Byte(bytes=38) result = bitmath.getsize(self.byte_file, system=bitmath.SI) self.assertEqual(result, expected) self.assertIs(type(result), bitmath.Byte) def test_getsize_kibibyte_system_SI(self): """SI: getsize reports the correct type and size for kibibyte sized files""" expected = bitmath.kB(bytes=1024) result = bitmath.getsize(self.kibibyte_file, system=bitmath.SI) self.assertEqual(result, expected) self.assertIs(type(result), bitmath.kB) # ***************************************************************** # listdir # ***************************************************************** def test_listdir_nosymlinks(self): """listdir: no symbolic links in tree measures right Assume a directory tree where no sub-directories are symbolic links:: $ tree ./tests/listdir_nosymlinks ./tests/listdir_nosymlinks `-- depth1 `-- depth2 |-- 1024_byte_file `-- 10_byte_file 2 directories, 2 files And the files, ``tests/listdir_nosymlinks/depth1/depth2/10_byte_file`` and ``tests/listdir_nosymlinks/depth1/depth2/1024_byte_file`` are 10 Bytes and 1024 Bytes in size, respectively. Then: >>> for f in bitmath.listdir('./tests/listdir_nosymlinks'): ... print(f) Would yield 2-tuple's of: ('/path/tests/listdir_nosymlinks/depth1/depth2/10_byte_file', Byte(10.0)) ('/path/tests/listdir_nosymlinks/depth1/depth2/1024_byte_file', KiB(1.0)) """ # Call with relpath=True so the paths are easier to verify contents = list(bitmath.listdir('./tests/listdir_nosymlinks/', relpath=True)) # Ensure the returned paths match the expected paths discovered_paths = [ pathlib.Path(contents[0][0]).as_posix(), pathlib.Path(contents[1][0]).as_posix(), ] expected_paths = [ 'tests/listdir_nosymlinks/depth1/depth2/10_byte_file', 'tests/listdir_nosymlinks/depth1/depth2/1024_byte_file' ] self.assertListEqualUnordered(discovered_paths, expected_paths) expected_sizes = [ bitmath.Byte(10.0), bitmath.Byte(1024.0) ] discovered_sizes = [ contents[0][1], contents[1][1] ] self.assertListEqualUnordered(discovered_sizes, expected_sizes) # 2018-03-18 - Commenting this out for now. This is failing during # RPM building. I have no idea why or when this began # happening. Tests work from the command line, but not during the # %check part of RPM building. # # It APPEARS that rpmbuild is dereferencing symlinks when # unpacking and copying the dist archive contents. Rather than # '10_byte_file_link' appearing as a link, it is a real file. # @unittest.expectedFailure # def test_listdir_symlinks_nofollow(self): # """listdir: symbolic links in tree not followed # Similar assumptions as in test_listdir_nosymlinks, except the # directory structure looks like this: # $ tree tests/listdir_symlinks # tests/listdir_symlinks # |-- 10_byte_file_link -> ../listdir/10_byte_file # `-- depth1 # `-- depth2 # `-- 10_byte_file # 2 directories, 2 files # """ # # Call with relpath=True so the paths are easier to verify # contents = list(bitmath.listdir('./tests/listdir_symlinks/', relpath=True)) # # Ensure the returned path matches the expected path # self.assertEqual(contents[0][0], 'tests/listdir_symlinks/depth1/depth2/10_byte_file') # # Ensure the measured size is what we expect # self.assertEqual(contents[0][1], bitmath.Byte(10.0)) def test_listdir_symlinks_follow(self): """listdir: symbolic links in tree are followed Same assumptions as in test_listdir_symlinks_nofollow. """ # Call with relpath=True so the paths are easier to verify contents = list(bitmath.listdir('./tests/listdir_symlinks/', followlinks=True, relpath=True)) # Ensure the returned path matches the expected path expected_paths = [ 'tests/listdir_symlinks/10_byte_file_link', 'tests/listdir_symlinks/depth1/depth2/10_byte_file' ] discovered_paths = [ pathlib.Path(contents[0][0]).as_posix(), pathlib.Path(contents[1][0]).as_posix() ] self.assertListEqualUnordered(discovered_paths, expected_paths) # Ensure the measured size is what we expect expected_sizes = [ bitmath.Byte(10.0), bitmath.Byte(10.0) ] discovered_sizes = [ contents[0][1], contents[1][1] ] self.assertListEqualUnordered(discovered_sizes, expected_sizes) def test_listdir_symlinks_follow_relpath_false(self): """listdir: symlinks followed, absolute paths are returned Same assumptions as in test_listdir_symlinks_follow. Difference is that the 0th item of the tuple returns a fully qualified path. """ contents = list(bitmath.listdir('./tests/listdir_symlinks/', followlinks=True)) # Ensure the returned path matches the expected path and # begins with the present working directory pwd = os.path.realpath('.') expected_paths = [ os.path.join(pwd, contents[0][0]), os.path.join(pwd, contents[1][0]) ] discovered_paths = [ contents[0][0], contents[1][0] ] self.assertListEqualUnordered(discovered_paths, expected_paths) # Ensure the measured size is what we expect expected_sizes = [ bitmath.Byte(10.0), bitmath.Byte(10.0) ] discovered_sizes = [ contents[0][1], contents[1][1] ] self.assertListEqualUnordered(discovered_sizes, expected_sizes) def test_listdir_filtering_nosymlinks(self): """listdir: no symbolic links in tree measures right with a filter Same assumptions as test_listdir_nosymlinks.""" # Call with relpath=True so the paths are easier to verify contents = list(bitmath.listdir('./tests/listdir_nosymlinks/', relpath=True, # Should only find 1 file, 1024_byte_file glob='1024*')) # Ensure the returned path matches the expected path self.assertEqual(pathlib.Path(contents[0][0]).as_posix(), 'tests/listdir_nosymlinks/depth1/depth2/1024_byte_file') # Ensure the measured size is what we expect self.assertEqual(contents[0][1], bitmath.KiB(1.0)) def test_listdir_filtering_empty_match_nosymlinks(self): """listdir: filtering with nosymlinks returns 0 matches for a filter Same assumptions as test_listdir_nosymlinks.""" # Call with relpath=True so the paths are easier to verify contents = list(bitmath.listdir('./tests/listdir_nosymlinks/', relpath=True, # Should find no matches glob='*notafile*')) # There should be one file discovered self.assertEqual(len(contents), int(0)) class TestListdirDeprecations(TestCase): def test_listdir_filter_kwarg_emits_deprecation_warning(self): """listdir() emits DeprecationWarning when called with filter= instead of glob=""" with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") list(bitmath.listdir('./tests/listdir_nosymlinks/', filter='*')) 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""" with warnings.catch_warnings(record=True): warnings.simplefilter("always") contents = list(bitmath.listdir('./tests/listdir_nosymlinks/', relpath=True, filter='1024*')) self.assertEqual(len(contents), 1) self.assertEqual(pathlib.Path(contents[0][0]).as_posix(), 'tests/listdir_nosymlinks/depth1/depth2/1024_byte_file') def test_listdir_unknown_kwarg_raises_typeerror(self): """listdir() raises TypeError for unrecognized keyword arguments""" with self.assertRaises(TypeError): list(bitmath.listdir('./tests/listdir_nosymlinks/', notaarg=True)) timlnx-bitmath-8354f39/tests/test_future_math.py000066400000000000000000000116751520504626100220530ustar00rootroot00000000000000# -*- coding: utf-8 -*- # SPDX-License-Identifier: MIT # The MIT License (MIT) # # SPDX-FileCopyrightText: 2014-2026 Tim Case # # 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, sublicense, 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. """ Test for "future" math operations. Reference: http://legacy.python.org/dev/peps/pep-0238/ """ from . import TestCase import bitmath class TestFutureMath(TestCase): def test_bitmath_div_bitmath_is_number(self): """truediv: bitmath / bitmath = number""" bm1 = bitmath.KiB(1) bm2 = bitmath.KiB(2) result = bm1 / bm2 self.assertEqual(result, 0.5) self.assertIs(type(result), float) def test_bitmath_div_number_is_bitmath(self): """truediv: bitmath / number = bitmath""" bm1 = bitmath.KiB(1) num1 = 2 result = bm1 / num1 self.assertEqual(result, bitmath.KiB(0.5)) self.assertIs(type(result), bitmath.KiB) def test_number_div_bitmath_is_number(self): """truediv: number / bitmath = number""" num1 = 2 bm1 = bitmath.KiB(1) result = num1 / bm1 self.assertEqual(result, 2.0) self.assertIs(type(result), float) def test_number_truediv_bitmath_is_number(self): """truediv: number / bitmath = number""" num1 = 2 bm1 = bitmath.KiB(1) result = bm1.__rtruediv__(num1) self.assertEqual(result, 2.0) self.assertIs(type(result), float) # -- Floor division ------------------------------------------------ def test_bitmath_floordiv_bitmath_is_int(self): """floordiv: bitmath // bitmath = int (whole divisions)""" result = bitmath.GiB(1) // bitmath.MiB(300) self.assertEqual(result, 3) self.assertIs(type(result), int) def test_bitmath_floordiv_bitmath_exact_fit(self): """floordiv: exact multiple yields exact int""" result = bitmath.GiB(1) // bitmath.MiB(1) self.assertEqual(result, 1024) self.assertIs(type(result), int) def test_bitmath_floordiv_number_preserves_lhs_type(self): """floordiv: bitmath // num returns LHS type""" result = bitmath.KiB(6) // 4 self.assertIs(type(result), bitmath.KiB) # 6 KiB = 6144 bytes; 6144 // 4 = 1536 bytes = 1.5 KiB self.assertEqual(result, bitmath.KiB(1.5)) # -- Modulo -------------------------------------------------------- def test_bitmath_mod_bitmath_preserves_lhs_type(self): """mod: bitmath % bitmath returns LHS type""" result = bitmath.GiB(1) % bitmath.MiB(300) self.assertIs(type(result), bitmath.GiB) # 1 GiB = 1073741824 bytes; 1073741824 % (300*1048576) = 130023424 bytes self.assertEqual(result.bytes, 130023424.0) def test_bitmath_mod_bitmath_exact_fit_is_zero(self): """mod: exact multiple yields zero in LHS unit""" result = bitmath.GiB(1) % bitmath.MiB(1) self.assertIs(type(result), bitmath.GiB) self.assertEqual(result.bytes, 0) def test_bitmath_mod_number_preserves_lhs_type(self): """mod: bitmath % num returns LHS type""" result = bitmath.KiB(5) % 1024 self.assertIs(type(result), bitmath.KiB) def test_mod_roundtrip_identity(self): """(a // b) * b + (a % b) == a""" a = bitmath.GiB(1) b = bitmath.MiB(300) q = a // b r = a % b self.assertEqual((q * b) + r, a) # -- divmod -------------------------------------------------------- def test_bitmath_divmod_bitmath(self): """divmod(bitmath, bitmath) = (int, bitmath of LHS type)""" q, r = divmod(bitmath.GiB(1), bitmath.MiB(300)) self.assertEqual(q, 3) self.assertIs(type(q), int) self.assertIs(type(r), bitmath.GiB) self.assertEqual(r.bytes, 130023424.0) def test_bitmath_divmod_exact_fit(self): """divmod on exact multiple: remainder is zero""" q, r = divmod(bitmath.GiB(10), bitmath.MiB(256)) self.assertEqual(q, 40) self.assertEqual(r.bytes, 0) timlnx-bitmath-8354f39/tests/test_hypothesis_properties.py000066400000000000000000000100611520504626100241670ustar00rootroot00000000000000# -*- coding: utf-8 -*- # SPDX-License-Identifier: MIT # The MIT License (MIT) # # SPDX-FileCopyrightText: 2014-2026 Tim Case # # 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, sublicense, 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. """ Hypothesis-driven property tests for the bitmath parser and unit conversions. These are fuzz-style tests: hypothesis generates inputs across the value/unit space and shrinks any counterexample it finds. Three invariants are checked: 1. Roundtrip: an instance formatted as "value unit_singular" and then re-parsed produces a Bitmath with the same underlying bit count. 2. Lossless conversion: x.to_DST().to_SRC() preserves x.bits across every (SRC, DST) pair in ALL_UNIT_TYPES. 3. Parser exception contract: parse_string raises only ValueError on bad input. A leaked IndexError, AttributeError, KeyError, or TypeError would be a bug. """ import math import pytest # Downstream packagers (Fedora/EPEL RPM builds) run %check without # installing test-only dependencies. Skip the whole module when # hypothesis isn't on the path rather than failing collection. pytest.importorskip("hypothesis") from hypothesis import given, settings, strategies as st # noqa: E402 import bitmath # noqa: E402 # Bracketed to avoid values that Python's str() renders in scientific # notation. The parser splits a value/unit string on the first # alphabetic character, which mis-handles the 'e' in '1e+16' or # '1e-05'. Scientific-notation file sizes aren't a realistic user # input, so the test space stays in the regular-notation range # (approximately [1e-4, 1e16)), plus exactly zero. nonneg_finite = st.one_of( st.just(0.0), st.floats( min_value=1e-4, max_value=1e15, allow_nan=False, allow_infinity=False, ), ) unit_names = list(bitmath.ALL_UNIT_TYPES) @given(value=nonneg_finite, unit=st.sampled_from(unit_names)) @settings(max_examples=200) def test_parse_string_roundtrip(value, unit): cls = getattr(bitmath, unit) original = cls(value) formatted = original.format("{value} {unit_singular}") parsed = bitmath.parse_string(formatted) assert math.isclose(parsed.bits, original.bits, rel_tol=1e-9, abs_tol=1e-9) @given( value=nonneg_finite, src=st.sampled_from(unit_names), dst=st.sampled_from(unit_names), ) @settings(max_examples=300) def test_unit_conversion_lossless(value, src, dst): src_cls = getattr(bitmath, src) original = src_cls(value) via = getattr(original, f"to_{dst}")() back = getattr(via, f"to_{src}")() assert math.isclose(back.bits, original.bits, rel_tol=1e-9, abs_tol=1e-9) @given(garbage=st.text()) @settings(max_examples=500) def test_parse_string_strict_only_raises_value_error(garbage): try: result = bitmath.parse_string(garbage) except ValueError: return assert isinstance(result, bitmath.Bitmath) @given(garbage=st.text()) @settings(max_examples=500) def test_parse_string_unsafe_only_raises_value_error(garbage): try: result = bitmath.parse_string(garbage, strict=False) except ValueError: return assert isinstance(result, bitmath.Bitmath) timlnx-bitmath-8354f39/tests/test_init.py000066400000000000000000000054311520504626100204640ustar00rootroot00000000000000# -*- coding: utf-8 -*- # SPDX-License-Identifier: MIT # The MIT License (MIT) # # SPDX-FileCopyrightText: 2015-2026 Tim Case # # 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, sublicense, 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. """ Test the __init__ method """ from . import TestCase import bitmath class TestInit(TestCase): def test___init__invalid_input_types(self): """__init__: can identify (in)valid input parameters, raise ValueError if detected""" invalid_inputs = ["one hundred", 100 + 6j, None] for invalid_input in invalid_inputs: with self.assertRaises(ValueError): bitmath.best_prefix(invalid_input) def test___init_multiple_kwargs(self): """__init__: respects argument mutual exclusivity""" # A 100 Byte object can be initialized with any *single* one # of these pairs: multi_kwargs = { "value": 100, # bitmath.Byte(100) "bytes": 100, # bitmath.Byte(100) "bits": 800 # bitmath.Bit(bytes=100) } with self.assertRaises(ValueError): bitmath.Byte(**multi_kwargs) def test___init__valid_inputs(self): """__init__: accepts valid inputs""" inputs = [ # Comments illustrate what the initialization calls look # like after the interpreter expands all the *arg/**kwarg # parameters. # # All pairs are equivalent to Byte(100) (used in the test # assertion, below) ((100,), dict()), # Byte(100) (tuple(), {"value": 100}), # Byte(value=100) (tuple(), {"bytes": 100}), # Byte(bytes=100) (tuple(), {"bits": 800}) # Byte(bits=800) ] for args, kwargs in inputs: self.assertEqual(bitmath.Byte(*args, **kwargs), bitmath.Byte(100)) timlnx-bitmath-8354f39/tests/test_instantiating.py000066400000000000000000000121751520504626100224000ustar00rootroot00000000000000# -*- coding: utf-8 -*- # SPDX-License-Identifier: MIT # The MIT License (MIT) # # SPDX-FileCopyrightText: 2014-2026 Tim Case # # 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, sublicense, 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. """Test that bitmath instances can only be instantiated with 0 or 1 arguments. Make sure the base 'bitmath.Bitmath' class can not be instantiated either. """ from . import TestCase import bitmath class TestInstantiating(TestCase): ################################################################## # First, the simple passing tests def test_init_no_arguments(self): """Instantiation works with no arguments""" bm = bitmath.Byte() # An instance with 0 arguments will have a value of 0 self.assertEqual(bm, bitmath.Byte(0)) def test_init_value(self): """Instantiation works with only 'value' provided""" bm1 = bitmath.Byte(1) bm2 = bitmath.Byte(value=1) # These instances will be equivalent to each other and # congruent to int(1) self.assertEqual(bm1, bm2) self.assertEqual(bm1, int(1)) self.assertEqual(bm2, int(1)) def test_init_bytes(self): """Instantiation works with the 'bytes' kw arg""" bm = bitmath.Byte(bytes=1024) # 1024 bytes is 1 KiB, these should be equal self.assertEqual(bm, bitmath.KiB(1)) self.assertEqual(bm.bytes, 1024) def test_init_bits(self): """Instantiation works with the 'bits' kw arg""" bm = bitmath.Byte(bits=8) # 8 bits is 1 byte, these should be equal self.assertEqual(bm, bitmath.Byte(1)) ################################################################## # Now, the invalid uses # value and bytes def test_bad_init_value_bytes(self): """Instantiation fails if value and bytes are both provided""" with self.assertRaises(ValueError): bitmath.Byte(value=1, bytes=1) # value and bits def test_bad_init_value_bits(self): """Instantiation fails if value and bits are both provided""" with self.assertRaises(ValueError): bitmath.Byte(value=1, bits=1) # bytes and bits def test_bad_init_bytes_bits(self): """Instantiation fails if bytes and bits are both provided""" with self.assertRaises(ValueError): bitmath.Byte(bytes=1, bits=1) # value and bytes and bits def test_bad_init_value_bytes_bits(self): """Instantiation fails if value and bytes and bits are all provided""" with self.assertRaises(ValueError): bitmath.Byte(value=1, bytes=1, bits=1) ################################################################## # Double check we can't create rogue instances of bitmath.Bitmath def test_bitmath_Bitmath_cannot_be_instantiated(self): """Instantiation fails if we try to instantiate bitmath.Bitmath""" with self.assertRaises(NotImplementedError): bitmath.Bitmath(1337) ################################################################## # ZiB, YiB, Zib, Yib — NIST large units added in 2.0.0 def test_ZiB_instantiation(self): """ZiB can be instantiated""" self.assertIsInstance(bitmath.ZiB(1), bitmath.ZiB) def test_YiB_instantiation(self): """YiB can be instantiated""" self.assertIsInstance(bitmath.YiB(1), bitmath.YiB) def test_Zib_instantiation(self): """Zib can be instantiated""" self.assertIsInstance(bitmath.Zib(1), bitmath.Zib) def test_Yib_instantiation(self): """Yib can be instantiated""" self.assertIsInstance(bitmath.Yib(1), bitmath.Yib) def test_ZiB_in_all_unit_types(self): """ZiB is listed in ALL_UNIT_TYPES""" self.assertIn('ZiB', bitmath.ALL_UNIT_TYPES) def test_YiB_in_all_unit_types(self): """YiB is listed in ALL_UNIT_TYPES""" self.assertIn('YiB', bitmath.ALL_UNIT_TYPES) def test_Zib_in_all_unit_types(self): """Zib is listed in ALL_UNIT_TYPES""" self.assertIn('Zib', bitmath.ALL_UNIT_TYPES) def test_Yib_in_all_unit_types(self): """Yib is listed in ALL_UNIT_TYPES""" self.assertIn('Yib', bitmath.ALL_UNIT_TYPES) timlnx-bitmath-8354f39/tests/test_parse_strict.py000066400000000000000000000146471520504626100222340ustar00rootroot00000000000000# -*- coding: utf-8 -*- # SPDX-License-Identifier: MIT # The MIT License (MIT) # # SPDX-FileCopyrightText: 2014-2026 Tim Case # # 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, sublicense, 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. """ Tests for parse_string (strict=True, the default) and public entry-point behaviour. """ import warnings from . import TestCase import bitmath class TestParseStrict(TestCase): def test_parse_b(self): """parse_string works on bit strings""" self.assertEqual( bitmath.parse_string("123b"), bitmath.Bit(123)) def test_parse_B(self): """parse_string works on byte strings""" self.assertEqual( bitmath.parse_string("321B"), bitmath.Byte(321)) def test_parse_Gb(self): """parse_string works on gigabit strings""" self.assertEqual( bitmath.parse_string("456Gb"), bitmath.Gb(456)) def test_parse_MiB(self): """parse_string works on mebibyte strings""" self.assertEqual( bitmath.parse_string("654 MiB"), bitmath.MiB(654)) ###################################################################### # NIST 'octet' based units def test_parse_Mio(self): """parse_string works on mebioctet strings""" self.assertEqual( bitmath.parse_string("654 Mio"), bitmath.MiB(654)) def test_parse_Eio(self): """parse_string works on exbioctet strings""" self.assertEqual( bitmath.parse_string("654 Eio"), bitmath.EiB(654)) def test_parse_Zio(self): """parse_string works on zebioctet strings""" self.assertEqual( bitmath.parse_string("654 Zio"), bitmath.ZiB(654)) def test_parse_Yio(self): """parse_string works on yobioctet strings""" self.assertEqual( bitmath.parse_string("654 Yio"), bitmath.YiB(654)) # SI 'octet' based units def test_parse_Mo(self): """parse_string works on megaoctet strings""" self.assertEqual( bitmath.parse_string("654 Mo"), bitmath.MB(654)) def test_parse_Eo(self): """parse_string works on exaoctet strings""" self.assertEqual( bitmath.parse_string("654 Eo"), bitmath.EB(654)) def test_parse_Zo(self): """parse_string works on zettaoctet strings""" self.assertEqual( bitmath.parse_string("654 Zo"), bitmath.ZB(654)) def test_parse_Yo(self): """parse_string works on yottaoctet strings""" self.assertEqual( bitmath.parse_string("654 Yo"), bitmath.YB(654)) ###################################################################### def test_parse_bad_float(self): """parse_string can identify invalid float values""" with self.assertRaises(ValueError): bitmath.parse_string("1.23.45 kb") def test_parse_bad_unit(self): """parse_string can identify invalid prefix units""" with self.assertRaises(ValueError): bitmath.parse_string("1.23 GIB") def test_parse_bad_unit2(self): """parse_string can identify other prefix units""" with self.assertRaises(ValueError): bitmath.parse_string("1.23 QB") def test_parse_no_unit(self): """parse_string can identify strings without units at all""" with self.assertRaises(ValueError): bitmath.parse_string("12345") def test_parse_string_non_string_input(self): """parse_string can identify a non-string input""" with self.assertRaises(ValueError): bitmath.parse_string(12345) def test_parse_string_unicode(self): """parse_string can handle a unicode string""" self.assertEqual( bitmath.parse_string(u"750 GiB"), bitmath.GiB(750)) ###################################################################### # Deprecated public entry point tests def test_parse_string_unsafe_deprecation_warning(self): """parse_string_unsafe emits DeprecationWarning as of 2.0.0""" with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") bitmath.parse_string_unsafe("100 GiB") self.assertEqual(len(w), 1) self.assertTrue(issubclass(w[0].category, DeprecationWarning)) self.assertIn("2.0.0", str(w[0].message)) self.assertIn("parse_string", str(w[0].message)) def test_parse_string_unsafe_request_NIST(self): """parse_string_unsafe still delegates correctly with explicit system""" with warnings.catch_warnings(record=True): warnings.simplefilter("always") _parsed = bitmath.parse_string_unsafe("100M", system=bitmath.NIST) self.assertEqual(_parsed, bitmath.MiB(100)) self.assertIs(type(_parsed), bitmath.MiB) _parsed2 = bitmath.parse_string_unsafe("100k", system=bitmath.NIST) self.assertEqual(_parsed2, bitmath.KiB(100)) self.assertIs(type(_parsed2), bitmath.KiB) _parsed3 = bitmath.parse_string_unsafe("100", system=bitmath.NIST) self.assertEqual(_parsed3, bitmath.Byte(100)) self.assertIs(type(_parsed3), bitmath.Byte) _parsed4 = bitmath.parse_string_unsafe("100kb", system=bitmath.NIST) self.assertEqual(_parsed4, bitmath.KiB(100)) self.assertIs(type(_parsed4), bitmath.KiB) timlnx-bitmath-8354f39/tests/test_parse_unsafe.py000066400000000000000000000205761520504626100222030ustar00rootroot00000000000000# -*- coding: utf-8 -*- # SPDX-License-Identifier: MIT # The MIT License (MIT) # # SPDX-FileCopyrightText: 2014-2026 Tim Case # # 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, sublicense, 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. """ Tests for parse_string with strict=False (the non-strict / formerly unsafe path). """ from . import TestCase import bitmath class TestParseUnsafe(TestCase): def test_parse_non_strict_bad_input_type(self): """parse_string strict=False can identify invalid input types""" with self.assertRaises(ValueError): bitmath.parse_string({'keyvalue': 'store'}, strict=False) def test_parse_non_strict_invalid_input(self): """parse_string strict=False raises ValueError for invalid units""" with self.assertRaises(ValueError): bitmath.parse_string("kitties!", strict=False) with self.assertRaises(ValueError): bitmath.parse_string('100 CiB', strict=False) with self.assertRaises(ValueError): bitmath.parse_string('100 J', strict=False) def test_parse_non_strict_good_number_input(self): """parse_string strict=False can parse unitless number inputs""" number_input = 100 string_input = "100" expected_result = bitmath.Byte(100) self.assertEqual( bitmath.parse_string(number_input, strict=False), expected_result) self.assertEqual( bitmath.parse_string(string_input, strict=False), expected_result) def test_parse_non_strict_handles_SI_K_unit(self): """parse_string strict=False can parse the upper/lowercase SI 'thousand' (k)""" thousand_lower = "100k" thousand_upper = "100K" expected_result = bitmath.kB(100) self.assertEqual( bitmath.parse_string(thousand_lower, strict=False, system=bitmath.SI), expected_result) self.assertEqual( bitmath.parse_string(thousand_upper, strict=False, system=bitmath.SI), expected_result) def test_parse_non_strict_NIST_units(self): """parse_string strict=False can parse abbreviated NIST units (Gi, Ki, ...)""" nist_input = "100 Gi" expected_result = bitmath.GiB(100) self.assertEqual( bitmath.parse_string(nist_input, strict=False), expected_result) def test_parse_non_strict_SI(self): """parse_string strict=False can parse all accepted SI inputs""" kilo_inputs = [ '100k', '100K', '100kb', '100KB', '100kB' ] expected_kilo_result = bitmath.kB(100) for ki in kilo_inputs: _parsed = bitmath.parse_string(ki, strict=False, system=bitmath.SI) self.assertEqual(_parsed, expected_kilo_result) self.assertIs(type(_parsed), type(expected_kilo_result)) other_inputs = [ '100g', '100G', '100gb', '100gB', '100GB' ] expected_gig_result = bitmath.GB(100) for gi in other_inputs: _parsed = bitmath.parse_string(gi, strict=False, system=bitmath.SI) self.assertEqual(_parsed, expected_gig_result) self.assertIs(type(_parsed), type(expected_gig_result)) def test_parse_non_strict_NIST(self): """parse_string strict=False can parse all accepted NIST inputs""" kilo_inputs = [ '100ki', '100Ki', '100kib', '100KiB', '100kiB' ] expected_kilo_result = bitmath.KiB(100) for ki in kilo_inputs: _parsed = bitmath.parse_string(ki, strict=False) self.assertEqual(_parsed, expected_kilo_result) self.assertIs(type(_parsed), type(expected_kilo_result)) other_inputs = [ '100gi', '100Gi', '100gib', '100giB', '100GiB' ] expected_gig_result = bitmath.GiB(100) for gi in other_inputs: _parsed = bitmath.parse_string(gi, strict=False) self.assertEqual(_parsed, expected_gig_result) self.assertIs(type(_parsed), type(expected_gig_result)) def test_parse_non_strict_default_system_is_NIST(self): """parse_string strict=False defaults to NIST for ambiguous single-letter units""" self.assertEqual( bitmath.parse_string("100M", strict=False), bitmath.MiB(100)) self.assertIs( type(bitmath.parse_string("100k", strict=False)), bitmath.KiB) def test_parse_non_strict_explicit_SI(self): """parse_string strict=False uses SI when system=bitmath.SI""" self.assertEqual( bitmath.parse_string("100M", strict=False, system=bitmath.SI), bitmath.MB(100)) self.assertIs( type(bitmath.parse_string("100k", strict=False, system=bitmath.SI)), bitmath.kB) def test_parse_non_strict_number_inputs_unaffected_by_system(self): """parse_string strict=False returns Byte() for plain numbers regardless of system""" self.assertEqual( bitmath.parse_string("100", strict=False, system=bitmath.NIST), bitmath.Byte(100)) self.assertEqual( bitmath.parse_string(100, strict=False, system=bitmath.SI), bitmath.Byte(100)) def test_parse_non_strict_github_issue_60(self): """parse_string strict=False can parse the examples reported in issue #60 https://github.com/timlnx/bitmath/issues/60 """ self.assertEqual( bitmath.parse_string('7.5KB', strict=False, system=bitmath.SI), bitmath.kB(7.5)) self.assertEqual( bitmath.parse_string('4.7MB', strict=False, system=bitmath.SI), bitmath.MB(4.7)) self.assertEqual( bitmath.parse_string('4.7M', strict=False, system=bitmath.SI), bitmath.MB(4.7)) def test_parse_non_strict_capital_B_is_Byte(self): """parse_string strict=False: lone 'B' parses as Byte""" self.assertIs(type(bitmath.parse_string("1B", strict=False)), bitmath.Byte) self.assertEqual(bitmath.parse_string("1 B", strict=False), bitmath.Byte(1)) def test_parse_non_strict_lowercase_b_is_Bit(self): """parse_string strict=False: lone 'b' parses as Bit""" self.assertIs(type(bitmath.parse_string("1b", strict=False)), bitmath.Bit) self.assertEqual(bitmath.parse_string("1 b", strict=False), bitmath.Bit(1)) def test_parse_non_strict_bit_word_forms(self): """parse_string strict=False: bit/bits/Bit/Bits/BIT all parse as Bit""" expected = bitmath.Bit(42) for unit in ('bit', 'bits', 'Bit', 'Bits', 'BIT', 'BITS'): result = bitmath.parse_string(f"42 {unit}", strict=False) self.assertEqual(result, expected, msg=f"Failed for unit '{unit}'") self.assertIs(type(result), bitmath.Bit, msg=f"Wrong type for unit '{unit}'") def test_parse_non_strict_byte_word_forms(self): """parse_string strict=False: byte/bytes/Byte/Bytes/BYTE all parse as Byte""" expected = bitmath.Byte(42) for unit in ('byte', 'bytes', 'Byte', 'Bytes', 'BYTE', 'BYTES'): result = bitmath.parse_string(f"42 {unit}", strict=False) self.assertEqual(result, expected, msg=f"Failed for unit '{unit}'") self.assertIs(type(result), bitmath.Byte, msg=f"Wrong type for unit '{unit}'") timlnx-bitmath-8354f39/tests/test_properties.py000066400000000000000000000076401520504626100217210ustar00rootroot00000000000000# -*- coding: utf-8 -*- # SPDX-License-Identifier: MIT # The MIT License (MIT) # # SPDX-FileCopyrightText: 2014-2026 Tim Case # # 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, sublicense, 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. """ Tests to verify that type properties are accessable and immutable """ from . import TestCase import bitmath class TestAttributeProperties(TestCase): def setUp(self): self.kib = bitmath.KiB(1) self.kib_bits = 8192 self.kib_bytes = 1024 self.kib_value = 1 def test_read_bits(self): """Read the 'bits' property of a bitmath type""" self.assertEqual(self.kib.bits, self.kib_bits) def test_read_bytes(self): """Read the 'bytes' property of a bitmath type""" self.assertEqual(self.kib.bytes, self.kib_bytes) def test_read_value(self): """Read the 'value' property of a bitmath type""" self.assertEqual(self.kib.value, self.kib_value) def test_write_property_fails(self): """bitmath type's properties are read-only""" with self.assertRaises(AttributeError): self.kib.value += 42 def test_ZiB_property(self): """ZiB property returns a ZiB instance""" self.assertIsInstance(self.kib.ZiB, bitmath.ZiB) def test_YiB_property(self): """YiB property returns a YiB instance""" self.assertIsInstance(self.kib.YiB, bitmath.YiB) def test_Zib_property(self): """Zib property returns a Zib instance""" self.assertIsInstance(self.kib.Zib, bitmath.Zib) def test_Yib_property(self): """Yib property returns a Yib instance""" self.assertIsInstance(self.kib.Yib, bitmath.Yib) class TestSystemPropertyInvalidBase(TestCase): def test_system_property_invalid_base_raises(self): """system property raises ValueError when _base is not 2 or 10""" obj = bitmath.MiB(1) 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) timlnx-bitmath-8354f39/tests/test_query_capacity.py000066400000000000000000000145101520504626100225410ustar00rootroot00000000000000# -*- coding: utf-8 -*- # SPDX-License-Identifier: MIT # The MIT License (MIT) # # SPDX-FileCopyrightText: 2026 Tim Case # # 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, sublicense, 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. """ Tests for bitmath.query_capacity() """ import os import pathlib from unittest import mock, skipUnless import bitmath from bitmath import Bitmath, Byte, Capacity from . import TestCase class TestQueryCapacity(TestCase): @skipUnless(os.name == 'posix', 'POSIX only') def test_query_capacity_posix_root(self): """query_capacity('/') returns a valid Capacity on POSIX""" result = bitmath.query_capacity("/") self.assertIsInstance(result, Capacity) self.assertIsInstance(result.total, Bitmath) self.assertIsInstance(result.used, Bitmath) self.assertIsInstance(result.free, Bitmath) self.assertGreater(result.total.bytes, 0) self.assertLessEqual(result.used.bytes + result.free.bytes, result.total.bytes) @skipUnless(os.name == 'posix', 'POSIX only') def test_query_capacity_posix_cwd(self): """query_capacity('.') returns valid Capacity for cwd on POSIX""" result = bitmath.query_capacity(".") self.assertIsInstance(result, Capacity) self.assertGreater(result.total.bytes, 0) self.assertLessEqual(result.used.bytes + result.free.bytes, result.total.bytes) @skipUnless(os.name == 'posix', 'POSIX only') def test_query_capacity_tuple_unpacking(self): """query_capacity result supports tuple unpacking""" t, u, f = bitmath.query_capacity("/") self.assertIsInstance(t, Bitmath) self.assertIsInstance(u, Bitmath) self.assertIsInstance(f, Bitmath) @skipUnless(os.name == 'posix', 'POSIX only') def test_query_capacity_bestprefix_false_returns_bytes(self): """bestprefix=False returns raw Byte instances""" result = bitmath.query_capacity("/", bestprefix=False) self.assertIs(type(result.total), Byte) self.assertIs(type(result.used), Byte) self.assertIs(type(result.free), Byte) @skipUnless(os.name == 'posix', 'POSIX only') def test_query_capacity_bestprefix_true_preserves_bytes(self): """bestprefix=True preserves underlying byte counts""" raw = bitmath.query_capacity("/", bestprefix=False) pretty = bitmath.query_capacity("/") self.assertEqual(raw.total.bytes, pretty.total.bytes) self.assertEqual(raw.used.bytes, pretty.used.bytes) self.assertEqual(raw.free.bytes, pretty.free.bytes) @skipUnless(os.name == 'posix', 'POSIX only') def test_query_capacity_system_nist_vs_si(self): """system kwarg switches between NIST (GiB) and SI (GB) prefixes""" nist = bitmath.query_capacity("/", system=bitmath.NIST) si = bitmath.query_capacity("/", system=bitmath.SI) # Same underlying bytes, different display units. self.assertEqual(nist.total.bytes, si.total.bytes) # NIST units end in 'iB' (e.g. GiB, MiB); SI units do not. # On a sufficiently large volume these will differ in unit class. if nist.total.bytes >= 1000: self.assertTrue(nist.total.unit.endswith('iB') or nist.total.unit == 'Byte') self.assertFalse(si.total.unit.endswith('iB')) @skipUnless(os.name == 'posix', 'POSIX only') def test_query_capacity_pathlike_input(self): """query_capacity accepts pathlib.Path input""" result = bitmath.query_capacity(pathlib.Path("/")) self.assertIsInstance(result, Capacity) self.assertGreater(result.total.bytes, 0) def test_query_capacity_nonexistent_path_raises(self): """query_capacity raises FileNotFoundError for a path that does not exist""" with self.assertRaises((FileNotFoundError, OSError)): bitmath.query_capacity("/nonexistent/path/that/should/not/exist/xyzzy") @skipUnless(os.name == 'nt', 'Windows only') def test_query_capacity_windows_drive_letter_normalization(self): """query_capacity normalizes bare drive letters on Windows""" c_bare = bitmath.query_capacity("C") c_colon = bitmath.query_capacity("C:") c_backslash = bitmath.query_capacity("C:\\") c_lower = bitmath.query_capacity("c") self.assertEqual(c_bare, c_colon) self.assertEqual(c_bare, c_backslash) self.assertEqual(c_bare, c_lower) @skipUnless(os.name == 'nt', 'Windows only') def test_query_capacity_windows_pathlike_drive_letter(self): """query_capacity normalizes a pathlib drive-letter input on Windows""" result = bitmath.query_capacity(pathlib.PureWindowsPath("C:")) self.assertIsInstance(result, Capacity) self.assertGreater(result.total.bytes, 0) class TestQueryCapacityWindowsDriveLetterMock(TestCase): def test_query_capacity_windows_drive_letter_normalization_mock(self): """query_capacity normalizes a bare drive letter via mocked os.name='nt'""" mock_usage = mock.MagicMock() mock_usage.total = 1_000_000_000_000 mock_usage.used = 500_000_000_000 mock_usage.free = 500_000_000_000 with mock.patch('bitmath.os.name', 'nt'): with mock.patch('bitmath.shutil.disk_usage', return_value=mock_usage) as mock_du: result = bitmath.query_capacity("c:") self.assertIsInstance(result, Capacity) mock_du.assert_called_once_with("C:\\") timlnx-bitmath-8354f39/tests/test_query_device_capacity.py000066400000000000000000000176631520504626100240740ustar00rootroot00000000000000# -*- coding: utf-8 -*- # SPDX-License-Identifier: MIT # The MIT License (MIT) # # SPDX-FileCopyrightText: 2015-2026 Tim Case # # 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, sublicense, 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. """ Test reading block device capacities """ from . import TestCase import bitmath import ctypes as real_ctypes import os import types from unittest import mock, skipUnless import struct from contextlib import ExitStack, contextmanager @contextmanager def nested(*contexts): with ExitStack() as stack: yield tuple(stack.enter_context(c) for c in contexts) device_file_no = mock.Mock(return_value=4) device = mock.MagicMock('file') device.fileno = device_file_no device.name = "/dev/sda" non_device_file = mock.MagicMock('file') non_device_file.name = "/home/" windows_device = mock.MagicMock('file') windows_device.fileno = mock.Mock(return_value=4) windows_device.name = r'\\.\PhysicalDrive0' windows_non_device = mock.MagicMock('file') windows_non_device.name = r'C:\somefile.txt' class TestQueryDeviceCapacity(TestCase): @skipUnless(os.name == 'posix', 'fcntl is POSIX only') def test_query_device_capacity_linux_everything_is_wonderful(self): """query device capacity works on a happy Linux host""" with nested( mock.patch('os.stat'), mock.patch('stat.S_ISBLK'), mock.patch('platform.system'), mock.patch('fcntl.ioctl'), ) as (os_stat, stat_is_block, plat_system, ioctl): os_stat.return_value = mock.Mock(st_mode=25008) stat_is_block.return_value = True plat_system.return_value = 'Linux' ioctl.return_value = struct.pack('L', 244140625) # = 'QJ\x8d\x0e\x00\x00\x00\x00' # = 244140625 ~= 244.140625 MB (in SI) bytes = bitmath.query_device_capacity(device) self.assertEqual(bytes, 244140625) self.assertEqual(ioctl.call_count, 1) ioctl.assert_called_once_with(4, 0x80081272, b'\x00' * struct.calcsize('L')) def test_query_device_capacity_macos_raises(self): """query_device_capacity raises NotImplementedError on macOS (SIP restriction)""" with mock.patch('bitmath.os.name', 'posix'): with mock.patch('bitmath.platform.system', return_value='Darwin'): with self.assertRaises(NotImplementedError) as ctx: bitmath.query_device_capacity(device) self.assertIn('SIP', str(ctx.exception)) @skipUnless(os.name == 'posix', 'fcntl is POSIX only') def test_query_device_capacity_device_not_block(self): """query device capacity aborts if a non-block-device is provided""" with nested( mock.patch('os.stat'), mock.patch('stat.S_ISBLK'), mock.patch('platform.system'), mock.patch('fcntl.ioctl'), ) as (os_stat, stat_is_block, plat_system, ioctl): os_stat.return_value = mock.Mock(st_mode=33204) # Force ISBLK to reject the input 'device' stat_is_block.return_value = False plat_system.return_value = 'Linux' with self.assertRaises(ValueError): bitmath.query_device_capacity(non_device_file) self.assertEqual(ioctl.call_count, 0) def test_query_device_capacity_windows_everything_is_wonderful(self): """query device capacity works on a happy Windows host""" expected_bytes = 1_000_000_000_000 # 1 TB with mock.patch('bitmath._query_device_capacity_windows', return_value=expected_bytes): with mock.patch('bitmath.os.name', 'nt'): result = bitmath.query_device_capacity(windows_device) self.assertEqual(result, bitmath.Byte(expected_bytes)) def test_query_device_capacity_windows_non_device_fails(self): """query device capacity rejects a non-device path on Windows""" with mock.patch('bitmath.os.name', 'nt'): with self.assertRaises(ValueError): bitmath.query_device_capacity(windows_non_device) def test_query_device_capacity_unsupported_platform_fails(self): """query device capacity fails on an unsupported platform""" # Derive a value that is guaranteed not to be in SUPPORTED_PLATFORMS. unsupported = next( p for p in ('os2', 'java', 'riscos', 'ce') if p not in bitmath.SUPPORTED_PLATFORMS ) with mock.patch('bitmath.os.name', unsupported): with self.assertRaises(NotImplementedError): bitmath.query_device_capacity(device) class TestQueryDeviceCapacityWindowsBody(TestCase): """Mock-based tests for _query_device_capacity_windows body. Run on all platforms by injecting ctypes and msvcrt into the bitmath namespace via mock.patch(..., create=True). """ def _make_mock_ctypes(self): mc = types.SimpleNamespace( Structure=real_ctypes.Structure, c_longlong=real_ctypes.c_longlong, c_uint=real_ctypes.c_uint, c_ulong=real_ctypes.c_ulong, c_byte=real_ctypes.c_byte, byref=real_ctypes.byref, sizeof=real_ctypes.sizeof, wintypes=types.SimpleNamespace(DWORD=real_ctypes.c_ulong), windll=mock.MagicMock(), ) mc.windll.kernel32.DeviceIoControl.return_value = 1 return mc def _make_mock_msvcrt(self): return types.SimpleNamespace(get_osfhandle=mock.Mock(return_value=999)) def _make_windows_device(self): fd = mock.MagicMock() fd.name = r'\\.\PhysicalDrive0' fd.fileno.return_value = 4 return fd def test_windows_body_success(self): """_query_device_capacity_windows succeeds via mocked ctypes and msvcrt""" mock_ctypes = self._make_mock_ctypes() mock_msvcrt = self._make_mock_msvcrt() device_fd = self._make_windows_device() with mock.patch('bitmath.ctypes', mock_ctypes, create=True): with mock.patch('bitmath.msvcrt', mock_msvcrt, create=True): result = bitmath._query_device_capacity_windows(device_fd) # DiskSize is 0 by default — mock DeviceIoControl does not fill the struct self.assertEqual(result, 0) mock_msvcrt.get_osfhandle.assert_called_once_with(4) mock_ctypes.windll.kernel32.DeviceIoControl.assert_called_once() def test_windows_body_ioctl_failure_raises_oserror(self): """_query_device_capacity_windows raises OSError when DeviceIoControl fails""" mock_ctypes = self._make_mock_ctypes() mock_ctypes.windll.kernel32.DeviceIoControl.return_value = 0 mock_ctypes.windll.kernel32.GetLastError.return_value = 5 mock_msvcrt = self._make_mock_msvcrt() device_fd = self._make_windows_device() with mock.patch('bitmath.ctypes', mock_ctypes, create=True): with mock.patch('bitmath.msvcrt', mock_msvcrt, create=True): with self.assertRaises(OSError): bitmath._query_device_capacity_windows(device_fd) timlnx-bitmath-8354f39/tests/test_representation.py000066400000000000000000000165551520504626100225740ustar00rootroot00000000000000# -*- coding: utf-8 -*- # SPDX-License-Identifier: MIT # The MIT License (MIT) # # SPDX-FileCopyrightText: 2014-2026 Tim Case # # 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, sublicense, 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. """ Tests to verify that string representations are accurate """ from . import TestCase import bitmath class TestRepresentation(TestCase): def setUp(self): self.kib = bitmath.KiB(1) self.kib_repr = 'KiB(1.0)' self.kib_str = '1.0 KiB' self.kib_unit = 'KiB' self.kib_system = 'NIST' self.kib_bin = '0b10000000000000' self.kib_binary = self.kib_bin self.kib_power = 10 self.kib_base = 2 self.half_mib = bitmath.MiB(0.5) self.half_mib_repr = 'MiB(0.5)' self.half_mib_str = '0.5 MiB' self.kB = bitmath.kB(1) self.kB_unit = 'kB' self.kb_system = 'SI' self.kib_str_changed = 'KiB 1.000' def test_kB_unit(self): """kB(1).unit is kB""" self.assertEqual(self.kib.unit, self.kib_unit) def test_kB_system(self): """kB(1).system is SI""" self.assertEqual(self.kib.system, self.kib_system) def test_kib_unit(self): """KiB(1).unit is KiB""" self.assertEqual(self.kib.unit, self.kib_unit) def test_kib_system(self): """KiB(1).system is NIST""" self.assertEqual(self.kib.system, self.kib_system) def test_kib_binary(self): """KiB(1).binary is binary""" self.assertEqual(self.kib.binary, self.kib_binary) def test_kib_bin(self): """KiB(1).bin (binary alias) is binary""" self.assertEqual(self.kib.bin, self.kib_bin) def test_kib_base(self): """KiB(1).base is 2""" self.assertEqual(self.kib.base, self.kib_base) def test_kib_power(self): """KiB(1).power (binary alias) is 10""" self.assertEqual(self.kib.power, self.kib_power) def test_whole_kib_repr(self): """KiB(1) looks correct in a terminal""" self.assertEqual(repr(self.kib), self.kib_repr) def test_whole_kib_str(self): """KiB(1) looks correct as a string""" self.assertEqual(str(self.kib), self.kib_str) def test_half_mib_repr(self): """MiB(0.5) looks correct in a terminal""" self.assertEqual(repr(self.half_mib), self.half_mib_repr) def test_half_mib_str(self): """MiB(0.5) looks correct as a string""" self.assertEqual(str(self.half_mib), self.half_mib_str) ################################################################## # Test custom formatting def test_print_two_digits_precision(self): """MiB(1/3.0) prints out with two digits of precision""" expected_result = "0.33MiB" fmt_str = "{value:.2f}{unit}" third_MiB = bitmath.MiB(1 / 3.0) actual_result = third_MiB.format(fmt_str) self.assertEqual(expected_result, actual_result) def test_print_scientific_four_digits_precision(self): """MiB(102.4754) prints out with four digits of precision""" expected_result = "102.5MiB" fmt_str = "{value:.4g}{unit}" third_MiB = bitmath.MiB(102.4754) actual_result = third_MiB.format(fmt_str) self.assertEqual(expected_result, actual_result) def test_longer_formatting_string(self): """KiB(12345) as a MiB (12.0556640625) truncates to 5 digits""" expected_result = "12.05566 MiB" fmt_str = "{value:.5f} {unit}" instance = bitmath.KiB(12345).to_MiB() actual_result = instance.format(fmt_str) self.assertEqual(expected_result, actual_result) def test_change_format_string(self): """KiB(1.0) looks right if changing fmt str in bitmath.KiB NOTE: This does NOT make use of the bitmath.format context manager. There is a separate test suite for that: test_context_manager""" orig_fmt_str = bitmath.format_string bitmath.format_string = "{unit} {value:.3f}" kib = bitmath.KiB(1) self.assertEqual(self.kib_str_changed, str(kib)) bitmath.format_string = orig_fmt_str def test_print_byte_singular(self): """Byte(1.0) prints out units in singular form""" expected_result = "1B" fmt_str = "{value:.2g}{unit}" one_Byte = bitmath.Byte(1.0) actual_result = one_Byte.format(fmt_str) self.assertEqual(expected_result, actual_result) ################################################################## # Test __format__ (PEP 3101 / format() / f-string support) # Original concept from PR #76 by Jonathan Eunice def test_dunder_format_empty_spec_returns_str(self): """__format__ with no spec returns str(instance)""" size = bitmath.MiB(2.847598437) self.assertEqual(format(size, ''), str(size)) def test_dunder_format_fstring_empty_spec(self): """f'{size}' with no spec returns the default string representation""" size = bitmath.KiB(1) self.assertEqual(f'{size}', '1.0 KiB') def test_dunder_format_precision(self): """__format__ with a precision spec formats self.value only""" size = bitmath.MiB(2.847598437) self.assertEqual(format(size, '.1f'), '2.8') def test_dunder_format_fstring_with_unit(self): """f-string with precision spec and explicit unit gives full representation""" size = bitmath.MiB(2.847598437) self.assertEqual(f'{size:.1f} {size.unit}', '2.8 MiB') def test_dunder_format_width_and_precision(self): """__format__ respects width and alignment specs""" size = bitmath.GiB(127.3) self.assertEqual(format(size, '>10.2f'), ' 127.30') def test_dunder_format_columnar_table(self): """__format__ produces correct columnar output across mixed units""" rows = [ (bitmath.GiB(127.3), 'GiB'), (bitmath.MiB(843.7), 'MiB'), ] results = [f'{size:>10.2f} {unit}' for size, unit in rows] self.assertEqual(results[0], ' 127.30 GiB') self.assertEqual(results[1], ' 843.70 MiB') def test_dunder_format_zero_precision(self): """__format__ with .0f formats as integer-looking value""" size = bitmath.KiB(1.9) self.assertEqual(format(size, '.0f'), '2') def test_dunder_format_scientific(self): """__format__ with 'e' spec formats in scientific notation""" size = bitmath.GiB(1) result = format(size, '.2e') self.assertEqual(result, '1.00e+00') timlnx-bitmath-8354f39/tests/test_rich_comparison.py000066400000000000000000000132611520504626100227000ustar00rootroot00000000000000# -*- coding: utf-8 -*- # SPDX-License-Identifier: MIT # The MIT License (MIT) # # SPDX-FileCopyrightText: 2014-2026 Tim Case # # 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, sublicense, 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. """ Test for rich comparison operations: LT, LE, EQ, NE, GT, GE """ from . import TestCase import bitmath class TestRichComparison(TestCase): def setUp(self): self.kib = bitmath.KiB(1) self.mib = bitmath.MiB(1) self.gib = bitmath.GiB(1) ################################################################## # Test bitmath comparisons def test_less_than(self): """One KiB is less than one MiB""" self.assertLess(self.kib, self.mib) def test_less_than_equal(self): """Smaller or equal is less than or equal to one MiB""" # True - 1 KiB is less than 1 MiB self.assertLessEqual(self.kib, self.mib) # True - 1024 KiB is equal to 1 MiB self.assertLessEqual(bitmath.KiB(1024), self.mib) def test_greater_than(self): """1 GiB is greater than 1 Mib""" self.assertGreater(self.gib, self.mib) def test_greater_than_equal(self): """Greater or equal is more than or equal to one MiB""" # True - 1 GiB is greater than 1 MiB self.assertGreaterEqual(self.gib, self.mib) # True - 1024 KiB is equal to 1 MiB self.assertGreaterEqual(self.mib, bitmath.GiB(1 / 1024.0)) ################################################################## # Same tests, but against numbers instead of bitmaths def test_less_than_num(self): """One KiB is less than int(2)""" self.assertLess(self.kib, 2) def test_less_than_equal_num(self): """Smaller or equal is less than or equal to int(2)""" # True - 1 KiB is less than 2 self.assertLessEqual(self.kib, 2) # True - 1024 KiB is equal to 1024 self.assertLessEqual(bitmath.KiB(1024), 1024) def test_greater_than_num(self): """1 GiB is greater than 0.5""" self.assertGreater(self.gib, 0.5) def test_greater_than_equal_num(self): """Greater or equal is more than or equal to int(1)""" # True - 1 GiB is equal to 1 self.assertGreaterEqual(self.gib, 1) # True - 1024 KiB is greater than 0.5 self.assertGreaterEqual(self.mib, 0.5) ################################################################## # Equality testing def test_equal(self): """Two equal values are actually equal""" self.assertEqual(self.mib, bitmath.KiB(1024)) def test_equal_false(self): """Unequal objects aren't equal""" self.assertNotEqual(self.kib, self.gib) ################################################################## # Equality testing against numbers def test_equal_num(self): """Two equal values are actually equal with numbers""" self.assertEqual(self.mib, 1) def test_equal_false_num(self): """Unequal objects aren't equal with numbers""" self.assertNotEqual(self.kib, 42) ################################################################## # Equality for large NIST units (regression for issue #54) # Python 2 had a float vs long comparison bug for these sizes. # Python 3 unifies integers, so these must compare equal. def test_ZiB_equal_direct(self): """ZiB(654) == ZiB(654)""" self.assertEqual(bitmath.ZiB(654), bitmath.ZiB(654)) def test_ZiB_equal_parsed(self): """parse_string('654 ZiB') == ZiB(654)""" self.assertEqual(bitmath.parse_string("654 ZiB"), bitmath.ZiB(654)) def test_YiB_equal_direct(self): """YiB(654) == YiB(654)""" self.assertEqual(bitmath.YiB(654), bitmath.YiB(654)) def test_YiB_equal_parsed(self): """parse_string('654 YiB') == YiB(654)""" self.assertEqual(bitmath.parse_string("654 YiB"), bitmath.YiB(654)) def test_Zib_equal_direct(self): """Zib(654) == Zib(654)""" self.assertEqual(bitmath.Zib(654), bitmath.Zib(654)) def test_Zib_equal_parsed(self): """parse_string('654 Zib') == Zib(654)""" self.assertEqual(bitmath.parse_string("654 Zib"), bitmath.Zib(654)) def test_Yib_equal_direct(self): """Yib(654) == Yib(654)""" self.assertEqual(bitmath.Yib(654), bitmath.Yib(654)) def test_Yib_equal_parsed(self): """parse_string('654 Yib') == Yib(654)""" self.assertEqual(bitmath.parse_string("654 Yib"), bitmath.Yib(654)) def test_ZiB_to_YiB_conversion(self): """1024 ZiB == 1 YiB""" self.assertEqual(bitmath.ZiB(1024).YiB, bitmath.YiB(1)) def test_Zib_to_Yib_conversion(self): """1024 Zib == 1 Yib""" self.assertEqual(bitmath.Zib(1024).Yib, bitmath.Yib(1)) timlnx-bitmath-8354f39/tests/test_rounding.py000066400000000000000000000132071520504626100213460ustar00rootroot00000000000000# -*- coding: utf-8 -*- # SPDX-License-Identifier: MIT # The MIT License (MIT) # # SPDX-FileCopyrightText: 2014-2026 Tim Case # # 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, sublicense, 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. """ Tests for math.floor(), math.ceil(), and round() on bitmath instances """ import math from . import TestCase import bitmath class TestFloor(TestCase): def test_floor_fractional_returns_same_type(self): """math.floor() returns the same unit type""" result = math.floor(bitmath.MiB(1.9)) self.assertIsInstance(result, bitmath.MiB) def test_floor_rounds_down(self): """math.floor() rounds the prefix value down""" self.assertEqual(math.floor(bitmath.MiB(1.9)), bitmath.MiB(1)) def test_floor_whole_number_unchanged(self): """math.floor() on a whole prefix value returns that value""" self.assertEqual(math.floor(bitmath.KiB(3)), bitmath.KiB(3)) def test_floor_negative_rounds_toward_negative_infinity(self): """math.floor() on a negative value rounds toward negative infinity""" self.assertEqual(math.floor(bitmath.GiB(-1.1)), bitmath.GiB(-2)) def test_floor_division_result(self): """math.floor() on a division result produces integer prefix value""" self.assertEqual(math.floor(bitmath.KiB(1) / 3), bitmath.KiB(0)) def test_floor_preserves_unit_across_types(self): """math.floor() works across all unit types""" for unit in [bitmath.Byte, bitmath.KiB, bitmath.MiB, bitmath.GiB, bitmath.kB, bitmath.MB]: result = math.floor(unit(1.7)) self.assertIsInstance(result, unit) self.assertEqual(result, unit(1)) class TestCeil(TestCase): def test_ceil_fractional_returns_same_type(self): """math.ceil() returns the same unit type""" result = math.ceil(bitmath.MiB(1.1)) self.assertIsInstance(result, bitmath.MiB) def test_ceil_rounds_up(self): """math.ceil() rounds the prefix value up""" self.assertEqual(math.ceil(bitmath.MiB(1.1)), bitmath.MiB(2)) def test_ceil_whole_number_unchanged(self): """math.ceil() on a whole prefix value returns that value""" self.assertEqual(math.ceil(bitmath.KiB(3)), bitmath.KiB(3)) def test_ceil_negative_rounds_toward_zero(self): """math.ceil() on a negative value rounds toward zero""" self.assertEqual(math.ceil(bitmath.GiB(-1.9)), bitmath.GiB(-1)) def test_ceil_division_result(self): """math.ceil() on a division result rounds up to next prefix unit""" self.assertEqual(math.ceil(bitmath.KiB(1) / 3), bitmath.KiB(1)) def test_ceil_preserves_unit_across_types(self): """math.ceil() works across all unit types""" for unit in [bitmath.Byte, bitmath.KiB, bitmath.MiB, bitmath.GiB, bitmath.kB, bitmath.MB]: result = math.ceil(unit(1.2)) self.assertIsInstance(result, unit) self.assertEqual(result, unit(2)) class TestRound(TestCase): def test_round_no_ndigits_returns_same_type(self): """round() with no ndigits returns the same unit type""" result = round(bitmath.GiB(3.7)) self.assertIsInstance(result, bitmath.GiB) def test_round_no_ndigits_rounds_to_nearest(self): """round() with no ndigits rounds to the nearest integer prefix value""" self.assertEqual(round(bitmath.GiB(3.7)), bitmath.GiB(4)) self.assertEqual(round(bitmath.GiB(3.2)), bitmath.GiB(3)) def test_round_with_ndigits_returns_same_type(self): """round(x, ndigits) returns the same unit type""" result = round(bitmath.KiB(1.555), 2) self.assertIsInstance(result, bitmath.KiB) def test_round_with_ndigits(self): """round(x, ndigits) rounds to the specified decimal precision""" self.assertEqual(round(bitmath.KiB(1.5), 0), bitmath.KiB(2)) self.assertEqual(round(bitmath.MiB(2.567), 1), bitmath.MiB(2.6)) def test_round_whole_number_unchanged(self): """round() on a whole prefix value returns that value""" self.assertEqual(round(bitmath.MiB(5)), bitmath.MiB(5)) def test_round_negative_value(self): """round() on a negative value rounds to nearest""" self.assertEqual(round(bitmath.GiB(-3.7)), bitmath.GiB(-4)) self.assertEqual(round(bitmath.GiB(-3.2)), bitmath.GiB(-3)) def test_floor_ceil_round_not_equal_for_fractional(self): """floor, ceil, and round give distinct results for fractional values""" val = bitmath.MiB(1.6) self.assertEqual(math.floor(val), bitmath.MiB(1)) self.assertEqual(math.ceil(val), bitmath.MiB(2)) self.assertEqual(round(val), bitmath.MiB(2)) timlnx-bitmath-8354f39/tests/test_sorting.py000066400000000000000000000104071520504626100212050ustar00rootroot00000000000000# -*- coding: utf-8 -*- # SPDX-License-Identifier: MIT # The MIT License (MIT) # # SPDX-FileCopyrightText: 2014-2026 Tim Case # # 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, sublicense, 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. """ Test proper sorting operations """ from . import TestCase import bitmath class TestSorting(TestCase): def test_sort_homogeneous_list(self): """Same types in a list can be sorted properly""" first = bitmath.kB(0) second = bitmath.kB(1337) third = bitmath.kB(2048) fourth = bitmath.kB(96783) # Put the objects into the array in 'random' order unsorted_list = [fourth, second, first, third] sorted_list = sorted(unsorted_list) self.assertIs(sorted_list[0], first) self.assertIs(sorted_list[1], second) self.assertIs(sorted_list[2], third) self.assertIs(sorted_list[3], fourth) def test_sort_heterogeneous_list(self): """Different types in a list can be sorted properly Define these with the bytes keyword so we don't lose our minds trying to figure out if the results are correct.""" first = bitmath.KiB(bytes=0) second = bitmath.GiB(bytes=1337) third = bitmath.Eb(bytes=2048) fourth = bitmath.Byte(bytes=96783) unsorted_list = [fourth, second, first, third] sorted_list = sorted(unsorted_list) self.assertIs(sorted_list[0], first) self.assertIs(sorted_list[1], second) self.assertIs(sorted_list[2], third) self.assertIs(sorted_list[3], fourth) def test_sort_key_bytes(self): """Bitmath types can be sorted by 'bytes' attribute""" first = bitmath.kB(0) second = bitmath.kB(1337) third = bitmath.kB(2048) fourth = bitmath.kB(96783) unsorted_list = [fourth, second, first, third] sorted_list = sorted(unsorted_list, key=lambda x: x.bytes) self.assertIs(sorted_list[0], first) self.assertIs(sorted_list[1], second) self.assertIs(sorted_list[2], third) self.assertIs(sorted_list[3], fourth) def test_sort_key_bits(self): """Bitmath types can be sorted by 'bits' attribute""" first = bitmath.kB(0) second = bitmath.kB(1337) third = bitmath.kB(2048) fourth = bitmath.kB(96783) unsorted_list = [fourth, second, first, third] sorted_list = sorted(unsorted_list, key=lambda x: x.bits) self.assertIs(sorted_list[0], first) self.assertIs(sorted_list[1], second) self.assertIs(sorted_list[2], third) self.assertIs(sorted_list[3], fourth) def test_sort_key_value(self): """Same types can be sorted by 'value' attribute This does not work on heterogeneous collections! The 'value' attribute varies between different bitmath types even if they are of equivalent size because it is the shortened representation of that prefix unit.""" first = bitmath.kB(0) second = bitmath.kB(1337) third = bitmath.kB(2048) fourth = bitmath.kB(96783) unsorted_list = [fourth, second, first, third] sorted_list = sorted(unsorted_list, key=lambda x: x.value) self.assertIs(sorted_list[0], first) self.assertIs(sorted_list[1], second) self.assertIs(sorted_list[2], third) self.assertIs(sorted_list[3], fourth) timlnx-bitmath-8354f39/tests/test_sum.py000066400000000000000000000060221520504626100203220ustar00rootroot00000000000000# -*- coding: utf-8 -*- # SPDX-License-Identifier: MIT # The MIT License (MIT) # # SPDX-FileCopyrightText: 2014-2026 Tim Case # # 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, sublicense, 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. """ Tests for bitmath.sum() - issue #103 """ from . import TestCase import builtins import bitmath class TestSum(TestCase): def test_sum_mixed_units_equals_manual_addition(self): """bitmath.sum() matches manual chained addition for mixed units""" items = [bitmath.Byte(1), bitmath.MiB(1), bitmath.GiB(1)] expected = bitmath.Byte(1) + bitmath.MiB(1) + bitmath.GiB(1) self.assertEqual(bitmath.sum(items), expected) def test_sum_returns_bitmath_instance(self): """bitmath.sum() returns a bitmath instance, not a float""" result = bitmath.sum([bitmath.Byte(1), bitmath.MiB(1), bitmath.GiB(1)]) self.assertIsInstance(result, bitmath.Bitmath) def test_sum_builtin_now_works(self): """built-in sum() now gives correct result via __radd__ identity fix""" items = [bitmath.Byte(1), bitmath.MiB(1), bitmath.GiB(1)] self.assertEqual(builtins.sum(items), bitmath.sum(items)) def test_sum_empty_iterable_returns_byte_zero(self): """bitmath.sum([]) returns Byte(0) by default""" result = bitmath.sum([]) self.assertEqual(result, bitmath.Byte(0)) def test_sum_empty_iterable_with_custom_start(self): """bitmath.sum([], start=MiB(0)) returns the start value""" result = bitmath.sum([], start=bitmath.MiB(0)) self.assertEqual(result, bitmath.MiB(0)) def test_sum_custom_start_unit(self): """bitmath.sum() with a custom start unit returns that unit type""" result = bitmath.sum([bitmath.KiB(1), bitmath.KiB(1)], start=bitmath.MiB(0)) self.assertIsInstance(result, bitmath.MiB) def test_sum_same_units(self): """bitmath.sum() works correctly for a list of same-unit instances""" result = bitmath.sum([bitmath.KiB(1), bitmath.KiB(2), bitmath.KiB(3)]) self.assertEqual(result, bitmath.KiB(6)) timlnx-bitmath-8354f39/tests/test_to_Type_conversion.py000066400000000000000000000247251520504626100234200ustar00rootroot00000000000000# -*- coding: utf-8 -*- # SPDX-License-Identifier: MIT # The MIT License (MIT) # # SPDX-FileCopyrightText: 2014-2026 Tim Case # # 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, sublicense, 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. """ Test to verify the bitmath.Type.to_Type() conversions work """ from . import TestCase import bitmath class TestToTypeConversion(TestCase): def setUp(self): self.bit = bitmath.Bit(1) self.byte = bitmath.Byte(1) # NIST units self.kib = bitmath.KiB(1) self.mib = bitmath.MiB(1) self.gib = bitmath.GiB(1) self.tib = bitmath.TiB(1) self.pib = bitmath.PiB(1) self.eib = bitmath.EiB(1) # SI units self.kb = bitmath.kB(1) self.mb = bitmath.MB(1) self.gb = bitmath.GB(1) self.tb = bitmath.TB(1) self.pb = bitmath.PB(1) self.eb = bitmath.EB(1) self.zb = bitmath.ZB(1) self.yb = bitmath.YB(1) def test_to_same_unit(self): """bitmath type converted to the same unit is properly converted""" to_kib = self.kib.to_KiB() self.assertIs(type(to_kib), bitmath.KiB) self.assertIs(type(self.kib.KiB), bitmath.KiB) def test_from_other(self): """MiB object from_other object""" mib_from_kib = bitmath.MiB.from_other(bitmath.KiB(1)) self.assertIs(type(mib_from_kib), bitmath.MiB) ################################################################## # to b's def test_to_Bit(self): """Convert to Bit""" self.assertIs(type(self.kb.to_Bit()), bitmath.Bit) self.assertIs(type(self.kb.Bit), bitmath.Bit) def test_to_Byte(self): """Convert to Byte""" self.assertIs(type(self.kb.to_Byte()), bitmath.Byte) self.assertIs(type(self.kb.Byte), bitmath.Byte) ################################################################## # to k's def test_to_KiB(self): """Convert to KiB""" to_kib = self.mib.to_KiB() self.assertIs(type(to_kib), bitmath.KiB) self.assertIs(type(self.mib.KiB), bitmath.KiB) def test_to_Kib(self): """Convert to Kib""" to_kib = self.mib.to_Kib() self.assertIs(type(to_kib), bitmath.Kib) self.assertIs(type(self.mib.Kib), bitmath.Kib) def test_to_kb(self): """Convert to kb""" to_kb = self.mib.to_kb() self.assertIs(type(to_kb), bitmath.kb) self.assertIs(type(self.mib.kb), bitmath.kb) ################################################################## # to m's def test_to_MiB(self): """Convert a bitmath GiB into a MiB""" to_mib = self.gib.to_MiB() self.assertIs(type(to_mib), bitmath.MiB) self.assertIs(type(self.gib.MiB), bitmath.MiB) def test_to_Mib(self): """Convert a bitmath GiB into a Mib""" to_mib = self.gib.to_Mib() self.assertIs(type(to_mib), bitmath.Mib) self.assertIs(type(self.gib.Mib), bitmath.Mib) def test_to_MB(self): """Convert a bitmath GiB into a MB""" to_mb = self.gib.to_MB() self.assertIs(type(to_mb), bitmath.MB) self.assertIs(type(self.gib.MB), bitmath.MB) def test_to_Mb(self): """Convert a bitmath GiB into a Mb""" to_mb = self.gib.to_Mb() self.assertIs(type(to_mb), bitmath.Mb) self.assertIs(type(self.gib.Mb), bitmath.Mb) ################################################################## # to g's def test_to_GiB(self): """Convert a bitmath TiB into a GiB""" to_gib = self.tib.to_GiB() self.assertIs(type(to_gib), bitmath.GiB) self.assertIs(type(self.tib.GiB), bitmath.GiB) def test_to_Gib(self): """Convert a bitmath GiB into a Gib""" to_gib = self.gib.to_Gib() self.assertIs(type(to_gib), bitmath.Gib) self.assertIs(type(self.tib.Gib), bitmath.Gib) def test_to_GB(self): """Convert a bitmath GiB into a GB""" to_gb = self.gib.to_GB() self.assertIs(type(to_gb), bitmath.GB) self.assertIs(type(self.tib.GB), bitmath.GB) def test_to_Gb(self): """Convert a bitmath GiB into a Gb""" to_gb = self.gib.to_Gb() self.assertIs(type(to_gb), bitmath.Gb) self.assertIs(type(self.tib.Gb), bitmath.Gb) ################################################################## # to t's def test_to_TiB(self): """Convert a bitmath PiB into a TiB""" to_tib = self.pib.to_TiB() self.assertIs(type(to_tib), bitmath.TiB) self.assertIs(type(self.pib.TiB), bitmath.TiB) def test_to_Tib(self): """Convert a bitmath GiB into a Tib""" to_tib = self.gib.to_Tib() self.assertIs(type(to_tib), bitmath.Tib) self.assertIs(type(self.pib.Tib), bitmath.Tib) def test_to_TB(self): """Convert a bitmath GiB into a TB""" to_tb = self.gib.to_TB() self.assertIs(type(to_tb), bitmath.TB) self.assertIs(type(self.pib.TB), bitmath.TB) def test_to_Tb(self): """Convert a bitmath GiB into a Tb""" to_tb = self.gib.to_Tb() self.assertIs(type(to_tb), bitmath.Tb) self.assertIs(type(self.pib.Tb), bitmath.Tb) ################################################################## # to p's def test_to_PiB(self): """Convert a bitmath TiB into a PiB""" to_pib = self.tib.to_PiB() self.assertIs(type(to_pib), bitmath.PiB) self.assertIs(type(self.tib.PiB), bitmath.PiB) def test_to_Pib(self): """Convert a bitmath GiB into a PiB""" to_pib = self.gib.to_Pib() self.assertIs(type(to_pib), bitmath.Pib) self.assertIs(type(self.gib.Pib), bitmath.Pib) def test_to_PB(self): """Convert a bitmath GiB into a PB""" to_pb = self.gib.to_PB() self.assertIs(type(to_pb), bitmath.PB) self.assertIs(type(self.gib.PB), bitmath.PB) def test_to_Pb(self): """Convert a bitmath GiB into a Pb""" to_pb = self.gib.to_Pb() self.assertIs(type(to_pb), bitmath.Pb) self.assertIs(type(self.gib.Pb), bitmath.Pb) ################################################################## # to e's def test_to_EiB(self): """Convert a bitmath PiB into a EiB""" to_eib = self.pib.to_EiB() self.assertIs(type(to_eib), bitmath.EiB) self.assertIs(type(self.pib.EiB), bitmath.EiB) def test_to_Eib(self): """Convert a bitmath GiB into a Eib""" to_eib = self.gib.to_Eib() self.assertIs(type(to_eib), bitmath.Eib) self.assertIs(type(self.pib.Eib), bitmath.Eib) def test_to_EB(self): """Convert a bitmath GiB into a EB""" to_eb = self.gib.to_EB() self.assertIs(type(to_eb), bitmath.EB) self.assertIs(type(self.pib.EB), bitmath.EB) def test_to_Eb(self): """Convert a bitmath GiB into a Eb""" to_eb = self.gib.to_Eb() self.assertIs(type(to_eb), bitmath.Eb) self.assertIs(type(self.pib.Eb), bitmath.Eb) ################################################################## # to z's def test_to_ZB(self): """Convert a bitmath GiB into a ZB""" to_zb = self.gib.to_ZB() self.assertIs(type(to_zb), bitmath.ZB) self.assertIs(type(self.pib.ZB), bitmath.ZB) def test_to_Zb(self): """Convert a bitmath GiB into a Zb""" to_zb = self.gib.to_Zb() self.assertIs(type(to_zb), bitmath.Zb) self.assertIs(type(self.pib.Zb), bitmath.Zb) ################################################################## # to y's def test_to_YB(self): """Convert a bitmath GiB into a YB""" to_yb = self.gib.to_YB() self.assertIs(type(to_yb), bitmath.YB) self.assertIs(type(self.pib.YB), bitmath.YB) def test_to_Yb(self): """Convert a bitmath GiB into a Yb""" to_yb = self.gib.to_Yb() self.assertIs(type(to_yb), bitmath.Yb) self.assertIs(type(self.pib.Yb), bitmath.Yb) ################################################################## # to other stuff def test_to_mib_from_bit(self): """Convert a bitmath Bit into a MiB""" to_mib = self.bit.to_MiB() self.assertIs(type(to_mib), bitmath.MiB) def test_converted_up_bitmath_value_equivalency(self): """Converted up type has an equivalent value to the original""" # Take a KiB, make a MiB from it. Test their equality to_mib = self.kib.to_MiB() self.assertEqual(self.kib, to_mib) # Two tests, to be "thorough" to_gib = self.mib.to_GiB() self.assertEqual(self.mib, to_gib) def test_converted_down_bitmath_value_equivalency(self): """Converted down type has an equivalent value to the original""" # Take a MiB, make a KiB from it. Test their equality to_kib = self.mib.to_KiB() self.assertEqual(to_kib, self.mib) def test_convert_nist_to_si(self): """Convert an NIST unit into an SI unit""" kb_from_kib = self.kib.to_kB() self.assertIs(type(kb_from_kib), bitmath.kB) def test_convert_si_to_nist(self): """Convert an SI unit into an NIST unit""" kib_from_kb = self.kb.to_KiB() self.assertIs(type(kib_from_kb), bitmath.KiB) ################################################################## # Naughty bad bad test cases def test_from_other_bad_input(self): """from_other raises if "other" isn't a bitmath instance""" with self.assertRaises(ValueError): bitmath.Byte.from_other(str("not a bitmath instance!")) timlnx-bitmath-8354f39/tests/test_to_built_in_conversion.py000066400000000000000000000031671520504626100243010ustar00rootroot00000000000000# -*- coding: utf-8 -*- # SPDX-License-Identifier: MIT # The MIT License (MIT) # # SPDX-FileCopyrightText: 2014-2026 Tim Case # # 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, sublicense, 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. """ Test to verify the int/float conversions work correctly """ from . import TestCase import bitmath class TestToBuiltInConversion(TestCase): def test_to_int(self): """int(bitmath) returns an int""" gib = bitmath.GiB(1337.8) self.assertIs(type(int(gib)), int) def test_to_float(self): """float(bitmath) returns a float""" gib = bitmath.GiB(1337.8) self.assertIs(type(float(gib)), float) timlnx-bitmath-8354f39/tests/test_unique_testcase_names.sh000077500000000000000000000004721520504626100240720ustar00rootroot00000000000000#!/bin/bash grep "^class" tests/*.py | awk '{ arr[$NF]++ } END { for (word in arr) { if (arr[word] != "1") { printf "DUPLICATES: %s %d\n", word, arr[word] } } }' | grep -q "DUPLICATES" if (( ! $? )); then echo "Error: Duplicate TestCase class names found" exit 1 fi timlnx-bitmath-8354f39/tests/test_utils.py000066400000000000000000000043421520504626100206610ustar00rootroot00000000000000# -*- coding: utf-8 -*- # SPDX-License-Identifier: MIT # The MIT License (MIT) # # SPDX-FileCopyrightText: 2014-2026 Tim Case # # 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, sublicense, 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. """ Test utility functions """ from . import TestCase import bitmath class TestUtils(TestCase): def test_capitalize_first_normal_word(self): """capitalize_first upcases just the first letter in a word""" word1 = "foo" expected1 = "Foo" self.assertEqual( bitmath.capitalize_first(word1), expected1) word2 = "foO" expected2 = "FoO" self.assertEqual( bitmath.capitalize_first(word2), expected2) def test_capitalize_first_starts_with_number(self): """capitalize_first doesn't change anything if the input begins with a number""" word = "1foo" expected = "1foo" self.assertEqual( bitmath.capitalize_first(word), expected) def test_capitalize_first_already_capped(self): """capitalize_first doesn't change anything if the input is already correct""" word = "Foo" expected = "Foo" self.assertEqual( bitmath.capitalize_first(word), expected)