diff --git a/.coveragerc b/.coveragerc index af76843..de2c8e3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,6 +1,5 @@ # -*- mode: conf -*- [run] -include = bitmath/__init__.py,bitmath/integrations.py [report] exclude_lines = # These should be impossible to trigger. @@ -14,11 +13,7 @@ exclude_lines = # Explicitly label a line as 'do not cover' pragma: no cover - # Stuff to skip only on Python 2.x. Insert a line like:" # pragma: - # PY2X no cover" And export the env var "PYVER" as "PY2X". To skip - # certain things on only Py3.x export PYVER as PY3X - # - # Review: http://nedbatchelder.com/code/coverage/config.html#h_Syntax if you need a refresher + # 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): # diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE/bug_report.md similarity index 100% rename from .github/ISSUE_TEMPLATE.md rename to .github/ISSUE_TEMPLATE/bug_report.md diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..f3a9fe7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,21 @@ +--- +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. + diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..0450a53 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,37 @@ +name: CodeQL Security Scan + +on: + push: + branches: ["master", "bitmath2"] + pull_request: + branches: ["master"] + schedule: + - cron: "0 0 * * 0" + +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@v6.0.2 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: python + + - name: Autobuild + uses: github/codeql-action/autobuild@v4 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 + with: + category: "/language:python" diff --git a/.github/workflows/makefile.yml b/.github/workflows/makefile.yml deleted file mode 100644 index dcdffd3..0000000 --- a/.github/workflows/makefile.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Makefile CI - -on: - push: - branches: [ "master", "2023-01-26-no-more-py2" ] - pull_request: - branches: [ "master", "2023-01-26-no-more-py2" ] - -jobs: - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - -# - name: configure -# run: ./configure - -# - name: Install dependencies -# run: make - - - name: Run unittests - run: make ci - -# - name: Run distcheck -# run: make distcheck diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml new file mode 100644 index 0000000..88bdca8 --- /dev/null +++ b/.github/workflows/python.yml @@ -0,0 +1,49 @@ +name: Python CI + +on: [push, pull_request] + +jobs: + build: + permissions: + pull-requests: write + env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + strategy: + matrix: + python-version: ["3.11", "3.12", "3.13"] + os: ["macos-latest", "ubuntu-latest"] + runs-on: ${{ matrix.os }} + steps: + - name: "GitHub Checks it out :sunglasses-face:" + uses: actions/checkout@v6.0.2 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + + - name: Verify Test Case Names Unique + 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,E722 bitmath/__init__.py tests/*.py + flake8 --select=F bitmath/__init__.py tests/*.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 + + - name: Coverage report on PR + if: github.event_name == 'pull_request' && matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12' + uses: MishaKav/pytest-coverage-comment@main + with: + pytest-xml-coverage-path: ./coverage.xml + title: "Test Coverage Report" diff --git a/.gitignore b/.gitignore index a9034a3..0229c8e 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,7 @@ pip-log.txt # Unit test / coverage reports .coverage +coverage.xml .tox nosetests.xml @@ -51,3 +52,4 @@ docsite/build/html docsite/build/doctrees bitmathenv3 bitmathenv2 +bitmath2 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 5c24c81..7b5c27a 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -34,7 +34,7 @@ This Code of Conduct applies both within project spaces and in public spaces whe ## Enforcement -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at timbielawa@gmail.com. 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. +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. diff --git a/LICENSE b/LICENSE index 69fe7b7..39eb0c5 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright © 2014 Tim Bielawa +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 diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 3e99086..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,4 +0,0 @@ -include README.rst LICENSE bitmath.1 NEWS.rst -graft bitmath -graft tests -recursive-include docsite/source *.rst diff --git a/Makefile b/Makefile index 531f8e4..b6647b4 100644 --- a/Makefile +++ b/Makefile @@ -3,11 +3,12 @@ # Makefile for bitmath # # useful targets: -# make sdist ---------------- produce a tarball +# 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 manpages (results are checked in) -# make pyflakes, make pycodestyle -- source code checks -# make test ----------------- run all unit tests (export LOG=true for /tmp/ logging) +# make docs ----------------- rebuild the docs +# make ci ----------------- run full test suite in virtualenv ######################################################## @@ -45,8 +46,17 @@ MANPAGES := bitmath.1 ###################################################################### # Documentation. YAY!!!! -docs: conf.py $(MANPAGES) docsite/source/index.rst - cd docsite; make html; cd - +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 conf.py $(MANPAGES) docsite/source/index.rst + . $(DOCSVENV)/bin/activate && cd docsite && make html # Add examples to the RTD docs by taking it from the README docsite/source/index.rst: docsite/source/index.rst.in README.rst VERSION @@ -55,7 +65,6 @@ docsite/source/index.rst: docsite/source/index.rst.in README.rst VERSION @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 @@ -70,10 +79,20 @@ docsite/source/index.rst: docsite/source/index.rst.in README.rst VERSION $(ASCII2MAN) viewdocs: docs - xdg-open docsite/build/html/index.html - -viewcover: - xdg-open cover/index.html + @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 conf.py: docsite/source/conf.py.in sed "s/%VERSION%/$(VERSION)/" $< > docsite/source/conf.py @@ -83,47 +102,32 @@ conf.py: docsite/source/conf.py.in python-bitmath.spec: python-bitmath.spec.in sed "s/%VERSION%/$(VERSION)/" $< > $@ -# Build the distutils setup file on the fly. -setup.py: setup.py.in VERSION python-bitmath.spec.in - sed -e "s/%VERSION%/$(VERSION)/" -e "s/%RELEASE%/$(RPMRELEASE)/" $< > $@ +build: clean + @echo "#############################################" + @echo "# Building sdist + wheel" + @echo "#############################################" + python -m build -# Upload sources to pypi/pypi-test -pypi: - python ./setup.py sdist upload +pypi: build + @echo "#############################################" + @echo "# Uploading to PyPI" + @echo "#############################################" + twine upload dist/* -pypitest: - python ./setup.py sdist upload -r test +pypitest: build + @echo "#############################################" + @echo "# Uploading to TestPyPI" + @echo "#############################################" + twine upload --repository testpypi dist/* # usage example: make tag TAG=1.1.0-1 tag: git tag -s -m $(TAG) $(TAG) -tests: uniquetestnames unittests pycodestyle pyflakes - : - -unittests: - @echo "#############################################" - @echo "# Running Unit Tests" - @echo "#############################################" - nosetests -v --with-coverage --cover-html --cover-package=bitmath --cover-min-percentage=90 - 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 bitmathenv bitmathenv2 bitmathenv3 docsite/build/html/ docsite/build/doctrees/ bitmath.egg-info - -pycodestyle: - @echo "#############################################" - @echo "# Running PEP8 Compliance Tests" - @echo "#############################################" - pycodestyle -v --ignore=E501,E722 bitmath/__init__.py tests/*.py - -pyflakes: - @echo "#############################################" - @echo "# Running Pyflakes Sanity Tests" - @echo "# Note: most import errors may be ignored" - @echo "#############################################" - -pyflakes bitmath/__init__.py tests/*.py + @rm -fR build cover dist rpm-build MANIFEST htmlcov .coverage bitmathenv3 docsite/build/html/ docsite/build/doctrees/ bitmath.egg-info uniquetestnames: @echo "#############################################" @@ -131,47 +135,24 @@ uniquetestnames: @echo "#############################################" ./tests/test_unique_testcase_names.sh -install: clean - python ./setup.py install +install: + pip install . mkdir -p /usr/share/man/man1/ gzip -9 -c bitmath.1 > /usr/share/man/man1/bitmath.1.gz -sdist: setup.py clean +sdist: clean @echo "#############################################" @echo "# Creating SDIST" @echo "#############################################" - python setup.py sdist - -deb: setup.py clean - git archive --format=tar --prefix=bitmath/ HEAD | gzip -9 > ../bitmath_$(VERSION).$(RPMRELEASE).orig.tar.gz - debuild -us -uc + python -m build --sdist -rpmcommon: sdist python-bitmath.spec setup.py +rpmcommon: sdist python-bitmath.spec @echo "#############################################" @echo "# Building (S)RPM Now" @echo "#############################################" @mkdir -p rpm-build @cp dist/$(NAME)-$(VERSION).$(RPMRELEASE).tar.gz rpm-build/$(VERSION).$(RPMRELEASE).tar.gz -srpm5: rpmcommon - rpmbuild --define "_topdir %(pwd)/rpm-build" \ - --define 'dist .el5' \ - --define "_builddir %{_topdir}" \ - --define "_rpmdir %{_topdir}" \ - --define "_srcrpmdir %{_topdir}" \ - --define "_specdir $(RPMSPECDIR)" \ - --define "_sourcedir %{_topdir}" \ - --define "_source_filedigest_algorithm 1" \ - --define "_binary_filedigest_algorithm 1" \ - --define "_binary_payload w9.gzdio" \ - --define "_source_payload w9.gzdio" \ - --define "_default_patch_fuzz 2" \ - -bs $(RPMSPEC) - @echo "#############################################" - @echo "$(PKGNAME) SRPM is built:" - @find rpm-build -maxdepth 2 -name '$(PKGNAME)*src.rpm' | awk '{print " " $$1}' - @echo "#############################################" - srpm: rpmcommon rpmbuild --define "_topdir %(pwd)/rpm-build" \ --define "_builddir %{_topdir}" \ @@ -198,88 +179,44 @@ rpm: rpmcommon @find rpm-build -maxdepth 2 -name '$(PKGNAME)*.rpm' | awk '{print " " $$1}' @echo "#############################################" -virtualenv2: - @echo "#############################################" - @echo "# Creating a virtualenv" - @echo "#############################################" - virtualenv $(NAME)env2 --python=python2 - . $(NAME)env2/bin/activate && pip install -r requirements.txt - -ci-unittests2: - @echo "#############################################" - @echo "# Running Unit Tests in virtualenv" - @echo "# Using python: $(shell ./bitmathenv2/bin/python --version 2>&1)" - @echo "#############################################" - . $(NAME)env2/bin/activate && export PYVER=PY2X && nosetests -v --with-coverage --cover-html --cover-min-percentage=90 --cover-package=bitmath tests/ - @echo "Testing argparse integration without progressbar dependency (#86)" - . $(NAME)env2/bin/activate && pip uninstall -y progressbar231 click - . $(NAME)env2/bin/activate && export PYVER=PY2X && nosetests -v --with-coverage --cover-html --cover-min-percentage=90 --cover-package=bitmath tests/test_argparse_type.py - -ci-list-deps2: - @echo "#############################################" - @echo "# Listing all pip deps" - @echo "#############################################" - . $(NAME)env2/bin/activate && pip freeze - -ci-pycodestyle2: - @echo "#############################################" - @echo "# Running PEP8 Compliance Tests in virtualenv" - @echo "#############################################" - . $(NAME)env2/bin/activate && pycodestyle -v --ignore=E501,E722 bitmath/__init__.py tests/*.py - -ci-pyflakes2: - @echo "#################################################" - @echo "# Running Pyflakes Compliance Tests in virtualenv" - @echo "#################################################" - . $(NAME)env2/bin/activate && pyflakes bitmath/__init__.py tests/*.py - -ci2: clean uniquetestnames virtualenv2 ci-list-deps2 ci-pycodestyle2 ci-pyflakes2 ci-unittests2 - : - -virtualenv3: +virtualenv: @echo "" @echo "#############################################" @echo "# Creating a virtualenv" @echo "#############################################" - virtualenv $(NAME)env3 --python=python3 - . $(NAME)env3/bin/activate && pip install -r requirements-py3.txt + @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-unittests3: +ci-unittests: virtualenv @echo "" @echo "#############################################" @echo "# Running Unit Tests in virtualenv" - @echo "# Using python: $(shell ./bitmathenv3/bin/python --version 2>&1)" + @echo "# Using python: $(shell ./$(NAME)env3/bin/python --version 2>&1)" @echo "#############################################" - . $(NAME)env3/bin/activate && export PYVER=PY3X && nosetests -v --with-coverage --cover-html --cover-package=bitmath tests/ - @echo "Testing argparse integration without progressbar dependency (#86)" - . $(NAME)env3/bin/activate && pip uninstall -y progressbar33 click - . $(NAME)env3/bin/activate && export PYVER=PY3X && nosetests -v --with-coverage --cover-html --cover-package=bitmath tests/test_argparse_type.py + . $(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-deps3: +ci-list-deps: @echo "" @echo "#############################################" @echo "# Listing all pip deps" @echo "#############################################" . $(NAME)env3/bin/activate && pip freeze -ci-pycodestyle3: +ci-pycodestyle: @echo "" @echo "#############################################" @echo "# Running PEP8 Compliance Tests in virtualenv" @echo "#############################################" . $(NAME)env3/bin/activate && pycodestyle -v --ignore=E501,E722 bitmath/__init__.py tests/*.py -ci-pyflakes3: +ci-flake8: @echo "" @echo "#################################################" - @echo "# Running Pyflakes Compliance Tests in virtualenv" + @echo "# Running Flake8 Compliance Tests in virtualenv" @echo "#################################################" - . $(NAME)env3/bin/activate && pyflakes bitmath/__init__.py tests/*.py + . $(NAME)env3/bin/activate && flake8 --select=F bitmath/__init__.py tests/*.py -ci3: clean uniquetestnames virtualenv3 ci-list-deps3 ci-pycodestyle3 ci-pyflakes3 ci-unittests3 +ci: clean uniquetestnames virtualenv ci-list-deps ci-pycodestyle ci-flake8 ci-unittests : - -ci: ci2 - : - -ci-all: ci2 ci3 diff --git a/NEWS.rst b/NEWS.rst index 9f83be2..843998b 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -5,6 +5,161 @@ NEWS :depth: 1 :local: +.. _bitmath-2.0.0: + +bitmath-2.0.0 +************* + +*Released: April 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.11 or later and quietly wishing it felt more modern — this release +is for you. + + +Breaking Changes +================ + +**Python support** + Python 3.11+ only. Python 2 and Python 3.7–3.10 are no longer + supported or tested. + +**parse_string() default system** + The default unit system when ``strict=False`` is now **NIST** + (binary). Previously it defaulted to SI. Code that relied on the + old default for ambiguous strings such as ``"1g"`` will get a + different result. See :ref:`parse-string-non-strict` for full details. + +**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. + +**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-normalised 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 + `_. + +**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 + `_. + + +Project Infrastructure +====================== + +The project infrastructure has been rebuilt to reflect how Python +projects are actually maintained in 2026: + +**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.11, 3.12, and 3.13 on both Ubuntu and + macOS, with actions pinned to current versions (``checkout@v4``, + ``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. + + +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 sits at 288 tests and 100% coverage. The +documentation has been comprehensively reviewed and updated. The +packaging is clean enough to pass ``twine check`` on the first attempt +(well, the second). + +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 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: @@ -44,7 +199,7 @@ bitmath-1.3.3-1 *************** `bitmath-1.3.3-1 -`__ was +`__ was published on 2018-08-23. @@ -58,7 +213,7 @@ 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. @@ -73,7 +228,7 @@ Changes `Alexander Kapshuna `_ has submitted `several fixes -`_ +`_ since the last release. Thanks! * Packaging requirements fixes @@ -81,11 +236,11 @@ since the last release. Thanks! * Subclassing and Type checking fixes/improvements `Marcus Kazmierczak `_ submitted `a fix -`_ for some broken +`_ for some broken documentation links. And `Dawid Gosławski `_ make sure our -`documentation `_ +`documentation `_ is accurate. @@ -98,7 +253,7 @@ bitmath-1.3.1-1 *************** `bitmath-1.3.1-1 -`__ was +`__ was published on 2016-07-17. Changes @@ -113,7 +268,7 @@ Changes * Inspired by `@darkblaze69 `_'s request in `#60 "Problems in parse_string" - `_. + `_. Project @@ -126,12 +281,12 @@ Project * Ubuntu builds inspired by `@hkraal `_ reporting an `installation issue - `_ on Ubuntu systems. + `_ on Ubuntu systems. **Documentation** -* `Cleaned up a lot `_ +* `Cleaned up a lot `_ of broken or re-directing links using output from the Sphinx ``make linkcheck`` command. @@ -142,7 +297,7 @@ bitmath-1.3.0-1 *************** `bitmath-1.3.0-1 -`__ was +`__ was published on 2016-01-08. Changes @@ -151,7 +306,7 @@ Changes **Bug Fixes** * Closed `GitHub Issue #55 - `_ "best_prefix for + `_ "best_prefix for negative values". Now :func:`bitmath.best_prefix` returns correct prefix units for negative values. Thanks `mbdm `_! @@ -163,7 +318,7 @@ bitmath-1.2.4-1 *************** `bitmath-1.2.4-1 -`__ was +`__ was published on 2015-11-30. Changes @@ -177,17 +332,17 @@ Changes * The :func:`bitmath.parse_string` function now can parse 'octet' based units. Enhancement requested in `#53 parse french unit names - `_ by `walidsa3d + `_ by `walidsa3d `_. **Bug Fixes** -* `#49 `_ - Fix handling +* `#49 `_ - Fix handling unicode input in the `bitmath.parse_string `__ function. Thanks `drewbrew `_! -* `#50 `_ - Update the +* `#50 `_ - Update the ``setup.py`` script to be python3.x compat. Thanks `ssut `_! @@ -221,7 +376,7 @@ bitmath-1.2.3-1 *************** `bitmath-1.2.3-1 -`__ was +`__ was published on 2015-01-03. Changes @@ -264,7 +419,7 @@ bitmath-1.2.0-1 *************** `bitmath-1.2.0-1 -`__ was +`__ was published on 2014-12-29. Changes @@ -280,7 +435,7 @@ Documentation ============= * The command-line ``bitmath`` tool now has a `proper manpage - `_ + `_ Project ======= @@ -297,10 +452,10 @@ bitmath-1.1.0-1 *************** `bitmath-1.1.0-1 -`_ was +`_ was published on 2014-12-20. -* `GitHub Milestone Tracker for 1.1.0 `_ +* `GitHub Milestone Tracker for 1.1.0 `_ Changes ======= @@ -308,12 +463,12 @@ Changes **Added Functionality** * New ``bitmath`` `command-line tool - `_ added. Provides + `_ 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 + submitted `_ by new contributor `tonycpsu `_. .. _bitmath-1.0.8-1: @@ -322,10 +477,10 @@ bitmath-1.0.5-1 through 1.0.8-1 ******************************* `bitmath-1.0.8-1 -`__ was +`__ was published on 2014-08-14. -* `GitHub Milestone Tracker for 1.0.8 `_ +* `GitHub Milestone Tracker for 1.0.8 `_ Major Updates ============= @@ -339,17 +494,17 @@ Major Updates (`pkg info `_) * merged 6 `pull requests - `_ + `_ from 3 `contributors - `_ + `_ Bug Fixes ========= * fixed some math implementation bugs - * `commutative multiplication `_ - * `true division `_ + * `commutative multiplication `_ + * `true division `_ Changes ======= @@ -384,13 +539,13 @@ 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 + `_ is stable around 95-100% @@ -400,7 +555,7 @@ bitmath-1.0.4-1 *************** This is the first release of **bitmath**. `bitmath-1.0.4-1 -`__ was +`__ was published on 2014-03-20. Project diff --git a/README.rst b/README.rst index 2a60c1d..a5776b2 100644 --- a/README.rst +++ b/README.rst @@ -1,21 +1,38 @@ -.. image:: https://api.travis-ci.org/tbielawa/bitmath.png - :target: https://travis-ci.org/tbielawa/bitmath/ - :align: right - :height: 19 - :width: 77 - -.. image:: https://coveralls.io/repos/tbielawa/bitmath/badge.png?branch=master - :target: https://coveralls.io/r/tbielawa/bitmath?branch=master - :align: right - :height: 19 - :width: 77 - .. image:: https://readthedocs.org/projects/bitmath/badge/?version=latest :target: http://bitmath.rtfd.org/ :align: right :height: 19 :width: 77 +.. image:: https://github.com/timlnx/bitmath/actions/workflows/python.yml/badge.svg + :target: https://github.com/timlnx/bitmath/actions/workflows/python.yml + +.. image:: https://img.shields.io/github/issues/timlnx/bitmath?style=flat-square + :target: https://github.com/timlnx/bitmath/issues + :alt: Open issues + +.. image:: https://img.shields.io/github/issues-pr/timlnx/bitmath?style=flat-square + :target: https://github.com/timlnx/bitmath/pulls + :alt: Open pull requests + +.. image:: https://img.shields.io/pypi/dm/bitmath?style=flat-square + :target: https://pypistats.org/packages/bitmath + :alt: PyPI - Package Downloads + +.. image:: https://img.shields.io/github/stars/timlnx/bitmath?style=flat-square + :target: https://pypistats.org/packages/bitmath + :alt: GitHub Project Popularity + +.. image:: https://img.shields.io/pypi/l/bitmath?style=flat-square + :target: https://opensource.org/licenses/MIT + :alt: PyPI - License + +.. image:: https://img.shields.io/pypi/implementation/bitmath?style=flat-square + :alt: PyPI - Implementation + +.. image:: https://img.shields.io/pypi/pyversions/bitmath?style=flat-square + :alt: PyPI - Python Version + bitmath ======= @@ -26,19 +43,19 @@ 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) * Rich comparison operations (``1024 Bytes == 1KiB``) -* bitwise operations (``<<``, ``>>``, ``&``, ``|``, ``^``) -* Reading a device's storage capacity (Linux/OS X support only) -* `argparse `_ - integration as a custom type -* `click `_ - integration as a custom parameter type -* `progressbar `_ - integration as a better file transfer speed widget -* String parsing +* 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 @@ -46,7 +63,7 @@ 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 = @@ -55,14 +72,14 @@ 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's almost 200 unittests? `Check them out for -yourself `_. +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-all`` target -in the Makefile: ``make ci-all``. Please file a bug report if you run -into issues. +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. @@ -72,34 +89,16 @@ Installation The easiest way to install bitmath is via ``dnf`` (or ``yum``) if you're on a Fedora/RHEL based distribution. bitmath is available in -the main Fedora repositories, as well as the `EPEL6 -`_ -and `EPEL7 -`_ -repositories. There are now dual python2.x and python3.x releases -available. - - -**Python 2.x**: - -.. code-block:: bash - - $ sudo dnf install python2-bitmath +the main Fedora repositories, as well as EPEL Repositories. As of 2023 +bitmath is only developed, tested, and supported for `currently +supported `_ Python releases. -**Python 3.x**: .. code-block:: bash $ sudo dnf install python3-bitmath -.. note:: - - **Upgrading**: If you have the old *python-bitmath* package - installed presently, you could also run ``sudo dnf update - python-bitmath`` instead - - **PyPi**: You could also install bitmath from `PyPi @@ -109,37 +108,19 @@ You could also install bitmath from `PyPi $ sudo pip install bitmath -.. note:: - - **pip** installs need pip >= 1.1. To workaround this, `download - bitmath `_, from - PyPi and then ``pip install bitmath-x.y.z.tar.gz``. See `issue #57 - `_ - for more information. - - -**PPA**: - -Ubuntu Xenial, Wily, Vivid, Trusty, and Precise users can install -bitmath from the `launchpad PPA -`_: - -.. code-block:: bash - - $ sudo add-apt-repository ppa:tbielawa/bitmath - $ sudo apt-get update - $ sudo apt-get install python-bitmath **Source**: -Or, if you want to install from source: +To install from source, clone the repository and use pip: .. code-block:: bash - $ sudo python ./setup.py install + $ git clone https://github.com/timlnx/bitmath.git + $ cd bitmath + $ pip install . -If you want the bitmath manpage installed as well: +To also install the ``bitmath`` manpage: .. code-block:: bash @@ -161,8 +142,6 @@ Topics include: * Context Managers * Module Variables * ``argparse`` integration - * ``click`` integration - * ``progressbar`` integration * The ``bitmath`` command-line Tool @@ -219,12 +198,12 @@ Arithmetic >>> import bitmath >>> log_size = bitmath.kB(137.4) >>> log_zipped_size = bitmath.Byte(987) - >>> print "Compression saved %s space" % (log_size - log_zipped_size) + >>> 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 + >>> print(songs_per_drive) 2457.6 @@ -237,7 +216,7 @@ File size unit conversion: >>> from bitmath import * >>> dvd_size = GiB(4.7) - >>> print "DVD Size in MiB: %s" % dvd_size.to_MiB() + >>> print("DVD Size in MiB: %s" % dvd_size.to_MiB()) DVD Size in MiB: 4812.8 MiB @@ -249,9 +228,9 @@ Select a human-readable unit >>> small_number = kB(100) >>> ugly_number = small_number.to_TiB() - >>> print ugly_number + >>> print(ugly_number) 9.09494701773e-08 TiB - >>> print ugly_number.best_prefix() + >>> print(ugly_number.best_prefix()) 97.65625 KiB @@ -279,7 +258,7 @@ Sorting KiB(2326.0), KiB(4003.0), KiB(48.0), KiB(1770.0), KiB(7892.0), KiB(4190.0)] - >>> print sorted(sizes) + >>> 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)] @@ -305,7 +284,7 @@ Example: ...: The instance is {bits} bits large ...: bytes/bits without trailing decimals: {bytes:.0f}/{bits:.0f}""" % str(ugly_number) - >>> print ugly_number.format(longer_format) + >>> 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 @@ -324,7 +303,7 @@ Utility Functions .. code-block:: python - >>> print bitmath.getsize('python-bitmath.spec') + >>> print(bitmath.getsize('python-bitmath.spec')) 3.7060546875 KiB **bitmath.parse_string()** @@ -335,9 +314,9 @@ Parse a string with standard units: >>> import bitmath >>> a_dvd = bitmath.parse_string("4.7 GiB") - >>> print type(a_dvd) + >>> print(type(a_dvd)) - >>> print a_dvd + >>> print(a_dvd) 4.7 GiB **bitmath.parse_string_unsafe()** @@ -348,7 +327,7 @@ Parse a string with ambiguous units: >>> import bitmath >>> a_gig = bitmath.parse_string_unsafe("1gb") - >>> print type(a_gig) + >>> print(type(a_gig)) >>> a_gig == bitmath.GB(1) True @@ -363,7 +342,7 @@ Parse a string with ambiguous units: >>> import bitmath >>> with open('/dev/sda') as fp: ... root_disk = bitmath.query_device_capacity(fp) - ... print root_disk.best_prefix() + ... print(root_disk.best_prefix()) ... 238.474937439 GiB @@ -372,7 +351,7 @@ Parse a string with ambiguous units: .. code-block:: python >>> for i in bitmath.listdir('./tests/', followlinks=True, relpath=True, bestprefix=True): - ... print i + ... print(i) ... ('tests/test_file_size.py', KiB(9.2900390625)) ('tests/test_basic_math.py', KiB(7.1767578125)) @@ -408,7 +387,7 @@ Formatting >>> with bitmath.format(fmt_str="[{value:.3f}@{unit}]"): ... for i in bitmath.listdir('./tests/', followlinks=True, relpath=True, bestprefix=True): - ... print i[1] + ... print(i[1]) ... [9.290@KiB] [7.177@KiB] @@ -439,88 +418,25 @@ Formatting ``argparse`` Integration ------------------------ -Example script using ``bitmath.integrations.bmargparse.BitmathType`` as an -argparser argument type: +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 - from bitmath.integrations.bmargparse import BitmathType - parser = argparse.ArgumentParser( - description="Arg parser with a bitmath type argument") - parser.add_argument('--block-size', - type=BitmathType, - required=True) - - results = parser.parse_args() - print "Parsed in: {PARSED}; Which looks like {TOKIB} as a Kibibit".format( - PARSED=results.block_size, - TOKIB=results.block_size.Kib) - -If ran as a script the results would be similar to this: - -.. code-block:: bash - - $ python ./bmargparse.py --block-size 100MiB - Parsed in: 100.0 MiB; Which looks like 819200.0 Kib as a Kibibit - -``click`` Integration ---------------------- - -Example script using ``bitmath.integrations.bmclick.BitmathType`` as an -click parameter type: - -.. code-block:: python - - import click - from bitmath.integrations.bmclick import BitmathType - - @click.command() - @click.argument('size', type=BitmathType()) - def best_prefix(size): - click.echo(size.best_prefix()) - -If ran as a script the results should be similar to this: - -.. code-block:: bash - - $ python ./bestprefix.py "1024 KiB" - 1.0 MiB - -``progressbar`` Integration ---------------------------- - -Use ``bitmath.integrations.bmprogressbar.BitmathFileTransferSpeed`` as a -``progressbar`` file transfer speed widget to monitor download speeds: - -.. code-block:: python - - import requests - import progressbar import bitmath - from bitmath.integrations.bmprogressbar import BitmathFileTransferSpeed - - FETCH = 'https://www.kernel.org/pub/linux/kernel/v3.0/patch-3.16.gz' - widgets = ['Bitmath Progress Bar Demo: ', ' ', - progressbar.Bar(marker=progressbar.RotatingMarker()), ' ', - BitmathFileTransferSpeed()] - - r = requests.get(FETCH, stream=True) - size = bitmath.Byte(int(r.headers['Content-Length'])) - pbar = progressbar.ProgressBar(widgets=widgets, maxval=int(size), - term_width=80).start() - chunk_size = 2048 - with open('/dev/null', 'wb') as fd: - for chunk in r.iter_content(chunk_size): - fd.write(chunk) - if (pbar.currval + chunk_size) < pbar.maxval: - pbar.update(pbar.currval + chunk_size) - pbar.finish() - - -If ran as a script the results would be similar to this: - -.. code-block:: bash - $ python ./smalldl.py - Bitmath Progress Bar Demo: ||||||||||||||||||||||||||||||||||||||||| 1.58 MiB/s + def BitmathType(value): + try: + return bitmath.parse_string(value) + except ValueError: + raise argparse.ArgumentTypeError( + f"{value!r} is not a recognised 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 diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..3502ed9 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,45 @@ +# 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: | + +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. I will respond and we will triage the issue, a disclosure statement will be made as soon as we understand the potential impact and have prepared a mitigation. + +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. + diff --git a/VERSION b/VERSION index e21e727..227cea2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.4.0 \ No newline at end of file +2.0.0 diff --git a/bitmath.1 b/bitmath.1 index ed9d199..8fb8fe8 100644 --- a/bitmath.1 +++ b/bitmath.1 @@ -2,12 +2,12 @@ .\" Title: bitmath .\" Author: [see the "AUTHOR" section] .\" Generator: DocBook XSL Stylesheets vsnapshot -.\" Date: 04/16/2026 +.\" Date: 04/17/2026 .\" Manual: python-bitmath -.\" Source: bitmath 1.4.0 +.\" Source: bitmath 2.0.0 .\" Language: English .\" -.TH "BITMATH" "1" "04/16/2026" "bitmath 1\&.4\&.0" "python\-bitmath" +.TH "BITMATH" "1" "04/17/2026" "bitmath 2\&.0\&.0" "python\-bitmath" .\" ----------------------------------------------------------------- .\" * Define some portability stuff .\" ----------------------------------------------------------------- @@ -56,18 +56,18 @@ for best human\-readability\&. .RE .SH "AUTHOR" .sp -Tim Bielawa +Tim Case (fmr\&. tbielawa) .sp For a complete list of contributors, please visit the GitHub charts page\&. .SH "COPYRIGHT" .sp -Copyright \(co 2014\-2016, Tim Bielawa\&. +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/tbielawa/bitmath +\fBThe bitmath GitHub Project\fR \(em https://github\&.com/timlnx/bitmath .sp \fBThe bitmath Documentation\fR \(em https://bitmath\&.readthedocs\&.org diff --git a/bitmath.1.asciidoc.in b/bitmath.1.asciidoc.in index 39afbb7..cc901a4 100644 --- a/bitmath.1.asciidoc.in +++ b/bitmath.1.asciidoc.in @@ -44,7 +44,7 @@ best human-readability. AUTHOR ------ -Tim Bielawa +Tim Case (fmr. tbielawa) For a complete list of contributors, please visit the GitHub charts page. @@ -52,7 +52,7 @@ page. COPYRIGHT --------- -Copyright © 2014-2016, Tim Bielawa. +Copyright © 2014-2026, Tim Case. bitmath is released under the terms of the "MIT" License. @@ -62,6 +62,6 @@ SEE ALSO -------- *units*(7) -*The bitmath GitHub Project* -- +*The bitmath GitHub Project* -- *The bitmath Documentation* -- diff --git a/bitmath/__init__.py b/bitmath/__init__.py index 6babc18..e6c5431 100644 --- a/bitmath/__init__.py +++ b/bitmath/__init__.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- +# SPDX-License-Identifier: MIT # The MIT License (MIT) # -# Copyright © 2014-2016 Tim Bielawa +# Copyright © 2014-2016 Tim Case # See GitHub Contributors Graph for more information # # Permission is hereby granted, free of charge, to any person @@ -36,25 +37,11 @@ man 7 units (from the Linux Documentation Project 'man-pages' package) -BEFORE YOU GET HASTY WITH EXCLUDING CODE FROM COVERAGE: If you -absolutely need to skip code coverage because of a strange Python 2.x -vs 3.x thing, use the fancy environment substitution stuff from the -.coverage RC file. In review: +* If you *NEED* to skip a statement because of something untestable: -* If you *NEED* to skip a statement because of Python 2.x issues add the following:: - - # pragma: PY2X no cover - -* If you *NEED* to skip a statement because of Python 3.x issues add the following:: - - # pragma: PY3X no cover - -In this configuration, statements which are skipped in 2.x are still -covered in 3.x, and the reverse holds true for tests skipped in 3.x. + # pragma: no cover """ -from __future__ import print_function - import argparse import contextlib import fnmatch @@ -64,6 +51,7 @@ import os.path import platform import sys +import threading # For device capacity reading in query_device_capacity(). Only supported # on posix systems for now. Will be addressed in issue #52 on GitHub. @@ -73,25 +61,21 @@ import struct -__all__ = ['Bit', 'Byte', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', +__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', 'kb', 'Mb', 'Gb', 'Tb', + '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', - 'ALL_UNIT_TYPES', 'NIST', 'NIST_PREFIXES', 'NIST_STEPS', + 'sum', 'ALL_UNIT_TYPES', 'NIST', 'NIST_PREFIXES', 'NIST_STEPS', 'SI', 'SI_PREFIXES', 'SI_STEPS'] -# Python 3.x compat -if sys.version > '3': - long = int # pragma: PY2X no cover - unicode = str # pragma: PY2X no cover - #: 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'] + 'Tib', 'TiB', 'Pib', 'PiB', 'Eib', 'EiB', 'Zib', 'ZiB', + 'Yib', 'YiB'] # ##################################################################### # Set up our module variables/constants @@ -132,7 +116,7 @@ #: All of the NIST prefixes -NIST_PREFIXES = ['Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei'] +NIST_PREFIXES = ['Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi'] #: Byte values represented by each NIST prefix unit NIST_STEPS = { @@ -143,7 +127,9 @@ 'Gi': 1073741824, 'Ti': 1099511627776, 'Pi': 1125899906842624, - 'Ei': 1152921504606846976 + 'Ei': 1152921504606846976, + 'Zi': 1180591620717411303424, + 'Yi': 1208925819614629174706176 } #: String representation, ex: ``13.37 MiB``, or ``42.0 kB`` @@ -152,6 +138,24 @@ #: 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 os_name(): # makes unittesting platform specific code easier @@ -174,7 +178,7 @@ class Bitmath(object): """The base class for all the other prefix classes""" # All the allowed input types - valid_types = (int, float, long) + valid_types = (int, float) def __init__(self, value=0, bytes=None, bits=None): """Instantiate with `value` by the unit, in plain bytes, or @@ -257,7 +261,7 @@ def _norm(self, value): :raises ValueError: if the input value is not a type of real number """ if isinstance(value, self.valid_types): - self._byte_value = value * self._unit_value + self._byte_value = float(value) * self._unit_value self._bit_value = self._byte_value * 8.0 else: raise ValueError("Initialization value '%s' is of an invalid type: %s. " @@ -270,30 +274,29 @@ def _norm(self, value): # Properties #: The mathematical base of an instance - base = property(lambda s: s._base) + 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))) - """The binary representation of an instance in binary 1s and 0s. Note + 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 as:: +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) + 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) + 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) + 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) + power = property(lambda s: s._power, doc="The mathematical power of an instance") @property def system(self): @@ -323,14 +326,11 @@ def unit(self): >>> Byte(1).unit == 'Byte' >>> Byte(1.1).unit == 'Bytes' >>> Gb(2).unit == 'Gbs' - """ - global format_plural - if self.prefix_value == 1: # If it's a '1', return it singular, no matter what return self._name_singular - elif format_plural: + elif _get_format_plural(): # Pluralization requested return self._name_plural else: @@ -347,7 +347,6 @@ def unit_plural(self): >>> KiB(1).unit_plural == 'KiB' >>> Byte(1024).unit_plural == 'Bytes' >>> Gb(1).unit_plural == 'Gb' - """ return self._name_plural @@ -387,7 +386,7 @@ def from_other(cls, item): >>> import bitmath >>> kib = bitmath.KiB.from_other(bitmath.MiB(1)) - >>> print kib + >>> print(kib) KiB(1024.0) """ @@ -400,18 +399,38 @@ def from_other(cls, item): ###################################################################### # The following implement the Python datamodel customization methods # - # Reference: http://docs.python.org/2.7/reference/datamodel.html#basic-customization + # Reference: https://docs.python.org/3/reference/datamodel.html#basic-customization def __repr__(self): """Representation of this object as you would expect to see in an interpreter""" - global _FORMAT_REPR + global _FORMAT_REPR # noqa: F824 return self.format(_FORMAT_REPR) def __str__(self): """String representation of this object""" - global format_string - return self.format(format_string) + if _get_bestprefix(): + return self.best_prefix().format(_get_format_string()) + return self.format(_get_format_string()) + + def __format__(self, fmt_spec): + """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): """Return a representation of this instance formatted with user @@ -451,7 +470,8 @@ def best_prefix(self, system=None): 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. +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. @@ -465,10 +485,20 @@ def best_prefix(self, system=None): This will return a value >= 0. The following determines the 'best prefix unit' for representation: -* result == 0, best represented as a Byte +* 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. + """ # Use absolute value so we don't return Bit's for *everything* @@ -476,7 +506,7 @@ def best_prefix(self, system=None): if abs(self) < Byte(1): return Bit.from_other(self) else: - if type(self) is Byte: # pylint: disable=unidiomatic-typecheck + if isinstance(self, Byte): _inst = self else: _inst = Byte.from_other(self) @@ -512,7 +542,10 @@ def best_prefix(self, system=None): # in the list. if _index == 0: - # Already a Byte() type, so return it. + # 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 elif _index >= len(_STEPS): # This is a really big number. Use the biggest prefix we've got @@ -521,9 +554,12 @@ def best_prefix(self, system=None): # There is an appropriate prefix unit to represent this _best_prefix = _STEPS[_index - 1] - _conversion_method = getattr( - self, - 'to_%sB' % _best_prefix) + # Preserve unit family: Bit-family -> 'to_Xib'/'to_Xb', + # Byte-family -> 'to_XiB'/'to_XB'. + if isinstance(self, Bit): + _conversion_method = getattr(self, 'to_%sb' % _best_prefix) + else: + _conversion_method = getattr(self, 'to_%sB' % _best_prefix) return _conversion_method() @@ -660,8 +696,12 @@ def to_Eb(self): Eb = property(lambda s: s.to_Eb()) ################################################################## - # The SI units go beyond the NIST units. They also have the Zetta - # and Yotta prefixes. + + def to_ZiB(self): + return ZiB(bits=self._bit_value) + + def to_Zib(self): + return Zib(bits=self._bit_value) def to_ZB(self): return ZB(bits=self._bit_value) @@ -669,19 +709,27 @@ def to_ZB(self): def to_Zb(self): return Zb(bits=self._bit_value) - # Properties + 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): + return YiB(bits=self._bit_value) + + def to_Yib(self): + return Yib(bits=self._bit_value) + def to_YB(self): return YB(bits=self._bit_value) def to_Yb(self): return Yb(bits=self._bit_value) - #: A new object representing this instance as a Yottabyte + 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()) @@ -777,7 +825,7 @@ def __mul__(self, other): - bm1 * bm2 = bm1 - bm * num = bm -- num * bm = num (see rmul) +- num * bm = bm (see rmul) """ if isinstance(other, numbers.Number): # bm * num @@ -789,18 +837,12 @@ def __mul__(self, other): _self = self.prefix_value * self._base ** self._power return (type(self))(bytes=_other * _self) - """The division operator (/) is implemented by these methods. The -__truediv__() method is used when __future__.division is in effect, -otherwise __div__() is used. If only one of these two methods is -defined, the object will not support division in the alternate -context; TypeError will be raised instead.""" - - def __div__(self, other): + def __truediv__(self, other): """Division: Supported operations with result types: - bm1 / bm2 = num - bm / num = bm -- num / bm = num (see rdiv) +- num / bm = num (see rtruediv) """ if isinstance(other, numbers.Number): # bm / num @@ -810,10 +852,6 @@ def __div__(self, other): # bm1 / bm2 return self._byte_value / float(other.bytes) - def __truediv__(self, other): - # num / bm - return self.__div__(other) - # def __floordiv__(self, other): # return NotImplemented @@ -843,6 +881,9 @@ def __truediv__(self, 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 @@ -854,25 +895,20 @@ def __rmul__(self, other): # num * bm = bm return self * other - def __rdiv__(self, other): - # num / bm = num - return other / float(self.value) - def __rtruediv__(self, other): # num / bm = num return other / float(self.value) - """Called to implement the built-in functions complex(), int(), -long(), and float(). Should return a value of the appropriate type. + """Called to implement the built-in functions complex(), int(), and +float(). Should return a value of the appropriate type. If one of those methods does not support the operation with the supplied arguments, it should return NotImplemented. -For bitmath purposes, these methods return the int/long/float +For bitmath purposes, these methods return the int/float equivalent of the this instances prefix Unix value. That is to say: - int(KiB(3.336)) would return 3 - - long(KiB(3.336)) would return 3L - float(KiB(3.336)) would return 3.336 """ @@ -880,14 +916,43 @@ def __int__(self): """Return this instances prefix unit as an integer""" return int(self.prefix_value) - def __long__(self): - """Return this instances prefix unit as a long integer""" - return long(self.prefix_value) # pragma: PY3X no cover - def __float__(self): """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. They are explicit opt-in operations for when integer prefix values are +needed. See the Rules for Math appendix in the bitmath documentation for the +design rationale behind floating-point representation. +""" + + 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 ################################################################## @@ -1011,6 +1076,22 @@ def _setup(self): Eio = EiB +class ZiB(Byte): + def _setup(self): + return (2, 70, 'ZiB', 'ZiBs') + + +Zio = ZiB + + +class YiB(Byte): + def _setup(self): + return (2, 80, 'YiB', 'YiBs') + + +Yio = YiB + + ###################################################################### # SI Prefixes for Byte based types class kB(Byte): @@ -1127,6 +1208,16 @@ def _setup(self): return (2, 60, 'Eib', 'Eibs') +class Zib(Bit): + def _setup(self): + return (2, 70, 'Zib', 'Zibs') + + +class Yib(Bit): + def _setup(self): + return (2, 80, 'Yib', 'Yibs') + + ###################################################################### # SI Prefixes for Bit based types class kb(Bit): @@ -1250,12 +1341,12 @@ def query_device_capacity(device_fd): # Confirm this character is right by running (on Linux): # # >>> import struct - # >>> print 8 == struct.calcsize('L') + # >>> 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/tbielawa/bitmath/issues/new if + # https://github.com/timlnx/bitmath/issues/new if # this does *not* work for you) ], # func is how the final result is decided. Because the @@ -1386,213 +1477,234 @@ def listdir(search_base, followlinks=False, filter='*', yield (_return_path, getsize(_path, bestprefix=bestprefix, system=system)) else: if os.path.isdir(_path) or os.path.islink(_path): - pass + pass # pragma: no cover else: yield (_return_path, getsize(_path, bestprefix=bestprefix, system=system)) -def parse_string(s): - """Parse a string with units and try to make a bitmath object out of -it. +def parse_string(s, system=NIST, strict=True): + """Parse a string with units and return a bitmath instance. String inputs may include whitespace characters between the value and the unit. - """ - # Strings only please - if not isinstance(s, (str, unicode)): - raise ValueError("parse_string only accepts string inputs but a %s was given" % - type(s)) - - # get the index of the first alphabetic character - try: - index = list([i.isalpha() for i in s]).index(True) - except ValueError: - # If there's no alphabetic characters we won't be able to .index(True) - raise ValueError("No unit detected, can not parse string '%s' into a bitmath object" % s) - - # 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("The unit %s is not a valid bitmath unit" % unit) - unit_class = globals()[unit] - - try: - val = float(val) - except ValueError: - raise - try: - return unit_class(val) - except: # pragma: no cover - raise ValueError("Can't parse string %s into a bitmath object" % s) - - -def parse_string_unsafe(s, system=SI): - """Attempt to parse a string with ambiguous units and try to make a -bitmath object out of it. - -This may produce inaccurate results if parsing shell output. For -example `ls` may say a 2730 Byte file is '2.7K'. 2730 Bytes == 2.73 kB -~= 2.666 KiB. See the documentation for all of the important details. - -Note the following caveats: - -* All inputs are assumed to be byte-based (as opposed to bit based) - -* Numerical inputs (those without any units) are assumed to be a - number of bytes - -* Inputs with single letter units (k, M, G, etc) are assumed to be SI - units (base-10). Set the `system` parameter to `bitmath.NIST` to - change this behavior. - -* Inputs with an `i` character following the leading letter (Ki, Mi, - Gi) are assumed to be NIST units (base 2) - -* Capitalization does not matter +: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) behaviour is identical to the original function. + When ``strict=False`` the behaviour of the former + ``parse_string_unsafe`` is applied. The ``system`` parameter + defaults to ``bitmath.NIST`` and is ignored when ``strict=True``. """ - if not isinstance(s, (str, unicode)) and \ - not isinstance(s, numbers.Number): - raise ValueError("parse_string_unsafe only accepts string/number inputs but a %s was given" % - type(s)) + if strict: + # Strings only please + if not isinstance(s, (str)): + raise ValueError("parse_string only accepts string inputs but a %s was given" % + type(s)) - ###################################################################### - # Is the input simple to parse? Just a number, or a number - # masquerading as a string perhaps? + # get the index of the first alphabetic character + try: + index = list([i.isalpha() for i in s]).index(True) + except ValueError: + # If there's no alphabetic characters we won't be able to .index(True) + raise ValueError("No unit detected, can not parse string '%s' into a bitmath object" % s) - # Test case: raw number input (easy!) - if isinstance(s, numbers.Number): - # It's just a number. Assume bytes - return Byte(s) + # 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("The unit %s is not a valid bitmath unit" % unit) + unit_class = globals()[unit] - # Test case: a number pretending to be a string - if isinstance(s, (str, unicode)): try: - # Can we turn it directly into a number? - return Byte(float(s)) + val = float(val) except ValueError: - # Nope, this is not a plain number - pass + raise + try: + return unit_class(val) + except: # pragma: no cover + raise ValueError("Can't parse string %s into a bitmath object" % s) - ###################################################################### - # At this point: - # - the input is also not just a number wrapped in a string - # - nor is is just a plain number type - # - # We need to do some more digging around now to figure out exactly - # what we were given and possibly normalize the input into a - # format we can recognize. + else: + # strict=False path (formerly parse_string_unsafe) + if not isinstance(s, (str)) and \ + not isinstance(s, numbers.Number): + raise ValueError("parse_string only accepts string/number inputs but a %s was given" % + type(s)) + + # 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 - # First we'll separate the number and the unit. - # - # Get the index of the first alphabetic character - try: - index = list([i.isalpha() for i in s]).index(True) - except ValueError: # pragma: no cover - # If there's no alphabetic characters we won't be able to .index(True) - raise ValueError("No unit detected, can not parse string '%s' into a bitmath object" % s) + # At this point the input is a string with a unit component. + # Separate the number and the unit. + try: + index = list([i.isalpha() for i in s]).index(True) + except ValueError: # pragma: no cover + raise ValueError("No unit detected, can not parse string '%s' into a bitmath object" % s) - # Split the string into the value and the unit - val, unit = s[:index], s[index:] + val, unit = s[:index], s[index:] - # Don't trust anything. We'll make sure the correct 'b' is in place. - unit = unit.rstrip('Bb') - unit += 'B' + # 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' - # At this point we can expect `unit` to be either: - # - # - 2 Characters (for SI, ex: kB or GB) - # - 3 Caracters (so NIST, ex: KiB, or GiB) - # - # A unit with any other number of chars is not a valid unit - - # SI - if len(unit) == 2: - # Has NIST parsing been requested? - if system == NIST: - # NIST units requested. Ensure the unit begins with a - # capital letter and is followed by an 'i' character. + 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) - # Insert an 'i' char after the first letter - _unit = list(unit) - _unit.insert(1, 'i') - # Collapse the list back into a 3 letter string - unit = ''.join(_unit) - unit_class = globals()[unit] + if unit[:2] in NIST_PREFIXES: + unit_class = globals()[unit] else: - # Default parsing (SI format) - # - # Edge-case checking: SI 'thousand' is a lower-case K - if unit.startswith('K'): - unit = unit.replace('K', 'k') - elif not unit.startswith('k'): - # Otherwise, ensure the first char is capitalized - unit = capitalize_first(unit) + raise ValueError("The unit %s is not a valid bitmath unit" % unit) - # This is an SI-type unit - if unit[0] in SI_PREFIXES: - unit_class = globals()[unit] - # NIST - elif len(unit) == 3: - unit = capitalize_first(unit) + try: + unit_class + except UnboundLocalError: + raise ValueError("The unit %s is not a valid bitmath unit" % unit) - # This is a NIST-type unit - if unit[:2] in NIST_PREFIXES: - unit_class = globals()[unit] - else: - # This is not a unit we recognize - raise ValueError("The unit %s is not a valid bitmath unit" % unit) + return unit_class(float(val)) - try: - unit_class - except UnboundLocalError: - raise ValueError("The unit %s is not a valid bitmath unit" % unit) - return unit_class(float(val)) +def parse_string_unsafe(s, system=NIST): + """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') + """ + import warnings + 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, start=None): + """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 ###################################################################### -# Contxt Managers +# Context Managers @contextlib.contextmanager def format(fmt_str=None, plural=False, bestprefix=False): - """Context manager for printing bitmath instances. + """Thread-safe context manager for printing bitmath instances. -``fmt_str`` - a formatting mini-language compat formatting string. See +``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's if they're +``plural`` - True enables printing instances with 's' if they're plural. False (default) prints them as singular (no trailing 's'). -``bestprefix`` - True enables printing instances in their best -human-readable representation. False, the default, prints instances -using their current prefix unit. - """ - if 'bitmath' not in globals(): - import bitmath - - if plural: - orig_fmt_plural = bitmath.format_plural - bitmath.format_plural = True +``bestprefix`` - True converts instances to their best human-readable +prefix unit before formatting. False (default) formats the instance +as its current prefix unit. - if fmt_str: - orig_fmt_str = bitmath.format_string - bitmath.format_string = fmt_str - - yield +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) - if plural: - bitmath.format_plural = orig_fmt_plural + _thread_local.format_string = fmt_str if fmt_str is not None else format_string + _thread_local.format_plural = plural + _thread_local.bestprefix = bestprefix - if fmt_str: - bitmath.format_string = orig_fmt_str + 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): diff --git a/bitmath/integrations/__init__.py b/bitmath/integrations/__init__.py deleted file mode 100644 index 3a249ec..0000000 --- a/bitmath/integrations/__init__.py +++ /dev/null @@ -1,35 +0,0 @@ -# -*- coding: utf-8 -*- -# The MIT License (MIT) -# -# Copyright © 2014-2016 Tim Bielawa -# 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. - -# Kept for backward compatibilty -from .bmargparse import BitmathType - -try: - from .bmprogressbar import BitmathFileTransferSpeed -except ImportError: - # Ignore missing dependency as argparse integration will fail if - # progressbar is not installed (#86). - pass diff --git a/bitmath/integrations/bmargparse.py b/bitmath/integrations/bmargparse.py deleted file mode 100644 index bd5fdb2..0000000 --- a/bitmath/integrations/bmargparse.py +++ /dev/null @@ -1,78 +0,0 @@ -# -*- coding: utf-8 -*- -# The MIT License (MIT) -# -# Copyright © 2014-2016 Tim Bielawa -# 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. - -import bitmath -import argparse - - -def BitmathType(bmstring): - """An 'argument type' for integrations with the argparse module. - -For more information, see -https://docs.python.org/2/library/argparse.html#type Of particular -interest to us is this bit: - - ``type=`` can take any callable that takes a single string - argument and returns the converted value - -I.e., ``type`` can be a function (such as this function) or a class -which implements the ``__call__`` method. - -Example usage of the bitmath.BitmathType argparser type: - - >>> import bitmath - >>> import argparse - >>> parser = argparse.ArgumentParser() - >>> parser.add_argument("--file-size", type=bitmath.BitmathType) - >>> parser.parse_args("--file-size 1337MiB".split()) - Namespace(file_size=MiB(1337.0)) - -Invalid usage includes any input that the bitmath.parse_string -function already rejects. Additionally, **UNQUOTED** arguments with -spaces in them are rejected (shlex.split used in the following -examples to conserve single quotes in the parse_args call): - - >>> parser = argparse.ArgumentParser() - >>> parser.add_argument("--file-size", type=bitmath.BitmathType) - >>> import shlex - - >>> # The following is ACCEPTABLE USAGE: - ... - >>> parser.parse_args(shlex.split("--file-size '1337 MiB'")) - Namespace(file_size=MiB(1337.0)) - - >>> # The following is INCORRECT USAGE because the string "1337 MiB" is not quoted! - ... - >>> parser.parse_args(shlex.split("--file-size 1337 MiB")) - error: argument --file-size: 1337 can not be parsed into a valid bitmath object -""" - try: - argvalue = bitmath.parse_string(bmstring) - except ValueError: - raise argparse.ArgumentTypeError("'%s' can not be parsed into a valid bitmath object" % - bmstring) - else: - return argvalue diff --git a/bitmath/integrations/bmclick.py b/bitmath/integrations/bmclick.py deleted file mode 100644 index a71c154..0000000 --- a/bitmath/integrations/bmclick.py +++ /dev/null @@ -1,65 +0,0 @@ -# -*- coding: utf-8 -*- -# The MIT License (MIT) -# -# Copyright © 2014-2016 Tim Bielawa -# 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. - -import bitmath -import click - - -class BitmathType(click.ParamType): - """An parameter type for integrations with the click module. - -For more information see https://click.palletsprojects.com/en/7.x/parameters/ -and https://click.palletsprojects.com/en/7.x/options/#basic-value-options - -Example usage of the click Bitmath type for a click argument: - - from bitmath.integrations.bmclick import BitmathType - - @click.command() - @click.argument('size', type=BitmathType) - def best_prefix(size): - click.echo(size.best_prefix()) - -It can also be used for click options: - - from bitmath.integrations.bmclick import BitmathType - - @click.command() - @click.option('--size', required=True, type=BitmathType) - def best_prefix(size): - click.echo(size.best_prefix()) -""" - name = 'bitmath' - - def convert(self, value, param, ctx): - try: - return bitmath.parse_string(value) - except ValueError: - self.fail("'%s' can not be parsed into a valid bitmath object" % - value) - - -BITMATH = BitmathType() diff --git a/bitmath/integrations/bmprogressbar.py b/bitmath/integrations/bmprogressbar.py deleted file mode 100644 index 06fa1e0..0000000 --- a/bitmath/integrations/bmprogressbar.py +++ /dev/null @@ -1,25 +0,0 @@ -import bitmath -import progressbar.widgets - - -class BitmathFileTransferSpeed(progressbar.widgets.Widget): - """Widget for showing the transfer speed (useful for file transfers).""" - __slots__ = ('system', 'format') - - def __init__(self, system=bitmath.NIST, format="{value:.2f} {unit}/s"): - self.system = system - self.format = format - - def update(self, pbar): - """Updates the widget with the current NIST/SI speed. - -Basically, this calculates the average rate of update and figures out -how to make a "pretty" prefix unit""" - - if pbar.seconds_elapsed < 2e-6 or pbar.currval < 2e-6: - scaled = bitmath.Byte() - else: - speed = pbar.currval / pbar.seconds_elapsed - scaled = bitmath.Byte(speed).best_prefix(system=self.system) - - return scaled.format(self.format) diff --git a/debian/.gitignore b/debian/.gitignore deleted file mode 100644 index e8653ae..0000000 --- a/debian/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -files -python-bitmath.debhelper.log -python-bitmath.postinst.debhelper -python-bitmath.prerm.debhelper -python-bitmath.substvars -python-bitmath/ diff --git a/debian/changelog b/debian/changelog deleted file mode 100644 index 333d576..0000000 --- a/debian/changelog +++ /dev/null @@ -1,12 +0,0 @@ -bitmath (1.3.1.1-1~ppa1~trusty1) trusty; urgency=low - - * New release - - -- Timothy Bielawa Sun, 17 Jul 2016 11:59:48 +0000 - - -bitmath (1.3.0.2-1~ppa1~trusty1) trusty; urgency=low - - * First deb package release - - -- Timothy Bielawa Sat, 02 Jul 2016 15:52:37 +0000 diff --git a/debian/compat b/debian/compat deleted file mode 100644 index 7f8f011..0000000 --- a/debian/compat +++ /dev/null @@ -1 +0,0 @@ -7 diff --git a/debian/control b/debian/control deleted file mode 100644 index 5111522..0000000 --- a/debian/control +++ /dev/null @@ -1,34 +0,0 @@ -Source: bitmath -Maintainer: Tim Bielawa -Section: python -Priority: optional -Build-Depends: python-setuptools (>= 0.6b3), python-all (>= 2.6.6-3), debhelper (>= 7.4.3) -Standards-Version: 3.9.5 -Homepage: http://bitmath.readthedocs.io/en/latest/ -Vcs-Browser: https://github.com/tbielawa/bitmath -Vcs-Git: https://github.com/tbielawa/bitmath.git - -Package: python-bitmath -Architecture: all -Homepage: http://bitmath.readthedocs.io/en/latest/ -Depends: ${misc:Depends}, ${python:Depends} -Description: Pythonic module for representing and manipulating file sizes - 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), and rich comparison operations (1024 Bytes == 1KiB), - bitwise 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 and the - progressbar module as a custom file transfer speed widget. - . - bitmath is thoroughly unittested, with almost 200 individual tests (a - number which is always increasing). bitmath's test-coverage is almost - always at 100%. diff --git a/debian/copyright b/debian/copyright deleted file mode 100644 index 69fe7b7..0000000 --- a/debian/copyright +++ /dev/null @@ -1,20 +0,0 @@ -The MIT License (MIT) - -Copyright © 2014 Tim Bielawa - -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. diff --git a/debian/docs b/debian/docs deleted file mode 100644 index 040474d..0000000 --- a/debian/docs +++ /dev/null @@ -1,3 +0,0 @@ -README.rst -NEWS.rst -docsite/source/ diff --git a/debian/python-bitmath.manpages b/debian/python-bitmath.manpages deleted file mode 100644 index 670e348..0000000 --- a/debian/python-bitmath.manpages +++ /dev/null @@ -1 +0,0 @@ -bitmath.1 diff --git a/debian/rules b/debian/rules deleted file mode 100755 index ed437d8..0000000 --- a/debian/rules +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/make -f - -# This file was automatically generated by stdeb 0.8.5 at -# Sat, 02 Jul 2016 15:52:37 +0000 - -%: - dh $@ --with python2 --buildsystem=python_distutils - - -override_dh_auto_clean: - python setup.py clean -a - find . -name \*.pyc -exec rm {} \; - - - -override_dh_auto_build: - python setup.py build --force - - - -override_dh_auto_install: - python setup.py install --force --root=debian/python-bitmath --no-compile -O0 --install-layout=deb - - - -override_dh_python2: - dh_python2 --no-guessing-versions - - - - diff --git a/debian/source/format b/debian/source/format deleted file mode 100644 index 163aaf8..0000000 --- a/debian/source/format +++ /dev/null @@ -1 +0,0 @@ -3.0 (quilt) diff --git a/debian/source/options b/debian/source/options deleted file mode 100644 index bcc4bbb..0000000 --- a/debian/source/options +++ /dev/null @@ -1 +0,0 @@ -extend-diff-ignore="\.egg-info$" \ No newline at end of file diff --git a/docsite/source/appendices/mixed_math.rst b/docsite/source/appendices/mixed_math.rst index 94ad2cb..6cf9eb2 100644 --- a/docsite/source/appendices/mixed_math.rst +++ b/docsite/source/appendices/mixed_math.rst @@ -20,7 +20,7 @@ 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. @@ -100,6 +100,7 @@ Internally, this is implemented as: *Division* The result will be a number type due to unit cancellation. +.. _appendix_math_mixed_types: Mixed Types: Addition and Subtraction ===================================== @@ -107,10 +108,23 @@ 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 **always** return a type from the -:py:mod:`numbers` family (integer, float, long, etc...). This rule is -true regardless of the placement of the operands, with respect to the -operator. +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 behaviour (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 @@ -160,7 +174,7 @@ Let's look at an example of this in action: In [9]: bm = PiB(24) - In [10]: print num + bm + In [10]: print(num + bm) 66.0 Equivalently, divorcing the bitmath instance from it's value (this is @@ -170,7 +184,7 @@ coercion): In [12]: bm_value = bm.value - In [13]: print num + 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 @@ -179,6 +193,26 @@ 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 ======================================== @@ -208,7 +242,7 @@ bitmath), the intention of ``MiB(100) / 10)`` is to separate .. code-block:: python In [4]: KiB(43) / 10 - Out[4]: KiB(4.2998046875) + 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 @@ -221,6 +255,68 @@ yourself what you would expect to get if you did this: +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. + +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 ========= diff --git a/docsite/source/appendices/related_projects.rst b/docsite/source/appendices/related_projects.rst index e74411a..e88a007 100644 --- a/docsite/source/appendices/related_projects.rst +++ b/docsite/source/appendices/related_projects.rst @@ -71,27 +71,5 @@ 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 `_ +* `Units Homepage & Docs `_ * Download available through ``pip``, or your distribution's package system - - -Unum -==== - - - *Unum stands for 'unit-numbers'. It is a Python module that allows - to define and manipulate true quantities, i.e. numbers with units - such as 60 seconds, [...], 30 dollars etc. The module validates - unit consistency in arithmetic expressions; it provides also - automatic conversion and output formatting. Unum is designed to be - reliable, easy-to-use, customizable and open to any unit - definition.* - -**Unum**, by Pierre X. Denis, is another extensible library for unit -manipulation. The module does not appear to have seen any activity in -quite some time. Looking over the docs gives me the impression that it -also has a tendency to pollute your namespace with objects like ``M`` -and anything else it pre-defines. - -* `Unum Homepage and Docs `_ -* `Unum Source Download `_ diff --git a/docsite/source/classes.rst b/docsite/source/classes.rst index 77cc762..e58aebd 100644 --- a/docsite/source/classes.rst +++ b/docsite/source/classes.rst @@ -119,7 +119,7 @@ Initializing :param int value: **Default: 0**. The value of the instance in *prefix units*. For example, if we were - instantiating a ``bitmath.KiB`` object to + instantiating a :class:`.KiB` object to represent 13.37 KiB, the ``value`` parameter would be **13.37**. For instance, ``k = bitmath.KiB(13.37)``. @@ -163,8 +163,8 @@ Class Method: from_other() ========================== bitmath **class objects** have one public class method, -:py:meth:`BitMathClass.from_other` which provides an -alternative way to initialize a bitmath class. +: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 @@ -176,8 +176,8 @@ bitmath class, however that is a valid use case. Instantiate any ``BitMathClass`` using another instance as reference for it's initial value. - The ``from_other()`` class method has one required parameter: an - instance of a bitmath class. + 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 @@ -201,10 +201,10 @@ bitmath class, however that is a valid use case. >>> a_mebibyte == a_mebibyte_sized_kibibyte True - >>> print a_mebibyte, a_mebibyte_sized_kibibyte + >>> print(a_mebibyte, a_mebibyte_sized_kibibyte) 1.0 MiB 1024.0 KiB - Or, using the :py:meth:`BitMathClass.from_other` class method: + Or, using the :py:meth:`.from_other` class method: .. code-block:: python :linenos: @@ -216,5 +216,5 @@ bitmath class, however that is a valid use case. >>> a_mebibyte == a_big_kibibyte True - >>> print a_mebibyte, a_big_kibibyte + >>> print(a_mebibyte, a_big_kibibyte) 1.0 MiB 1024.0 KiB diff --git a/docsite/source/conf.py b/docsite/source/conf.py index 2c724be..29683c7 100644 --- a/docsite/source/conf.py +++ b/docsite/source/conf.py @@ -48,16 +48,16 @@ # General information about the project. project = u'bitmath' -copyright = u'2014-2016, Tim Bielawa' +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 = '1.4.0' +version = '2.0.0' # The full version, including alpha/beta/rc tags. -release = '1.4.0' +release = '2.0.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -105,7 +105,6 @@ # a list of builtin themes. if _RTD_THEME: html_theme = "sphinx_rtd_theme" - html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] else: html_theme = 'default' @@ -200,7 +199,7 @@ # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'bitmath.tex', u'bitmath Documentation', - u'Tim Bielawa', 'manual'), + u'Tim Case', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -230,7 +229,7 @@ # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'bitmath', u'bitmath Documentation', - [u'Tim Bielawa'], 1) + [u'Tim Case'], 1) ] # If true, show URL addresses after external links. @@ -244,7 +243,7 @@ # dir menu entry, description, category) texinfo_documents = [ ('index', 'bitmath', u'bitmath Documentation', - u'Tim Bielawa', 'bitmath', 'One line description of project.', + u'Tim Case', 'bitmath', 'One line description of project.', 'Miscellaneous'), ] diff --git a/docsite/source/conf.py.in b/docsite/source/conf.py.in index 162184e..0d50a27 100644 --- a/docsite/source/conf.py.in +++ b/docsite/source/conf.py.in @@ -48,7 +48,7 @@ master_doc = 'index' # General information about the project. project = u'bitmath' -copyright = u'2014-2016, Tim Bielawa' +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 @@ -105,7 +105,6 @@ pygments_style = 'sphinx' # a list of builtin themes. if _RTD_THEME: html_theme = "sphinx_rtd_theme" - html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] else: html_theme = 'default' @@ -200,7 +199,7 @@ latex_elements = { # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'bitmath.tex', u'bitmath Documentation', - u'Tim Bielawa', 'manual'), + u'Tim Case', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -230,7 +229,7 @@ latex_documents = [ # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'bitmath', u'bitmath Documentation', - [u'Tim Bielawa'], 1) + [u'Tim Case'], 1) ] # If true, show URL addresses after external links. @@ -244,7 +243,7 @@ man_pages = [ # dir menu entry, description, category) texinfo_documents = [ ('index', 'bitmath', u'bitmath Documentation', - u'Tim Bielawa', 'bitmath', 'One line description of project.', + u'Tim Case', 'bitmath', 'One line description of project.', 'Miscellaneous'), ] diff --git a/docsite/source/contact.rst b/docsite/source/contact.rst index 9b3ec87..b68e2b3 100644 --- a/docsite/source/contact.rst +++ b/docsite/source/contact.rst @@ -3,7 +3,7 @@ Contact ####### -Hi, I'm Tim Bielawa, the bitmath maintainer. Would you like to get in +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: @@ -18,13 +18,7 @@ ahead: 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: tbielawa `_ - -**Tweet-Tweet** - I have been known to tweet from time to time. - - * `@tbielawa `_ - + * `GitHub: timlnx `_ **Bugs/Issues/Requests** All contributions related directly to the bitmath project, i.e. bug @@ -34,17 +28,5 @@ ahead: * :ref:`Contributing ` -**Saying hello** - I'm on the `freenode `_ IRC network Monday - through Friday, from around 9am EST through 5pm EST. - - * Issue a ``/who tbielawa*`` command to the server, a handle with the - netmask ``~tbielawa@redhat/tbielawa`` will appear for you to - ``/query`` if I'm online. - **E-Mail** - If you want to contact me directly, `clone the project - `_ and look at any of `my - bitmath commits - `_ - to find my email address. + You can reach me directly at `bitmath@lnx.cx `_. diff --git a/docsite/source/contributing.rst b/docsite/source/contributing.rst index ab23ed5..178033f 100644 --- a/docsite/source/contributing.rst +++ b/docsite/source/contributing.rst @@ -10,33 +10,48 @@ This section describes the guidelines for contributing to bitmath. :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 are encounter an issue with the bitmath library, please use the +If you encounter an issue with the bitmath library, please use the provided template. -* `Open a new issue `_ -* `View open issues `_ +* `Open a new issue `_ +* `View open issues `_ + +.. _contributing_code_style: -Code Style/Formatting -********************* +Code Style and Formatting +************************* -Please conform to :pep:`0008` for code formatting. This specification -outlines the style that is required for patches. +Two static analysis checks run on every pull request as part of the +GitHub Actions CI workflow, and locally via ``make ci``: -Your code must follow this (or note why it can't) before patches will -be accepted. There is one consistent exception to this rule: +* ``pycodestyle`` — checks code style, with **E501** (line too long) + and **E722** (bare ``except``) ignored. +* ``flake8 --select=F`` — runs pyflakes error checks only (undefined + names, unused imports, etc.). Style checks are disabled. -**E501** - Line too long +A PR cannot be merged until both pass. Run ``make ci`` locally to +check before submitting. -The ``pycodestyle`` tests for bitmath include a ``--ignore`` option to -automatically exclude **E501** errors from the tests. +.. _contributing_commit_messages: Commit Messages *************** @@ -46,60 +61,34 @@ Please write `intelligent commit messages For example:: - Capitalized, short (50 chars or less) summary + Short summary (50 chars or less) - More detailed explanatory text, if necessary. Wrap it to about 72 - characters or so. In some contexts, the first line is treated as - the subject of an email and the rest of the text as the body. The - blank line separating the summary from the body is critical (unless - you omit the body entirely); tools like rebase can get confused if - you run the two together. + 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." This convention matches up with commit - messages generated by commands like git merge and git revert. - - Further paragraphs come after blank lines. + "Fixed bug" or "Fixes bug." - Bullet points are okay, too - - Typically a hyphen or asterisk is used for the bullet, followed - by a single space, with blank lines in between, but conventions - vary here - - - Use a hanging indent +.. _contributing_pull_requests: Pull Requests ************* -After a `pull request `_ is -submitted on GitHub two automatic processes are started: +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. -#. `Travis-CI `_ clones the - new pull request and runs the :ref:`automated test suite - `. -#. `Coveralls `_ clones - the new pull request and determines if the request would increase - or decrease the overall code test coverage. +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. -Please check back shortly after submitting a pull request to verify -that the Travis-CI process passes. - - -What Happens If The Build Breaks -================================ - -Pull requests which break the build will be looked at closely and you -may be asked to fix the tests. - -The bitmath project **welcomes all contributors** so **it's OK** if -you're unable to fix the tests yourself. Just leave a comment in the -pull request explaining so if that is the case. - -Likewise, if Coveralls indicates the pull request would decrease the -overall test-coverage, and you aren't able to fix it yourself, just -leave a comment in the pull request. +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_automated_tests: @@ -107,155 +96,134 @@ leave a comment in the pull request. Automated Tests *************** -Write `unittests `_ -for any new functionality, `if you are up to the task`. This is not a -requirement, but it does get you a lot of karma. +Write `unittests `_ +for any new functionality if you are up to the task. It is not a hard +requirement, but it greatly helps. -All bitmath code includes unit tests to verify expected -functionality. In the rest of this section we'll learn how the unit -tests are put together and how to interact with them. +All bitmath code includes unit tests to verify expected functionality. + + +.. _contributing_components: Components ========== -bitmath unit tests are integrated with/depend on the following items: +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. -* `Travis CI `_ - Free online service - providing `continuous integration` functionality for open source - projects. Tests are ran automatically on every git - commit. Integrates with GitHub to notify you if a pull request - passes or fails all unitests. +* `pytest `_ — Test runner used + to execute the unittest-based test suite, collect results, and report + coverage. -* `Coveralls `_ - Free - online service providing code test coverage reporting. Integrates - with GitHub to notify you if a pull-request would improve/decrease - overall code test coverage. +* `pytest-cov `_ — Coverage plugin + for pytest. The project aims for high coverage; reasonable exceptions + can always be discussed in the pull request. -* `unittest `_ - - Python unit testing framework. All bitmath tests are written using - this framework. +* `pycodestyle `_ — Checks + Python code style. -* `nose `_ - Per the **nose** - website: "`extends unittest to make testing easier`". **nose** is - used to run our unit tests. +* `pyflakes `_ — Checks Python + source files for errors. -* `coverage `_ - A tool for - measuring code coverage of Python programs. For bitmath we require a - minimum test coverage of **90%**. This is invoked by **nose** +* `virtualenv `_ — Creates an + isolated Python environment. The ``make ci`` target manages this automatically. -* `pycodestyle `_ - A tool to check Python - code against some of the style conventions in :pep:`0008`. +* `Makefile `_ — Orchestrates + all build and test tasks. See :ref:`contributing_makefile_targets` + below. -* `pyflakes `_ - A simple - program which checks Python source files for errors. -* `virtualenv `_ - A tool to - create isolated Python environments. Allows us to install additional - package dependencies without requiring access to the system - site-packages directory. +.. _contributing_makefile_targets: -* `Makefiles `_ - Utility scripts - used for project building and testing. How bitmath uses - **Makefiles** is described later in this section. +Makefile Targets +================ +All development tasks are driven through ``make``. The targets most +relevant to contributors are: -Targets -======= +.. note:: -In the scope of this document, we use the term `target` in the context -of `makefile targets`. For the purpose of this documentation, we can -think of these `targets` as pre-defined commands coded in a -makefile. bitmath testing targets include: + These targets are how you test your changes locally and clean up + afterwards before opening a pull request. -* ``ci`` - Run the tests exactly how they are ran in Travis-CI. The - ``ci`` target automatically calls the ``pycodestyle``, ``pyflakes``, - ``uniquetestnames``, and ``unittests`` targets. -* ``ci3`` - Is the same as the ``ci`` target, except it runs using the - Python 3.x interpreter. -* ``unittests`` - Run the functional test suite. -* ``pycodestyle`` - Run :pep:`0008` syntax checks. -* ``pyflakes`` - Run `pyflakes` error checks. -* ``clean`` - Remove temporary files and build artifacts from the - checked-out repository. -* ``uniquetestnames`` - Ensures no unit tests have the same name. -* ``tests`` - A quicker version of ``ci``. Different from ``ci`` in - that ``tests`` uses libraries installed on the local development - workstation. ``tests`` runs the ``unittests``, ``pycodestyle``, - ``uniquetestnames``, and ``pyflakes`` tests automatically. +``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 ``pyflakes``. Run this before opening a pull + request. **This is the same check GitHub Actions runs.** -To ensure the highest degree of confidence in test results you should -**always use** the ``ci`` and ``ci3`` targets. +``make clean`` + Removes the virtualenv, compiled ``*.pyc`` files, ``__pycache__`` + directories, and build artifacts. Run ``make clean; make ci`` for a + guaranteed fresh test run. -When Travis-CI runs an integration test, it calls the ``ci`` and -``ci3`` targets. +``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 bitmath test suite is invoked via the Makefile. The following is -an example of how to run the ``ci`` test target manually: +The simplest way to run the full test suite locally is: .. code-block:: console - :linenos: - :emphasize-lines: 2 - [~/Projects/bitmath] 17:22:21 (master) $ 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 ############################################# - virtualenv bitmathenv - New python executable in bitmathenv/bin/python - Installing setuptools, pip...done. - . bitmathenv/bin/activate && pip install -r requirements.txt - Downloading/unpacking python-coveralls (from -r requirements.txt (line 1)) - Downloading python_coveralls-2.4.3-py2.py3-none-any.whl - Downloading/unpacking nose (from -r requirements.txt (line 2)) - - ... snip ... - - Convert a bitmath GiB into a Tb ... ok - Convert a bitmath PiB into a TiB ... ok - Convert a bitmath GiB into a Tib ... ok - Convert to kb ... ok - Convert a bitmath Bit into a MiB ... ok - bitmath type converted to the same unit is properly converted ... ok - float(bitmath) returns a float ... ok - int(bitmath) returns an int ... ok - long(bitmath) returns a long ... ok - - Name Stmts Miss Cover Missing - --------------------------------------- - bitmath 440 1 99% 1152 - ---------------------------------------------------------------------- - Ran 163 tests in 0.035s - - OK - : - -On line **2** we see how to call a makefile target. In this case it's -quite straightforward: ``make ci``. Other targets are called in the -same way. For example, to run the ``clean`` target, you run the -command ``make clean``. To run the Python 3.x test suite, you would -run the command ``make ci3``. - - -Troubleshooting -=============== - -If you find yourself unable to run the unit tests: - -#. `Search `_ for relevant error messages - -#. **Read** the error message closely. The solution could be hidden in - the error message output. The problem could be as simple as a - missing dependency - -#. If you are unable to figure out all the necessary dependencies to - run the tests, file an issue on that specific projects GitHub issue - tracker. Include the full error message. + ... (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 ======================== + +A passing run shows 100% coverage. The exact test count grows as new +tests are added. Any regression in coverage is a failure. + +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. diff --git a/docsite/source/copyright.rst b/docsite/source/copyright.rst index f9e4f2c..c428388 100644 --- a/docsite/source/copyright.rst +++ b/docsite/source/copyright.rst @@ -2,7 +2,7 @@ Copyright ######### The MIT License (MIT) -Copyright © 2014-2016 Tim Bielawa +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 diff --git a/docsite/source/index.rst b/docsite/source/index.rst index e3a14bc..6d04939 100644 --- a/docsite/source/index.rst +++ b/docsite/source/index.rst @@ -1,15 +1,32 @@ -.. image:: https://api.travis-ci.org/tbielawa/bitmath.png - :target: https://travis-ci.org/tbielawa/bitmath/ - :align: right - :height: 19 - :width: 77 +.. image:: https://github.com/tbielawa/bitmath/actions/workflows/python.yml/badge.svg + :target: https://github.com/tbielawa/bitmath/actions/workflows/python.yml + :alt: Build Status on GitHub -.. image:: https://coveralls.io/repos/tbielawa/bitmath/badge.png?branch=master - :target: https://coveralls.io/github/tbielawa/bitmath - :align: right - :height: 19 - :width: 77 +.. image:: https://img.shields.io/github/issues/tbielawa/bitmath?style=flat-square + :target: https://github.com/tbielawa/bitmath/issues + :alt: Open Issues +.. image:: https://img.shields.io/github/issues-pr/tbielawa/bitmath?style=flat-square + :target: https://github.com/tbielawa/bitmath/pulls + :alt: Open Pull Requests + +.. image:: https://img.shields.io/pypi/dm/bitmath?style=flat-square + :target: https://pypistats.org/packages/bitmath + :alt: PyPI - Package Popularity + +.. image:: https://img.shields.io/github/stars/tbielawa/bitmath?style=flat-square + :target: https://pypistats.org/packages/bitmath + :alt: GitHub Project Popularity + +.. image:: https://img.shields.io/pypi/l/bitmath?style=flat-square + :target: https://opensource.org/licenses/MIT + :alt: PyPI - License + +.. image:: https://img.shields.io/pypi/implementation/bitmath?style=flat-square + :alt: PyPI - Implementation + +.. image:: https://img.shields.io/pypi/pyversions/bitmath?style=flat-square + :alt: PyPI - Python Version bitmath ####### @@ -20,27 +37,26 @@ 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) * Rich comparison operations (``1024 Bytes == 1KiB``) -* bitwise operations (``<<``, ``>>``, ``&``, ``|``, ``^``) -* Reading a device's storage capacity (Linux/OS X support only) -* `argparse `_ - integration as a custom type -* `click `_ - integration as a custom parameter type -* `progressbar `_ - integration as a better file transfer speed widget -* String parsing +* 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 = @@ -49,9 +65,9 @@ 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's almost 200 unittests? `Check them out for +And did we mention there are nearly 300 unit tests? `Check them out for yourself `_. * :ref:`Examples ` after the TOC. @@ -60,72 +76,31 @@ yourself `_. Installation ############ -The easiest way to install bitmath is via ``dnf`` (or ``yum``) if -you're on a Fedora/RHEL based distribution. bitmath is available in -the main Fedora repositories, as well as the EPEL6 and EPEL7 -repositories. There are now dual python2.x and python3.x releases -available. - +bitmath is available in Fedora and EPEL repositories, as well as +directly available via `PyPI +`_. As of 2023 bitmath is only +developed, tested, and supported for `currently supported +`_ Python releases. -**Python 2.x**: - -.. code-block:: bash - - $ sudo dnf install python2-bitmath - -**Python 3.x**: +**Package Managers** .. code-block:: bash $ sudo dnf install python3-bitmath + $ pip install --user bitmath -.. note:: - - **Upgrading**: If you have the old *python-bitmath* package - installed presently, you could also run ``sudo dnf update - python-bitmath`` instead +**Source** -**PyPi**: - -You could also install bitmath from `PyPi -`_ if you like: - -.. code-block:: bash - - $ sudo pip install bitmath - -.. note:: - - **pip** installs need pip >= 1.1. To workaround this, `download - bitmath `_, from - PyPi and then ``pip install bitmath-x.y.z.tar.gz``. See `issue #57 - `_ - for more information. - - -**PPA**: - -Ubuntu Xenial, Wily, Vivid, Trusty, and Precise users can install -bitmath from the `launchpad PPA -`_: +To install from source, clone the repository and use pip: .. code-block:: bash - $ sudo add-apt-repository ppa:tbielawa/bitmath - $ sudo apt-get update - $ sudo apt-get install python-bitmath - - -**Source**: + $ git clone https://github.com/timlnx/bitmath.git + $ cd bitmath + $ pip install . -Or, if you want to install from source: - -.. code-block:: bash - - $ sudo python ./setup.py install - -If you want the bitmath manpage installed as well: +To also install the ``bitmath`` manpage: .. code-block:: bash @@ -145,6 +120,7 @@ Contents instances.rst simple_examples.rst real_life_examples.rst + integration_examples.rst contributing.rst appendices.rst NEWS.rst @@ -167,12 +143,12 @@ Arithmetic >>> import bitmath >>> log_size = bitmath.kB(137.4) >>> log_zipped_size = bitmath.Byte(987) - >>> print "Compression saved %s space" % (log_size - log_zipped_size) + >>> 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 + >>> print(songs_per_drive) 2457.6 @@ -185,7 +161,7 @@ File size unit conversion: >>> from bitmath import * >>> dvd_size = GiB(4.7) - >>> print "DVD Size in MiB: %s" % dvd_size.to_MiB() + >>> print("DVD Size in MiB: %s" % dvd_size.to_MiB()) DVD Size in MiB: 4812.8 MiB @@ -197,9 +173,9 @@ Select a human-readable unit >>> small_number = kB(100) >>> ugly_number = small_number.to_TiB() - >>> print ugly_number + >>> print(ugly_number) 9.09494701773e-08 TiB - >>> print ugly_number.best_prefix() + >>> print(ugly_number.best_prefix()) 97.65625 KiB @@ -227,7 +203,7 @@ Sorting KiB(2326.0), KiB(4003.0), KiB(48.0), KiB(1770.0), KiB(7892.0), KiB(4190.0)] - >>> print sorted(sizes) + >>> 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)] @@ -253,7 +229,7 @@ Example: ...: The instance is {bits} bits large ...: bytes/bits without trailing decimals: {bytes:.0f}/{bits:.0f}""" % str(ugly_number) - >>> print ugly_number.format(longer_format) + >>> 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 @@ -272,7 +248,7 @@ Utility Functions .. code-block:: python - >>> print bitmath.getsize('python-bitmath.spec') + >>> print(bitmath.getsize('python-bitmath.spec')) 3.7060546875 KiB **bitmath.parse_string()** @@ -283,9 +259,9 @@ Parse a string with standard units: >>> import bitmath >>> a_dvd = bitmath.parse_string("4.7 GiB") - >>> print type(a_dvd) + >>> print(type(a_dvd)) - >>> print a_dvd + >>> print(a_dvd) 4.7 GiB **bitmath.parse_string_unsafe()** @@ -296,7 +272,7 @@ Parse a string with ambiguous units: >>> import bitmath >>> a_gig = bitmath.parse_string_unsafe("1gb") - >>> print type(a_gig) + >>> print(type(a_gig)) >>> a_gig == bitmath.GB(1) True @@ -311,7 +287,7 @@ Parse a string with ambiguous units: >>> import bitmath >>> with open('/dev/sda') as fp: ... root_disk = bitmath.query_device_capacity(fp) - ... print root_disk.best_prefix() + ... print(root_disk.best_prefix()) ... 238.474937439 GiB @@ -320,7 +296,7 @@ Parse a string with ambiguous units: .. code-block:: python >>> for i in bitmath.listdir('./tests/', followlinks=True, relpath=True, bestprefix=True): - ... print i + ... print(i) ... ('tests/test_file_size.py', KiB(9.2900390625)) ('tests/test_basic_math.py', KiB(7.1767578125)) @@ -356,7 +332,7 @@ Formatting >>> with bitmath.format(fmt_str="[{value:.3f}@{unit}]"): ... for i in bitmath.listdir('./tests/', followlinks=True, relpath=True, bestprefix=True): - ... print i[1] + ... print(i[1]) ... [9.290@KiB] [7.177@KiB] @@ -387,88 +363,25 @@ Formatting ``argparse`` Integration ------------------------ -Example script using ``bitmath.integrations.bmargparse.BitmathType`` as an -argparser argument type: +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 - from bitmath.integrations.bmargparse import BitmathType - parser = argparse.ArgumentParser( - description="Arg parser with a bitmath type argument") - parser.add_argument('--block-size', - type=BitmathType, - required=True) - - results = parser.parse_args() - print "Parsed in: {PARSED}; Which looks like {TOKIB} as a Kibibit".format( - PARSED=results.block_size, - TOKIB=results.block_size.Kib) - -If ran as a script the results would be similar to this: - -.. code-block:: bash - - $ python ./bmargparse.py --block-size 100MiB - Parsed in: 100.0 MiB; Which looks like 819200.0 Kib as a Kibibit - -``click`` Integration ---------------------- - -Example script using ``bitmath.integrations.bmclick.BitmathType`` as an -click parameter type: - -.. code-block:: python - - import click - from bitmath.integrations.bmclick import BitmathType - - @click.command() - @click.argument('size', type=BitmathType()) - def best_prefix(size): - click.echo(size.best_prefix()) - -If ran as a script the results should be similar to this: - -.. code-block:: bash - - $ python ./bestprefix.py "1024 KiB" - 1.0 MiB - -``progressbar`` Integration ---------------------------- - -Use ``bitmath.integrations.bmprogressbar.BitmathFileTransferSpeed`` as a -``progressbar`` file transfer speed widget to monitor download speeds: - -.. code-block:: python - - import requests - import progressbar import bitmath - from bitmath.integrations.bmprogressbar import BitmathFileTransferSpeed - - FETCH = 'https://www.kernel.org/pub/linux/kernel/v3.0/patch-3.16.gz' - widgets = ['Bitmath Progress Bar Demo: ', ' ', - progressbar.Bar(marker=progressbar.RotatingMarker()), ' ', - BitmathFileTransferSpeed()] - - r = requests.get(FETCH, stream=True) - size = bitmath.Byte(int(r.headers['Content-Length'])) - pbar = progressbar.ProgressBar(widgets=widgets, maxval=int(size), - term_width=80).start() - chunk_size = 2048 - with open('/dev/null', 'wb') as fd: - for chunk in r.iter_content(chunk_size): - fd.write(chunk) - if (pbar.currval + chunk_size) < pbar.maxval: - pbar.update(pbar.currval + chunk_size) - pbar.finish() - - -If ran as a script the results would be similar to this: - -.. code-block:: bash - $ python ./smalldl.py - Bitmath Progress Bar Demo: ||||||||||||||||||||||||||||||||||||||||| 1.58 MiB/s + def BitmathType(value): + try: + return bitmath.parse_string(value) + except ValueError: + raise argparse.ArgumentTypeError( + f"{value!r} is not a recognised 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 diff --git a/docsite/source/index.rst.in b/docsite/source/index.rst.in index 1626c23..7f1b7d1 100644 --- a/docsite/source/index.rst.in +++ b/docsite/source/index.rst.in @@ -1,15 +1,32 @@ -.. image:: https://api.travis-ci.org/tbielawa/bitmath.png - :target: https://travis-ci.org/tbielawa/bitmath/ - :align: right - :height: 19 - :width: 77 +.. image:: https://github.com/tbielawa/bitmath/actions/workflows/python.yml/badge.svg + :target: https://github.com/tbielawa/bitmath/actions/workflows/python.yml + :alt: Build Status on GitHub -.. image:: https://coveralls.io/repos/tbielawa/bitmath/badge.png?branch=master - :target: https://coveralls.io/github/tbielawa/bitmath - :align: right - :height: 19 - :width: 77 +.. image:: https://img.shields.io/github/issues/tbielawa/bitmath?style=flat-square + :target: https://github.com/tbielawa/bitmath/issues + :alt: Open Issues +.. image:: https://img.shields.io/github/issues-pr/tbielawa/bitmath?style=flat-square + :target: https://github.com/tbielawa/bitmath/pulls + :alt: Open Pull Requests + +.. image:: https://img.shields.io/pypi/dm/bitmath?style=flat-square + :target: https://pypistats.org/packages/bitmath + :alt: PyPI - Package Popularity + +.. image:: https://img.shields.io/github/stars/tbielawa/bitmath?style=flat-square + :target: https://pypistats.org/packages/bitmath + :alt: GitHub Project Popularity + +.. image:: https://img.shields.io/pypi/l/bitmath?style=flat-square + :target: https://opensource.org/licenses/MIT + :alt: PyPI - License + +.. image:: https://img.shields.io/pypi/implementation/bitmath?style=flat-square + :alt: PyPI - Implementation + +.. image:: https://img.shields.io/pypi/pyversions/bitmath?style=flat-square + :alt: PyPI - Python Version bitmath ####### @@ -20,25 +37,26 @@ 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) * Rich comparison operations (``1024 Bytes == 1KiB``) -* bitwise operations (``<<``, ``>>``, ``&``, ``|``, ``^``) -* Reading a device's storage capacity (Linux/OS X support only) -* `argparse `_ - integration as a custom type -* `progressbar `_ - integration as a better file transfer speed widget -* String parsing +* 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 = @@ -47,9 +65,9 @@ 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's almost 200 unittests? `Check them out for +And did we mention there are nearly 300 unit tests? `Check them out for yourself `_. * :ref:`Examples ` after the TOC. @@ -58,72 +76,31 @@ yourself `_. Installation ############ -The easiest way to install bitmath is via ``dnf`` (or ``yum``) if -you're on a Fedora/RHEL based distribution. bitmath is available in -the main Fedora repositories, as well as the EPEL6 and EPEL7 -repositories. There are now dual python2.x and python3.x releases -available. +bitmath is available in Fedora and EPEL repositories, as well as +directly available via `PyPI +`_. As of 2023 bitmath is only +developed, tested, and supported for `currently supported +`_ Python releases. - -**Python 2.x**: - -.. code-block:: bash - - $ sudo dnf install python2-bitmath - -**Python 3.x**: +**Package Managers** .. code-block:: bash $ sudo dnf install python3-bitmath - -.. note:: - - **Upgrading**: If you have the old *python-bitmath* package - installed presently, you could also run ``sudo dnf update - python-bitmath`` instead - - -**PyPi**: - -You could also install bitmath from `PyPi -`_ if you like: - -.. code-block:: bash - - $ sudo pip install bitmath - -.. note:: - - **pip** installs need pip >= 1.1. To workaround this, `download - bitmath `_, from - PyPi and then ``pip install bitmath-x.y.z.tar.gz``. See `issue #57 - `_ - for more information. - - -**PPA**: - -Ubuntu Xenial, Wily, Vivid, Trusty, and Precise users can install -bitmath from the `launchpad PPA -`_: - -.. code-block:: bash - - $ sudo add-apt-repository ppa:tbielawa/bitmath - $ sudo apt-get update - $ sudo apt-get install python-bitmath + $ pip install --user bitmath -**Source**: +**Source** -Or, if you want to install from source: +To install from source, clone the repository and use pip: .. code-block:: bash - $ sudo python ./setup.py install + $ git clone https://github.com/timlnx/bitmath.git + $ cd bitmath + $ pip install . -If you want the bitmath manpage installed as well: +To also install the ``bitmath`` manpage: .. code-block:: bash @@ -143,6 +120,7 @@ Contents instances.rst simple_examples.rst real_life_examples.rst + integration_examples.rst contributing.rst appendices.rst NEWS.rst diff --git a/docsite/source/instances.rst b/docsite/source/instances.rst index 89e693e..ce97f2c 100644 --- a/docsite/source/instances.rst +++ b/docsite/source/instances.rst @@ -21,20 +21,21 @@ bitmath objects have several instance attributes: .. code-block:: python - >>> b = bitmath.Byte(1337) - >>> print b.base + >>> print(bitmath.Byte(1337).base) 2 + >>> print(bitmath.kB(1337).base) + 10 .. py:attribute:: BitMathInstance.binary The `Python binary representation - `_ of the + `_ of the instance's value (in bits) .. code-block:: python >>> b = bitmath.Byte(1337) - >>> print b.binary + >>> print(b.binary) 0b10100111001000 .. py:attribute:: BitMathInstance.bin @@ -43,23 +44,32 @@ bitmath objects have several instance attributes: .. py:attribute:: BitMathInstance.bits - The number of bits in the object + The number of bits in the object, as a floating-point value. .. code-block:: python >>> b = bitmath.Byte(1337) - >>> print b.bits + >>> 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 + The number of bytes in the object, as a floating-point value. .. code-block:: python >>> b = bitmath.Byte(1337) - >>> print b.bytes - 1337 + >>> print(b.bytes) + 1337.0 .. py:attribute:: BitMathInstance.power @@ -68,7 +78,7 @@ bitmath objects have several instance attributes: .. code-block:: python >>> b = bitmath.Byte(1337) - >>> print b.power + >>> print(b.power) 0 .. py:attribute:: BitMathInstance.system @@ -78,7 +88,7 @@ bitmath objects have several instance attributes: .. code-block:: python >>> b = bitmath.Byte(1337) - >>> print b.system + >>> print(b.system) NIST .. py:attribute:: BitMathInstance.value @@ -88,7 +98,7 @@ bitmath objects have several instance attributes: .. code-block:: python >>> b = bitmath.Byte(1337) - >>> print b.value + >>> print(b.value) 1337.0 .. py:attribute:: BitMathInstance.unit @@ -98,7 +108,7 @@ bitmath objects have several instance attributes: .. code-block:: python >>> b = bitmath.Byte(1337) - >>> print b.unit + >>> print(b.unit) Byte .. py:attribute:: BitMathInstance.unit_plural @@ -108,7 +118,7 @@ bitmath objects have several instance attributes: .. code-block:: python >>> b = bitmath.Byte(1337) - >>> print b.unit_plural + >>> print(b.unit_plural) Bytes .. py:attribute:: BitMathInstance.unit_singular @@ -119,7 +129,7 @@ bitmath objects have several instance attributes: .. code-block:: python >>> b = bitmath.Byte(1337) - >>> print b.unit_singular + >>> print(b.unit_singular) Byte @@ -136,8 +146,8 @@ and what you can expect their printed representation to look like: :linenos: >>> dvd_capacity = GB(4.7) - >>> print "Capacity in bits: %s\nbytes: %s\n" % \ - (dvd_capacity.bits, dvd_capacity.bytes) + >>> print("Capacity in bits: %s\nbytes: %s\n" % \ + (dvd_capacity.bits, dvd_capacity.bytes)) Capacity in bits: 37600000000.0 bytes: 4700000000.0 @@ -181,12 +191,12 @@ classes. You can even ``to_THING()`` an instance into itself again: True >>> another_mib = one_mib.to_MiB() - >>> print one_mib, one_mib_in_kb, another_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 + >>> print(six_TB, six_TB_in_bits) 6.0 TB 4.8e+13 Bit >>> six_TB == six_TB_in_bits @@ -239,7 +249,7 @@ even easier to read. >>> for _rate in tx_rate(): - ... print "Rate: %s/second" % Bit(_rate) + ... print("Rate: %s/second" % Bit(_rate)) ... time.sleep(1) Rate: 100.0 Bit/sec @@ -258,7 +268,7 @@ 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") + ... print(Bit(_rate).best_prefix().format("Rate: {value:.3f} {unit}/sec")) ... time.sleep(1) Rate: 12.500 Byte/sec @@ -293,7 +303,7 @@ bitmath instances come with a verbose built-in string representation: .. code-block:: python >>> leet_bits = Bit(1337) - >>> print leet_bits + >>> print(leet_bits) 1337.0 Bit However, for instances which aren't whole numbers (as in ``MiB(1/3.0) @@ -327,7 +337,7 @@ First, for reference, the default formatting: .. code-block:: python >>> ugly_number = MB(50).to_MiB() / 8.0 - >>> print ugly_number + >>> print(ugly_number) 5.96046447754 MiB Now, let's use the :py:meth:`format` method to limit that to two @@ -335,7 +345,7 @@ digits of precision: .. code-block:: python - >>> print ugly_number.format("{value:.2f}{unit}") + >>> print(ugly_number.format("{value:.2f}{unit}")) 5.96 MiB By changing the **2** character, you increase or decrease the @@ -364,7 +374,7 @@ of how an attribute may be referenced multiple times. ...: The instance is {bits} bits large ...: bytes/bits without trailing decimals: {bytes:.0f}/{bits:.0f}""" % str(ugly_number) - >>> print ugly_number.format(longer_format) + >>> 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 @@ -379,6 +389,125 @@ of how an attribute may be referenced multiple times. .. 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 @@ -401,11 +530,11 @@ classes. Under the covers these properties call ``to_THING``. >>> one_mib == one_mib.kb True - >>> print one_mib, one_mib.kb, one_mib.MiB + >>> 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 + >>> print(six_TB, six_TB.Bit) 6.0 TB 4.8e+13 Bit >>> six_TB == six_TB.Bit @@ -426,7 +555,7 @@ 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:: diff --git a/docsite/source/integration_examples.rst b/docsite/source/integration_examples.rst new file mode 100644 index 0000000..6dcaa0a --- /dev/null +++ b/docsite/source/integration_examples.rst @@ -0,0 +1,216 @@ +.. _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 recognised 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 recognised bitmath unit string (examples: 10MiB, 1.5GiB, 500kB) + + +.. _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 recognised 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 recognised 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 diff --git a/docsite/source/module.rst b/docsite/source/module.rst index 48cf3e3..d4062fd 100644 --- a/docsite/source/module.rst +++ b/docsite/source/module.rst @@ -30,7 +30,7 @@ bitmath.getsize() :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 ``bitmath.Byte`` instance. + 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` @@ -46,7 +46,7 @@ bitmath.getsize() .. code-block:: python >>> import bitmath - >>> print bitmath.getsize('./bitmath/__init__.py') + >>> print(bitmath.getsize('./bitmath/__init__.py')) 33.3583984375 KiB Let's say we want to see the results in bytes. We can do this by @@ -55,7 +55,7 @@ bitmath.getsize() .. code-block:: python >>> import bitmath - >>> print bitmath.getsize('./bitmath/__init__.py', bestprefix=False) + >>> print(bitmath.getsize('./bitmath/__init__.py', bestprefix=False)) 34159.0 Byte Recall, the default for representation is with the best @@ -67,11 +67,11 @@ bitmath.getsize() :linenos: :emphasize-lines: 1-4 - >>> print bitmath.getsize('./bitmath/__init__.py') + >>> print(bitmath.getsize('./bitmath/__init__.py')) 33.3583984375 KiB - >>> print bitmath.getsize('./bitmath/__init__.py', system=bitmath.NIST) + >>> print(bitmath.getsize('./bitmath/__init__.py', system=bitmath.NIST)) 33.3583984375 KiB - >>> print bitmath.getsize('./bitmath/__init__.py', system=bitmath.SI) + >>> print(bitmath.getsize('./bitmath/__init__.py', system=bitmath.SI)) 34.159 kB We can see in lines **1** → **4** that the same result is returned @@ -86,7 +86,7 @@ bitmath.listdir() .. function:: listdir(search_base[, followlinks=False[, filter='*'[, 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 @@ -99,7 +99,7 @@ bitmath.listdir() enables directory link following :param string filter: **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 @@ -110,7 +110,7 @@ bitmath.listdir() :py:func:`os.path.realpath` to normalize path references :param bool bestprefix: **Default:** ``False``, returns - ``bitmath.Byte`` instances. Set to ``True`` + :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 @@ -122,8 +122,8 @@ bitmath.listdir() * This function does **not** return tuples for directory entities. Including directories in results is `scheduled for - introduction `_ - in the upcoming 1.1.0 release. + introduction `_ + in an upcoming release. * Symlinks to **files** are followed automatically @@ -163,12 +163,12 @@ bitmath.listdir() >>> import bitmath >>> for f in bitmath.listdir('./some_files'): - ... print f + ... print(f) ... ('/tmp/tmp.P5lqtyqwPh/some_files/first_file', Byte(1337.0)) ('/tmp/tmp.P5lqtyqwPh/some_files/deeper_files/second_file', Byte(13370.0)) >>> for f in bitmath.listdir('./some_files', relpath=True): - ... print f + ... print(f) ... ('some_files/first_file', Byte(1337.0)) ('some_files/deeper_files/second_file', Byte(13370.0)) @@ -183,7 +183,7 @@ bitmath.listdir() .. code-block:: python >>> for f in bitmath.listdir('./some_files', filter='second*'): - ... print f + ... print(f) ... ('/tmp/tmp.P5lqtyqwPh/some_files/deeper_files/second_file', Byte(13370.0)) @@ -194,7 +194,7 @@ bitmath.listdir() .. code-block:: python >>> files = list(bitmath.listdir('./some_files')) - >>> print files + >>> print(files) [('/tmp/tmp.P5lqtyqwPh/some_files/first_file', Byte(1337.0)), ('/tmp/tmp.P5lqtyqwPh/some_files/deeper_files/second_file', Byte(13370.0))] Here's a more advanced example where we will sum the size of all @@ -205,36 +205,103 @@ bitmath.listdir() .. code-block:: python >>> discovered_files = [f[1] for f in bitmath.listdir('./some_files')] - >>> print discovered_files + >>> print(discovered_files) [Byte(1337.0), Byte(13370.0)] - >>> print reduce(lambda x,y: x+y, discovered_files) + >>> print(reduce(lambda x,y: x+y, discovered_files)) 14707.0 Byte - >>> print reduce(lambda x,y: x+y, discovered_files).best_prefix() + >>> print(reduce(lambda x,y: x+y, discovered_files).best_prefix()) 14.3623046875 KiB - >>> print reduce(lambda x,y: x+y, discovered_files).best_prefix().format("{value:.3f} {unit}") + >>> print(reduce(lambda x,y: x+y, discovered_files).best_prefix().format("{value:.3f} {unit}")) 14.362 KiB .. versionadded:: 1.0.7 +.. _bitmath_sum: -bitmath.parse_string() -====================== +bitmath.sum() +============= -.. function:: parse_string(str_repr) +.. function:: sum(iterable[, start=None]) - .. versionadded:: 1.1.0 + 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:: - Parse a string representing a unit into a proper bitmath - object. All non-string inputs are rejected and will raise a - :py:exc:`ValueError`. Strings without units are also rejected. See - the examples below for additional clarity. + 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. - :param string str_repr: The string to parse. May contain whitespace - between the value and the unit. - :return: A bitmath object representing ``str_repr`` - :raises ValueError: if ``str_repr`` can not be parsed + 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: @@ -242,24 +309,22 @@ bitmath.parse_string() >>> import bitmath >>> a_dvd = bitmath.parse_string("4.7 GiB") - >>> print type(a_dvd) + >>> print(type(a_dvd)) - >>> print a_dvd + >>> print(a_dvd) 4.7 GiB .. caution:: - Caution is advised if you are reading values from an unverified - external source, such as output from a shell command or a - generated file. Many applications (even ``/usr/bin/ls``) still - do not produce file size strings with valid (or even correct) - prefix units unless `specially configured to do so - `_. See - :py:func:`bitmath.parse_string_unsafe` as an alternative. + 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 it is - recommended that calls to :py:func:`bitmath.parse_string` are - wrapped in a ``try`` statement: + To protect your application from unexpected runtime errors, wrap + calls in a ``try`` statement: .. code-block:: python @@ -267,13 +332,12 @@ bitmath.parse_string() >>> try: ... a_dvd = bitmath.parse_string("4.7 G") ... except ValueError: - ... print "Error while parsing string into bitmath object" + ... print("Error while parsing string into bitmath object") ... Error while parsing string into bitmath object - Here we can see some more examples of invalid input, as well as two - acceptable inputs: + Here are some more examples of valid and invalid input: .. code-block:: python @@ -281,9 +345,9 @@ bitmath.parse_string() >>> 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() + ... print("Parsed size into %s" % bitmath.parse_string(size).best_prefix()) ... except ValueError: - ... print "Could not parse input: %s" % size + ... print("Could not parse input: %s" % size) ... Could not parse input: 1337 Could not parse input: 1337.7 @@ -296,7 +360,7 @@ bitmath.parse_string() .. versionchanged:: 1.2.4 Added support for parsing *octet* units via issue `#53 - parse french units - `_. The `usage + `_. The `usage `_ of "octet" is still common in some `RFCs `_, as well @@ -313,165 +377,169 @@ bitmath.parse_string() >>> import bitmath >>> a_mebibyte = bitmath.parse_string("1 MiB") >>> a_mebioctet = bitmath.parse_string("1 Mio") - >>> print a_mebibyte, a_mebioctet + >>> print(a_mebibyte, a_mebioctet) 1.0 MiB 1.0 MiB - >>> print bitmath.parse_string("1Po") + >>> print(bitmath.parse_string("1Po")) 1.0 PB - >>> print bitmath.parse_string("1337 Eio") + >>> print(bitmath.parse_string("1337 Eio")) 1337.0 EiB - Notice how on lines **4** and **5** that the variable - ``a_mebibyte`` from the input ``1 MiB`` is exactly equivalent to - ``a_mebioctet`` from the different input ``1 Mio``. This is because - after :py:mod:`bitmath` parses the octet units the results are - normalized into their **standard** NIST/SI equivalents - automatically. + 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`` behaviour is identical to earlier versions. + ``system`` defaults to :py:data:`bitmath.NIST` and is only + consulted when ``strict=False``. + + .. versionadded:: 1.1.0 - .. note:: - If your input isn't compatible with - :py:func:`bitmath.parse_string` you can try using - :py:func:`bitmath.parse_string_unsafe` - instead. :py:func:`bitmath.parse_string_unsafe` is more - forgiving with input. Please read the documentation carefully so - you understand the risks you assume using the ``unsafe`` parser. +.. _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 behaviour 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() ============================= -.. function:: parse_string_unsafe(repr[, system=bitmath.SI]) +.. deprecated:: 2.0.0 - .. versionadded:: 1.3.1 - - Parse a string or number into a proper bitmath object. This is the - less strict version of the :py:func:`bitmath.parse_string` - function. While :py:func:`bitmath.parse_string` only accepts SI and - NIST defined unit prefixes, :py:func:`bitmath.parse_string_unsafe` - accepts *non-standard* units such as those often displayed in - command-line output. Examples following the description. - - :param repr: The value to parse. May contain whitespace between the - value and the unit. - - :param system: :py:func:`bitmath.parse_string_unsafe` defaults to - parsing units as ``SI`` (base-10) units. Set the - ``system`` parameter to :py:data:`bitmath.NIST` if - you know your input is in ``NIST`` (base-2) format. - - :return: A bitmath object representing ``repr`` - :raises ValueError: if ``repr`` can not be parsed - - Use of this function comes with several caveats: - - * All inputs are assumed to be byte-based (as opposed to bit based) - * Numerical inputs (those without any units) are assumed to be a number of bytes - * Inputs with single letter units (``k``, ``M``, ``G``, etc) are - assumed to be SI units (base-10). See the ``system`` parameter - description **above** to change this behavior - * Inputs with an ``i`` character following the leading letter (``Ki``, - ``Mi``, ``Gi``) are assumed to be NIST units (base-2) - * Capitalization does not matter - - What exactly are these *non-standard* units? Generally speaking - non-standard units will not include enough information to be able - to identify exactly which unit system is being used. This is caused - by mis-capitalized characters (capital ``k``'s for SI *kilo* units - when they should be lower case), or omitted Byte or Bit - suffixes. You can find examples of non-standard units in many - common command line functions or parameters. For example: - - * The ``ls`` command will print out single-letter units when given - the ``-h`` option flag - * Running ``qemu-img info virtualdisk.img`` will also report with - single letter units - * The ``df`` command also uses single-letter units - * `Kubernetes - `_ will - display items like *memory limits* using two letter NIST units - (ex: ``memory: 2370Mi``) - - Given those considerations, understanding exactly what values you - are feeding into this function is crucial to getting accurate - results. You can control the output of some commands with various - option flags. For example, you could ensure the GNU ``ls`` and - ``df`` commands print with SI values by providing the ``--si`` - option flag. By default those commands will print out using NIST - (base-2) values. - - In this example let's pretend we're parsing the output of running - ``df -H / /boot /home`` on our filesystems. Assume the output is - saved into a file called ``/tmp/df-output.txt`` and looks like - this:: - - 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 - - Now let's read this file, parse the ``Used`` column, and then print - out the space used (line **7**): + ``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 - :linenos: - :emphasize-lines: 7 - - >>> with open('/tmp/df-output.txt', 'r') as fp: - ... # Skip parsing the 'df' header column - ... _ = fp.readline() - ... for line in fp.readlines(): - ... cols = line.split()[0:4] - ... print """Filesystem: %s - ... - Used: %s""" % (cols[0], bitmath.parse_string_unsafe(cols[1])) - 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 we had ran the ``df`` command with the ``-h`` option (instead of - ``-H``) we will get base-2 (NIST) output. That would look like - this:: - - Filesystem Size Used Avail Use% Mounted on - /dev/mapper/luks-ca8d5493-72bb-4691-afe1 100G 59G 36G 63% / - /dev/sda1 477M 373M 75M 84% /boot - /dev/mapper/vg_deepfryer-lv_home 120G 110G 4.4G 97% /home - - Because we switch from ``SI`` output to ``NIST`` output the values - displayed are slightly different. **However** they still print - using the same prefix unit, ``G``. We can tell - :py:func:`bitmath.parse_string_unsafe` that the input is ``NIST`` - (base-2) by giving ``bitmath.NIST`` to the ``system`` parameter - like this (line **8**): + + # 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 - :linenos: - :emphasize-lines: 8 - - >>> with open('/tmp/df-output.txt', 'r') as fp: - ... # Skip parsing the 'df' header column - ... _ = fp.readline() - ... for line in fp.readlines(): - ... cols = line.split()[0:4] - ... print """Filesystem: %s - ... - Used: %s""" % (cols[0], - ... bitmath.parse_string_unsafe(cols[1], \ - ... 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 printed use the proper NIST prefix unit syntax now: - Capital **G** followed by a lower-case **i** ending with a capital - **B**, ``GiB``. + 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 behaviour, + 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_device_capacity() @@ -513,7 +581,7 @@ bitmath.query_device_capacity() >>> import bitmath >>> with open("/dev/sda") as device: ... size = bitmath.query_device_capacity(device).best_prefix() - ... print "Device %s capacity: %s (%s Bytes)" % (device.name, size, size_bytes) + ... print("Device %s capacity: %s (%s Bytes)" % (device.name, size, size_bytes)) Device /dev/sda capacity: 238.474937439 GiB (2.56060514304e+11 Bytes) @@ -532,7 +600,7 @@ Context Managers **************** This section describes all of the `context managers -`_ +`_ provided by the bitmath class. .. warning:: @@ -620,8 +688,8 @@ bitmath.format() 'always_plural': always_plural_kbs } - print """None of the following will be pluralized, because that feature is turned off - """ + print("""None of the following will be pluralized, because that feature is turned off + """) test_string = """ One unit of 'Bit': {not_plural} @@ -630,18 +698,18 @@ bitmath.format() several items of a unit will always be pluralized in normal US English speech: {always_plural}""" - print test_string.format(**formatting_args) + print(test_string.format(**formatting_args)) - print """ + print(""" ---------------------------------------------------------------------- - """ + """) - print """Now, we'll use the bitmath.format() context manager + 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) + 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, @@ -685,13 +753,13 @@ bitmath.format() :linenos: >>> import bitmath - >>> print "Some instances: %s, %s" % (bitmath.KiB(1 / 3.0), bitmath.Bit(512)) + >>> print("Some instances: %s, %s" % (bitmath.KiB(1 / 3.0), bitmath.Bit(512))) Some instances: 0.333333333333 KiB, 512.0 Bit >>> with bitmath.format("{value:e}-{unit}"): - ... print "Some instances: %s, %s" % (bitmath.KiB(1 / 3.0), bitmath.Bit(512)) + ... print("Some instances: %s, %s" % (bitmath.KiB(1 / 3.0), bitmath.Bit(512))) ... Some instances: 3.333333e-01-KiB, 5.120000e+02-Bit - >>> print "Some instances: %s, %s" % (bitmath.KiB(1 / 3.0), bitmath.Bit(512)) + >>> print("Some instances: %s, %s" % (bitmath.KiB(1 / 3.0), bitmath.Bit(512))) Some instances: 0.333333333333 KiB, 512.0 Bit @@ -736,7 +804,7 @@ behavior. .. code-block:: python >>> from bitmath import * - >>> print MiB(1337), kb(0.1234567), Byte(0) + >>> print(MiB(1337), kb(0.1234567), Byte(0)) 1337.0 MiB 0.1234567 kb 0.0 Byte We can make these instances print however we want to. Let's wrap @@ -748,7 +816,7 @@ behavior. >>> import bitmath >>> bitmath.format_string = "[{value:.2f}-{unit}]" - >>> print bitmath.MiB(1337), bitmath.kb(0.1234567), bitmath.Byte(0) + >>> print(bitmath.MiB(1337), bitmath.kb(0.1234567), bitmath.Byte(0)) [1337.00-MiB] [0.12-kb] [0.00-Byte] .. py:data:: format_plural @@ -763,7 +831,7 @@ behavior. .. code-block:: python >>> import bitmath - >>> print bitmath.MiB(1337) + >>> print(bitmath.MiB(1337)) 1337.0 MiB And now we'll enable pluralization (line **2**): @@ -774,10 +842,10 @@ behavior. >>> import bitmath >>> bitmath.format_plural = True - >>> print bitmath.MiB(1337) + >>> print(bitmath.MiB(1337)) 1337.0 MiBs >>> bitmath.format_plural = False - >>> print bitmath.MiB(1337) + >>> print(bitmath.MiB(1337)) 1337.0 MiB On line **5** we disable pluralization again and then see that the @@ -810,7 +878,9 @@ behavior. 'G': 1000000000, 'T': 1000000000000, 'P': 1000000000000000, - 'E': 1000000000000000000 + 'E': 1000000000000000000, + 'Z': 1000000000000000000000, + 'Y': 1000000000000000000000000 } @@ -832,7 +902,9 @@ behavior. 'Gi': 1073741824, 'Ti': 1099511627776, 'Pi': 1125899906842624, - 'Ei': 1152921504606846976 + 'Ei': 1152921504606846976, + 'Zi': 1180591620717411303424, + 'Yi': 1208925819614629174706176 } @@ -843,215 +915,17 @@ behavior. .. code-block:: python - ALL_UNIT_TYPES = ['b', 'B', 'kb', 'kB', 'Mb', 'MB', 'Gb', 'GB', - 'Tb', 'TB', 'Pb', 'PB', 'Eb', 'EB', 'Kib', 'KiB', 'Mib', - 'MiB', 'Gib', 'GiB', 'Tib', 'TiB', 'Pib', 'PiB', 'Eib', - 'EiB'] + 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'] -.. py:module:: bitmath.integrations +.. _bitmath_3rd_party_module_integrations: 3rd Party Module Integrations ***************************** -This section describes the various ways in which :py:mod:`bitmath` can -be integrated with other 3rd pary modules. - -To see a full demo of the :mod:`argparse` and :mod:`progressbar` -integrations, as well as a comprehensive demonstrations of the full -capabilities of the bitmath library, see :ref:`Creating Download -Progress Bars ` in the -*Real Life Examples* section. - -.. _bitmath_BitmathType: - -argparse -======== - -.. versionadded:: 1.2.0 - -The `argparse module -`_ (part of stdlib) -is used to parse command line arguments. By default, parsed options -and arguments are turned into strings. However, one useful feature -:py:mod:`argparse` provides is the ability to `specify what datatype -`_ any given -argument or option should be interpreted as. - -.. function:: BitmathType(bmstring) - - The :func:`BitmathType` factory creates objects that can be passed - to the type argument of `ArgumentParser.add_argument() - `_. Arguments - that have :func:`BitmathType` objects as their type will - automatically parse the command line argument into a matching - :ref:`bitmath object `. - - :param str bmstring: The command-line option to parse into a - bitmath object - :returns: A bitmath object representing ``bmstring`` - :raises ValueError: on any input that - :py:func:`bitmath.parse_string` already rejects - :raises ValueError: on **unquoted inputs** with whitespace - separating the value from the unit (e.g., - ``--some-option 10 MiB`` is bad, but - ``--some-option '10 MiB'`` is good) - - Let's take a look at a more in-depth example. - - A feature found in many command-line utilities is the ability to - specify some kind of file size using a string which roughly - describes some kind of parameter. For example, let's look at the - :program:`du` (disk usage) command. Invoking it as :option:`du -B` - allows one to specify a desired block-size scaling factor in - printed results. - - Let's say we wanted to implement a similar mechanism in an - application of our own. Except, instead of abbreviating down to - ambiguous capital letters, we accept scaling factors as - :ref:`properly written values ` with associated - units. Such as **10 MiB**, or **1 MB**. - - To accomplish this, we'll use :py:mod:`argparse` to create an - argument parser and add one option to it, ``--block-size``. This - option will have a type of :func:`BitmathType` set. - - .. code-block:: python - :linenos: - :emphasize-lines: 3,6,7 - - >>> import argparse, bitmath - >>> parser = argparse.ArgumentParser() - >>> parser.add_argument('--block-size', type=bitmath.BitmathType) - >>> args = "--block-size 1MiB" - >>> results = parser.parse_args(args.split()) - >>> print type(results.block_size) - - - On line **3** we add the ``--block-size`` option to the parser, - explicitly defining it's type as :func:`BitmathType`. In lines - **6** and **7** when we parse the provided arguments we find that - :py:mod:`argparse` has automatically created a bitmath object for - us. - - If an invalid scaling factor is provided by the user, such as one - which does not represent a recognizable unit, the bitmath library - will automatically detect this for us and signal to the argument - parser that an error has occurred. - - -.. _bitmath_BitmathFileTransferSpeed: - -progressbar -=========== - -.. versionadded:: 1.2.1 - -The `progressbar module -`_ is typically -used to display the progress of a long running task, such as a file -transfer operation. The module provides widgets for custom formatting -how exactly the 'progress' is displayed. Some examples include: -overall percentage complete, estimated time until completion, and an -ASCII progress bar which fills as the operation continues. - -While :mod:`progressbar` already includes a widget suitable for -displaying `file transfer rates -`_, -this widget does not support customizing its presentation, and is -limited to only prefix units from the SI system. - - -.. class:: BitmathFileTransferSpeed([system=bitmath.NIST, [format="{value:.2f} {unit}/s"]]) - - The :class:`BitmathFileTransferSpeed` class is a more functional - replacement for the upstream `FileTransferSpeed - `_ - widget. - - While both widgets are able to calculate average transfer rates - over a period of time, the :class:`BitmathFileTransferSpeed` widget - adds new support for `NIST `_ prefix units (the - upstream widget only supports SI prefix units). - - In addition to NIST unit support, :class:`BitmathFileTransferSpeed` - enables the user to have **full control** over the look and feel of - the displayed rates. - - :param system: **Default:** :py:data:`bitmath.NIST`. The preferred - system of units for the printed rate. - :type system: One of :py:data:`bitmath.NIST` or :py:data:`bitmath.SI` - :param string format: a formatting mini-language compat formatting - string. **Default** ``{value:.2f} {unit}/s`` - (e.g., ``13.37 GiB/s``) - - .. note:: - - See :ref:`instance attributes ` for a list - of available formatting items. See the section on - :ref:`formatting bitmath instances ` for more - information on this topic. - - - Use :class:`BitmathFileTransferSpeed` exactly like the upstream - ``FileTransferSpeed`` widget (example copied and modified from the - progressbar project page): - - .. code-block:: python - :linenos: - :emphasize-lines: 2,4 - - >>> from progressbar import ProgressBar, Percentage, Bar, ETA, RotatingMarker - >>> from bitmath.integrations import BitmathFileTransferSpeed - >>> widgets = ['Something: ', Percentage(), ' ', Bar(marker=RotatingMarker()), - ... ' ', ETA(), ' ', BitmathFileTransferSpeed()] - >>> pbar = ProgressBar(widgets=widgets, maxval=10000000).start() - >>> for i in range(1000000): - ... # do something - ... pbar.update(10*i+1) - >>> pbar.finish() - - If this was ran from a script we would see output similar to the - following:: - - Something: 100% ||||||||||||||||||||||||||||||||||| Time: 0:00:01 9.27 MiB/s - - If we wanted behavior identical to :class:`FileTransferSpeed` we - would set the ``system`` parameter to :py:data:`bitmath.SI` (line - **5** below): - - .. code-block:: python - :linenos: - :emphasize-lines: 5 - - >>> import bitmath - >>> # ... - >>> widgets = ['Something: ', Percentage(), ' ', Bar(marker=RotatingMarker()), - ... ' ', ETA(), ' ', - ... BitmathFileTransferSpeed(system=bitmath.SI)] - >>> pbar = ProgressBar(widgets=widgets, maxval=10000000).start() - >>> # ... - - If this was ran from a script we would see output similar to the - following:: - - Something: 100% ||||||||||||||||||||||||||||||||||| Time: 0:00:01 9.80 MB/s - - Note how the only difference is in the displayed unit. The former - example produced a rate with a unit of ``MiB`` (a NIST unit) - whereas the latter examples unit is ``MB`` (an SI unit). - - As noted previously, :class:`BitmathFileTransferSpeed` allows for - full control over the formatting of the calculated rate of - transfer. - - For example, if we wished to see the rate printed using more - verbose language and plauralized units, we could do exactly that by - constructing our widget in the following way: - - .. code-block:: python - - BitmathFileTransferSpeed(format="{value:.2f} {unit_plural} per second") - - And if this were run from a script like the previous examples:: - - Something: 100% ||||||||||||||||||||||||||||||||||| Time: 0:00:01 9.41 MiBs per second +Self-contained, copy-paste examples for integrating :mod:`bitmath` with +:mod:`argparse`, `click `_, and +`progressbar2 `_ are collected in +the :ref:`Integration Examples ` chapter. diff --git a/docsite/source/real_life_examples.rst b/docsite/source/real_life_examples.rst index e92bd82..4966859 100644 --- a/docsite/source/real_life_examples.rst +++ b/docsite/source/real_life_examples.rst @@ -23,8 +23,8 @@ as such: :linenos: >>> import bitmath - >>> downstream = bitmath.Mib(50) - >>> print downstream.to_MB() + >>> downstream = bitmath.Mb(50) + >>> print(downstream.to_MB()) MB(6.25) This tells us that if our ISP advertises **50Mbps** we can expect to @@ -51,7 +51,7 @@ in this case is: :emphasize-lines: 3 >>> song_size = GB(5) / 1000 - >>> print song_size + >>> print(song_size) 0.005GB Or, using ``best_prefix``, (line **2**) to generate a more @@ -62,7 +62,7 @@ human-readable form: :emphasize-lines: 2 >>> song_size = GB(5) / 1000 - >>> print song_size.best_prefix() + >>> print(song_size.best_prefix()) 5.0MB That's great, if you have normal radio-length songs. But how many of @@ -78,7 +78,7 @@ MB) large. >>> ipod_capacity = GB(5) >>> bootleg_size = MB(19.5) - >>> print ipod_capacity / bootleg_size + >>> print(ipod_capacity / bootleg_size) 256.41025641 The result on line **4** tells tells us that we could fit **256** @@ -102,7 +102,7 @@ returns). We can use ``bitmath`` to do that too: >>> these_files = os.listdir('.') >>> for f in these_files: ... f_size = Byte(os.path.getsize(f)) - ... print "%s - %s" % (f, f_size.to_KiB()) + ... print("%s - %s" % (f, f_size.to_KiB())) test_basic_math.py - 3.048828125 KiB __init__.py - 0.1181640625 KiB @@ -111,7 +111,7 @@ returns). We can use ``bitmath`` to do that too: Alternatively, we could simplify things and use -:ref:`bitmath.getsize() ` to read the file size +:func:`bitmath.getsize` to read the file size directly into a bitmath object: .. code-block:: python @@ -122,7 +122,7 @@ directly into a bitmath object: >>> import bitmath >>> these_files = os.listdir('.') >>> for f in these_files: - ... print "%s - %s" % (f, bitmath.getsize(f)) + ... print("%s - %s" % (f, bitmath.getsize(f))) test_basic_math.py - 3.048828125 KiB __init__.py - 0.1181640625 KiB @@ -249,24 +249,22 @@ using the :py:mod:`bitmath` library. Let's see how: >>> from bitmath import GB - >>> tx = 1/8.0 + >>> tx = 1/8 >>> rtt = 0.199 * 10**-3 >>> bdp = (GB(tx * rtt)).to_Byte() - >>> print bdp.to_KiB() + >>> print(bdp.to_KiB()) KiB(24.2919921875) -.. note:: - To avoid integer rounding during division, don't forget to divide by ``8.0`` rather than ``8`` We could shorten that even further: .. code-block:: python - >>> print (GB((1/8.0) * (0.199 * 10**-3))).to_Byte() + >>> print((GB((1/8) * (0.199 * 10**-3))).to_Byte()) 24875.0Byte **Get the current kernel parameters** @@ -285,7 +283,7 @@ Recall, these values are in bytes. What are they in KiB? .. code-block:: python - >>> print Byte(212992).to_KiB() + >>> print(Byte(212992).to_KiB()) KiB(208.0) This means our core networking buffer sizes are set to 208KiB @@ -335,11 +333,11 @@ size is ``4096 bytes``, but you can check by running the command: >>> sys_buffer = Byte(sys_pages * page_size) - >>> print sys_buffer.to_MiB() + >>> print(sys_buffer.to_MiB()) 2192.4375MiB - >>> print sys_buffer.to_GiB() + >>> print(sys_buffer.to_GiB()) 2.14105224609GiB @@ -363,8 +361,8 @@ 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 = 4235 - net.core.wmem_max = 4235 + net.core.rmem_max = 24875 + net.core.wmem_max = 24875 Set the **per-socket** buffer sizes: @@ -384,12 +382,10 @@ connections. Creating Download Progress Bars ******************************* - -.. literalinclude:: ../../full_demo.py - -* View the the source for the `demo suite - `_ - on GitHub +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: @@ -411,21 +407,10 @@ 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 - - >>> import bitmath - >>> fh = open('/dev/sda', 'r') - >>> sda_capacity = bitmath.query_device_capacity(fh) - >>> fh.close() - >>> print sda_capacity.best_prefix() - 238.474937439 GiB - -We can simplify this so that the file handle is automatically closed -for us by using the ``with`` context manager. .. code-block:: python >>> with open('/dev/sda', 'r') as fh: ... sda_capacity = bitmath.query_device_capacity(fh) - >>> print sda_capacity.best_prefix() + >>> print(sda_capacity.best_prefix()) 238.474937439 GiB diff --git a/docsite/source/simple_examples.rst b/docsite/source/simple_examples.rst index bb180a6..14e8d15 100644 --- a/docsite/source/simple_examples.rst +++ b/docsite/source/simple_examples.rst @@ -72,7 +72,7 @@ Math works mostly like you expect it to, except for a few edge-cases: +----------------+-------------------+---------------------+---------------------------------------+ | Division | ``bm1`` / ``bm2`` | ``type(num)`` | ``KiB(1) / KiB(2)`` = ``0.5`` | +----------------+-------------------+---------------------+---------------------------------------+ -| Division | ``bm`` / ``num`` | ``type(bm)`` | ``KiB(1) / 3`` = ``0.3330078125KiB`` | +| Division | ``bm`` / ``num`` | ``type(bm)`` | ``KiB(6) / 4`` = ``KiB(1.5)`` | +----------------+-------------------+---------------------+---------------------------------------+ | Division | ``num`` / ``bm`` | ``type(num)`` | ``3 / KiB(2)`` = ``1.5`` | +----------------+-------------------+---------------------+---------------------------------------+ @@ -145,7 +145,7 @@ Rich Comparison *************** Rich Comparison (as per the `Python Basic Customization -`_ +`_ magic methods) ``<``, ``<=``, ``==``, ``!=``, ``>``, ``>=`` is fully supported: @@ -192,19 +192,287 @@ out sorted by increasing magnitude (lines **10** and **11**, and >>> for f in os.listdir('./tests/'): ... sizes.append(KiB(os.path.getsize('./tests/' + f))) - >>> print sizes + >>> 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) + >>> 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) + >>> 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) - [KiB(7892.0), KiB(7337.0), KiB(4190.0), KiB(4003.0), KiB(2326.0), KiB(2178.0), KiB(2126.0), KiB(1770.0), KiB(1441.0), KiB(48.0)] + >>> 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. diff --git a/full_demo.py b/full_demo.py deleted file mode 100755 index 6a492f6..0000000 --- a/full_demo.py +++ /dev/null @@ -1,172 +0,0 @@ -#!/usr/bin/env python -from __future__ import print_function -import logging -import time -import bitmath -from bitmath.integrations.bmargparse import BitmathType -from bitmath.integrations.bmprogressbar import BitmathFileTransferSpeed -import argparse -import requests -import progressbar -import os -import tempfile -import atexit -import random -from functools import reduce - -# Files of various sizes to use in the demo. -# -# Moar here: https://www.kernel.org/pub/linux/kernel/v3.0/?C=S;O=D -REMOTES = [ - # patch-3.0.70.gz 20-Mar-2013 20:02 1.0M - 'https://www.kernel.org/pub/linux/kernel/v3.0/patch-3.4.92.xz', - - # patch-3.16.gz 03-Aug-2014 22:39 8.0M - 'https://www.kernel.org/pub/linux/kernel/v3.0/patch-3.16.gz', - - # patch-3.2.gz 05-Jan-2012 00:43 22M - 'https://www.kernel.org/pub/linux/kernel/v3.0/patch-3.2.gz', -] - -###################################################################### -p = argparse.ArgumentParser(description='bitmath demo suite') -p.add_argument('-d', '--down', help="Download Rate", - type=BitmathType, - default=bitmath.MiB(4)) - -p.add_argument('-s', '--slowdown', - help='Randomly pause to slow down the transfer rate', - action='store_true', default=False) - -args = p.parse_args() - -###################################################################### -# Save our example files somewhere. And then clean up every trace that -# anything every happened there. shhhhhhhhhhhhhhhh -DESTDIR = tempfile.mkdtemp('demosuite', 'bitmath') -@atexit.register -def cleanup(): - for f in os.listdir(DESTDIR): - os.remove(os.path.join(DESTDIR, f)) - os.rmdir(DESTDIR) - -###################################################################### -for f in REMOTES: - print(""" -######################################################################""") - fname = os.path.basename(f) - # An array of widgets to design our progress bar. Note how we use - # BitmathFileTransferSpeed - widgets = ['Bitmath Demo Suite (%s): ' % fname, - progressbar.Percentage(), ' ', - progressbar.Bar(marker=progressbar.RotatingMarker()), ' ', - progressbar.ETA(), ' ', - BitmathFileTransferSpeed()] - - # The 'stream' keyword lets us http GET files in - # chunks. http://docs.python-requests.org/en/latest/user/quickstart/#raw-response-content - r = requests.get(f, stream=True) - # We haven't began receiving the payload content yet, we have only - # just received the response headers. Of interest is the - # 'content-length' header which describes our payload in bytes - # - # http://bitmath.readthedocs.org/en/latest/classes.html#bitmath.Byte - size = bitmath.Byte(int(r.headers['Content-Length'])) - - # Demonstrate 'with' context handler, allowing us to customize all - # bitmath string printing within the indented block. We don't need - # all that precision anyway, just two points should do. - # - # http://bitmath.readthedocs.org/en/latest/module.html#bitmath-format - with bitmath.format("{value:.2f} {unit}"): - print("Downloading %s (%s) in %s chunks" % (f, - size.best_prefix(), - args.down.best_prefix())) - - # We have to save these files somewhere - save_path = os.path.join(DESTDIR, fname) - print("Saving to: %s" % save_path) - print("") - - # OK. Let's create our actual progress bar now. See the 'maxval' - # keyword? That's the size of our payload in bytes. - pbar = progressbar.ProgressBar( - widgets=widgets, - maxval=int(size)).start() - - ###################################################################### - # Open a new file for binary writing and write 'args.down' size - # chunks into it until we've received the entire payload - with open(save_path, 'wb') as fd: - # The 'iter_content' method accepts integer values of - # bytes. Lucky for us, 'args.down' is a bitmath instance and - # has a 'bytes' attribute we can feed into the method call. - for chunk in r.iter_content(int(args.down.bytes)): - fd.write(chunk) - # The progressbar will end the entire cosmos as we know it - # if we try to .update() it beyond it's MAXVAL - # parameter. - # - # That's something I'd like to avoid taking the - # responsibility for. - if (pbar.currval + args.down.bytes) < pbar.maxval: - pbar.update(pbar.currval + int(args.down.bytes)) - - # We can add an pause to artificially speed up/slowdown - # the transfer rate. Allows us to see different units. - if args.slowdown: - # randomly slow down 1/5 of the time - if random.randrange(0, 100) % 5 == 0: - time.sleep(random.randrange(0, 500) * 0.01) - - # Nothing to see here. Go home. - pbar.finish() - -###################################################################### -print(""" -###################################################################### -List downloaded contents -* Filter for .xz files only -""") - -for p,bm in bitmath.listdir(DESTDIR, - filter='*.xz'): - print(p, bm) - -###################################################################### -print(""" -###################################################################### -List downloaded contents -* Filter for .gz files only -* Print using best human readable prefix -""") - -for p,bm in bitmath.listdir(DESTDIR, - filter='*.gz', - bestprefix=True): - print(p, bm) - -###################################################################### -print(""" -###################################################################### -List downloaded contents -* No filter set, to display all files -* Limit precision of printed file size to 3 digits -* Print using best human readable prefix -""") - -for p,bm in bitmath.listdir(DESTDIR, - bestprefix=True): - with bitmath.format("{value:.3f} {unit}"): - print(p, bm) - -###################################################################### -print(""" -###################################################################### -Sum the size of all downloaded files together -* Print with best prefix and 3 digits of precision -""") - -discovered_files = [f[1] for f in bitmath.listdir(DESTDIR)] -total_size = reduce(lambda x,y: x+y, discovered_files).best_prefix().format("{value:.3f} {unit}") -print("Total size of %s downloaded items: %s" % (len(discovered_files), total_size)) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..1125ef7 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,80 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "bitmath" +version = "2.0.0" +description = "Pythonic module for representing and manipulating file sizes with different prefix notations (file size unit conversion)" +readme = "README.rst" +requires-python = ">=3.11" +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.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.publish.index] +disable = true diff --git a/requirements-py3.txt b/requirements-py3.txt deleted file mode 100644 index 298e2cb..0000000 --- a/requirements-py3.txt +++ /dev/null @@ -1,9 +0,0 @@ -python-coveralls -coverage==4.0.3 -mock -nose -nose-cover3 -pyflakes -pycodestyle -progressbar33 -click diff --git a/requirements.txt b/requirements.txt index ca41d5b..ae1ba7e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,4 @@ -python-coveralls -coverage==4.0.3 -mock -nose -pyflakes +flake8 pycodestyle -progressbar231 -click +pytest +pytest-cov diff --git a/setup.py b/setup.py deleted file mode 100644 index d2f9f35..0000000 --- a/setup.py +++ /dev/null @@ -1,80 +0,0 @@ -# -*- coding: utf-8 -*- -# The MIT License (MIT) -# -# Copyright © 2014 Tim Bielawa -# -# 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. - -from __future__ import print_function -try: - from setuptools import setup -except ImportError: - print("Command line script will not be created.") - from distutils.core import setup - - -pypi_notice = open('README.rst', 'r').read() - -setup( - name='bitmath', - version='1.4.0.1', - description='Pythonic module for representing and manipulating file sizes with different prefix notations (file size unit conversion)', - long_description=pypi_notice, - maintainer='Tim Bielawa', - maintainer_email='timbielawa@gmail.com', - url='https://github.com/tbielawa/bitmath', - license='MIT', - package_dir={'bitmath': 'bitmath'}, - packages=['bitmath', 'bitmath.integrations'], - 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 :: MacOS :: MacOS X', - 'Operating System :: POSIX :: Linux', - 'Operating System :: POSIX', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.6', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.1', - 'Programming Language :: Python :: 3.2', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python', - 'Topic :: Scientific/Engineering :: Mathematics', - 'Topic :: Software Development :: Libraries :: Python Modules', - 'Topic :: Software Development :: Libraries', - 'Topic :: Software Development :: User Interfaces', - 'Topic :: Software Development :: Widget Sets', - 'Topic :: System :: Filesystems', - 'Topic :: Text Processing :: Filters', - 'Topic :: Utilities' - ], - entry_points = { - 'console_scripts': [ - 'bitmath = bitmath:cli_script', - ], - } -) diff --git a/setup.py.in b/setup.py.in deleted file mode 100644 index a688c5a..0000000 --- a/setup.py.in +++ /dev/null @@ -1,80 +0,0 @@ -# -*- coding: utf-8 -*- -# The MIT License (MIT) -# -# Copyright © 2014 Tim Bielawa -# -# 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. - -from __future__ import print_function -try: - from setuptools import setup -except ImportError: - print("Command line script will not be created.") - from distutils.core import setup - - -pypi_notice = open('README.rst', 'r').read() - -setup( - name='bitmath', - version='%VERSION%.%RELEASE%', - description='Pythonic module for representing and manipulating file sizes with different prefix notations (file size unit conversion)', - long_description=pypi_notice, - maintainer='Tim Bielawa', - maintainer_email='timbielawa@gmail.com', - url='https://github.com/tbielawa/bitmath', - license='MIT', - package_dir={'bitmath': 'bitmath'}, - packages=['bitmath', 'bitmath.integrations'], - 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 :: MacOS :: MacOS X', - 'Operating System :: POSIX :: Linux', - 'Operating System :: POSIX', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.6', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.1', - 'Programming Language :: Python :: 3.2', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python', - 'Topic :: Scientific/Engineering :: Mathematics', - 'Topic :: Software Development :: Libraries :: Python Modules', - 'Topic :: Software Development :: Libraries', - 'Topic :: Software Development :: User Interfaces', - 'Topic :: Software Development :: Widget Sets', - 'Topic :: System :: Filesystems', - 'Topic :: Text Processing :: Filters', - 'Topic :: Utilities' - ], - entry_points = { - 'console_scripts': [ - 'bitmath = bitmath:cli_script', - ], - } -) diff --git a/tests/__init__.py b/tests/__init__.py index c2cd580..eda90bc 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- +# SPDX-License-Identifier: MIT # The MIT License (MIT) # -# Copyright © 2014 Tim Bielawa +# Copyright © 2014 Tim Case # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation files diff --git a/tests/test_argparse_type.py b/tests/test_argparse_type.py deleted file mode 100644 index 14ec4f6..0000000 --- a/tests/test_argparse_type.py +++ /dev/null @@ -1,88 +0,0 @@ -# -*- coding: utf-8 -*- -# The MIT License (MIT) -# -# Copyright © 2014 Tim Bielawa -# -# 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 argparse 'BitmathType' integration -""" - -from . import TestCase -import bitmath -from bitmath.integrations.bmargparse import BitmathType -import argparse -import shlex - - -class TestArgparseType(TestCase): - def setUp(self): - """Needful for the tests""" - # A simple one-argument parser that only accept one value. - self.parser_one_arg = argparse.ArgumentParser() - self.parser_one_arg.add_argument("--one-arg", type=BitmathType) - - # This parser take one argument, '--two-args'. It requires two values. - self.parser_two_args = argparse.ArgumentParser() - self.parser_two_args.add_argument("--two-args", type=BitmathType, - nargs=2) - - def _parse_one_arg(self, arg_str): - return self.parser_one_arg.parse_args(shlex.split(arg_str)) - - def _parse_two_args(self, arg_str): - return self.parser_two_args.parse_args(shlex.split(arg_str)) - - def test_BitmathType_good_one_arg(self): - """Argparse: BitmathType - Works when given a correct parameter""" - args = "--one-arg 1000EB" - result = self._parse_one_arg(args) - self.assertEqual(bitmath.EB(1000), result.one_arg) - - def test_BitmathType_good_two_args(self): - """Argparse: BitmathType - Works when given two correct parameters""" - args = "--two-args 1337B 0.001GiB" - result = self._parse_two_args(args) - self.assertEqual(len(result.two_args), 2) - self.assertIn(bitmath.Byte(1337), result.two_args) - self.assertIn(bitmath.GiB(0.001), result.two_args) - - def test_BitmathType_bad_wtfareyoudoing(self): - """Argparse: BitmathType - Notices when horrendously incorrect args are provided""" - args = "--one-arg 2098329324kdsjflksdjf" - with self.assertRaises(SystemExit): - self._parse_one_arg(args) - - def test_BitmathType_good_spaces_in_value(self): - """Argparse: BitmathType - 'Quoted values' can be separated from the units by whitespace""" - args = "--two-args '100 MiB' '200 KiB'" - result = self._parse_two_args(args) - self.assertEqual(len(result.two_args), 2) - self.assertIn(bitmath.MiB(100), result.two_args) - self.assertIn(bitmath.KiB(200), result.two_args) - - def test_BitmathType_bad_spaces_in_value(self): - """Argparse: BitmathType - Unquoted values separated from their units are detected""" - args = "--one-arg 1337 B" - with self.assertRaises(SystemExit): - self._parse_one_arg(args) diff --git a/tests/test_basic_math.py b/tests/test_basic_math.py index a938e86..d0705c1 100644 --- a/tests/test_basic_math.py +++ b/tests/test_basic_math.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- +# SPDX-License-Identifier: MIT # The MIT License (MIT) # -# Copyright © 2014 Tim Bielawa +# Copyright © 2014 Tim Case # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation files @@ -125,6 +126,13 @@ def test_number_add_bitmath_is_number(self): 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): diff --git a/tests/test_best_prefix_BASE.py b/tests/test_best_prefix_BASE.py index aa98396..8ccc799 100644 --- a/tests/test_best_prefix_BASE.py +++ b/tests/test_best_prefix_BASE.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- +# SPDX-License-Identifier: MIT # The MIT License (MIT) # -# Copyright © 2014 Tim Bielawa +# Copyright © 2014 Tim Case # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation files @@ -41,11 +42,15 @@ def test_byte_round_down(self): self.assertIs(type(half_byte.best_prefix()), bitmath.Bit) def test_bit_round_up(self): - """best_prefix_base: 2 Bytes (as a Bit()) round up into a Byte()""" + """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) - # Bit(16) should round up into Byte(2) - self.assertIs(type(two_bytes.best_prefix()), bitmath.Byte) + 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()""" diff --git a/tests/test_best_prefix_NIST.py b/tests/test_best_prefix_NIST.py index d979706..5928572 100644 --- a/tests/test_best_prefix_NIST.py +++ b/tests/test_best_prefix_NIST.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- +# SPDX-License-Identifier: MIT # The MIT License (MIT) # -# Copyright © 2014 Tim Bielawa +# Copyright © 2014 Tim Case # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation files @@ -141,3 +142,58 @@ 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) diff --git a/tests/test_best_prefix_SI.py b/tests/test_best_prefix_SI.py index c6d5ce1..35c80fa 100644 --- a/tests/test_best_prefix_SI.py +++ b/tests/test_best_prefix_SI.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- +# SPDX-License-Identifier: MIT # The MIT License (MIT) # -# Copyright © 2014 Tim Bielawa +# Copyright © 2014 Tim Case # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation files @@ -141,3 +142,52 @@ 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) diff --git a/tests/test_bitwise_operations.py b/tests/test_bitwise_operations.py index a6a9353..96cdae5 100644 --- a/tests/test_bitwise_operations.py +++ b/tests/test_bitwise_operations.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- +# SPDX-License-Identifier: MIT # The MIT License (MIT) # -# Copyright © 2014 Tim Bielawa +# Copyright © 2014 Tim Case # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation files diff --git a/tests/test_cli.py b/tests/test_cli.py index 6f46a4b..0a0adf1 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- +# SPDX-License-Identifier: MIT # The MIT License (MIT) # -# Copyright © 2014 Tim Bielawa +# Copyright © 2014 Tim Case # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation files diff --git a/tests/test_click_type.py b/tests/test_click_type.py deleted file mode 100644 index 4e22adf..0000000 --- a/tests/test_click_type.py +++ /dev/null @@ -1,114 +0,0 @@ -# -*- coding: utf-8 -*- -# The MIT License (MIT) -# -# Copyright © 2014 Tim Bielawa -# -# 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 click 'Bitmath' type integration -""" - -from . import TestCase -import bitmath -from bitmath.integrations.bmclick import BitmathType, BITMATH -import click -from click.testing import CliRunner - - -class TestClickType(TestCase): - def setUp(self): - self.runner = CliRunner() - - def test_click_BitmathType_good_one_arg(self): - @click.command() - @click.argument('arg', type=BitmathType()) - def func(arg): - click.echo(arg) - - result = self.runner.invoke(func, ['1000EB']) - self.assertFalse(result.exception) - self.assertEqual(result.output.splitlines(), [str(bitmath.EB(1000))]) - - def test_click_BitmathType_good_one_opt(self): - @click.command() - @click.option('--opt', type=BitmathType()) - def func(opt): - click.echo(opt) - - result = self.runner.invoke(func, ['--opt', '1007TB']) - self.assertFalse(result.exception) - self.assertEqual(result.output.splitlines(), [str(bitmath.TB(1007))]) - - def test_click_BitmathType_good_two_args(self): - @click.command() - @click.argument('arg1', type=BitmathType()) - @click.argument('arg2', type=BitmathType()) - def func(arg1, arg2): - click.echo(arg1) - click.echo(arg2) - - result = self.runner.invoke(func, ['1337B', '0.001GiB']) - self.assertFalse(result.exception) - self.assertEqual(result.output.splitlines(), [str(bitmath.Byte(1337)), - str(bitmath.GiB(0.001))]) - - def test_click_BitmathType_bad_wtfareyoudoing(self): - @click.command() - @click.argument('arg', type=BitmathType()) - def func(arg): - click.echo(arg) - - result = self.runner.invoke(func, ['2098329324kdsjflksdjf']) - self.assertTrue(result.exception) - - def test_click_BitmathType_good_spaces_in_value(self): - @click.command() - @click.argument('arg1', type=BitmathType()) - @click.argument('arg2', type=BitmathType()) - def func(arg1, arg2): - click.echo(arg1) - click.echo(arg2) - - result = self.runner.invoke(func, ['100 MiB', '200 KiB']) - self.assertFalse(result.exception) - self.assertEqual(result.output.splitlines(), [str(bitmath.MiB(100)), - str(bitmath.KiB(200))]) - - def test_click_BitmathType_bad_spaces_in_value(self): - @click.command() - @click.argument('arg', type=BitmathType()) - def func(arg): - click.echo(arg) - - result = self.runner.invoke(func, ['1000', 'EB']) - self.assertTrue(result.exception) - - def test_click_BITMATH_good_one_arg(self): - @click.command() - @click.argument('arg', type=BITMATH) - def func(arg): - click.echo(arg) - - result = self.runner.invoke(func, ['1234.5 TiB']) - self.assertFalse(result.exception) - self.assertEqual(result.output.splitlines(), [str(bitmath.TiB(1234.5))]) diff --git a/tests/test_context_manager.py b/tests/test_context_manager.py index 612ed44..c59bcb0 100644 --- a/tests/test_context_manager.py +++ b/tests/test_context_manager.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- +# SPDX-License-Identifier: MIT # The MIT License (MIT) # -# Copyright © 2014 Tim Bielawa +# Copyright © 2014 Tim Case # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation files @@ -85,12 +86,12 @@ def test_print_byte_plural_fmt_in_mgr(self): self.assertEqual(expected_result, actual_result) def test_print_GiB_plural_fmt_in_mgr(self): - """TiB(1/3.0) prints out units in plural form, setting the fmt str in the mgr""" - expected_result = "3Bytes" + """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_Bytes = bitmath.Byte(3.0) - actual_result = str(three_Bytes) + 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): @@ -101,3 +102,41 @@ def test_print_GiB_singular_fmt_in_mgr(self): 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") diff --git a/tests/test_context_manager_thread_safe.py b/tests/test_context_manager_thread_safe.py new file mode 100644 index 0000000..cfad417 --- /dev/null +++ b/tests/test_context_manager_thread_safe.py @@ -0,0 +1,211 @@ +# -*- coding: utf-8 -*- +# SPDX-License-Identifier: MIT +# The MIT License (MIT) +# +# Copyright © 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 Bytes": + errors.put(AssertionError( + "plural thread: expected '3.0 Bytes', got %r" % result)) + elif not expect_plural and result != "3.0 Byte": + errors.put(AssertionError( + "singular thread: expected '3.0 Byte', 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() diff --git a/tests/test_file_size.py b/tests/test_file_size.py index 389a14c..ce740cd 100644 --- a/tests/test_file_size.py +++ b/tests/test_file_size.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- +# SPDX-License-Identifier: MIT # The MIT License (MIT) # -# Copyright © 2014 Tim Bielawa +# Copyright © 2014 Tim Case # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation files @@ -99,7 +100,7 @@ def test_listdir_nosymlinks(self): Then: >>> for f in bitmath.listdir('./tests/listdir_nosymlinks'): - ... print f + ... print(f) Would yield 2-tuple's of: diff --git a/tests/test_future_math.py b/tests/test_future_math.py index c644072..3ba1ba6 100644 --- a/tests/test_future_math.py +++ b/tests/test_future_math.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- +# SPDX-License-Identifier: MIT # The MIT License (MIT) # -# Copyright © 2014 Tim Bielawa +# Copyright © 2014 Tim Case # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation files @@ -30,7 +31,6 @@ Reference: http://legacy.python.org/dev/peps/pep-0238/ """ -from __future__ import division from . import TestCase import bitmath @@ -61,9 +61,9 @@ def test_number_div_bitmath_is_number(self): self.assertIs(type(result), float) def test_number_truediv_bitmath_is_number(self): - """truediv: number // bitmath = number""" + """truediv: number / bitmath = number""" num1 = 2 bm1 = bitmath.KiB(1) - result = bm1.__rdiv__(num1) + result = bm1.__rtruediv__(num1) self.assertEqual(result, 2.0) self.assertIs(type(result), float) diff --git a/tests/test_init.py b/tests/test_init.py index aa290f9..8b48bac 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- +# SPDX-License-Identifier: MIT # The MIT License (MIT) # -# Copyright © 2015 Tim Bielawa +# Copyright © 2015 Tim Case # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation files diff --git a/tests/test_instantiating.py b/tests/test_instantiating.py index 4723f25..6269760 100644 --- a/tests/test_instantiating.py +++ b/tests/test_instantiating.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- +# SPDX-License-Identifier: MIT # The MIT License (MIT) # -# Copyright © 2014 Tim Bielawa +# Copyright © 2014 Tim Case # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation files @@ -102,3 +103,38 @@ 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) diff --git a/tests/test_parse.py b/tests/test_parse.py index 7c21405..ddce52c 100644 --- a/tests/test_parse.py +++ b/tests/test_parse.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- +# SPDX-License-Identifier: MIT # The MIT License (MIT) # -# Copyright © 2014 Tim Bielawa +# Copyright © 2014 Tim Case # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation files @@ -71,6 +72,18 @@ def test_parse_Eio(self): 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""" @@ -84,6 +97,18 @@ def test_parse_Eo(self): 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): @@ -119,63 +144,59 @@ def test_parse_string_unicode(self): ###################################################################### - def test_parse_unsafe_bad_input_type(self): - """parse_string_unsafe can identify invalid input types""" + def test_parse_non_strict_bad_input_type(self): + """parse_string strict=False can identify invalid input types""" with self.assertRaises(ValueError): - invalid_input = {'keyvalue': 'store'} - bitmath.parse_string_unsafe(invalid_input) + bitmath.parse_string({'keyvalue': 'store'}, strict=False) - def test_parse_unsafe_invalid_input(self): - """parse_string_unsafe explodes when given invalid units""" - invalid_input_str = "kitties!" + def test_parse_non_strict_invalid_input(self): + """parse_string strict=False raises ValueError for invalid units""" with self.assertRaises(ValueError): - bitmath.parse_string_unsafe(invalid_input_str) + bitmath.parse_string("kitties!", strict=False) with self.assertRaises(ValueError): - bitmath.parse_string_unsafe('100 CiB') + bitmath.parse_string('100 CiB', strict=False) with self.assertRaises(ValueError): - bitmath.parse_string_unsafe('100 J') + bitmath.parse_string('100 J', strict=False) - def test_parse_unsafe_good_number_input(self): - """parse_string_unsafe can parse unitless number inputs""" + 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_unsafe(number_input), + bitmath.parse_string(number_input, strict=False), expected_result) self.assertEqual( - bitmath.parse_string_unsafe(string_input), + bitmath.parse_string(string_input, strict=False), expected_result) - def test_parse_unsafe_handles_SI_K_unit(self): - """parse_string_unsafe can parse the upper/lowercase SI 'thousand' (k)""" + 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_unsafe(thousand_lower), + bitmath.parse_string(thousand_lower, strict=False, system=bitmath.SI), expected_result) self.assertEqual( - bitmath.parse_string_unsafe(thousand_upper), + bitmath.parse_string(thousand_upper, strict=False, system=bitmath.SI), expected_result) - def test_parse_unsafe_NIST_units(self): - """parse_string_unsafe can parse abbreviated NIST units (Gi, Ki, ...)""" + 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_unsafe(nist_input), + bitmath.parse_string(nist_input, strict=False), expected_result) - def test_parse_unsafe_SI(self): - """parse_string_unsafe can parse all accepted SI inputs""" - # Begin with the kilo unit because it's the most tricky (SI - # defines the unit as a lower-case 'k') + def test_parse_non_strict_SI(self): + """parse_string strict=False can parse all accepted SI inputs""" kilo_inputs = [ '100k', '100K', @@ -186,11 +207,10 @@ def test_parse_unsafe_SI(self): expected_kilo_result = bitmath.kB(100) for ki in kilo_inputs: - _parsed = bitmath.parse_string_unsafe(ki) + _parsed = bitmath.parse_string(ki, strict=False, system=bitmath.SI) self.assertEqual(_parsed, expected_kilo_result) self.assertIs(type(_parsed), type(expected_kilo_result)) - # Now check for other easier to parse prefixes other_inputs = [ '100g', '100G', @@ -198,18 +218,15 @@ def test_parse_unsafe_SI(self): '100gB', '100GB' ] - expected_gig_result = bitmath.GB(100) for gi in other_inputs: - _parsed = bitmath.parse_string_unsafe(gi) + _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_unsafe_NIST(self): - """parse_string_unsafe can parse all accepted NIST inputs""" - # Begin with the kilo unit because it's the most tricky (SI - # defines the unit as a lower-case 'k') + def test_parse_non_strict_NIST(self): + """parse_string strict=False can parse all accepted NIST inputs""" kilo_inputs = [ '100ki', '100Ki', @@ -220,11 +237,10 @@ def test_parse_unsafe_NIST(self): expected_kilo_result = bitmath.KiB(100) for ki in kilo_inputs: - _parsed = bitmath.parse_string_unsafe(ki) + _parsed = bitmath.parse_string(ki, strict=False) self.assertEqual(_parsed, expected_kilo_result) self.assertIs(type(_parsed), type(expected_kilo_result)) - # Now check for other easier to parse prefixes other_inputs = [ '100gi', '100Gi', @@ -232,71 +248,86 @@ def test_parse_unsafe_NIST(self): '100giB', '100GiB' ] - expected_gig_result = bitmath.GiB(100) for gi in other_inputs: - _parsed = bitmath.parse_string_unsafe(gi) + _parsed = bitmath.parse_string(gi, strict=False) self.assertEqual(_parsed, expected_gig_result) self.assertIs(type(_parsed), type(expected_gig_result)) - def test_parse_string_unsafe_request_NIST(self): - """parse_string_unsafe can convert to NIST on request""" - unsafe_input = "100M" - _parsed = bitmath.parse_string_unsafe(unsafe_input, system=bitmath.NIST) - expected = bitmath.MiB(100) - - self.assertEqual(_parsed, expected) - self.assertIs(type(_parsed), type(expected)) - - unsafe_input2 = "100k" - _parsed2 = bitmath.parse_string_unsafe(unsafe_input2, system=bitmath.NIST) - expected2 = bitmath.KiB(100) - - self.assertEqual(_parsed2, expected2) - self.assertIs(type(_parsed2), type(expected2)) - - unsafe_input3 = "100" - _parsed3 = bitmath.parse_string_unsafe(unsafe_input3, system=bitmath.NIST) - expected3 = bitmath.Byte(100) - - self.assertEqual(_parsed3, expected3) - self.assertIs(type(_parsed3), type(expected3)) - - unsafe_input4 = "100kb" - _parsed4 = bitmath.parse_string_unsafe(unsafe_input4, system=bitmath.NIST) - expected4 = bitmath.KiB(100) - - self.assertEqual(_parsed4, expected4) - self.assertIs(type(_parsed4), type(expected4)) - - ###################################################################### + 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_string_unsafe_github_issue_60(self): - """parse_string_unsafe can parse the examples reported in issue #60 + def test_parse_non_strict_github_issue_60(self): + """parse_string strict=False can parse the examples reported in issue #60 -https://github.com/tbielawa/bitmath/issues/60 +https://github.com/timlnx/bitmath/issues/60 """ - issue_input1 = '7.5KB' - _parsed1 = bitmath.parse_string_unsafe(issue_input1) - expected_result1 = bitmath.kB(7.5) - self.assertEqual( - _parsed1, - expected_result1) - - issue_input2 = '4.7MB' - _parsed2 = bitmath.parse_string_unsafe(issue_input2) - expected_result2 = bitmath.MB(4.7) + bitmath.parse_string('7.5KB', strict=False, system=bitmath.SI), + bitmath.kB(7.5)) self.assertEqual( - _parsed2, - expected_result2) - - issue_input3 = '4.7M' - _parsed3 = bitmath.parse_string_unsafe(issue_input3) - expected_result3 = bitmath.MB(4.7) + bitmath.parse_string('4.7MB', strict=False, system=bitmath.SI), + bitmath.MB(4.7)) self.assertEqual( - _parsed3, - expected_result3) + bitmath.parse_string('4.7M', strict=False, system=bitmath.SI), + bitmath.MB(4.7)) + + def test_parse_string_unsafe_deprecation_warning(self): + """parse_string_unsafe emits DeprecationWarning as of 2.0.0""" + import warnings + 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""" + import warnings + 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) diff --git a/tests/test_progressbar.py b/tests/test_progressbar.py deleted file mode 100644 index 4a78f3d..0000000 --- a/tests/test_progressbar.py +++ /dev/null @@ -1,92 +0,0 @@ -# -*- coding: utf-8 -*- -# The MIT License (MIT) -# -# Copyright © 2014 Tim Bielawa -# -# 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 progressbar 'FileTransferSpeed' integration -""" - -from . import TestCase -import bitmath -from bitmath.integrations.bmprogressbar import BitmathFileTransferSpeed -try: - from unittest import mock -except ImportError: - import mock -import progressbar - - -class TestProgressbar(TestCase): - def setUp(self): - """Needful for the tests""" - self.widget_NIST = BitmathFileTransferSpeed(system=bitmath.NIST) - self.widget_SI = BitmathFileTransferSpeed(system=bitmath.SI) - self.widget_formatted = BitmathFileTransferSpeed(format="{value:.6f} {unit_plural} per second") - - def test_FileTransferSpeed_0_seconds(self): - """Widget renders 0 correctly when no seconds have elapsed""" - pbar = mock.MagicMock(progressbar.ProgressBar) - pbar.seconds_elapsed = 0 - pbar.currval = 0 - update = self.widget_NIST.update(pbar) - self.assertEqual(update, '0.00 Byte/s') - - def test_FileTransferSpeed_1_seconds_Bytes(self): - """Widget renders a non-zero rate after time has elapsed in Bytes""" - pbar = mock.MagicMock(progressbar.ProgressBar) - pbar.seconds_elapsed = 1 - pbar.currval = 512 - update = self.widget_NIST.update(pbar) - self.assertEqual(update, '512.00 Byte/s') - - def test_FileTransferSpeed_10_seconds_MiB(self): - """Widget renders a rate after time has elapsed in MiB/s""" - pbar = mock.MagicMock(progressbar.ProgressBar) - pbar.seconds_elapsed = 10 - # Let's say we've downloaded 512 MiB in that time (we need - # that value in Bytes, though) - pbar.currval = bitmath.MiB(512).bytes - update = self.widget_NIST.update(pbar) - # 512 MiB in 10 seconds is equal to a rate of 51.20 MiB/s - self.assertEqual(update, '51.20 MiB/s') - - def test_FileTransferSpeed_10_seconds_MB(self): - """Widget renders a rate after time has elapsed in MB/s""" - pbar = mock.MagicMock(progressbar.ProgressBar) - pbar.seconds_elapsed = 10 - # Let's say we've downloaded 512 MB in that time (we need that - # value in Bytes, though) - pbar.currval = bitmath.MB(512).bytes - update = self.widget_SI.update(pbar) - # 512 MB in 10 seconds is equal to a rate of 51.20 MB/s - self.assertEqual(update, '51.20 MB/s') - - def test_FileTransferSpeed_custom_format(self): - """Widget renders a custom format string""" - pbar = mock.MagicMock(progressbar.ProgressBar) - pbar.seconds_elapsed = 10 - pbar.currval = 10240 - update = self.widget_formatted.update(pbar) - self.assertEqual(update, '1.000000 KiBs per second') diff --git a/tests/test_properties.py b/tests/test_properties.py index 74e9ddc..ece0e21 100644 --- a/tests/test_properties.py +++ b/tests/test_properties.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- +# SPDX-License-Identifier: MIT # The MIT License (MIT) # -# Copyright © 2014 Tim Bielawa +# Copyright © 2014 Tim Case # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation files @@ -56,3 +57,19 @@ 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) diff --git a/tests/test_query_device_capacity.py b/tests/test_query_device_capacity.py index bda2e7e..171b56c 100644 --- a/tests/test_query_device_capacity.py +++ b/tests/test_query_device_capacity.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- +# SPDX-License-Identifier: MIT # The MIT License (MIT) # -# Copyright © 2015 Tim Bielawa +# Copyright © 2015 Tim Case # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation files @@ -30,29 +31,16 @@ from . import TestCase import bitmath -try: - from unittest import mock -except ImportError: - import mock +from unittest import mock 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) -try: - # Python 3.3+ - from contextlib import ExitStack, contextmanager -except ImportError: - # Python 2.x - from contextlib import nested -else: - @contextmanager - def nested(*contexts): - """Emulation of contextlib.nested in terms of ExitStack - - Has the problems for which "nested" was removed from Python; see: - https://docs.python.org/2/library/contextlib.html#contextlib.nested - But for mock.patch, these do not matter. - """ - 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') diff --git a/tests/test_representation.py b/tests/test_representation.py index 1403eb0..d4ea7b1 100644 --- a/tests/test_representation.py +++ b/tests/test_representation.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- +# SPDX-License-Identifier: MIT # The MIT License (MIT) # -# Copyright © 2014 Tim Bielawa +# Copyright © 2014 Tim Case # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation files @@ -146,3 +147,52 @@ def test_print_byte_singular(self): 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') diff --git a/tests/test_rich_comparison.py b/tests/test_rich_comparison.py index c8f2bcd..c52818d 100644 --- a/tests/test_rich_comparison.py +++ b/tests/test_rich_comparison.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- +# SPDX-License-Identifier: MIT # The MIT License (MIT) # -# Copyright © 2014 Tim Bielawa +# Copyright © 2014 Tim Case # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation files @@ -105,3 +106,48 @@ def test_equal_num(self): 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)) diff --git a/tests/test_rounding.py b/tests/test_rounding.py new file mode 100644 index 0000000..73eff23 --- /dev/null +++ b/tests/test_rounding.py @@ -0,0 +1,133 @@ +# -*- coding: utf-8 -*- +# SPDX-License-Identifier: MIT +# The MIT License (MIT) +# +# Copyright © 2014 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)) diff --git a/tests/test_sorting.py b/tests/test_sorting.py index f036cfa..5bcf08b 100644 --- a/tests/test_sorting.py +++ b/tests/test_sorting.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- +# SPDX-License-Identifier: MIT # The MIT License (MIT) # -# Copyright © 2014 Tim Bielawa +# Copyright © 2014 Tim Case # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation files diff --git a/tests/test_sum.py b/tests/test_sum.py new file mode 100644 index 0000000..0631331 --- /dev/null +++ b/tests/test_sum.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +# SPDX-License-Identifier: MIT +# The MIT License (MIT) +# +# Copyright © 2014 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)) diff --git a/tests/test_to_Type_conversion.py b/tests/test_to_Type_conversion.py index be558c2..818fd57 100644 --- a/tests/test_to_Type_conversion.py +++ b/tests/test_to_Type_conversion.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- +# SPDX-License-Identifier: MIT # The MIT License (MIT) # -# Copyright © 2014 Tim Bielawa +# Copyright © 2014 Tim Case # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation files diff --git a/tests/test_to_built_in_conversion.py b/tests/test_to_built_in_conversion.py index 0f29066..4450b63 100644 --- a/tests/test_to_built_in_conversion.py +++ b/tests/test_to_built_in_conversion.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- +# SPDX-License-Identifier: MIT # The MIT License (MIT) # -# Copyright © 2014 Tim Bielawa +# Copyright © 2014 Tim Case # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation files @@ -25,16 +26,11 @@ """ -Test to verify the int/float/long conversions work correctly +Test to verify the int/float conversions work correctly """ from . import TestCase import bitmath -import sys - -# Python 3.x compat -if sys.version > '3': - long = int class TestToBuiltInConversion(TestCase): @@ -48,8 +44,3 @@ def test_to_float(self): """float(bitmath) returns a float""" gib = bitmath.GiB(1337.8) self.assertIs(type(float(gib)), float) - - def test_to_long(self): - """long(bitmath) returns a long""" - gib = bitmath.GiB(1337.8) - self.assertIs(type(long(gib)), long) diff --git a/tests/test_utils.py b/tests/test_utils.py index 4b8d70f..9882cb4 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- +# SPDX-License-Identifier: MIT # The MIT License (MIT) # -# Copyright © 2014 Tim Bielawa +# Copyright © 2014 Tim Case # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation files