pax_global_header00006660000000000000000000000064143643357020014521gustar00rootroot0000000000000052 comment=15e529d4b5fd943cde4ee80eb5010bdaa8e0611c prefixed-0.7.0/000077500000000000000000000000001436433570200133335ustar00rootroot00000000000000prefixed-0.7.0/.github/000077500000000000000000000000001436433570200146735ustar00rootroot00000000000000prefixed-0.7.0/.github/workflows/000077500000000000000000000000001436433570200167305ustar00rootroot00000000000000prefixed-0.7.0/.github/workflows/tests.yml000066400000000000000000000033101436433570200206120ustar00rootroot00000000000000name: Tests on: push: pull_request: release: schedule: # Every Thursday at 1 AM - cron: '0 1 * * 4' jobs: Tests: runs-on: ${{ matrix.os || 'ubuntu-latest' }} name: ${{ startsWith(matrix.toxenv, 'py') && matrix.python-version || format('{0} ({1})', matrix.toxenv, matrix.python-version) }} ${{ matrix.optional && '[OPTIONAL]' }} continue-on-error: ${{ matrix.optional || false }} strategy: fail-fast: false matrix: python-version: ['3.10'] toxenv: [lint, docs, codecov] include: - python-version: 3.11 toxenv: py311 - python-version: 3.9 toxenv: py39 - python-version: 3.8 toxenv: py38 - python-version: 3.7 toxenv: py37 - python-version: 3.6 toxenv: py36 os: ubuntu-20.04 - python-version: 3.5 toxenv: py35 os: ubuntu-20.04 - python-version: 2.7 toxenv: py27 os: ubuntu-20.04 - python-version: pypy-2.7 toxenv: pypy27 - python-version: pypy-3.9 toxenv: pypy39 - python-version: '3.12-dev' optional: true toxenv: py312 toxpython: 3.12 env: TOXENV: ${{ matrix.toxenv }} TOXPYTHON: python${{ matrix.toxpython || matrix.python-version }} steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install tox run: pip install tox - name: Run tox run: tox -- --verbose prefixed-0.7.0/.gitignore000066400000000000000000000003171436433570200153240ustar00rootroot00000000000000*.egg-info *.pyc __pycache__ # Testing .cache # Editors .spyproject .vscode # Tox .tox # Coverage htmlcov .coverage coverage.xml # Setup dist build # Misc notes # Spec *.spec # Specialist .specialist prefixed-0.7.0/.sourcery.yaml000066400000000000000000000000421436433570200161440ustar00rootroot00000000000000refactor: python_version: '3.3' prefixed-0.7.0/LICENSE000066400000000000000000000405261436433570200143470ustar00rootroot00000000000000Mozilla Public License Version 2.0 ================================== 1. Definitions -------------- 1.1. "Contributor" means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software. 1.2. "Contributor Version" means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor's Contribution. 1.3. "Contribution" means Covered Software of a particular Contributor. 1.4. "Covered Software" means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof. 1.5. "Incompatible With Secondary Licenses" means (a) that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or (b) that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License. 1.6. "Executable Form" means any form of the work other than Source Code Form. 1.7. "Larger Work" means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. 1.8. "License" means this document. 1.9. "Licensable" means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License. 1.10. "Modifications" means any of the following: (a) any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or (b) any new file in Source Code Form that contains any Covered Software. 1.11. "Patent Claims" of a Contributor means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version. 1.12. "Secondary License" means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses. 1.13. "Source Code Form" means the form of the work preferred for making modifications. 1.14. "You" (or "Your") means an individual or a legal entity exercising rights under this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, "control" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 2. License Grants and Conditions -------------------------------- 2.1. Grants Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: (a) under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and (b) under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version. 2.2. Effective Date The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution. 2.3. Limitations on Grant Scope The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor: (a) for any code that a Contributor has removed from Covered Software; or (b) for infringements caused by: (i) Your and any other third party's modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or (c) under Patent Claims infringed by Covered Software in the absence of its Contributions. This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4). 2.4. Subsequent Licenses No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3). 2.5. Representation Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License. 2.6. Fair Use This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents. 2.7. Conditions Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1. 3. Responsibilities ------------------- 3.1. Distribution of Source Form All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients' rights in the Source Code Form. 3.2. Distribution of Executable Form If You distribute Covered Software in Executable Form then: (a) such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and (b) You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients' rights in the Source Code Form under this License. 3.3. Distribution of a Larger Work You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s). 3.4. Notices You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies. 3.5. Application of Additional Terms You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction. 4. Inability to Comply Due to Statute or Regulation --------------------------------------------------- If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. 5. Termination -------------- 5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice. 5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate. 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination. ************************************************************************ * * * 6. Disclaimer of Warranty * * ------------------------- * * * * Covered Software is provided under this License on an "as is" * * basis, without warranty of any kind, either expressed, implied, or * * statutory, including, without limitation, warranties that the * * Covered Software is free of defects, merchantable, fit for a * * particular purpose or non-infringing. The entire risk as to the * * quality and performance of the Covered Software is with You. * * Should any Covered Software prove defective in any respect, You * * (not any Contributor) assume the cost of any necessary servicing, * * repair, or correction. This disclaimer of warranty constitutes an * * essential part of this License. No use of any Covered Software is * * authorized under this License except under this disclaimer. * * * ************************************************************************ ************************************************************************ * * * 7. Limitation of Liability * * -------------------------- * * * * Under no circumstances and under no legal theory, whether tort * * (including negligence), contract, or otherwise, shall any * * Contributor, or anyone who distributes Covered Software as * * permitted above, be liable to You for any direct, indirect, * * special, incidental, or consequential damages of any character * * including, without limitation, damages for lost profits, loss of * * goodwill, work stoppage, computer failure or malfunction, or any * * and all other commercial damages or losses, even if such party * * shall have been informed of the possibility of such damages. This * * limitation of liability shall not apply to liability for death or * * personal injury resulting from such party's negligence to the * * extent applicable law prohibits such limitation. Some * * jurisdictions do not allow the exclusion or limitation of * * incidental or consequential damages, so this exclusion and * * limitation may not apply to You. * * * ************************************************************************ 8. Litigation ------------- Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party's ability to bring cross-claims or counter-claims. 9. Miscellaneous ---------------- This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor. 10. Versions of the License --------------------------- 10.1. New Versions Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number. 10.2. Effect of New Versions You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward. 10.3. Modified Versions If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License). 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached. Exhibit A - Source Code Form License Notice ------------------------------------------- This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. You may add additional accurate notices of copyright ownership. Exhibit B - "Incompatible With Secondary Licenses" Notice --------------------------------------------------------- This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0. prefixed-0.7.0/MANIFEST.in000066400000000000000000000000661436433570200150730ustar00rootroot00000000000000include LICENSE setup_helpers.py README.* graft tests prefixed-0.7.0/README.rst000066400000000000000000000221611436433570200150240ustar00rootroot00000000000000.. start-badges | |docs| |gh_actions| |codecov| | |pypi| |supported-versions| |supported-implementations| .. |docs| image:: https://img.shields.io/readthedocs/prefixed.svg?style=plastic&logo=read-the-docs :target: https://prefixed.readthedocs.org :alt: Documentation Status .. |gh_actions| image:: https://img.shields.io/github/actions/workflow/status/Rockhopper-Technologies/prefixed/tests.yml?event=push&logo=github-actions&style=plastic :target: https://github.com/Rockhopper-Technologies/prefixed/actions/workflows/tests.yml :alt: GitHub Actions Status .. |travis| image:: https://img.shields.io/travis/com/Rockhopper-Technologies/prefixed.svg?style=plastic&logo=travis :target: https://travis-ci.com/Rockhopper-Technologies/prefixed :alt: Travis-CI Build Status .. |codecov| image:: https://img.shields.io/codecov/c/github/Rockhopper-Technologies/prefixed.svg?style=plastic&logo=codecov :target: https://codecov.io/gh/Rockhopper-Technologies/prefixed :alt: Coverage Status .. |pypi| image:: https://img.shields.io/pypi/v/prefixed.svg?style=plastic&logo=pypi :alt: PyPI Package latest release :target: https://pypi.python.org/pypi/prefixed .. |supported-versions| image:: https://img.shields.io/pypi/pyversions/prefixed.svg?style=plastic&logo=pypi :alt: Supported versions :target: https://pypi.python.org/pypi/prefixed .. |supported-implementations| image:: https://img.shields.io/pypi/implementation/prefixed.svg?style=plastic&logo=pypi :alt: Supported implementations :target: https://pypi.python.org/pypi/prefixed .. end-badges Overview ======== Prefixed provides an alternative implementation of the built-in float_ which supports formatted output with `SI (decimal)`_ and `IEC (binary)`_ prefixes. .. code-block:: python >>> from prefixed import Float >>> f'{Float(3250):.2h}' '3.25k' >>> '{:.2h}s'.format(Float(.00001534)) '15.34μs' >>> '{:.2k}B'.format(Float(42467328)) '40.50MiB' >>> f'{Float(2048):.2m}B' '2.00KB' Because `prefixed.Float`_ inherits from the built-in float_, it behaves exactly the same in most cases. When a math operation is performed with another real number type (float_, int_), the result will be a `prefixed.Float`_ instance. Presentation Types ^^^^^^^^^^^^^^^^^^ Additional presentation types ``'h'``, ``'H'``, ``'k'``, ``'K'``, ``'m'``, and ``'M'`` are supported for f-strings and `format()`_. +---------+-------------------------------------------------------------------+ | Type | Meaning | +=========+===================================================================+ | ``'h'`` | SI format. Outputs the number with closest divisible SI prefix. | | | (k, M, G, ...) | +---------+-------------------------------------------------------------------+ | ``'H'`` | Same as ``'h'`` with precision indicating significant digits. | +---------+-------------------------------------------------------------------+ | ``'k'`` | IEC Format. Outputs the number with closest divisible IEC prefix. | | | (Ki, Mi, Gi, ...) | +---------+-------------------------------------------------------------------+ | ``'K'`` | Same as ``'k'`` with precision indicating significant digits. | +---------+-------------------------------------------------------------------+ | ``'m'`` | Short IEC Format. Same as ``'k'`` but only a single character. | | | (K, M, G, ...) | +---------+-------------------------------------------------------------------+ | ``'M'`` | Same as ``'m'`` with precision indicating significant digits. | +---------+-------------------------------------------------------------------+ | | | +---------+-------------------------------------------------------------------+ | ``'j'`` | Alias for ``'k'`` - DEPRECATED | +---------+-------------------------------------------------------------------+ | ``'J'`` | Alias for ``'m'`` - DEPRECATED | +---------+-------------------------------------------------------------------+ String Initialization ^^^^^^^^^^^^^^^^^^^^^ When initializing from strings, SI and IEC prefixes are honored .. code-block:: python >>> Float('2k') Float(2000.0) >>> Float('2Ki') Float(2048.0) Additional Flags ^^^^^^^^^^^^^^^^ An additional format flag '!' is available which adds a space before the prefix .. code-block:: python >>> f'{Float(3250):!.2h}' '3.25 k' Significant Digits ^^^^^^^^^^^^^^^^^^ When the ``'H'``, ``'K``, or ``'M'`` presentation types are used, precision is treated as the number of `significant digits`_ to include. Standard rounding will occur for the final digit. .. code-block:: python >>> f'{Float(1246):.3h}' '1.246k' >>> f'{Float(1246):.3H}' '1.25k' By default, trailing zeros are removed. .. code-block:: python >>> f'{Float(1000):.3H}' '1k' To preserve trailing zeros, include the ``'#'`` flag. .. code-block:: python >>> f'{Float(1000):#.3H}' '1.00k' Adjustable Thresholds ^^^^^^^^^^^^^^^^^^^^^ An additional field, margin, can be specified which lowers or raises the threshold for for each prefix by the given percentage. Margin is specified before precision with the syntax ``%[-]digit+``. .. code-block:: python >>> f'{Float(950):.2h}' '950.00' >>> f'{Float(950):%-5.2h}' '0.95k' >>> f'{Float(1000):%5.2h}' '1000.00' >>> f'{Float(1050):%5.2h}' '1.05k' .. _SI (decimal): https://en.wikipedia.org/wiki/Metric_prefix .. _IEC (binary): https://en.wikipedia.org/wiki/Binary_prefix .. _signifigant digits: https://en.wikipedia.org/wiki/Significant_figures Supported Prefixes ================== SI (Decimal) Prefixes ^^^^^^^^^^^^^^^^^^^^^ +--------+--------+----------+ | Prefix | Name | Base | +========+========+==========+ | Q | Quetta | |10^30| | +--------+--------+----------+ | R | Ronna | |10^27| | +--------+--------+----------+ | Y | Yotta | |10^24| | +--------+--------+----------+ | Z | Zetta | |10^21| | +--------+--------+----------+ | E | Exa | |10^18| | +--------+--------+----------+ | P | Peta | |10^15| | +--------+--------+----------+ | T | Tera | |10^12| | +--------+--------+----------+ | G | Giga | |10^9| | +--------+--------+----------+ | M | Mega | |10^6| | +--------+--------+----------+ | k | Kilo | |10^3| | +--------+--------+----------+ | m | Milli | |10^-3| | +--------+--------+----------+ | μ | Micro | |10^-6| | +--------+--------+----------+ | n | Nano | |10^-9| | +--------+--------+----------+ | p | Pico | |10^-12| | +--------+--------+----------+ | f | Femto | |10^-15| | +--------+--------+----------+ | a | Atto | |10^-18| | +--------+--------+----------+ | z | Zepto | |10^-21| | +--------+--------+----------+ | y | Yocto | |10^-24| | +--------+--------+----------+ | r | Ronto | |10^-27| | +--------+--------+----------+ | q | Quecto | |10^-30| | +--------+--------+----------+ IEC (Binary) Prefixes ^^^^^^^^^^^^^^^^^^^^^ +--------+------+--------+ | Prefix | Name | Base | +========+======+========+ | Y | Yobi | |2^80| | +--------+------+--------+ | Z | Zebi | |2^70| | +--------+------+--------+ | E | Exbi | |2^60| | +--------+------+--------+ | P | Pedi | |2^50| | +--------+------+--------+ | T | Tebi | |2^40| | +--------+------+--------+ | G | Gibi | |2^30| | +--------+------+--------+ | M | Mebi | |2^20| | +--------+------+--------+ | K | Kibi | |2^10| | +--------+------+--------+ .. _SI (decimal): https://en.wikipedia.org/wiki/Metric_prefix .. _IEC (binary): https://en.wikipedia.org/wiki/Binary_prefix .. _float: https://docs.python.org/3/library/functions.html#float .. _int: https://docs.python.org/3/library/functions.html#int .. _prefixed.Float: https://prefixed.readthedocs.io/en/stable/api.html#prefixed.Float .. _format(): https://docs.python.org/3/library/functions.html#format .. |10^30| replace:: 10\ :sup:`30`\ .. |10^27| replace:: 10\ :sup:`27`\ .. |10^24| replace:: 10\ :sup:`24`\ .. |10^21| replace:: 10\ :sup:`21`\ .. |10^18| replace:: 10\ :sup:`18`\ .. |10^15| replace:: 10\ :sup:`15`\ .. |10^12| replace:: 10\ :sup:`12`\ .. |10^9| replace:: 10\ :sup:`9`\ .. |10^6| replace:: 10\ :sup:`6`\ .. |10^3| replace:: 10\ :sup:`3`\ .. |10^-3| replace:: 10\ :sup:`-3`\ .. |10^-6| replace:: 10\ :sup:`-6`\ .. |10^-9| replace:: 10\ :sup:`-9`\ .. |10^-12| replace:: 10\ :sup:`-12`\ .. |10^-15| replace:: 10\ :sup:`-15`\ .. |10^-18| replace:: 10\ :sup:`-18`\ .. |10^-21| replace:: 10\ :sup:`-21`\ .. |10^-24| replace:: 10\ :sup:`-24`\ .. |10^-27| replace:: 10\ :sup:`-27`\ .. |10^-30| replace:: 10\ :sup:`-30`\ .. |2^80| replace:: 2\ :sup:`80`\ .. |2^70| replace:: 2\ :sup:`70`\ .. |2^60| replace:: 2\ :sup:`60`\ .. |2^50| replace:: 2\ :sup:`50`\ .. |2^40| replace:: 2\ :sup:`40`\ .. |2^30| replace:: 2\ :sup:`30`\ .. |2^20| replace:: 2\ :sup:`20`\ .. |2^10| replace:: 2\ :sup:`10`\ prefixed-0.7.0/doc/000077500000000000000000000000001436433570200141005ustar00rootroot00000000000000prefixed-0.7.0/doc/api.rst000066400000000000000000000006461436433570200154110ustar00rootroot00000000000000.. Copyright 2017 - 2020 Avram Lubkin, All Rights Reserved This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. :github_url: https://github.com/Rockhopper-Technologies/prefixed API Reference ============= .. py:module:: prefixed .. autoclass:: Float([x]) :members: prefixed-0.7.0/doc/conf.py000066400000000000000000000045511436433570200154040ustar00rootroot00000000000000# Configuration file for the Sphinx documentation builder. # # This file only contains a selection of the most common options. For a full # list see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html # -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # import os import sys sys.path.insert(0, os.path.abspath('..')) from setup_helpers import get_version # noqa: E402 # -- Project information ----------------------------------------------------- project = 'Prefixed' copyright = '2023, Avram Lubkin' author = 'Avram Lubkin' # The full version, including alpha/beta/rc tags version = get_version('../prefixed/__init__.py') release = version # -- General configuration --------------------------------------------------- # The master toctree document. master_doc = 'index' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.napoleon'] if os.environ.get('READTHEDOCS') != 'True': extensions.append('sphinxcontrib.spelling') # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = [] # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = 'sphinx_rtd_theme' # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". # html_static_path = ['_static'] html_static_path = [''] # Remove static path since we have no files htmlhelp_basename = 'prefixed' intersphinx_mapping = {'python': ('https://docs.python.org/3', None)} prefixed-0.7.0/doc/format_spec.rst000066400000000000000000000101071436433570200171330ustar00rootroot00000000000000.. Copyright 2017 - 2023 Avram Lubkin, All Rights Reserved This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. :github_url: https://github.com/Rockhopper-Technologies/prefixed Format Specification ==================== .. code-block:: abnf format_spec ::= [[fill]align][sign][#][0][![!]][width][grouping_option][%[-]margin][.precision][type] fill ::= align ::= "<" | ">" | "=" | "^" sign ::= "+" | "-" | " " width ::= digit+ grouping_option ::= "_" | "," precision ::= digit+ type ::= "e" | "E" | "f" | "F" | "g" | "G" | "h" | "H" | "k" | "K" | "m" | "M" | "n" | "%" Prefixed-specific fields are defined below. Descriptions of standard fields can be found in the `Format Specification Mini-Language`_ documentation. Prefixed-specific fields ^^^^^^^^^^^^^^^^^^^^^^^^ Flags ----- +----------+----------------------------------------------------------+ | Flag | Meaning | +==========+==========================================================+ | ``'!'`` | Add a single space between number and prefix | +----------+----------------------------------------------------------+ | ``'!!'`` | Same as ``'!'``, but drop space if there is no prefix | +----------+----------------------------------------------------------+ Margin ------ By default, a prefix will be used when the magnitude of that prefix is reached. For example, ``format(Float(999), '.1h')`` will result in ``'999.0'`` and ``format(Float(1000), '.1h')`` will result in ``'1.0k'``. Margin specifies the percentage to raise or lower these thresholds. .. code-block:: python >>> f'{Float(950):%-5.2h}' '0.95k' >>> f'{Float(1000):%5.2h}' '1000.00' Presentation Types ------------------ +---------+-------------------------------------------------------------------+ | Type | Meaning | +=========+===================================================================+ | ``'h'`` | SI format. Outputs the number with closest divisible SI prefix. | | | (k, M, G, ...) | +---------+-------------------------------------------------------------------+ | ``'H'`` | Same as ``'h'`` with precision indicating significant digits. | +---------+-------------------------------------------------------------------+ | ``'k'`` | IEC Format. Outputs the number with closest divisible IEC prefix. | | | (Ki, Mi, Gi, ...) | +---------+-------------------------------------------------------------------+ | ``'K'`` | Same as ``'k'`` with precision indicating significant digits. | +---------+-------------------------------------------------------------------+ | ``'m'`` | Short IEC Format. Same as ``'k'`` but only a single character. | | | (K, M, G, ...) | +---------+-------------------------------------------------------------------+ | ``'M'`` | Same as ``'m'`` with precision indicating significant digits. | +---------+-------------------------------------------------------------------+ | | | +---------+-------------------------------------------------------------------+ | ``'j'`` | Alias for ``'k'`` - DEPRECATED | +---------+-------------------------------------------------------------------+ | ``'J'`` | Alias for ``'m'`` - DEPRECATED | +---------+-------------------------------------------------------------------+ .. _Format Specification Mini-Language: https://docs.python.org/3/library/string.html#formatspecprefixed-0.7.0/doc/index.rst000066400000000000000000000113151436433570200157420ustar00rootroot00000000000000.. Copyright 2020 - 2022 Avram Lubkin, All Rights Reserved This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. :github_url: https://github.com/Rockhopper-Technologies/prefixed .. toctree:: :hidden: self prefixes.rst format_spec.rst api.rst Overview ======== Prefixed provides an alternative implementation of the built-in :py:class:`float` which supports formatted output with `SI (decimal)`_ and `IEC (binary)`_ prefixes. .. code-block:: python >>> from prefixed import Float >>> f'{Float(3250):.2h}' '3.25k' >>> '{:.2h}s'.format(Float(.00001534)) '15.34μs' >>> '{:.2k}B'.format(Float(42467328)) '40.50MiB' >>> f'{Float(2048):.2m}B' '2.00KB' Because :py:class:`prefixed.Float` inherits from the built-in :py:class:`float`, it behaves exactly the same in most cases. When a math operation is performed with another real number type (:py:class:`float`, :py:class:`int`), the result will be a :py:class:`prefixed.Float` instance. Presentation Types ^^^^^^^^^^^^^^^^^^ Additional presentation types ``'h'``, ``'H'``, ``'k'``, ``'K'``, ``'m'``, and ``'M'`` are supported for f-strings and :py:func:`format`. +---------+-------------------------------------------------------------------+ | Type | Meaning | +=========+===================================================================+ | ``'h'`` | SI format. Outputs the number with closest divisible SI prefix. | | | (k, M, G, ...) | +---------+-------------------------------------------------------------------+ | ``'H'`` | Same as ``'h'`` with precision indicating significant digits. | +---------+-------------------------------------------------------------------+ | ``'k'`` | IEC Format. Outputs the number with closest divisible IEC prefix. | | | (Ki, Mi, Gi, ...) | +---------+-------------------------------------------------------------------+ | ``'K'`` | Same as ``'k'`` with precision indicating significant digits. | +---------+-------------------------------------------------------------------+ | ``'m'`` | Short IEC Format. Same as ``'k'`` but only a single character. | | | (K, M, G, ...) | +---------+-------------------------------------------------------------------+ | ``'M'`` | Same as ``'m'`` with precision indicating significant digits. | +---------+-------------------------------------------------------------------+ | | | +---------+-------------------------------------------------------------------+ | ``'j'`` | Alias for ``'k'`` - DEPRECATED | +---------+-------------------------------------------------------------------+ | ``'J'`` | Alias for ``'m'`` - DEPRECATED | +---------+-------------------------------------------------------------------+ String Initialization ^^^^^^^^^^^^^^^^^^^^^ When initializing from strings, SI and IEC prefixes are honored .. code-block:: python >>> Float('2k') Float(2000.0) >>> Float('2Ki') Float(2048.0) Additional Flags ^^^^^^^^^^^^^^^^ An additional format flag '!' is available which adds a space before the prefix .. code-block:: python >>> f'{Float(3250):!.2h}' '3.25 k' Significant Digits ^^^^^^^^^^^^^^^^^^ When the ``'H'``, ``'K``, or ``'M'`` presentation types are used, precision is treated as the number of `significant digits`_ to include. Standard rounding will occur for the final digit. .. code-block:: python >>> f'{Float(1246):.3h}' '1.246k' >>> f'{Float(1246):.3H}' '1.25k' By default, trailing zeros are removed. .. code-block:: python >>> f'{Float(1000):.3H}' '1k' To preserve trailing zeros, include the ``'#'`` flag. .. code-block:: python >>> f'{Float(1000):#.3H}' '1.00k' Adjustable Thresholds ^^^^^^^^^^^^^^^^^^^^^ An additional field, margin, can be specified which lowers or raises the threshold for for each prefix by the given percentage. Margin is specified before precision with the syntax ``%[-]digit+``. .. code-block:: python >>> f'{Float(950):.2h}' '950.00' >>> f'{Float(950):%-5.2h}' '0.95k' >>> f'{Float(1000):%5.2h}' '1000.00' >>> f'{Float(1050):%5.2h}' '1.05k' .. _SI (decimal): https://en.wikipedia.org/wiki/Metric_prefix .. _IEC (binary): https://en.wikipedia.org/wiki/Binary_prefix .. _signifigant digits: https://en.wikipedia.org/wiki/Significant_figures prefixed-0.7.0/doc/prefixes.rst000066400000000000000000000063521436433570200164650ustar00rootroot00000000000000.. Copyright 2020 - 2022 Avram Lubkin, All Rights Reserved This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. :github_url: https://github.com/Rockhopper-Technologies/prefixed Supported Prefixes ================== SI (Decimal) Prefixes ^^^^^^^^^^^^^^^^^^^^^ +--------+--------+----------+ | Prefix | Name | Base | +========+========+==========+ | Q | Quetta | |10^30| | +--------+--------+----------+ | R | Ronna | |10^27| | +--------+--------+----------+ | Y | Yotta | |10^24| | +--------+--------+----------+ | Z | Zetta | |10^21| | +--------+--------+----------+ | E | Exa | |10^18| | +--------+--------+----------+ | P | Peta | |10^15| | +--------+--------+----------+ | T | Tera | |10^12| | +--------+--------+----------+ | G | Giga | |10^9| | +--------+--------+----------+ | M | Mega | |10^6| | +--------+--------+----------+ | k | Kilo | |10^3| | +--------+--------+----------+ | m | Milli | |10^-3| | +--------+--------+----------+ | μ | Micro | |10^-6| | +--------+--------+----------+ | n | Nano | |10^-9| | +--------+--------+----------+ | p | Pico | |10^-12| | +--------+--------+----------+ | f | Femto | |10^-15| | +--------+--------+----------+ | a | Atto | |10^-18| | +--------+--------+----------+ | z | Zepto | |10^-21| | +--------+--------+----------+ | y | Yocto | |10^-24| | +--------+--------+----------+ | r | Ronto | |10^-27| | +--------+--------+----------+ | q | Quecto | |10^-30| | +--------+--------+----------+ IEC (Binary) Prefixes ^^^^^^^^^^^^^^^^^^^^^ +--------+------+--------+ | Prefix | Name | Base | +========+======+========+ | Y | Yobi | |2^80| | +--------+------+--------+ | Z | Zebi | |2^70| | +--------+------+--------+ | E | Exbi | |2^60| | +--------+------+--------+ | P | Pedi | |2^50| | +--------+------+--------+ | T | Tebi | |2^40| | +--------+------+--------+ | G | Gibi | |2^30| | +--------+------+--------+ | M | Mebi | |2^20| | +--------+------+--------+ | K | Kibi | |2^10| | +--------+------+--------+ .. |10^30| replace:: 10\ :sup:`30`\ .. |10^27| replace:: 10\ :sup:`27`\ .. |10^24| replace:: 10\ :sup:`24`\ .. |10^21| replace:: 10\ :sup:`21`\ .. |10^18| replace:: 10\ :sup:`18`\ .. |10^15| replace:: 10\ :sup:`15`\ .. |10^12| replace:: 10\ :sup:`12`\ .. |10^9| replace:: 10\ :sup:`9`\ .. |10^6| replace:: 10\ :sup:`6`\ .. |10^3| replace:: 10\ :sup:`3`\ .. |10^-3| replace:: 10\ :sup:`-3`\ .. |10^-6| replace:: 10\ :sup:`-6`\ .. |10^-9| replace:: 10\ :sup:`-9`\ .. |10^-12| replace:: 10\ :sup:`-12`\ .. |10^-15| replace:: 10\ :sup:`-15`\ .. |10^-18| replace:: 10\ :sup:`-18`\ .. |10^-21| replace:: 10\ :sup:`-21`\ .. |10^-24| replace:: 10\ :sup:`-24`\ .. |10^-27| replace:: 10\ :sup:`-27`\ .. |10^-30| replace:: 10\ :sup:`-30`\ .. |2^80| replace:: 2\ :sup:`80`\ .. |2^70| replace:: 2\ :sup:`70`\ .. |2^60| replace:: 2\ :sup:`60`\ .. |2^50| replace:: 2\ :sup:`50`\ .. |2^40| replace:: 2\ :sup:`40`\ .. |2^30| replace:: 2\ :sup:`30`\ .. |2^20| replace:: 2\ :sup:`20`\ .. |2^10| replace:: 2\ :sup:`10`\prefixed-0.7.0/doc/spelling_wordlist.txt000066400000000000000000000002211436433570200204000ustar00rootroot00000000000000μ Atto Exa Exbi Femto Gi Gibi Giga ki Kibi Mebi Milli Nano Pedi Peta Pico Quecto Quetta Ronna Ronto Tebi Tera Yobi Yocto Yotta Zebi Zepto Zetta prefixed-0.7.0/prefixed/000077500000000000000000000000001436433570200151415ustar00rootroot00000000000000prefixed-0.7.0/prefixed/__init__.py000066400000000000000000000352541436433570200172630ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright 2020 - 2023 Avram Lubkin, All Rights Reserved # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. """ **Prefixed Package** Numbers with support for formatting with SI and IEC prefixes """ import itertools from math import floor, log10 import re import sys __version__ = '0.7.0' try: BASESTRING = basestring except NameError: BASESTRING = str RE_FORMAT_SPEC = re.compile( # fill: requires align - capture if second char is align char r'(?P.(?=[\<\>\=\^]))?' # align: <>=^ r'(?P[\<\>\=\^])?' # sign +-(space) r'(?P[\+\- ])?' # # Alternative form (Only numeric classes) r'(?P\#)?' # 0: same as 0=, Ignored if fill/align is given r'(?P0)?' # !: Add space before prefix r'(?P!!?)?' # width: integer r'(?P\d+)?' # grouping_option: ,_ r'(?P[,_])?' # margin: r'(?:%(?P-?\d+))?' # .precision: integer r'(?:\.(?P\d+))?' # spec_type: Single non-numeric character r'(?P\D)?$' ) RE_PREFIX = re.compile( r'(?P[-+]?\d+\.?(?:\d+)?(?:[eE]?\d)?) ?(?P(?:[a-zA-Z\u03bc]|\xce\xbc)i?)$' ) SI_SMALL = { 1e-30: 'q', # Quecto 1e-27: 'r', # Ronto 1e-24: 'y', # Yocto 1e-21: 'z', # Zepto 1e-18: 'a', # Atto 1e-15: 'f', # Femto 1e-12: 'p', # Pico 1e-9: 'n', # Nano 1e-6: 'μ', # Micro 1e-3: 'm', # Milli } SI_LARGE = { 1e3: 'k', # Kilo 1e6: 'M', # Mega 1e9: 'G', # Giga 1e12: 'T', # Tera 1e15: 'P', # Peta 1e18: 'E', # Exa 1e21: 'Z', # Zetta 1e24: 'Y', # Yotta 1e27: 'R', # Ronna 1e30: 'Q', # Quetta } SI_SMALLEST = 1e-30 SI_MAGNITUDE = {val: key for key, val in itertools.chain(SI_SMALL.items(), SI_LARGE.items())} IEC_PREFIXES = { 2**10: 'K', # Kibi 2**20: 'M', # Mebi 2**30: 'G', # Gibi 2**40: 'T', # Tebi 2**50: 'P', # Pedi 2**60: 'E', # Exbi 2**70: 'Z', # Zebi 2**80: 'Y', # Yobi } IEC_MAGNITUDE = {val: key for key, val in IEC_PREFIXES.items()} SPEC_FIELDS = ('fill', 'align', 'sign', 'alt', 'zero', 'width', 'grouping') # Use OrderedDict for older versions of Python if sys.version_info[:2] < (3, 7): # pragma: no cover from collections import OrderedDict SI_SMALL = OrderedDict(sorted(SI_SMALL.items())) SI_LARGE = OrderedDict(sorted(SI_LARGE.items())) IEC_PREFIXES = OrderedDict(sorted(IEC_PREFIXES.items())) def raise_from_none(exc): # pragma: no cover """ Convenience function to raise from None in a Python 2/3 compatible manner """ raise exc if sys.version_info[0] >= 3: # pragma: no branch exec('def raise_from_none(exc):\n raise exc from None') # pylint: disable=exec-used DEPRECATED = {'j': 'k', 'J': 'm'} def _convert(value, spec): """ Convert value to value, prefix pair based on format spec value, prefix, and spec are returned spec may be modified to account for prefix length """ absolute_value = abs(value) if spec['type'] in 'hH': prefixes = SI_LARGE if absolute_value >= 1.0 else SI_SMALL else: prefixes = IEC_PREFIXES if absolute_value >= 1.0 else {} margin = 1.0 if spec['margin'] is None else (100.0 + float(spec['margin'])) / 100.0 if prefixes is SI_SMALL and 0 < absolute_value < SI_SMALLEST * margin: magnitude = SI_SMALLEST else: magnitude = 0 for next_mag in prefixes: if absolute_value // (next_mag * margin): magnitude = next_mag else: break if magnitude: value /= magnitude prefix = '%s%s%s' % ('' if spec['prefix_space'] is None else ' ', prefixes[magnitude], 'i' if spec['type'] in 'kK' else '') if spec['width'] is not None: width = int(spec['width']) if width: spec['width'] = str(width - len(prefix)) else: prefix = ' ' if spec['prefix_space'] == '!' else '' return value, prefix, spec # pylint: disable=super-with-arguments class Float(float): """ Subclass of the built-in :py:class:`float` class Key differences: - When a math operation is performed with another real number type (:py:class:`float`, :py:class:`int`), the result will be a :py:class:`prefixed.Float` instance. - Additional presentation types ``'h'``, ``'H'``, ``'k'``, ``'K'``, ``'m'``, and ``'M'`` are supported for f-strings and :py:func:`format`. +---------+-------------------------------------------------------------------+ | Type | Meaning | +=========+===================================================================+ | ``'h'`` | SI format. Outputs the number with closest divisible SI prefix. | | | (k, M, G, ...) | +---------+-------------------------------------------------------------------+ | ``'H'`` | Same as ``'h'`` with precision indicating significant digits. | +---------+-------------------------------------------------------------------+ | ``'k'`` | IEC Format. Outputs the number with closest divisible IEC prefix. | | | (Ki, Mi, Gi, ...) | +---------+-------------------------------------------------------------------+ | ``'K'`` | Same as ``'k'`` with precision indicating significant digits. | +---------+-------------------------------------------------------------------+ | ``'m'`` | Short IEC Format. Same as ``'k'`` but only a single character. | | | (K, M, G, ...) | +---------+-------------------------------------------------------------------+ | ``'M'`` | Same as ``'m'`` with precision indicating significant digits. | +---------+-------------------------------------------------------------------+ | | | +---------+-------------------------------------------------------------------+ | ``'j'`` | Alias for ``'k'`` - DEPRECATED | +---------+-------------------------------------------------------------------+ | ``'J'`` | Alias for ``'m'`` - DEPRECATED | +---------+-------------------------------------------------------------------+ - When initializing from strings, SI and IEC prefixes are honored .. code-block:: python >>> Float('2k') Float(2000.0) >>> Float('2Ki') Float(2048.0) - An additional format flag '!' is available which adds a space before the prefix .. code-block:: python >>> f'{Float(3250):!.2h}' '3.25 k' - When the ``'H'``, ``'K``, or ``'M'`` presentation types are used, precision is treated as the number of significant digits to include. Standard rounding will occur for the final digit. .. code-block:: python >>> f'{Float(1246):.3h}' '1.246k' >>> f'{Float(1246):.3H}' '1.25k' By default, trailing zeros are removed. .. code-block:: python >>> f'{Float(1000):.3H}' '1k' To preserve trailing zeros, include the ``'#'`` flag. .. code-block:: python >>> f'{Float(1000):#.3H}' '1.00k' - An additional field, margin, can be specified which lowers or raises the threshold for for each prefix by the given percentage. Margin is specified before precision with the syntax ``%[-]digit+``. .. code-block:: python >>> f'{Float(950):.2h}' '950.00' >>> f'{Float(950):%-5.2h}' '0.95k' >>> f'{Float(1000):%5.2h}' '1000.00' >>> f'{Float(1050):%5.2h}' '1.05k' """ def __new__(cls, value=0.0): convert_value = value if isinstance(value, BASESTRING): match = RE_PREFIX.match(value) if match: prefix = match.group('prefix') if prefix[-1] == 'i': magnitude = IEC_MAGNITUDE.get(prefix[0]) else: magnitude = SI_MAGNITUDE.get(prefix) if magnitude: convert_value = float(match.group('value')) * magnitude try: new = super(Float, cls).__new__(cls, convert_value) except ValueError: raise_from_none( ValueError('Could not convert %s to Float: %r' % (value.__class__.__name__, value)) ) except TypeError: raise_from_none( TypeError("Can't convert %s to Float: %r" % (value.__class__.__name__, value)) ) return new def __repr__(self): return 'Float(%s)' % super(Float, self).__repr__() def __str__(self): return str(float(self)) def __format__(self, format_spec): # Parse format spec match = RE_FORMAT_SPEC.match(format_spec) if match is None: raise ValueError('Invalid format specifier') spec = match.groupdict() # Handle deprecated spec types if spec['type'] in DEPRECATED: spec['type'] = DEPRECATED[spec['type']] # If not a spec we handle, use float.__format__(() if spec['type'] not in {'h', 'H', 'k', 'K', 'm', 'M'}: return super(Float, self).__format__(format_spec) # Determine value and prefix value, prefix, spec = _convert(float(self), spec) precision = int(spec['precision']) if spec['precision'] else None # Adjust precision for significant digits if spec['type'] in 'HKM': precision = precision or 6 # Try to avoid floating point variance by limiting trailing decimals if value >= 1: value = round(value, precision + 1) # In Python 2.7, floor sometimes returns a float, so coerce with int int_digits = 1 if value == 0.0 else int(floor(log10(abs(value)))) + 1 value = round(value, precision - int_digits) precision = max(0, precision - int_digits) if precision and not spec['alt']: preformat = value.__format__('.%df' % precision) precision -= (len(preformat) - len(preformat.rstrip('0'))) # Remove trailing decimal when no decimal places are occupied elif spec['alt']: spec['alt'] = None # Compose new format spec new_spec = ''.join(spec[key] for key in SPEC_FIELDS if spec[key] is not None) if precision is None: new_spec += 'f' else: new_spec = '%s.%if' % (new_spec, precision) # Format with new format spec return '%s%s' % (value.__format__(new_spec), prefix) def __abs__(self): return self.__class__(super(Float, self).__abs__()) def __add__(self, value): try: return self.__class__(super(Float, self).__add__(value)) except TypeError: return NotImplemented def __div__(self, value): # pragma: no cover """ Old style division. Implemented to support Python 2.7 """ try: return self.__class__(super(Float, self).__div__(value)) # pylint: disable=no-member except TypeError: return NotImplemented def __divmod__(self, value): try: return tuple(self.__class__(val) for val in super(Float, self).__divmod__(value)) except TypeError: return NotImplemented def __floordiv__(self, value): try: return self.__class__(super(Float, self).__floordiv__(value)) except TypeError: return NotImplemented def __mod__(self, value): try: return self.__class__(super(Float, self).__mod__(value)) except TypeError: return NotImplemented def __mul__(self, value): try: return self.__class__(super(Float, self).__mul__(value)) except TypeError: return NotImplemented def __neg__(self): return self.__class__(super(Float, self).__neg__()) def __pos__(self): return self.__class__(super(Float, self).__pos__()) def __pow__(self, value): try: return self.__class__(super(Float, self).__pow__(value)) except TypeError: return NotImplemented def __radd__(self, value): try: return self.__class__(super(Float, self).__radd__(value)) except TypeError: return NotImplemented def __rdiv__(self, value): # pragma: no cover """ Old style division. Implemented to support Python 2.7 """ try: return self.__class__(super(Float, self).__rdiv__(value)) # pylint: disable=no-member except TypeError: return NotImplemented def __rdivmod__(self, value): try: return tuple(self.__class__(val) for val in super(Float, self).__rdivmod__(value)) except TypeError: return NotImplemented def __rfloordiv__(self, value): try: return self.__class__(super(Float, self).__rfloordiv__(value)) except TypeError: return NotImplemented def __rmod__(self, value): try: return self.__class__(super(Float, self).__rmod__(value)) except TypeError: return NotImplemented def __rmul__(self, value): try: return self.__class__(super(Float, self).__rmul__(value)) except TypeError: return NotImplemented def __rpow__(self, value): try: return self.__class__(super(Float, self).__rpow__(value)) except TypeError: return NotImplemented def __rsub__(self, value): try: return self.__class__(super(Float, self).__rsub__(value)) except TypeError: return NotImplemented def __rtruediv__(self, value): try: return self.__class__(super(Float, self).__rtruediv__(value)) except TypeError: return NotImplemented def __sub__(self, value): try: return self.__class__(super(Float, self).__sub__(value)) except TypeError: return NotImplemented def __truediv__(self, value): try: return self.__class__(super(Float, self).__truediv__(value)) except TypeError: return NotImplemented prefixed-0.7.0/pylintrc000066400000000000000000000011711436433570200151220ustar00rootroot00000000000000 [DESIGN] # Maximum number of branch for function / method body max-branches=15 [MESSAGES CONTROL] disable= consider-using-f-string, # Python 2 redundant-u-string-prefix, # Python 2 [SPELLING] # Spelling dictionary name. spelling-dict=en_US # List of comma separated words that should not be checked. spelling-ignore-words= atto, Avram, exa, exbi, femto, func, gi, gibi, giga, IEC, ki, kibi, Lubkin, mebi, milli, MPL, nano, pedi, peta, pragma, pico, py, quecto, quetta, repr, ronna, ronto, sphinxcontrib, tebi, tera, yobi, yocto, yotta, zebi, zepto, zettaprefixed-0.7.0/setup.cfg000066400000000000000000000010571436433570200151570ustar00rootroot00000000000000[bdist_wheel] universal=1 [metadata] description_file = README.rst license_files = LICENSE [flake8] builtins = __path__ max-line-length = 100 [pycodestyle] max-line-length = 100 [coverage:run] branch = True source = prefixed [coverage:report] show_missing: True fail_under: 100 exclude_lines = pragma: no cover [build_sphinx] source-dir = doc build-dir = build/doc all_files = True fresh-env = True warning-is-error = 1 keep-going = 1 [aliases] spelling=build_sphinx --builder spelling html=build_sphinx --builder html [doc8] max-line-length=100 prefixed-0.7.0/setup.py000066400000000000000000000037601436433570200150530ustar00rootroot00000000000000#!/usr/bin/env python # Copyright 2020 - 2022 Avram Lubkin, All Rights Reserved # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. """ Prefixed Library setup file """ import os from setuptools import setup, find_packages from setup_helpers import get_version, readme setup( name='prefixed', version=get_version(os.path.join('prefixed', '__init__.py')), description="Prefixed alternative numeric library", long_description=readme('README.rst'), author='Avram Lubkin', author_email='avylove@rockhopper.net', maintainer='Avram Lubkin', maintainer_email='avylove@rockhopper.net', url='https://github.com/Rockhopper-Technologies/prefixed', project_urls={'Documentation': 'https://prefixed.readthedocs.io'}, license='MPLv2.0', zip_safe=False, install_requires=[], tests_require=['unittest2; python_version < "2.7"'], packages=find_packages(exclude=['tests', 'tests.*', 'examples']), test_suite='tests', classifiers=[ 'Development Status :: 4 - Beta', 'Intended Audience :: Developers', 'License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)', 'Operating System :: POSIX', 'Operating System :: Microsoft :: Windows', 'Programming Language :: Python', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Terminals', ], keywords='si iec prefix nist', ) prefixed-0.7.0/setup_helpers.py000066400000000000000000000155271436433570200166010ustar00rootroot00000000000000# Copyright 2017 - 2023 Avram Lubkin, All Rights Reserved # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. """ Functions to help with build and setup """ import contextlib import datetime import io import os import re import subprocess import sys RE_VERSION = re.compile(r'__version__\s*=\s*[\'\"](.+)[\'\"]$') DIR_SPELLING = 'build/doc/spelling/' def get_version(filename, encoding='utf8'): """ Get __version__ definition out of a source file """ with io.open(filename, encoding=encoding) as sourcecode: for line in sourcecode: version = RE_VERSION.match(line) if version: return version.group(1) return None def readme(filename, encoding='utf8'): """ Read the contents of a file """ with io.open(filename, encoding=encoding) as source: return source.read() def print_spelling_errors(filename, encoding='utf8'): """ Print misspelled words returned by sphinxcontrib-spelling """ try: filesize = os.stat(filename).st_size except FileNotFoundError: filesize = 0 if filesize: sys.stdout.write('Misspelled Words:\n') with io.open(filename, encoding=encoding) as wordlist: for line in wordlist: sys.stdout.write(' ' + line) return 1 if filesize else 0 def print_all_spelling_errors(path): """ Print all spelling errors in the path """ rtn = 0 if not os.path.isdir(path): return rtn for filename in os.listdir(path): if print_spelling_errors(os.path.join(path, filename)): rtn = 1 return rtn def spelling_clean_dir(path): """ Remove spelling files from path """ if not os.path.isdir(path): return for filename in os.listdir(path): os.unlink(os.path.join(path, filename)) def check_rst2html(path): """ Checks for warnings when doing ReST to HTML conversion """ from docutils.core import publish_file # pylint: disable=import-error,import-outside-toplevel stderr = io.StringIO() # This will exit with status if there is a bad enough error with contextlib.redirect_stderr(stderr): output = publish_file(source_path=path, writer_name='html', enable_exit_status=True, destination_path='/dev/null') warning_text = stderr.getvalue() if warning_text or not output: print(warning_text) return 1 return 0 def _get_changed_files(): """ Get files in current repository that have been changed Ignore changes to copyright lines """ changed = [] # Get list of changed files process = subprocess.run( ('git', 'status', '--porcelain=1'), stdout=subprocess.PIPE, check=True, text=True ) for entry in process.stdout.splitlines(): # Ignore deleted files if entry[1] == 'D': continue # Construct diff command filename = entry[3:].strip() diff_cmd = ['git', 'diff', filename] if entry[0].strip(): diff_cmd.insert(-1, '--cached') # Find files with changes that aren't only for copyright process = subprocess.run(diff_cmd, stdout=subprocess.PIPE, check=True, text=True) for line in process.stdout.splitlines(): if line[0] != '+' or line[:3] == '+++': # Ignore everything but the new contents continue if re.search(r'copyright.*20\d\d', line, re.IGNORECASE): # Ignore copyright line continue changed.append(filename) break return changed def check_copyrights(): """ Check files recursively to ensure year of last change is in copyright line """ this_year = str(datetime.date.today().year) changed_now = _get_changed_files() # Look for copyright lines process = subprocess.run( ('git', 'grep', '-i', 'copyright'), stdout=subprocess.PIPE, check=True, text=True ) rtn = 0 for entry in process.stdout.splitlines(): modified = None # Get the year in the copyright line filename, text = entry.split(':', 1) match = re.match(r'.*(20\d\d)', text) if match: year = match.group(1) # If file is in current changes, use this year if filename in changed_now: modified = this_year # Otherwise, try to get the year of last commit that wasn't only updating copyright else: git_log = subprocess.run( ('git', '--no-pager', 'log', '-U0', filename), stdout=subprocess.PIPE, check=True, text=True ) for line in git_log.stdout.splitlines(): # Get year if line.startswith('Date: '): modified = line.split()[5] # Skip blank line and lines that aren't changes if not line.strip() or line[0] != '+' or line[:3] == '+++': continue # Stop looking on the first line we hit that isn't a copyright if re.search(r'copyright.*20\d\d', line, re.IGNORECASE) is None: break # Special case for Sphinx configuration if filename == 'doc/conf.py' and modified != this_year: # Get the latest change date for docs process = subprocess.run( ('git', 'log', '-1', '--pretty=format:%cs', 'doc/*.rst'), stdout=subprocess.PIPE, check=True, text=True ) modified = process.stdout[:4] # Compare modified date to copyright year if modified and modified != year: rtn = 1 print('%s: %s [%s]' % (filename, text, modified)) return rtn if __name__ == '__main__': # Do nothing if no arguments were given if len(sys.argv) < 2: sys.exit(0) # Print misspelled word list if sys.argv[1] == 'spelling-clean': spelling_clean_dir(DIR_SPELLING) sys.exit(0) # Print misspelled word list if sys.argv[1] == 'spelling': if len(sys.argv) > 2: sys.exit(print_spelling_errors(sys.argv[2])) else: sys.exit(print_all_spelling_errors(DIR_SPELLING)) # Check file for Rest to HTML conversion if sys.argv[1] == 'rst2html': if len(sys.argv) < 3: sys.exit('Missing filename for ReST to HTML check') sys.exit(check_rst2html(sys.argv[2])) # Check copyrights if sys.argv[1] == 'copyright': sys.exit(check_copyrights()) # Unknown option else: sys.stderr.write('Unknown option: %s' % sys.argv[1]) sys.exit(1) prefixed-0.7.0/tests/000077500000000000000000000000001436433570200144755ustar00rootroot00000000000000prefixed-0.7.0/tests/__init__.py000066400000000000000000000000001436433570200165740ustar00rootroot00000000000000prefixed-0.7.0/tests/test_float.py000066400000000000000000000567241436433570200172310ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright 2020 - 2023 Avram Lubkin, All Rights Reserved # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. """ Test file for prefixed.Float """ import sys from prefixed import Float if sys.version_info[0] < 3: import unittest2 as unittest else: import unittest class TestFloat(unittest.TestCase): """ General tests for prefixed.Float """ def test_repr(self): """ Test repr output """ self.assertEqual(repr(Float(1.0)), 'Float(1.0)') def test_str(self): """ Test repr output """ self.assertEqual(str(Float(1.0)), '1.0') # pylint: disable=expression-not-assigned class TestFloatFormatting(unittest.TestCase): """ Tests for prefixed.Float input and output """ def test_output_zero(self): """ Output when value is zero """ zero = Float(0) self.assertEqual(format(zero, '.2h'), '0.00') self.assertEqual(format(zero, '.2H'), '0') self.assertEqual(format(zero, '#.3H'), '0.00') self.assertEqual(format(zero, '.2k'), '0.00') self.assertEqual(format(zero, '.2m'), '0.00') self.assertEqual(format(zero, '.2K'), '0') self.assertEqual(format(zero, '.2M'), '0') self.assertEqual(format(zero, '#.3K'), '0.00') self.assertEqual(format(zero, '#.3M'), '0.00') def test_output_si_large_pos(self): """ Output for large (>=1) positive numbers in SI format """ tests = ( # (float, .2h, .2H, #.3H) (1, '1.00', '1', '1.00'), (11, '11.00', '11', '11.0'), (101, '101.00', '100', '101'), (1010, '1.01k', '1k', '1.01k'), (10010, '10.01k', '10k', '10.0k'), (100010, '100.01k', '100k', '100k'), (1.01e6, '1.01M', '1M', '1.01M'), (1.001e7, '10.01M', '10M', '10.0M'), (1.0001e8, '100.01M', '100M', '100M'), (1.01e9, '1.01G', '1G', '1.01G'), (1.001e10, '10.01G', '10G', '10.0G'), (1.0001e11, '100.01G', '100G', '100G'), (1.01e12, '1.01T', '1T', '1.01T'), (1.001e13, '10.01T', '10T', '10.0T'), (1.0001e14, '100.01T', '100T', '100T'), (1.01e15, '1.01P', '1P', '1.01P'), (1.001e16, '10.01P', '10P', '10.0P'), (1.0001e17, '100.01P', '100P', '100P'), (1.01e18, '1.01E', '1E', '1.01E'), (1.001e19, '10.01E', '10E', '10.0E'), (1.0001e20, '100.01E', '100E', '100E'), (1.01e21, '1.01Z', '1Z', '1.01Z'), (1.001e22, '10.01Z', '10Z', '10.0Z'), (1.0001e23, '100.01Z', '100Z', '100Z'), (1.01e24, '1.01Y', '1Y', '1.01Y'), (1.001e25, '10.01Y', '10Y', '10.0Y'), (1.0001e26, '100.01Y', '100Y', '100Y'), (1.01e27, '1.01R', '1R', '1.01R'), (1.001e28, '10.01R', '10R', '10.0R'), (1.0001e29, '100.01R', '100R', '100R'), (1.01e30, '1.01Q', '1Q', '1.01Q'), (1.001e31, '10.01Q', '10Q', '10.0Q'), (1.0001e32, '100.01Q', '100Q', '100Q'), # Larger than largest magnitude (1.01e33, '1010.00Q', '1000Q', '1010Q'), (1.001e34, '10010.00Q', '10000Q', '10000Q'), (1.0001e35, '100010.00Q', '100000Q', '100000Q'), (10, '10.00', '10', '10.0'), (1e2, '100.00', '100', '100'), (1e3, '1.00k', '1k', '1.00k'), (1e4, '10.00k', '10k', '10.0k'), (1e5, '100.00k', '100k', '100k'), (1e6, '1.00M', '1M', '1.00M'), (1e7, '10.00M', '10M', '10.0M'), (1e8, '100.00M', '100M', '100M'), (1e9, '1.00G', '1G', '1.00G'), (1e10, '10.00G', '10G', '10.0G'), (1e11, '100.00G', '100G', '100G'), (1e12, '1.00T', '1T', '1.00T'), (1e13, '10.00T', '10T', '10.0T'), (1e14, '100.00T', '100T', '100T'), (1e15, '1.00P', '1P', '1.00P'), (1e16, '10.00P', '10P', '10.0P'), (1e17, '100.00P', '100P', '100P'), (1e18, '1.00E', '1E', '1.00E'), (1e19, '10.00E', '10E', '10.0E'), (1e20, '100.00E', '100E', '100E'), (1e21, '1.00Z', '1Z', '1.00Z'), (1e22, '10.00Z', '10Z', '10.0Z'), (1e23, '100.00Z', '100Z', '100Z'), (1e24, '1.00Y', '1Y', '1.00Y'), (1e25, '10.00Y', '10Y', '10.0Y'), (1e26, '100.00Y', '100Y', '100Y'), (1e27, '1.00R', '1R', '1.00R'), (1e28, '10.00R', '10R', '10.0R'), (1e29, '100.00R', '100R', '100R'), (1e30, '1.00Q', '1Q', '1.00Q'), (1e31, '10.00Q', '10Q', '10.0Q'), (1e32, '100.00Q', '100Q', '100Q'), # Larger than largest magnitude (1e33, '1000.00Q', '1000Q', '1000Q'), (1e34, '10000.00Q', '10000Q', '10000Q'), (1e35, '100000.00Q', '100000Q', '100000Q'), ) for test in tests: with self.subTest(test=test): self.assertEqual(format(Float(test[0]), '.2h'), test[1]) self.assertEqual(format(Float(test[0]), '.2H'), test[2]) self.assertEqual(format(Float(test[0]), '#.3H'), test[3]) def test_output_si_small_pos(self): """ Output for small (<1) positive numbers in SI format """ tests = ( # (float, .2h, .2H, #.3H) (1e-1, '100.00m', '100m', '100m'), (1e-2, '10.00m', '10m', '10.0m'), (1e-3, '1.00m', '1m', '1.00m'), (1e-4, '100.00μ', '100μ', '100μ'), (1e-5, '10.00μ', '10μ', '10.0μ'), (1e-6, '1.00μ', '1μ', '1.00μ'), (1e-7, '100.00n', '100n', '100n'), (1e-8, '10.00n', '10n', '10.0n'), (1e-9, '1.00n', '1n', '1.00n'), (1e-10, '100.00p', '100p', '100p'), (1e-11, '10.00p', '10p', '10.0p'), (1e-12, '1.00p', '1p', '1.00p'), (1e-13, '100.00f', '100f', '100f'), (1e-14, '10.00f', '10f', '10.0f'), (1e-15, '1.00f', '1f', '1.00f'), (1e-16, '100.00a', '100a', '100a'), (1e-17, '10.00a', '10a', '10.0a'), (1e-18, '1.00a', '1a', '1.00a'), (1e-19, '100.00z', '100z', '100z'), (1e-20, '10.00z', '10z', '10.0z'), (1e-21, '1.00z', '1z', '1.00z'), (1e-22, '100.00y', '100y', '100y'), (1e-23, '10.00y', '10y', '10.0y'), (1e-24, '1.00y', '1y', '1.00y'), (1e-25, '100.00r', '100r', '100r'), (1e-26, '10.00r', '10r', '10.0r'), (1e-27, '1.00r', '1r', '1.00r'), (1e-28, '100.00q', '100q', '100q'), (1e-29, '10.00q', '10q', '10.0q'), (1e-30, '1.00q', '1q', '1.00q'), # Smaller than smallest magnitude (1e-31, '0.10q', '0.1q', '0.100q'), (1e-32, '0.01q', '0.01q', '0.0100q'), (1e-33, '0.00q', '0.001q', '0.00100q'), ) for test in tests: with self.subTest(test=test): self.assertEqual(format(Float(test[0]), '.2h'), test[1]) self.assertEqual(format(Float(test[0]), '.2H'), test[2]) self.assertEqual(format(Float(test[0]), '#.3H'), test[3]) def test_output_iec_pos(self): """ Output for positive numbers in IEC format """ tests = ( # (float, .2k, .2m, .2K, .2M, #.3K, #.3M) (2, '2.00', '2.00', '2', '2', '2.00', '2.00'), (2**10, '1.00Ki', '1.00K', '1Ki', '1K', '1.00Ki', '1.00K'), (2**20, '1.00Mi', '1.00M', '1Mi', '1M', '1.00Mi', '1.00M'), (2**30, '1.00Gi', '1.00G', '1Gi', '1G', '1.00Gi', '1.00G'), (2**40, '1.00Ti', '1.00T', '1Ti', '1T', '1.00Ti', '1.00T'), (2**50, '1.00Pi', '1.00P', '1Pi', '1P', '1.00Pi', '1.00P'), (2**60, '1.00Ei', '1.00E', '1Ei', '1E', '1.00Ei', '1.00E'), (2**70, '1.00Zi', '1.00Z', '1Zi', '1Z', '1.00Zi', '1.00Z'), (2**80, '1.00Yi', '1.00Y', '1Yi', '1Y', '1.00Yi', '1.00Y'), ) for test in tests: with self.subTest(test=test): self.assertEqual(format(Float(test[0]), '.2k'), test[1]) self.assertEqual(format(Float(test[0]), '.2m'), test[2]) self.assertEqual(format(Float(test[0]), '.2K'), test[3]) self.assertEqual(format(Float(test[0]), '.2M'), test[4]) self.assertEqual(format(Float(test[0]), '#.3K'), test[5]) self.assertEqual(format(Float(test[0]), '#.3M'), test[6]) def test_input_output_si_large(self): """ Large (>1) numbers input matches output """ for num in ('1.00', '11.00', '101.00', '1.01k', '10.01k', '100.01k', '1.01M', '10.01M', '100.01M', '1.01G', '10.01G', '100.01G', '1.01T', '10.01T', '100.01T', '1.01P', '10.01P', '100.01P', '1.01E', '10.01E', '100.01E', '1.01Z', '10.01Z', '100.01Z', '1.01Y', '10.01Y', '100.01Y', '0.00', '10.00', '100.00', '1.00k', '10.00k', '100.00k', '1.00M', '10.00M', '100.00M', '1.00G', '10.00G', '100.00G', '1.00T', '10.00T', '100.00T', '1.00P', '10.00P', '100.00P', '1.00E', '10.00E', '100.00E', '1.00Z', '10.00Z', '100.00Z', '1.00Y', '10.00Y', '100.00Y'): self.assertEqual(format(Float(num), '.2h'), num) self.assertEqual(format(Float('-' + num), '.2h'), '-' + num) self.assertEqual(format(Float('+' + num), '.2h'), num) def test_input_output_si_small(self): """ Large (>1) numbers input matches output """ for num in ('100.00m', '10.00m', '1.00m', '100.00μ', '10.00μ', '1.00μ', '100.00n', '10.00n', '1.00n', '100.00p', '10.00p', '1.00p', '100.00f', '10.00f', '1.00f', '100.00a', '10.00a', '1.00a', '100.00z', '10.00z', '1.00z', '100.00y', '10.00y', '1.00y', '100.01m', '10.01m', '1.01m', '100.01μ', '10.01μ', '1.01μ', '100.01n', '10.01n', '1.01n', '100.01p', '10.01p', '1.01p', '100.01f', '10.01f', '1.01f', '100.01a', '10.01a', '1.01a', '100.01z', '10.01z', '1.01z', '100.01y', '10.01y', '1.01y'): self.assertEqual(format(Float(num), '.2h'), num) self.assertEqual(format(Float('-' + num), '.2h'), '-' + num) self.assertEqual(format(Float('+' + num), '.2h'), num) def test_input_output_iec(self): """ Large (>1) numbers input matches output """ for num in ('1.00', '11.00', '101.00', '1.01Ki', '10.01Ki', '100.01Ki', '1.01Mi', '10.01Mi', '100.01Mi', '1.01Gi', '10.01Gi', '100.01Gi', '1.01Ti', '10.01Ti', '100.01Ti', '1.01Pi', '10.01Pi', '100.01Pi', '1.01Ei', '10.01Ei', '100.01Ei', '1.01Zi', '10.01Zi', '100.01Zi', '1.01Yi', '10.01Yi', '100.01Yi', '0.00', '10.00', '100.00', '1.00Ki', '10.00Ki', '100.00Ki', '1.00Mi', '10.00Mi', '100.00Mi', '1.00Gi', '10.00Gi', '100.00Gi', '1.00Ti', '10.00Ti', '100.00Ti', '1.00Pi', '10.00Pi', '100.00Pi', '1.00Ei', '10.00Ei', '100.00Ei', '1.00Zi', '10.00Zi', '100.00Zi', '1.00Yi', '10.00Yi', '100.00Yi'): short_form = num[:-1] if num[-1] == 'i' else num self.assertEqual(format(Float(num), '.2k'), num) self.assertEqual(format(Float(num), '.2m'), short_form) self.assertEqual(format(Float('-' + num), '.2k'), '-' + num) self.assertEqual(format(Float('-' + num), '.2m'), '-' + short_form) self.assertEqual(format(Float('+' + num), '.2k'), num) self.assertEqual(format(Float('+' + num), '.2m'), short_form) def test_unicode(self): """ For Python 2, test Unicode strings behave the same """ for num in (u'1.00', u'11.00', u'101.00', u'1.01Ki', u'10.01Ki', u'100.01Ki', u'1.01Mi', u'10.01Mi', u'100.01Mi', u'1.01Gi', u'10.01Gi', u'100.01Gi', u'1.01Ti', u'10.01Ti', u'100.01Ti', u'1.01Pi', u'10.01Pi', u'100.01Pi', u'1.01Ei', u'10.01Ei', u'100.01Ei', u'1.01Zi', u'10.01Zi', u'100.01Zi', u'1.01Yi', u'10.01Yi', u'100.01Yi', u'0.00', u'10.00', u'100.00', u'1.00Ki', u'10.00Ki', u'100.00Ki', u'1.00Mi', u'10.00Mi', u'100.00Mi', u'1.00Gi', u'10.00Gi', u'100.00Gi', u'1.00Ti', u'10.00Ti', u'100.00Ti', u'1.00Pi', u'10.00Pi', u'100.00Pi', u'1.00Ei', u'10.00Ei', u'100.00Ei', u'1.00Zi', u'10.00Zi', u'100.00Zi', u'1.00Yi', u'10.00Yi', u'100.00Yi'): short_form = num[:-1] if num[-1] == 'i' else num self.assertEqual(format(Float(num), '.2k'), num) self.assertEqual(format(Float(num), '.2m'), short_form) self.assertEqual(format(Float('-' + num), '.2k'), '-' + num) self.assertEqual(format(Float('-' + num), '.2m'), '-' + short_form) self.assertEqual(format(Float('+' + num), '.2k'), num) self.assertEqual(format(Float('+' + num), '.2m'), short_form) def test_invalid_prefix(self): """ Invalid prefix provided """ with self.assertRaises(ValueError): Float('100D') def test_invalid_type(self): """ Invalid type provided """ with self.assertRaises(TypeError): Float(3j) def test_space(self): """ A single space between value and prefix is accepted """ self.assertEqual(format(Float('3 k'), '.2h'), '3.00k') with self.assertRaises(ValueError): Float('100\tk') def test_invalid_format_spec(self): """ Invalid format spec provided """ with self.assertRaises(ValueError): format(Float(3), '100.f') def test_standard_format_type(self): """ Standard format type provided """ self.assertEqual(format(Float(3), '.2f'), format(3.0, '.2f')) def test_no_format_type(self): """ No format type provided """ self.assertEqual(format(Float(3), '.2'), format(3.0, '.2')) def test_no_precision(self): """ No precision provided """ self.assertEqual(format(Float(3000), 'h'), '%fk' % 3.0) self.assertEqual(format(Float(3001), 'H'), '3.001k') self.assertEqual(format(Float(3001), '#H'), '3.00100k') self.assertEqual(format(Float(4147.2), 'k'), '%fKi' % 4.05) self.assertEqual(format(Float(4147.2), 'm'), '%fK' % 4.05) self.assertEqual(format(Float(4147.2), 'K'), '4.05Ki') self.assertEqual(format(Float(4147.2), 'M'), '4.05K') self.assertEqual(format(Float(4147.2), '#K'), '4.05000Ki') self.assertEqual(format(Float(4147.2), '#M'), '4.05000K') def test_width(self): """ Width specified """ self.assertEqual(format(Float(3000), '6.2h'), ' 3.00k') self.assertEqual(format(Float(3000), '4.2h'), '3.00k') self.assertEqual(format(Float(3000), '00.2h'), '3.00k') def test_exclamation(self): """ Flag for space before prefix """ # Single flag leaves trailing space self.assertEqual(format(Float(500), '!7.2h'), ' 500.00 ') self.assertEqual(format(Float(500), '!4.2h'), '500.00 ') self.assertEqual(format(Float(500), '!.2h'), '500.00 ') # Double flag removes trailing space self.assertEqual(format(Float(500), '!!7.2h'), ' 500.00') self.assertEqual(format(Float(500), '!!4.2h'), '500.00') self.assertEqual(format(Float(500), '!!.2h'), '500.00') # Single flag with prefix self.assertEqual(format(Float(3000), '!7.2h'), ' 3.00 k') self.assertEqual(format(Float(3000), '!4.2h'), '3.00 k') self.assertEqual(format(Float(3000), '!.2h'), '3.00 k') # Double Flag, no difference since prefix is added self.assertEqual(format(Float(3000), '!!7.2h'), ' 3.00 k') self.assertEqual(format(Float(3000), '!!4.2h'), '3.00 k') self.assertEqual(format(Float(3000), '!!.2h'), '3.00 k') def test_margin(self): """ Confirm variable margins """ self.assertEqual(format(Float(950), '.2h'), '950.00') self.assertEqual(format(Float(950), '%-5.2h'), '0.95k') self.assertEqual(format(Float(1000), '%-5.2h'), '1.00k') self.assertEqual(format(Float(949.9), '%-5.2h'), '949.90') self.assertEqual(format(Float(1000), '%5.2h'), '1000.00') self.assertEqual(format(Float(1049), '%5.2h'), '1049.00') self.assertEqual(format(Float(1050), '%5.2h'), '1.05k') def test_deprecated(self): """ Confirm deprecated format specifiers function """ self.assertEqual(format(Float(2048), '.2j'), '2.00Ki') self.assertEqual(format(Float(2048), '.2J'), '2.00K') # Unicode for Python 2 self.assertEqual(format(Float(2048), u'.2j'), '2.00Ki') self.assertEqual(format(Float(2048), u'.2J'), '2.00K') class TestFloatMath(unittest.TestCase): """ Tests for prefixed.Float math """ def test_abs(self): """ Absolute value """ val1 = abs(Float(1000)) val2 = abs(Float(-1000)) self.assertEqual(val1, val2) self.assertEqual(val1, Float(1000)) self.assertEqual(val1, 1000.0) self.assertIsInstance(val1, Float) self.assertIsInstance(val2, Float) def test_signs(self): """ Positive and negative signs """ val = - Float(1000) self.assertEqual(val, -1000.0) self.assertIsInstance(val, Float) val = - Float(-1000) self.assertEqual(val, 1000.0) self.assertIsInstance(val, Float) val = + Float(1000) self.assertEqual(val, 1000.0) self.assertIsInstance(val, Float) val = + Float(-1000) self.assertEqual(val, -1000.0) self.assertIsInstance(val, Float) def test_add(self): """ Addition """ samples = ( (Float(1.0), 1), (Float(1.0), 1.0), (1, Float(1.0)), (1.0, Float(1.0)) ) for num1, num2 in samples: sum1 = num1 + num2 self.assertEqual(sum1, 2.0) self.assertIsInstance(sum1, Float) with self.assertRaises(TypeError): Float(1.0) + object() with self.assertRaises(TypeError): object() + Float(1.0) def test_sub(self): """ Subtraction """ samples = ( (Float(2.0), 1), (Float(2.0), 1.0), (2, Float(1.0)), (2.0, Float(1.0)) ) for num1, num2 in samples: diff = num1 - num2 self.assertEqual(diff, 1.0) self.assertIsInstance(diff, Float) with self.assertRaises(TypeError): Float(1.0) - object() with self.assertRaises(TypeError): object() - Float(1.0) def test_mul(self): """ Multiplication """ samples = ( (Float(2.0), 2), (Float(2.0), 2.0), (2, Float(2.0)), (2.0, Float(2.0)) ) for num1, num2 in samples: prod = num1 * num2 self.assertEqual(prod, 4.0) self.assertIsInstance(prod, Float) with self.assertRaises(TypeError): Float(2.0) * object() with self.assertRaises(TypeError): object() * Float(2.0) def test_div(self): """ Division """ samples = ( (Float(2.0), 2), (Float(2.0), 2.0), (2, Float(2.0)), (2.0, Float(2.0)) ) for num1, num2 in samples: quot = num1 / num2 self.assertEqual(quot, 1.0) self.assertIsInstance(quot, Float) with self.assertRaises(TypeError): Float(2.0) / object() with self.assertRaises(TypeError): object() / Float(2.0) def test_floor_div(self): """ Floor division """ samples = ( (Float(2.4), 2), (Float(2.4), 2.0), (2, Float(1.5)), (2.4, Float(2.0)) ) for num1, num2 in samples: quot = num1 // num2 self.assertEqual(quot, 1.0) self.assertIsInstance(quot, Float) with self.assertRaises(TypeError): Float(2.0) // object() with self.assertRaises(TypeError): object() // Float(2.0) def test_mod(self): """ Modulus """ samples = ( (Float(3.0), 2), (Float(3.0), 2.0), (3, Float(2.0)), (3.0, Float(2.0)) ) for num1, num2 in samples: rem = num1 % num2 self.assertEqual(rem, 1.0) self.assertIsInstance(rem, Float) with self.assertRaises(TypeError): Float(3.0) % object() with self.assertRaises(TypeError): object() % Float(3.0) def test_divmod(self): """ Division modulus """ samples = ( (Float(3.0), 2), (Float(3.0), 2.0), (3, Float(2.0)), (3.0, Float(2.0)) ) for num1, num2 in samples: quot, rem = divmod(num1, num2) self.assertEqual(quot, 1.0) self.assertIsInstance(quot, Float) self.assertEqual(rem, 1.0) self.assertIsInstance(rem, Float) with self.assertRaises(TypeError): divmod(Float(3.0), object()) with self.assertRaises(TypeError): divmod(object(), Float(3.0)) def test_pow(self): """ Exponentiation """ samples = ( (Float(3.0), 2), (Float(3.0), 2.0), (3, Float(2.0)), (3.0, Float(2.0)) ) for num1, num2 in samples: prod = pow(num1, num2) prod2 = num1**num2 self.assertEqual(prod, prod2) self.assertEqual(prod, 9.0) self.assertIsInstance(prod, Float) self.assertIsInstance(prod2, Float) with self.assertRaises(TypeError): pow(Float(3.0), object()) with self.assertRaises(TypeError): pow(object(), Float(3.0)) with self.assertRaises(TypeError): Float(3.0) ** object() with self.assertRaises(TypeError): object() ** Float(3.0) prefixed-0.7.0/tests/test_formatspec.py000066400000000000000000000074771436433570200202700ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright 2020 - 2023 Avram Lubkin, All Rights Reserved # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. """ Test file for prefixed format spec parsing """ import unittest from prefixed import RE_FORMAT_SPEC FIELDS = ('fill', 'align', 'sign', 'alt', 'zero', 'prefix_space', 'width', 'grouping', 'margin', 'precision', 'type') class FormatSpec(unittest.TestCase): """ Tests for format spec regular expression """ def test_fill_align(self): """ Test fill """ for align in '<>=^': spec = RE_FORMAT_SPEC.match('.%s' % align).groupdict() self.assertEqual(spec.pop('fill'), '.') self.assertEqual(spec.pop('align'), align) self.assertTrue(all(field is None for field in spec.values())) def test_align(self): """ Test alignment """ for align in '<>=^': spec = RE_FORMAT_SPEC.match(align).groupdict() self.assertEqual(spec.pop('align'), align) self.assertTrue(all(field is None for field in spec.values())) def test_sign(self): """ Test sign """ for sign in '+- ': spec = RE_FORMAT_SPEC.match(sign).groupdict() self.assertEqual(spec.pop('sign'), sign) self.assertTrue(all(field is None for field in spec.values())) def test_alt(self): """ Test alternative form """ spec = RE_FORMAT_SPEC.match('#').groupdict() self.assertEqual(spec.pop('alt'), '#') self.assertTrue(all(field is None for field in spec.values())) def test_zero(self): """ Test zero alias """ spec = RE_FORMAT_SPEC.match('0').groupdict() self.assertEqual(spec.pop('zero'), '0') self.assertTrue(all(field is None for field in spec.values())) def test_space_prefix(self): """ Test space before prefix flag """ for example in ('!', '!!'): spec = RE_FORMAT_SPEC.match(example).groupdict() self.assertEqual(spec.pop('prefix_space'), example) self.assertTrue(all(field is None for field in spec.values())) def test_width(self): """ Test width """ spec = RE_FORMAT_SPEC.match('40').groupdict() self.assertEqual(spec.pop('width'), '40') self.assertTrue(all(field is None for field in spec.values())) def test_grouping(self): """ Test grouping options """ for opt in ',_': spec = RE_FORMAT_SPEC.match(opt).groupdict() self.assertEqual(spec.pop('grouping'), opt) self.assertTrue(all(field is None for field in spec.values())) def test_margin(self): """ Test margin """ spec = RE_FORMAT_SPEC.match('%4').groupdict() self.assertEqual(spec.pop('margin'), '4') self.assertTrue(all(field is None for field in spec.values())) spec = RE_FORMAT_SPEC.match('%-4').groupdict() self.assertEqual(spec.pop('margin'), '-4') self.assertTrue(all(field is None for field in spec.values())) def test_precision(self): """ Test precision """ spec = RE_FORMAT_SPEC.match('.4').groupdict() self.assertEqual(spec.pop('precision'), '4') self.assertTrue(all(field is None for field in spec.values())) def test_type(self): """ Test format type """ for item in ('f', 'h', 'k', '?', '%'): spec = RE_FORMAT_SPEC.match(item).groupdict() self.assertEqual(spec.pop('type'), item) self.assertTrue(all(field is None for field in spec.values())) prefixed-0.7.0/tox.ini000066400000000000000000000034051436433570200146500ustar00rootroot00000000000000[tox] ignore_basepython_conflict = tRUE envlist = lint copyright coverage docs PY3{9,8,7,6,5} py27 pypy{27,39} [testenv] basepython = python3.10 usedevelop = True deps = pypy27,py27: unittest2 commands = {envpython} -m unittest discover -s {toxinidir}/tests {posargs} [testenv:flake8] skip_install = True deps = flake8 commands = flake8 prefixed setup.py setup_helpers.py tests [testenv:pylint] skip_install = True ignore_errors=True deps = pylint pyenchant commands = pylint prefixed setup setup_helpers tests [testenv:specialist] basepython = python3.11 skip_install = True ignore_errors=True deps = specialist >= 0.2.1 # -h --output {toxinidir}\.specialist commands = {envpython} -m specialist --output {toxinidir}/.specialist --targets prefixed/*.py -m unittest discover -s {toxinidir}/tests {posargs} [testenv:copyright] skip_install = True ignore_errors = True commands = {envpython} setup_helpers.py copyright [testenv:lint] skip_install = True ignore_errors=True deps = {[testenv:flake8]deps} {[testenv:pylint]deps} commands = {[testenv:flake8]commands} {[testenv:pylint]commands} [testenv:coverage] deps = coverage commands = coverage run -m unittest discover -s {toxinidir}/tests {posargs} coverage report [testenv:codecov] passenv = CI CODECOV_* GITHUB_* deps = {[testenv:coverage]deps} codecov commands = {[testenv:coverage]commands} codecov [testenv:docs] deps = sphinx sphinxcontrib-spelling sphinx_rtd_theme commands= {envpython} setup_helpers.py spelling-clean sphinx-build -vWEa --keep-going -b spelling doc build/doc {envpython} setup_helpers.py spelling sphinx-build -vWEa --keep-going -b html doc build/doc