pax_global_header00006660000000000000000000000064141507735760014531gustar00rootroot0000000000000052 comment=6b9bfc06451e6e36289e18626619b68ba6b09318 django-pint-0.6.3/000077500000000000000000000000001415077357600137515ustar00rootroot00000000000000django-pint-0.6.3/.converagerc000066400000000000000000000010311415077357600162430ustar00rootroot00000000000000[run] branch = True source = django-pint [paths] source = src/ */site-packages/ [report] # Regexes for lines to exclude from consideration exclude_lines = # Have to re-enable the standard pragma pragma: no cover # Don't complain about missing debug-only code: def __repr__ if self\.debug # Don't complain if tests don't hit defensive assertion code: raise AssertionError raise NotImplementedError # Don't complain if non-runnable code isn't run: if 0: if __name__ == .__main__.: django-pint-0.6.3/.coveragerc000066400000000000000000000011221415077357600160660ustar00rootroot00000000000000# .coveragerc to control coverage.py [run] branch = True source = django_pint # omit = bad_file.py [paths] source = src/ */site-packages/ [report] # Regexes for lines to exclude from consideration exclude_lines = # Have to re-enable the standard pragma pragma: no cover # Don't complain about missing debug-only code: def __repr__ if self\.debug # Don't complain if tests don't hit defensive assertion code: raise AssertionError raise NotImplementedError # Don't complain if non-runnable code isn't run: if 0: if __name__ == .__main__.: django-pint-0.6.3/.env.example000066400000000000000000000001471415077357600161760ustar00rootroot00000000000000POSTGRES_HOST=postgres POSTGRES_PASSWORD=django_pint POSTGRES_USER=django_pint POSTGRES_DB=django_pint django-pint-0.6.3/.gitignore000066400000000000000000000010731415077357600157420ustar00rootroot00000000000000# Temporary and binary files *~ *.py[cod] *.so *.pyc *.cfg !.isort.cfg !setup.cfg *.orig *.log *.pot __pycache__/* .cache/* .*.swp */.ipynb_checkpoints/* .DS_Store .env local.py # Project files .ropeproject .project .pydevproject .settings .idea tags # Package files *.egg *.eggs/ .installed.cfg *.egg-info # Unittest and coverage htmlcov/* .coverage .tox junit.xml coverage.xml .pytest_cache/ # Build and docs folder/files build/* dist/* sdist/* docs/README.rst docs/api/* docs/_rst/* docs/_build/* cover/* MANIFEST tests/local.py # Per-project virtualenvs .venv*/ django-pint-0.6.3/.pre-commit-config.yaml000066400000000000000000000013201415077357600202260ustar00rootroot00000000000000exclude: '^docs/conf.py' repos: - repo: git://github.com/pre-commit/pre-commit-hooks rev: v3.3.0 hooks: - id: trailing-whitespace - id: check-added-large-files - id: check-ast - id: check-json - id: check-merge-conflict - id: check-xml - id: check-yaml - id: debug-statements - id: end-of-file-fixer - id: requirements-txt-fixer - id: mixed-line-ending args: ['--fix=auto'] - repo: https://github.com/PyCQA/isort rev: 5.6.4 hooks: - id: isort - repo: https://github.com/psf/black rev: 20.8b1 hooks: - id: black language_version: python3 - repo: https://gitlab.com/pycqa/flake8 rev: 3.8.4 hooks: - id: flake8 args: ['--max-line-length=88'] # default of Black django-pint-0.6.3/.readthedocs.yml000066400000000000000000000002271415077357600170400ustar00rootroot00000000000000version: 2 sphinx: configuration: docs/conf.py fail_on_warning: true python: version: 3.8 install: - requirements: docs/requirements.txt django-pint-0.6.3/.travis.yml000066400000000000000000000044111415077357600160620ustar00rootroot00000000000000os: linux arch: arm64-graviton2 virt: lxd dist: focal group: edge language: python services: postgresql cache: - directories: - "$HOME/.cache/pre-commit" - "$TRAVIS_BUILD_DIR/.eggs" - pip stages: - lint - test - deploy jobs: fast_finish: true include: - stage: lint python: '3.9' install: travis_retry pip install pre-commit script: pre-commit run --all-files env: DJANGO="none" - stage: test python: '3.6' env: DJANGO=2.2 - stage: test python: '3.6' env: DJANGO=3.0 - stage: test python: '3.6' env: DJANGO=3.1 - stage: test python: '3.6' env: DJANGO=3.2 - stage: test python: '3.7' env: DJANGO=2.2 - stage: test python: '3.7' env: DJANGO=3.0 - stage: test python: '3.7' env: DJANGO=3.1 - stage: test python: '3.7' env: DJANGO=3.2 - stage: test python: '3.8' env: DJANGO=2.2 - stage: test python: '3.8' env: DJANGO=3.0 - stage: test python: '3.8' env: DJANGO=3.1 - stage: test python: '3.8' env: DJANGO=3.2 - stage: test python: '3.9' env: DJANGO=3.1 - stage: test python: '3.9' env: DJANGO=3.2 - stage: deploy python: '3.9' env: STAGE="release" script: skip install: skip install: - travis_retry pip install tox tox-travis before_script: - bash ci_setup_postgres.sh script: tox after_success: - bash <(curl -s https://codecov.io/bash) deploy: provider: pypi username: __token__ server: https://upload.pypi.org/legacy/ distributions: sdist bdist_wheel password: secure: VEpg5XE97eZj5MB7WEfgeqB3oEUK/je09YIn6H2zE0KSLite4KCrrDI5qexKHObkxRfDhWA9WBua5LjEHMpBbF/HUCyhbWF1Mx4/Hoi2QdQTGOP2ADcKVS27BzxSIJV1QbCyADyDbNvXWvqbbJWTXsI2ixTYuwObHiNT4ZgpLC79QHqQNhWzbai4I/izEhLm+uJOhuaQC+XESL35/HAVFb0qiA+kUhZXK++cdFaBwrZkNQ5XfKPOdBxwHg6ZN9gWXfkGyyqw6lGi+CguXahRmUM7NFvACKxR5rq818S0G/OoILikJBqali7Q7mAMD5sKw6XDz3/DCZj4vKtipxCuhPewMwDKEaMQ/dhZU0owNlk0uSQb68WujmRThWyL/oLZsRCxor1BHViNhaeWFgelcJeUhPRiSOsWkWUnt9kNZr0YUn6Fh/k7gkWiItlAwDvt/nZSYuTzzzrqDNV2kA7vy1JsZ2uFd8bQOS6zFKRj37RdXvgdbh2zHRFezD0qGynNxRByAxjUlxR2ht8ySkxYfonF/jbXvqBy2Tr1XosssGNEuG+RCZ+PlbMMzYKyIYNL+GbiTBSImL3WHJS75ix5BQiqyTBOmhMBHt1aojqzsHO6Y62th0FTQzzOQxrMp5gf012K8wb0joHW2MAc7A6S09NgCUjI4lubL0N66iZIdJU= on: tags: true condition: "$STAGE =~ ^(release)$" django-pint-0.6.3/AUTHORS.rst000066400000000000000000000005561415077357600156360ustar00rootroot00000000000000============ Contributors ============ * Ben Harling (Original Author and Maintainer 2016-2020) * Carli* Freudenberg * Robert Roskam * Cornel Vaideanu * Alex Bhandari * Jonas Haag * Igor Kozyrenko django-pint-0.6.3/CHANGELOG.rst000066400000000000000000000040621415077357600157740ustar00rootroot00000000000000========= Changelog ========= Version 0.6.3 ============= - fix error with Django 3.2 (`issue #36`_) - remove PrecisionError - restructure function a bit, add more type annotations Version 0.6.2 ============= - only a internal technical release as the PyPi token had to be removed due to security breach before and no new token was set before releasing 0.6.1 Version 0.6.1 ============= - Fix wrong mixin type for ``DecimalQuantityFormField`` (`merge request #31 from ikseek`_) - Fix ``BigIntegerQuantityField`` and ``IntegerQuantityField`` showing wrong widget in django admin `issue #34`_ Version 0.6 =========== - Added ``DecimalQuantityField`` - Improved Testing a lot, the different field types are tested individually. Now we have a total of 142 tests covering 98% of the code. Version 0.5 =========== - API Change: Units are now defined project wide in settings and not by defining ureg for Fields - Change of Maintainer to `Carli* Freudenberg`_ - Ported code to work with current version of Django (2.2., 3.0, 3.2) and Python (3.6 - 3.9) - added test for merge requests - use `black`_ to format code - using pytest instead of deprecated django-nose - Allow custom ureg and integer unit field (`merge request #11 from jonashaag`_) - pass base_unit from field to widget (`merge request #5 from cornelv`_) - now using PyScaffold for versioned release - added documentation and uploaded to readthedocs.org - using pre-commit (also in CI) - improved travis ci builds - Created Changelog file Version 0.4 =========== - Last release of Maintainer `Ben Harling`_ .. _Ben Harling: https://github.com/bharling .. _Carli* Freudenberg: https://github.com/CarliJoy .. _merge request #11 from jonashaag: https://github.com/CarliJoy/django-pint/pull/11 .. _merge request #5 from cornelv: https://github.com/CarliJoy/django-pint/pull/5 .. _merge request #31 from ikseek: https://github.com/CarliJoy/django-pint/pull/31 .. _issue #34: https://github.com/CarliJoy/django-pint/issues/34 .. _black: https://github.com/psf/black .. _issue #36: https://github.com/CarliJoy/django-pint/issues/36 django-pint-0.6.3/Dockerfile000066400000000000000000000005751415077357600157520ustar00rootroot00000000000000FROM python:3.8-slim # install system dependencies RUN apt-get update RUN apt-get install -y build-essential libpq-dev curl gettext git postgresql-client RUN pip3 install --upgrade wheel setuptools pip RUN pip3 install pre-commit psycopg2-binary ipdb WORKDIR /django-pint # copy application files COPY . /django-pint RUN pre-commit install RUN pip install -e '.[testing]' django-pint-0.6.3/LICENSE.txt000066400000000000000000000021701415077357600155740ustar00rootroot00000000000000The MIT License (MIT) Copyright (C) 2020 django-pint authors (see AUTHORS file) Copyright (c) 2020 Carli* Freudenberg 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. django-pint-0.6.3/MANIFEST.in000066400000000000000000000000251415077357600155040ustar00rootroot00000000000000include *.md LICENSE django-pint-0.6.3/README.md000066400000000000000000000153771415077357600152450ustar00rootroot00000000000000 [![Build Status](https://api.travis-ci.com/CarliJoy/django-pint.svg?branch=master)](https://travis-ci.com/github/CarliJoy/django-pint) [![codecov](https://codecov.io/gh/CarliJoy/django-pint/branch/master/graph/badge.svg?token=I3M4CLILXE)](https://codecov.io/gh/CarliJoy/django-pint) [![PyPI Downloads](https://img.shields.io/pypi/dm/django-pint.svg?maxAge=2592000?style=plastic)](https://pypistats.org/packages/django-pint) [![Python Versions](https://img.shields.io/pypi/pyversions/django-pint.svg)](https://pypi.org/project/django-pint/) [![PyPI Version](https://img.shields.io/pypi/v/django-pint.svg?maxAge=2592000?style=plastic)](https://pypi.org/project/django-pint/) [![Project Status](https://img.shields.io/pypi/status/django-pint.svg)](https://pypi.org/project/SyncGitlab2MSProject/) [![Wheel Build](https://img.shields.io/pypi/wheel/django-pint.svg)](https://pypi.org/project/django-pint/) [![Code Style Black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Documentation Status](https://readthedocs.org/projects/django-pint/badge/?version=latest)](https://django-pint.readthedocs.io/en/latest/?badge=latest) # Django Quantity Field A Small django field extension allowing you to store quantities in certain units and perform conversions easily. Uses [pint](https://github.com/hgrecco/pint) behind the scenes. Also contains a form field class and form widget that allows a user to choose alternative units to input data. The cleaned_data will output the value in the base_units defined for the field, eg: you specify you want to store a value in grams but will allow users to input either grams or ounces. ## Compatibility Requires django >= 2.2, and python 3.6/3.7/3.8/3.9/3.10 Test with the following combinations: * Django 2.2 (Python 3.6, 3.7, 3.8) * Django 3.0 (Python 3.6, 3.7, 3.8) * Django 3.1 (Python 3.6, 3.7, 3.8, 3.9) * Django 3.2 (Python 3.6, 3.7, 3.8, 3.9, 3.10) ## Installation pip install django-pint ## Simple Example Best way to illustrate is with an example # app/models.py from django.db import models from quantityfield.fields import QuantityField class HayBale(models.Model): weight = QuantityField('tonne') Quantities are stored as float (Django FloatField) and retrieved like any other field >> bale = HayBale.objects.create(weight=1.2) >> bale = HayBale.objects.first() >> bale.weight >> bale.weight.magnitude 1.2 >> bale.weight.units 'tonne' >> bale.weight.to('kilogram') >> bale.weight.to('pound') If your base unit is atomic (i.e. can be represented by an integer), you may also use `IntegerQuantityField` and `BigIntegerQuantityField`. If you prefer exact units you can use the `DecimalQuantityField` You can also pass Quantity objects to be stored in models. These are automatically converted to the units defined for the field ( but can be converted to something else when retrieved of course ). >> from quantityfield.units import ureg >> Quantity = ureg.Quantity >> pounds = Quantity(500 * ureg.pound) >> bale = HayBale.objects.create(weight=pounds) >> bale.weight Use the inbuilt form field and widget to allow input of quantity values in different units from quantityfield.fields import QuantityFormField class HayBaleForm(forms.Form): weight = QuantityFormField(base_units='gram', unit_choices=['gram', 'ounce', 'milligram']) The form will render a float input and a select widget to choose the units. Whenever cleaned_data is presented from the above form the weight field value will be a Quantity with the units set to grams (values are converted from the units input by the user ). You also can add the `unit_choices` directly to the `ModelField`. It will be propagated correctly. For comparative lookups, query values will be coerced into the correct units when comparing values, this means that comparing 1 ounce to 1 tonne should yield the correct results. less_than_a_tonne = HayBale.objects.filter(weight__lt=Quantity(2000 * ureg.pound)) You can also use a custom Pint unit registry in your project `settings.py` # project/settings.py from pint import UnitRegistry # django-pint will set the DJANGO_PINT_UNIT_REGISTER automatically # as application_registry DJANGO_PINT_UNIT_REGISTER = UnitRegistry('your_units.txt') DJANGO_PINT_UNIT_REGISTER.define('beer_bootle_weight = 0.8 * kg = beer') # app/models.py class HayBale(models.Model): # now you can use your custom units in your models custom_unit = QuantityField('beer') Note: As the [documentation from pint](https://pint.readthedocs.io/en/latest/tutorial.html#using-pint-in-your-projects) states quite clearly: For each project there should be only one unit registry. Please note that if you change the unit registry for an already created project with data in a database, you could invalidate your data! So be sure you know what you are doing! Still only adding units should be okay. ## Set Up Local Testing As SQL Lite is not very strict in handling types we use Postgres for testing. This will bring up some possible pitfalls using proper databases. To get the test running please install `postgresql` on your OS. You need to have `psycopg2-binary` installed (see `tox.ini` for further requirements) and a user with the proper permissions set. See `ci_setup_postgres.sh` for an example on HowTo set it up. Or simply run: `sudo -u postgres ./ci_setup_postgres.sh`. You can also use you local credentials by creating a `tests/local.py` file. See `test/settings.py` for a description. ## Local development environment with Docker To run a local development environment with Docker you need to run the following steps: This is helpful if you have troubles installing `postgresql` or `psycopg2-binary`. 1. `git clone` your fork 2. run `cp .env.example .env` 3. edit `.env` file and change it with your credentials ( the postgres host should match the service name in docker-file so you can use "postgres" ) 4. run `cp tests/local.py.docker-example tests/local.py` 5. run `docker-compose up` in the root folder, this should build and start 2 containers, one for postgres and the other one python dependencies. Note you have to be in the [docker](https://stackoverflow.com/a/47078951/3813064) group for this to work. 6. open a new terminal and run `docker-compose exec app bash`, this should open a ssh console in the docker container 7. you can run `pytest` inside the container to see the result of the tests. django-pint-0.6.3/ci_setup_postgres.sh000077500000000000000000000014071415077357600200530ustar00rootroot00000000000000psql -c "create database django_pint;" -U postgres # Settings done according to tutorial https://www.digitalocean.com/community/tutorials/how-to-set-up-django-with-postgres-nginx-and-gunicorn-on-ubuntu-18-04 # Please not you might have to edit your pg_hba.conf in your local installation # see https://docs.boundlessgeo.com/suite/1.1.1/dataadmin/pgGettingStarted/firstconnect.html#allowing-local-connections psql -c "CREATE USER django_pint WITH PASSWORD 'not_secure_in_testing';" -U postgres psql -c "ALTER ROLE django_pint SET client_encoding TO 'utf8';" -U postgres psql -c "ALTER ROLE django_pint SET timezone TO 'UTC';" -U postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE django_pint TO django_pint;" -U postgres psql -c "ALTER ROLE django_pint CREATEDB;" -U postgres django-pint-0.6.3/docker-compose.yaml000066400000000000000000000007331415077357600175520ustar00rootroot00000000000000version: '3' volumes: postgres_data: {} services: postgres: image: postgres:12-alpine volumes: - postgres_data:/var/lib/postgresql/data # DB persistance env_file: - .env ports: - "5432:5432" app: &app build: context: . dockerfile: Dockerfile image: django-pint depends_on: - postgres volumes: - .:/django-pint ports: - "8000:8000" env_file: - .env command: sleep 5d django-pint-0.6.3/docs/000077500000000000000000000000001415077357600147015ustar00rootroot00000000000000django-pint-0.6.3/docs/Makefile000066400000000000000000000022021415077357600163350ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line, and also # from the environment for the first two. SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build SOURCEDIR = . BUILDDIR = _build AUTODOCDIR = api # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $?), 1) $(error "The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from https://sphinx-doc.org/") endif .PHONY: help clean Makefile # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) clean: rm -rf $(BUILDDIR)/* $(AUTODOCDIR) # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) django-pint-0.6.3/docs/_static/000077500000000000000000000000001415077357600163275ustar00rootroot00000000000000django-pint-0.6.3/docs/_static/.gitignore000066400000000000000000000000221415077357600203110ustar00rootroot00000000000000# Empty directory django-pint-0.6.3/docs/authors.rst000066400000000000000000000000511415077357600171140ustar00rootroot00000000000000.. _authors: .. include:: ../AUTHORS.rst django-pint-0.6.3/docs/changelog.rst000066400000000000000000000000531415077357600173600ustar00rootroot00000000000000.. _changes: .. include:: ../CHANGELOG.rst django-pint-0.6.3/docs/conf.py000066400000000000000000000252121415077357600162020ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import os import sys import inspect import shutil __location__ = os.path.join( os.getcwd(), os.path.dirname(inspect.getfile(inspect.currentframe())) ) import m2r2 # Manually convert documentation readme_rst_content = m2r2.parse_from_file(os.path.join(__location__, "..", "README.md")) with open(os.path.join(__location__, "README.rst"), "w") as f: f.write(readme_rst_content) # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.join(__location__, "../src")) # -- Run sphinx-apidoc ------------------------------------------------------ # This hack is necessary since RTD does not issue `sphinx-apidoc` before running # `sphinx-build -b html . _build/html`. See Issue: # https://github.com/rtfd/readthedocs.org/issues/1139 # DON'T FORGET: Check the box "Install your project inside a virtualenv using # setup.py install" in the RTD Advanced Settings. # Additionally it helps us to avoid running apidoc manually try: # for Sphinx >= 1.7 from sphinx.ext import apidoc except ImportError: from sphinx import apidoc output_dir = os.path.join(__location__, "api") module_dir = os.path.join(__location__, "../src/quantityfield") try: shutil.rmtree(output_dir) except FileNotFoundError: pass try: import sphinx from pkg_resources import parse_version cmd_line_template = "sphinx-apidoc -f -o {outputdir} {moduledir}" cmd_line = cmd_line_template.format(outputdir=output_dir, moduledir=module_dir) args = cmd_line.split(" ") if parse_version(sphinx.__version__) >= parse_version("1.7"): args = args[1:] apidoc.main(args) except Exception as e: print("Running `sphinx-apidoc` failed!\n{}".format(e)) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ "sphinx.ext.autodoc", "sphinx.ext.intersphinx", "sphinx.ext.todo", "sphinx.ext.autosummary", "sphinx.ext.viewcode", "sphinx.ext.coverage", "sphinx.ext.doctest", "sphinx.ext.ifconfig", "sphinx.ext.mathjax", "sphinx.ext.napoleon", "sphinx_rtd_theme", "recommonmark", ] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # Setup and Activate django so build are not failing import django.conf import django import pint django.conf.settings.configure( DATABASES={ "default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"} }, SECRET_KEY="not very secret in tests", USE_I18N=True, USE_L10N=True, # Use common Middleware MIDDLEWARE=( "django.middleware.common.CommonMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", ), INSTALLED_APPS=[ "django.contrib.auth", "django.contrib.admin", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.sites", "django.contrib.flatpages", "quantityfield", ], DJANGO_PINT_UNIT_REGISTER=pint.UnitRegistry(), ) django.setup() # To configure AutoStructify def setup(app): from recommonmark.transform import AutoStructify app.add_config_value( "recommonmark_config", { "auto_toc_tree_section": "Contents", "enable_eval_rst": True, "enable_math": True, "enable_inline_math": True, }, True, ) app.add_transform(AutoStructify) # The suffix of source filenames. source_suffix = [".rst", ".md"] # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = "index" # General information about the project. project = u"django-pint" copyright = u"2020, Carli* Freudenberg" # 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 = "" # Is set by calling `setup.py docs` # The full version, including alpha/beta/rc tags. release = "" # Is set by calling `setup.py docs` # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. # keep_warnings = False # -- 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" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {"sidebar_width": "300px", "page_width": "1200px"} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". try: from django_pint import __version__ as version except ImportError: pass else: release = version # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # html_logo = "" # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = "django_pint-doc" # -- Options for LaTeX output -------------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # 'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ( "index", "user_guide.tex", u"django-pint Documentation", u"Carli* Freudenberg", "manual", ), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # latex_logo = "" # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # If true, show page references after internal links. # latex_show_pagerefs = False # If true, show URL addresses after external links. # latex_show_urls = False # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_domain_indices = True # -- External mapping ------------------------------------------------------------ python_version = ".".join(map(str, sys.version_info[0:2])) intersphinx_mapping = { "sphinx": ("http://www.sphinx-doc.org/en/stable", None), "python": ("https://docs.python.org/" + python_version, None), "matplotlib": ("https://matplotlib.org", None), "numpy": ("https://docs.scipy.org/doc/numpy", None), "sklearn": ("http://scikit-learn.org/stable", None), "pandas": ("http://pandas.pydata.org/pandas-docs/stable", None), "scipy": ("https://docs.scipy.org/doc/scipy/reference", None), } django-pint-0.6.3/docs/index.rst000066400000000000000000000024231415077357600165430ustar00rootroot00000000000000=========== django-pint =========== .. include:: README.rst Contents ======== .. toctree:: :maxdepth: 2 License Authors Changelog Module Reference Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` .. _toctree: http://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html .. _reStructuredText: http://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html .. _references: http://www.sphinx-doc.org/en/stable/markup/inline.html .. _Python domain syntax: http://sphinx-doc.org/domains.html#the-python-domain .. _Sphinx: http://www.sphinx-doc.org/ .. _Python: http://docs.python.org/ .. _Numpy: http://docs.scipy.org/doc/numpy .. _SciPy: http://docs.scipy.org/doc/scipy/reference/ .. _matplotlib: https://matplotlib.org/contents.html# .. _Pandas: http://pandas.pydata.org/pandas-docs/stable .. _Scikit-Learn: http://scikit-learn.org/stable .. _autodoc: http://www.sphinx-doc.org/en/stable/ext/autodoc.html .. _Google style: https://github.com/google/styleguide/blob/gh-pages/pyguide.md#38-comments-and-docstrings .. _NumPy style: https://numpydoc.readthedocs.io/en/latest/format.html .. _classical style: http://www.sphinx-doc.org/en/stable/domains.html#info-field-lists django-pint-0.6.3/docs/license.rst000066400000000000000000000001031415077357600170470ustar00rootroot00000000000000.. _license: ======= License ======= .. include:: ../LICENSE.txt django-pint-0.6.3/docs/requirements.txt000066400000000000000000000001511415077357600201620ustar00rootroot00000000000000Django>=2.2 m2r2>=0.2.5 pint>=0.16 recommonmark>=0.6.0 six>=1.15.0 Sphinx>=3.3.1 sphinx-rtd-theme>=0.5.0 django-pint-0.6.3/makefile000066400000000000000000000004561415077357600154560ustar00rootroot00000000000000install: python setup.py develop pip install -r sandbox_requirements.txt pip install -r test_requirements.txt test: python setup.py develop pip install -r test_requirements.txt release: python setup.py sdist upload git push --tags clean: find . -name "*.pyc" -delete rm -rf *.egg-info dist django-pint-0.6.3/manage.py000066400000000000000000000011731415077357600155550ustar00rootroot00000000000000#!/usr/bin/env python """Django's command-line utility for administrative tasks.""" import os import sys def main(): os.environ.setdefault("DJANGO_SETTINGS_MODULE", "RoWoOekostromDB.settings") try: from django.core.management import execute_from_command_line except ImportError as exc: raise ImportError( "Couldn't import Django. Are you sure it's installed and " "available on your PYTHONPATH environment variable? Did you " "forget to activate a virtual environment?" ) from exc execute_from_command_line(sys.argv) if __name__ == "__main__": main() django-pint-0.6.3/pyproject.toml000066400000000000000000000004671415077357600166740ustar00rootroot00000000000000[build-system] # AVOID CHANGING REQUIRES: IT WILL BE UPDATED BY PYSCAFFOLD! requires = ["setuptools>=46.1.0", "setuptools_scm[toml]>=5", "wheel"] build-backend = "setuptools.build_meta" [tool.setuptools_scm] # See configuration details in https://github.com/pypa/setuptools_scm version_scheme = "no-guess-dev" django-pint-0.6.3/setup.cfg000066400000000000000000000110251415077357600155710ustar00rootroot00000000000000# This file is used to configure your project. # Read more about the various options under: # http://setuptools.readthedocs.io/en/latest/setuptools.html#configuring-setup-using-setup-cfg-files [metadata] name = django-pint description = "Quantity Field for Django using pint library for automated unit conversions" author = Ben Harling, Carli* Freudenberg author_email = kound@posteo.de license = MIT long_description = file: README.md long_description_content_type = text/markdown; charset=UTF-8; variant=GFM url = https://github.com/CarliJoy/django-pint/ project_urls = Documentation = https://django-pint.readthedocs.io/en/latest/ # Change if running only on Windows, Mac or Linux (comma-separated) platforms = any # Add here all kinds of additional classifiers as defined under # https://pypi.python.org/pypi?%3Aaction=list_classifiers classifiers = Development Status :: 4 - Beta Programming Language :: Python Programming Language :: Python :: 3 :: Only 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 Framework :: Django Framework :: Django :: 2.2 Framework :: Django :: 3.0 Framework :: Django :: 3.1 Framework :: Django :: 3.2 Intended Audience :: Developers Intended Audience :: Science/Research License :: OSI Approved :: MIT License Operating System :: OS Independent Topic :: Scientific/Engineering Topic :: Software Development :: Libraries :: Python Modules [options] zip_safe = False packages = find_namespace: include_package_data = True package_dir = =src # DON'T CHANGE THE FOLLOWING LINE! IT WILL BE UPDATED BY PYSCAFFOLD! # Add here dependencies of your project (semicolon/line-separated), e.g. install_requires = importlib-metadata; python_version<"3.8" Django>=2.2 pint>=0.16 # The usage of test_requires is discouraged, see `Dependency Management` docs # of PyScaffold # tests_require = pytest; pytest-cov # Require a specific Python version, e.g. Python 2.7 or >= 3.4 python_requires = >=3.6 [options.packages.find] where = src exclude = tests [options.extras_require] # Add here additional requirements for extra features, to install with: # `pip install django-pint[testing]` like: # testing = pytest; tox # Add here test requirements (semicolon/line-separated) testing = pytest>=6.1 pytest-cov>=2.1 pytest-django>=0.4 tox>=3.2 build_doc = sphinx recommonmark>=0.6.0 m2r2 [options.entry_points] # Add here console scripts like: # console_scripts = # script_name = django_pint.module:function # For example: # console_scripts = # fibonacci = django_pint.skeleton:run # And any other entry points, for example: # pyscaffold.cli = # awesome = pyscaffoldext.awesome.extension:AwesomeExtension [test] # py.test options when running `python setup.py test` # addopts = --verbose # extras = True [tool:pytest] # Options for py.test: # Specify command line options as you would do when invoking py.test directly. # e.g. --cov-report html (or xml) for html/xml output or --junitxml junit.xml # in order to write a coverage file that can be read by Jenkins. addopts = --cov quantityfield --cov-report=xml --cov-report=term-missing --verbose norecursedirs = dist build .tox testpaths = tests DJANGO_SETTINGS_MODULE = tests.settings [aliases] dists = bdist_wheel [bdist_wheel] # Use this option if your package is pure-python universal = 1 [build_sphinx] source_dir = docs build_dir = build/sphinx [devpi:upload] # Options for the devpi: PyPI server and packaging tool # VCS export must be deactivated since we are using setuptools-scm no-vcs = 1 formats = bdist_wheel [flake8] # Some sane defaults for the code style checker flake8 max-line-length = 88 extend-ignore = E203, W503 # ^ Black-compatible # E203 and W503 have edge cases handled by black exclude = .tox build dist .eggs docs/conf.py .venv* per-file-ignores = tests/settings.py:E402,F401 tests/dummyapp/admin.py:F405 [isort] profile=black skip=.tox,.venv*,build,dist known_standard_library=setuptools,pkg_resources known_test=pytest known_django=django known_first_party=django_pint known_pandas=pandas,numpy sections=FUTURE,STDLIB,TEST,DJANGO,THIRDPARTY,PANDAS,FIRSTPARTY,LOCALFOLDER [pyscaffold] # PyScaffold's parameters when the project was created. # This will be used when updating. Do not change! version = 4.1 package = django_pint extensions = markdown pre_commit tox travis django-pint-0.6.3/setup.py000066400000000000000000000013021415077357600154570ustar00rootroot00000000000000""" Setup file for django-pint. Use setup.cfg to configure your project. This file was generated with PyScaffold 4.1. PyScaffold helps you to put up the scaffold of your new Python project. Learn more under: https://pyscaffold.org/ """ from setuptools import setup if __name__ == "__main__": try: setup(use_scm_version={"version_scheme": "no-guess-dev"}) except: # noqa print( "\n\nAn error occurred while building the project, " "please ensure you have the most updated version of setuptools, " "setuptools_scm and wheel with:\n" " pip install -U setuptools setuptools_scm wheel\n\n" ) raise django-pint-0.6.3/src/000077500000000000000000000000001415077357600145405ustar00rootroot00000000000000django-pint-0.6.3/src/django_pint/000077500000000000000000000000001415077357600170345ustar00rootroot00000000000000django-pint-0.6.3/src/django_pint/__init__.py000066400000000000000000000011061415077357600211430ustar00rootroot00000000000000import sys if sys.version_info[:2] >= (3, 8): # TODO: Import directly (no need for conditional) when `python_requires = >= 3.8` from importlib.metadata import PackageNotFoundError, version # pragma: no cover else: from importlib_metadata import PackageNotFoundError, version # pragma: no cover try: # Change here if project is renamed and does not equal the package name dist_name = "django-pint" __version__ = version(dist_name) except PackageNotFoundError: # pragma: no cover __version__ = "unknown" finally: del version, PackageNotFoundError django-pint-0.6.3/src/quantityfield/000077500000000000000000000000001415077357600174225ustar00rootroot00000000000000django-pint-0.6.3/src/quantityfield/__init__.py000066400000000000000000000007411415077357600215350ustar00rootroot00000000000000from pkg_resources import DistributionNotFound, get_distribution try: # Change here if project is renamed and does not equal the package name dist_name = "django-pint" __version__ = get_distribution(dist_name).version except DistributionNotFound: # pragma: no cover # We don't expect this to be executed, as this would mean the configuration # for the python module is wrong __version__ = "unknown" finally: del get_distribution, DistributionNotFound django-pint-0.6.3/src/quantityfield/fields.py000066400000000000000000000345061415077357600212520ustar00rootroot00000000000000from django import forms from django.contrib.admin.options import FORMFIELD_FOR_DBFIELD_DEFAULTS from django.core.exceptions import ValidationError from django.db import models from django.utils import formats from django.utils.translation import gettext_lazy as _ import datetime import warnings from decimal import Decimal from pint import Quantity from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union, cast from quantityfield.helper import check_matching_unit_dimension from .units import ureg from .widgets import QuantityWidget DJANGO_JSON_SERIALIZABLE_BASE = Union[ None, bool, str, int, float, complex, datetime.datetime ] DJANGO_JSON_SERIALIZABLE = Union[ Sequence[DJANGO_JSON_SERIALIZABLE_BASE], Dict[str, DJANGO_JSON_SERIALIZABLE_BASE] ] NUMBER_TYPE = Union[int, float, Decimal] class QuantityFieldMixin(object): to_number_type: Callable[[Any], NUMBER_TYPE] # TODO: Move these stuff into an Protocol or anything # better defining a Mixin value_from_object: Callable[[Any], Any] name: str validate: Callable run_validators: Callable """A Django Model Field that resolves to a pint Quantity object""" def __init__( self, base_units: str, *args, unit_choices: Optional[List[str]] = None, **kwargs ): """ Create a Quantity field :param base_units: Unit description of base unit :param unit_choices: If given the possible unit choices with the same dimension like the base_unit """ if not isinstance(base_units, str): raise ValueError( 'QuantityField must be defined with base units, eg: "gram"' ) self.ureg = ureg # we do this as a way of raising an exception if some crazy unit was supplied. unit = getattr(self.ureg, base_units) # noqa: F841 # if we've not hit an exception here, we should be all good self.base_units = base_units if unit_choices is None: self.unit_choices: List[str] = [self.base_units] else: self.unit_choices = unit_choices # Check if all unit_choices are valid check_matching_unit_dimension(self.ureg, self.base_units, self.unit_choices) super(QuantityFieldMixin, self).__init__(*args, **kwargs) @property def units(self) -> str: return self.base_units def deconstruct( self, ) -> Tuple[ str, str, Sequence[DJANGO_JSON_SERIALIZABLE], Dict[str, DJANGO_JSON_SERIALIZABLE], ]: """ Return enough information to recreate the field as a 4-tuple: * The name of the field on the model, if contribute_to_class() has been run. * The import path of the field, including the class:e.g. django.db.models.IntegerField This should be the most portable version, so less specific may be better. * A list of positional arguments. * A dict of keyword arguments. """ super_deconstruct = getattr(super(), "deconstruct", None) if not callable(super_deconstruct): raise NotImplementedError( "Tried to use Mixin on a class that has no deconstruct function. " ) name, path, args, kwargs = super_deconstruct() kwargs["base_units"] = self.base_units kwargs["unit_choices"] = self.unit_choices return name, path, args, kwargs def fix_unit_registry(self, value: Quantity) -> Quantity: """ Check if the UnitRegistry from settings is used. If not try to fix it but give a warning. """ if isinstance(value, Quantity): if not isinstance(value, self.ureg.Quantity): # Could be fatal if different unit registers are used but we assume # the same is used within one project # As we warn for this behaviour, we assume that the programmer # will fix it and do not include more checks! warnings.warn( "Trying to set value from a different unit register for " "quantityfield. " "We assume the naming is equal but best use the same register as" " for creating the quantityfield.", RuntimeWarning, ) return value.magnitude * self.ureg(str(value.units)) else: return value else: raise ValueError(f"Value '{value}' ({type(value)} is not a quantity.") def get_prep_value(self, value: Any) -> Optional[NUMBER_TYPE]: """ Perform preliminary non-db specific value checks and conversions. Make sure that we compare/use only values without a unit """ # we store the value in the base units defined for this field if value is None: return None if isinstance(value, Quantity): quantity = self.fix_unit_registry(value) magnitude = quantity.to(self.base_units).magnitude else: magnitude = value try: return self.to_number_type(magnitude) except (TypeError, ValueError) as e: raise e.__class__( "Field '%s' expected a number but got %r." % (self.name, value), ) from e def value_to_string(self, obj) -> str: value = self.value_from_object(obj) return str(self.get_prep_value(value)) def from_db_value(self, value: Any, *args, **kwargs) -> Optional[Quantity]: if value is None: return None return self.ureg.Quantity(value * getattr(self.ureg, self.base_units)) def to_python(self, value) -> Optional[Quantity]: if isinstance(value, Quantity): return self.fix_unit_registry(value) if value is None: return None to_number = getattr(super(), "to_python") if not callable(to_number): raise NotImplementedError( "Mixin not used with a class that has to_python function" ) value = cast(NUMBER_TYPE, to_number(value)) return self.ureg.Quantity(value * getattr(self.ureg, self.base_units)) def clean(self, value, model_instance) -> Quantity: """ Convert the value's type and run validation. Validation errors from to_python() and validate() are propagated. Return the correct value if no error is raised. This is a copy from djangos implementation but modified so that validators are only checked against the magnitude as otherwise the default database validators will not fail because of comparison errors """ value = self.to_python(value) check_value = self.get_prep_value(value) self.validate(check_value, model_instance) self.run_validators(check_value) return value # TODO: Add tests, understand, add super call if required """ # This code is untested and not documented. It also does not call the super method Therefore it is commented out for the moment (even so it is likely required) def get_prep_lookup(self, lookup_type, value): if lookup_type in ["lt", "gt", "lte", "gte"]: if isinstance(value, self.ureg.Quantity): v = value.to(self.base_units) return v.magnitude return value """ def formfield(self, **kwargs): defaults = { "form_class": self.form_field_class, "base_units": self.base_units, "unit_choices": self.unit_choices, } defaults.update(kwargs) return super(QuantityFieldMixin, self).formfield(**defaults) class QuantityFormFieldMixin(object): """This formfield allows a user to choose which units they wish to use to enter a value, but the value is yielded in the base_units """ to_number_type: Callable[[Any], NUMBER_TYPE] # TODO: Move these stuff into an Protocol or anything # better defining a Mixin validate: Callable run_validators: Callable error_messages: Dict[str, str] empty_values: Sequence[Any] localize: bool def __init__(self, *args, **kwargs): self.ureg = ureg self.base_units = kwargs.pop("base_units", None) if self.base_units is None: raise ValueError( "QuantityFormField requires a base_units kwarg of a " "single unit type (eg: grams)" ) self.units = kwargs.pop("unit_choices", [self.base_units]) if self.base_units not in self.units: self.units.append(self.base_units) check_matching_unit_dimension(self.ureg, self.base_units, self.units) def is_special_admin_widget(widget) -> bool: """ There are some special django admin widgets, defined in django/contrib/admin/options.py in the variable FORMFIELD_FOR_DBFIELD_DEFAULTS The intention for Integer and BigIntegerField is only to define the width. They are set through a complicated process of the modelform_factory setting formfield_callback to ModelForm.formfield_fo_dbfield As they will overwrite our Widget we check for them and will ignore them, if they are set as attribute. We still will allow subclasses, so the end user has still the possibility to use this widget. """ WIDGETS_TO_IGNORE = [ FORMFIELD_FOR_DBFIELD_DEFAULTS[models.IntegerField], FORMFIELD_FOR_DBFIELD_DEFAULTS[models.BigIntegerField], ] classes_to_ignore = [ ignored_widget["widget"].__name__ for ignored_widget in WIDGETS_TO_IGNORE ] return getattr(widget, "__name__") in classes_to_ignore widget = kwargs.get("widget", None) if widget is None or is_special_admin_widget(widget): widget = QuantityWidget( base_units=self.base_units, allowed_types=self.units ) kwargs["widget"] = widget super(QuantityFormFieldMixin, self).__init__(*args, **kwargs) def prepare_value(self, value): if isinstance(value, Quantity): return value.to(self.base_units) else: return value def clean(self, value): """ General idea, first try to extract the correct number like done in the other classes and then follow the same procedure as in the django default field """ if isinstance(value, list) or isinstance(value, tuple): val = value[0] units = value[1] else: # If no multi widget is used val = value units = self.base_units if val in self.empty_values: # Make sure the correct functions are called also in case of empty values self.validate(None) self.run_validators(None) return None if units not in self.units: raise ValidationError(_("%(units)s is not a valid choice") % locals()) if self.localize: val = formats.sanitize_separators(value) try: val = self.to_number_type(val) except (ValueError, TypeError): raise ValidationError(self.error_messages["invalid"], code="invalid") val = self.ureg.Quantity(val * getattr(self.ureg, units)).to(self.base_units) self.validate(val.magnitude) self.run_validators(val.magnitude) return val class QuantityFormField(QuantityFormFieldMixin, forms.FloatField): to_number_type = float class QuantityField(QuantityFieldMixin, models.FloatField): form_field_class = QuantityFormField to_number_type = float class IntegerQuantityFormField(QuantityFormFieldMixin, forms.IntegerField): to_number_type = int class IntegerQuantityField(QuantityFieldMixin, models.IntegerField): form_field_class = IntegerQuantityFormField to_number_type = int class BigIntegerQuantityField(QuantityFieldMixin, models.BigIntegerField): form_field_class = IntegerQuantityFormField to_number_type = int class DecimalQuantityFormField(QuantityFormFieldMixin, forms.DecimalField): to_number_type = Decimal class DecimalQuantityField(QuantityFieldMixin, models.DecimalField): form_field_class = DecimalQuantityFormField to_number_type = Decimal def __init__( self, base_units: str, *args, unit_choices: Optional[List[str]] = None, verbose_name: str = None, name: str = None, max_digits: int = None, decimal_places: int = None, **kwargs, ): # We try to be friendly as default django, if there are missing argument # we throw an error early if not isinstance(max_digits, int) or not isinstance(decimal_places, int): raise ValueError( _( "Invalid initialization for DecimalQuantityField! " "We expect max_digits and decimal_places to be set as integers." ) ) # and we also check the values to be sane if decimal_places < 0 or max_digits < 1 or decimal_places > max_digits: raise ValueError( _( "Invalid initialization for DecimalQuantityField! " "max_digits and decimal_places need to positive and max_digits" "needs to be larger than decimal_places and at least 1. " "So max_digits=%(max_digits)s and " "decimal_plactes=%(decimal_places)s " "are not valid parameters." ) % locals() ) super().__init__( base_units, *args, unit_choices=unit_choices, verbose_name=verbose_name, name=name, max_digits=max_digits, decimal_places=decimal_places, **kwargs, ) def get_db_prep_save(self, value, connection) -> Decimal: """ Get Value that shall be saved to database, make sure it is transformed """ converted = self.to_python(value) magnitude = self.get_prep_value(converted) return connection.ops.adapt_decimalfield_value( magnitude, self.max_digits, self.decimal_places ) django-pint-0.6.3/src/quantityfield/helper.py000066400000000000000000000010641415077357600212540ustar00rootroot00000000000000from pint import DimensionalityError, UnitRegistry from typing import List def check_matching_unit_dimension( ureg: UnitRegistry, base_units: str, units_to_check: List[str] ) -> None: """ Check if all units_to_check have the same Dimension like the base_units If not :raise DimensionalityError """ base_unit = getattr(ureg, base_units) for unit_string in units_to_check: unit = getattr(ureg, unit_string) if unit.dimensionality != base_unit.dimensionality: raise DimensionalityError(base_unit, unit) django-pint-0.6.3/src/quantityfield/settings.py000066400000000000000000000005351415077357600216370ustar00rootroot00000000000000__version__ = "0.4" from django.conf import settings from pint import UnitRegistry, set_application_registry # Define default unit register DJANGO_PINT_UNIT_REGISTER = getattr( settings, "DJANGO_PINT_UNIT_REGISTER", UnitRegistry() ) # Set as default application registry for i.e. for pickle set_application_registry(DJANGO_PINT_UNIT_REGISTER) django-pint-0.6.3/src/quantityfield/units.py000066400000000000000000000002231415077357600211330ustar00rootroot00000000000000from .settings import DJANGO_PINT_UNIT_REGISTER # The unit register that was defined in the settings (shortcurt) ureg = DJANGO_PINT_UNIT_REGISTER django-pint-0.6.3/src/quantityfield/widgets.py000066400000000000000000000016501415077357600214440ustar00rootroot00000000000000from django.forms.widgets import MultiWidget, NumberInput, Select import re from .units import ureg class QuantityWidget(MultiWidget): def __init__(self, *, attrs=None, base_units=None, allowed_types=None): self.ureg = ureg choices = self.get_choices(allowed_types) self.base_units = base_units attrs = attrs or {} attrs.setdefault("step", "any") widgets = (NumberInput(attrs=attrs), Select(attrs=attrs, choices=choices)) super(QuantityWidget, self).__init__(widgets, attrs) def get_choices(self, allowed_types=None): allowed_types = allowed_types or dir(self.ureg) return [(x, x) for x in allowed_types] def decompress(self, value): non_decimal = re.compile(r"[^\d.]+") if value: number_value = non_decimal.sub("", str(value)) return [number_value, self.base_units] return [None, self.base_units] django-pint-0.6.3/tests/000077500000000000000000000000001415077357600151135ustar00rootroot00000000000000django-pint-0.6.3/tests/__init__.py000066400000000000000000000000001415077357600172120ustar00rootroot00000000000000django-pint-0.6.3/tests/conftest.py000066400000000000000000000003271415077357600173140ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Dummy conftest.py for django_pint. If you don't know what this is for, just leave it empty. Read more about conftest.py under: https://pytest.org/latest/plugins.html """ django-pint-0.6.3/tests/dummyapp/000077500000000000000000000000001415077357600167475ustar00rootroot00000000000000django-pint-0.6.3/tests/dummyapp/__init__.py000066400000000000000000000000001415077357600210460ustar00rootroot00000000000000django-pint-0.6.3/tests/dummyapp/admin.py000066400000000000000000000017711415077357600204170ustar00rootroot00000000000000from django.contrib import admin from .models import * # noqa: F401, F403 class ReadOnlyEditing(admin.ModelAdmin): def get_readonly_fields(self, request, obj=None): if obj is not None: return list(self.get_fields(request)) return [] admin.site.register(BigIntFieldSaveModel, ReadOnlyEditing) admin.site.register(ChoicesDefinedInModel, ReadOnlyEditing) admin.site.register(ChoicesDefinedInModelInt, ReadOnlyEditing) admin.site.register(CustomUregDecimalHayBale, ReadOnlyEditing) admin.site.register(CustomUregHayBale, ReadOnlyEditing) admin.site.register(DecimalFieldSaveModel, ReadOnlyEditing) admin.site.register(EmptyHayBaleBigInt, ReadOnlyEditing) admin.site.register(EmptyHayBaleDecimal, ReadOnlyEditing) admin.site.register(EmptyHayBaleFloat, ReadOnlyEditing) admin.site.register(EmptyHayBaleInt, ReadOnlyEditing) admin.site.register(FloatFieldSaveModel, ReadOnlyEditing) admin.site.register(HayBale, ReadOnlyEditing) admin.site.register(IntFieldSaveModel, ReadOnlyEditing) django-pint-0.6.3/tests/dummyapp/asgi.py000066400000000000000000000006031415077357600202430ustar00rootroot00000000000000""" ASGI config for testproject project. It exposes the ASGI callable as a module-level variable named ``application``. For more information on this file, see https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/ """ from django.core.asgi import get_asgi_application import os os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") application = get_asgi_application() django-pint-0.6.3/tests/dummyapp/forms.py000066400000000000000000000053621415077357600204550ustar00rootroot00000000000000from django import forms from quantityfield.fields import ( DecimalQuantityFormField, IntegerQuantityFormField, QuantityFormField, ) from tests.dummyapp.models import ( BigIntFieldSaveModel, DecimalFieldSaveModel, FloatFieldSaveModel, IntFieldSaveModel, ) class DefaultFormFloat(forms.ModelForm): weight = QuantityFormField(base_units="gram", unit_choices=["ounce", "gram"]) class Meta: model = FloatFieldSaveModel fields = "__all__" class DefaultFormInt(forms.ModelForm): weight = IntegerQuantityFormField(base_units="gram", unit_choices=["ounce", "gram"]) class Meta: model = IntFieldSaveModel fields = "__all__" class DefaultFormBigInt(forms.ModelForm): weight = IntegerQuantityFormField(base_units="gram", unit_choices=["ounce", "gram"]) class Meta: model = BigIntFieldSaveModel fields = "__all__" class DefaultFormDecimal(forms.ModelForm): weight = DecimalQuantityFormField(base_units="gram", unit_choices=["ounce", "gram"]) class Meta: model = DecimalFieldSaveModel fields = "__all__" class DefaultFormFieldsFloat(forms.ModelForm): weight = forms.FloatField() class Meta: model = FloatFieldSaveModel fields = "__all__" class DefaultFormFieldsDecimal(forms.ModelForm): weight = forms.FloatField() class Meta: model = DecimalFieldSaveModel fields = "__all__" class DefaultFormFieldsInt(forms.ModelForm): weight = forms.IntegerField() class Meta: model = IntFieldSaveModel fields = "__all__" class DefaultFormFieldsBigInt(forms.ModelForm): weight = forms.IntegerField() class Meta: model = BigIntFieldSaveModel fields = "__all__" class DefaultWidgetsFormFloat(forms.ModelForm): weight = QuantityFormField( base_units="gram", unit_choices=["ounce", "gram"], widget=forms.NumberInput ) class Meta: model = FloatFieldSaveModel fields = "__all__" class DefaultWidgetsFormDecimal(forms.ModelForm): weight = QuantityFormField( base_units="gram", unit_choices=["ounce", "gram"], widget=forms.NumberInput ) class Meta: model = DecimalFieldSaveModel fields = "__all__" class DefaultWidgetsFormInt(forms.ModelForm): weight = IntegerQuantityFormField( base_units="gram", unit_choices=["ounce", "gram"], widget=forms.NumberInput ) class Meta: model = IntFieldSaveModel fields = "__all__" class DefaultWidgetsFormBigInt(forms.ModelForm): weight = IntegerQuantityFormField( base_units="gram", unit_choices=["ounce", "gram"], widget=forms.NumberInput ) class Meta: model = BigIntFieldSaveModel fields = "__all__" django-pint-0.6.3/tests/dummyapp/helper.py000066400000000000000000000017021415077357600206000ustar00rootroot00000000000000from django.db.models.base import ModelBase from typing import Dict from quantityfield.fields import QuantityFieldMixin from .models import * # noqa: F401, F403 def get_test_models() -> Dict[str, ModelBase]: """ Get a list of all Test models """ result = {} for name, obj in globals().items(): if not name.startswith("_"): if isinstance(obj, ModelBase): if not obj._meta.abstract: if obj._meta.app_config.name.endswith("dummyapp"): result[name] = obj return result def print_admins(): for model in sorted(get_test_models().keys()): print(f"admin.site.register({model}, ReadOnlyEditing)") def print_test_admin_choices(): for model_name, model in get_test_models().items(): for field in model._meta.fields: if isinstance(field, QuantityFieldMixin): print(f"(models.{model_name}, '{field.name}'),") django-pint-0.6.3/tests/dummyapp/migrations/000077500000000000000000000000001415077357600211235ustar00rootroot00000000000000django-pint-0.6.3/tests/dummyapp/migrations/0001_initial.py000066400000000000000000000247741415077357600236040ustar00rootroot00000000000000# Generated by Django 3.1.3 on 2021-09-26 11:13 from django.db import migrations, models import quantityfield.fields class Migration(migrations.Migration): initial = True dependencies = [] operations = [ migrations.CreateModel( name="BigIntFieldSaveModel", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ("name", models.CharField(max_length=20)), ( "weight", quantityfield.fields.BigIntegerQuantityField( base_units="gram", unit_choices=["gram"] ), ), ], options={ "abstract": False, }, ), migrations.CreateModel( name="ChoicesDefinedInModel", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ( "weight", quantityfield.fields.QuantityField( base_units="kilogram", unit_choices=["milligram", "pounds"] ), ), ], ), migrations.CreateModel( name="ChoicesDefinedInModelInt", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ( "weight", quantityfield.fields.IntegerQuantityField( base_units="kilogram", unit_choices=["milligram", "pounds"] ), ), ], ), migrations.CreateModel( name="CustomUregDecimalHayBale", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ( "custom_decimal", quantityfield.fields.DecimalQuantityField( base_units="custom", decimal_places=2, max_digits=10, unit_choices=["custom"], ), ), ], ), migrations.CreateModel( name="CustomUregHayBale", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ( "custom", quantityfield.fields.QuantityField( base_units="custom", unit_choices=["custom"] ), ), ( "custom_int", quantityfield.fields.IntegerQuantityField( base_units="custom", unit_choices=["custom"] ), ), ( "custom_bigint", quantityfield.fields.BigIntegerQuantityField( base_units="custom", unit_choices=["custom"] ), ), ], ), migrations.CreateModel( name="DecimalFieldSaveModel", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ("name", models.CharField(max_length=20)), ( "weight", quantityfield.fields.DecimalQuantityField( base_units="gram", decimal_places=2, max_digits=10, unit_choices=["gram"], ), ), ], options={ "abstract": False, }, ), migrations.CreateModel( name="EmptyHayBaleBigInt", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ("name", models.CharField(max_length=20)), ( "weight", quantityfield.fields.BigIntegerQuantityField( base_units="gram", null=True, unit_choices=["gram"] ), ), ], ), migrations.CreateModel( name="EmptyHayBaleDecimal", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ("name", models.CharField(max_length=20)), ( "weight", quantityfield.fields.DecimalQuantityField( base_units="gram", decimal_places=2, max_digits=10, null=True, unit_choices=["gram"], ), ), ( "compare", models.DecimalField(decimal_places=2, max_digits=10, null=True), ), ], ), migrations.CreateModel( name="EmptyHayBaleFloat", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ("name", models.CharField(max_length=20)), ( "weight", quantityfield.fields.QuantityField( base_units="gram", null=True, unit_choices=["gram"] ), ), ], ), migrations.CreateModel( name="EmptyHayBaleInt", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ("name", models.CharField(max_length=20)), ( "weight", quantityfield.fields.IntegerQuantityField( base_units="gram", null=True, unit_choices=["gram"] ), ), ], ), migrations.CreateModel( name="FloatFieldSaveModel", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ("name", models.CharField(max_length=20)), ( "weight", quantityfield.fields.QuantityField( base_units="gram", unit_choices=["gram"] ), ), ], options={ "abstract": False, }, ), migrations.CreateModel( name="HayBale", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ("name", models.CharField(max_length=20)), ( "weight", quantityfield.fields.QuantityField( base_units="gram", unit_choices=["gram"] ), ), ( "weight_int", quantityfield.fields.IntegerQuantityField( base_units="gram", blank=True, null=True, unit_choices=["gram"] ), ), ( "weight_bigint", quantityfield.fields.BigIntegerQuantityField( base_units="gram", blank=True, null=True, unit_choices=["gram"] ), ), ], ), migrations.CreateModel( name="IntFieldSaveModel", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ("name", models.CharField(max_length=20)), ( "weight", quantityfield.fields.IntegerQuantityField( base_units="gram", unit_choices=["gram"] ), ), ], options={ "abstract": False, }, ), ] django-pint-0.6.3/tests/dummyapp/migrations/__init__.py000066400000000000000000000000001415077357600232220ustar00rootroot00000000000000django-pint-0.6.3/tests/dummyapp/models.py000066400000000000000000000043711415077357600206110ustar00rootroot00000000000000from django.db import models from django.db.models import DecimalField from quantityfield.fields import ( BigIntegerQuantityField, DecimalQuantityField, IntegerQuantityField, QuantityField, ) class FieldSaveModel(models.Model): name = models.CharField(max_length=20) weight = ... class Meta: abstract = True class FloatFieldSaveModel(FieldSaveModel): weight = QuantityField("gram") class IntFieldSaveModel(FieldSaveModel): weight = IntegerQuantityField("gram") class BigIntFieldSaveModel(FieldSaveModel): weight = BigIntegerQuantityField("gram") class DecimalFieldSaveModel(FieldSaveModel): weight = DecimalQuantityField("gram", max_digits=10, decimal_places=2) class HayBale(models.Model): name = models.CharField(max_length=20) weight = QuantityField("gram") weight_int = IntegerQuantityField("gram", blank=True, null=True) weight_bigint = BigIntegerQuantityField("gram", blank=True, null=True) class EmptyHayBaleFloat(models.Model): name = models.CharField(max_length=20) weight = QuantityField("gram", null=True) class EmptyHayBaleInt(models.Model): name = models.CharField(max_length=20) weight = IntegerQuantityField("gram", null=True) class EmptyHayBaleBigInt(models.Model): name = models.CharField(max_length=20) weight = BigIntegerQuantityField("gram", null=True) class EmptyHayBaleDecimal(models.Model): name = models.CharField(max_length=20) weight = DecimalQuantityField("gram", null=True, max_digits=10, decimal_places=2) # Value to compare with default implementation compare = DecimalField(max_digits=10, decimal_places=2, null=True) class CustomUregHayBale(models.Model): # Custom is defined in settings in conftest.py custom = QuantityField("custom") custom_int = IntegerQuantityField("custom") custom_bigint = BigIntegerQuantityField("custom") class CustomUregDecimalHayBale(models.Model): custom_decimal = DecimalQuantityField("custom", max_digits=10, decimal_places=2) class ChoicesDefinedInModel(models.Model): weight = QuantityField("kilogram", unit_choices=["milligram", "pounds"]) class ChoicesDefinedInModelInt(models.Model): weight = IntegerQuantityField("kilogram", unit_choices=["milligram", "pounds"]) django-pint-0.6.3/tests/dummyapp/urls.py000066400000000000000000000013611415077357600203070ustar00rootroot00000000000000"""testproject URL Configuration The `urlpatterns` list routes URLs to views. For more information please see: https://docs.djangoproject.com/en/3.2/topics/http/urls/ Examples: Function views 1. Add an import: from my_app import views 2. Add a URL to urlpatterns: path('', views.home, name='home') Class-based views 1. Add an import: from other_app.views import Home 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') Including another URLconf 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin from django.urls import path urlpatterns = [ path("admin/", admin.site.urls), ] django-pint-0.6.3/tests/dummyapp/wsgi.py000066400000000000000000000006031415077357600202710ustar00rootroot00000000000000""" WSGI config for testproject project. It exposes the WSGI callable as a module-level variable named ``application``. For more information on this file, see https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/ """ from django.core.wsgi import get_wsgi_application import os os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") application = get_wsgi_application() django-pint-0.6.3/tests/local.py.docker-example000066400000000000000000000002751415077357600214620ustar00rootroot00000000000000import os PG_HOST = os.environ.get('POSTGRES_HOST') PG_DATABASE = os.environ.get('POSTGRES_DB') PG_USER = os.environ.get('POSTGRES_USER') PG_PASSWORD = os.environ.get('POSTGRES_PASSWORD') django-pint-0.6.3/tests/manage.py000077500000000000000000000012171415077357600167210ustar00rootroot00000000000000#!/usr/bin/env python """Django's command-line utility for administrative tasks.""" import os import sys def main(): """Run administrative tasks.""" os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") try: from django.core.management import execute_from_command_line except ImportError as exc: raise ImportError( "Couldn't import Django. Are you sure it's installed and " "available on your PYTHONPATH environment variable? Did you " "forget to activate a virtual environment?" ) from exc execute_from_command_line(sys.argv) if __name__ == "__main__": main() django-pint-0.6.3/tests/settings.py000066400000000000000000000062721415077357600173340ustar00rootroot00000000000000from pathlib import Path from pint import UnitRegistry # Allow user specific postgres credentials to be provided # in a local.py file try: from .local import PG_PASSWORD, PG_USER except ImportError: # Define the defaults Travis CI/CD if any parameter was unset PG_USER = "django_pint" PG_PASSWORD = "not_secure_in_testing" try: from .local import PG_DATABASE except ImportError: PG_DATABASE = "django_pint" try: from .local import PG_HOST except ImportError: PG_HOST = "localhost" try: from .local import PG_PORT except ImportError: PG_PORT = "" # Try to find guess the correct loading string for the dummy app, # which dependes on the PYTHON_PATH (that can differ between local # testing and a pytest run. dummy_app_load_string: str = "" try: import tests.dummyapp except ImportError: try: import dummyapp except ImportError: raise ImportError( "Neither `tests.dummyapp' nor 'dummyapp' has been " " found in the PYTHON_PATH." ) else: dummy_app_load_string = "dummyapp" else: dummy_app_load_string = "tests.dummyapp" # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent ALLOWED_HOSTS = ["127.0.0.1", "localhost"] DEBUG = True STATIC_URL = "/static/" TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": [], "APP_DIRS": True, "OPTIONS": { "context_processors": [ "django.template.context_processors.debug", "django.template.context_processors.request", "django.contrib.auth.context_processors.auth", "django.contrib.messages.context_processors.messages", ], }, }, ] DATABASES = { "default": { "ENGINE": "django.db.backends.postgresql", "USER": PG_USER, "NAME": PG_DATABASE, "HOST": PG_HOST, "PORT": PG_PORT, "PASSWORD": PG_PASSWORD, "TEST": { "NAME": "mytestdatabase", }, }, } # not very secret in tests SECRET_KEY = "5tb#evac8q447#b7u8w5#yj$yq3%by!a-5t7$4@vrj$al1-u3c" USE_I18N = True USE_L10N = True # Use common Middleware MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", ] INSTALLED_APPS = [ "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", "quantityfield", dummy_app_load_string, ] ROOT_URLCONF = f"{dummy_app_load_string}.urls" custom_ureg = UnitRegistry() custom_ureg.define("custom = [custom]") custom_ureg.define("kilocustom = 1000 * custom") DJANGO_PINT_UNIT_REGISTER = custom_ureg WSGI_APPLICATION = f"{dummy_app_load_string}.wsgi.application" DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" django-pint-0.6.3/tests/test_admin.py000066400000000000000000000027201415077357600176150ustar00rootroot00000000000000import pytest import django.contrib.admin from django.contrib.admin import ModelAdmin from django.db.models import Model from django.forms import Field, ModelForm from typing import Dict from quantityfield.widgets import QuantityWidget from tests.dummyapp import models @pytest.mark.parametrize( "model, field", [ (models.FloatFieldSaveModel, "weight"), (models.IntFieldSaveModel, "weight"), (models.BigIntFieldSaveModel, "weight"), (models.DecimalFieldSaveModel, "weight"), (models.HayBale, "weight"), (models.HayBale, "weight_int"), (models.HayBale, "weight_bigint"), (models.EmptyHayBaleFloat, "weight"), (models.EmptyHayBaleInt, "weight"), (models.EmptyHayBaleBigInt, "weight"), (models.EmptyHayBaleDecimal, "weight"), (models.CustomUregHayBale, "custom"), (models.CustomUregHayBale, "custom_int"), (models.CustomUregHayBale, "custom_bigint"), (models.CustomUregDecimalHayBale, "custom_decimal"), (models.ChoicesDefinedInModel, "weight"), (models.ChoicesDefinedInModelInt, "weight"), ], ) def test_admin_widgets(model: Model, field: str): """ Test that all admin pages deliver the correct widget """ admin: ModelAdmin = django.contrib.admin.site._registry[model] form: ModelForm = admin.get_form({})() form_fields: Dict[str, Field] = form.fields assert type(form_fields[field].widget) == QuantityWidget django-pint-0.6.3/tests/test_field.py000066400000000000000000000376371415077357600176270ustar00rootroot00000000000000import pytest from django.core.serializers import deserialize, serialize from django.db import transaction from django.db.models import Field, Model from django.test import TestCase import json import warnings from decimal import Decimal from pint import DimensionalityError, UndefinedUnitError, UnitRegistry from typing import Type, Union from quantityfield.fields import ( BigIntegerQuantityField, DecimalQuantityField, IntegerQuantityField, QuantityField, QuantityFieldMixin, ) from quantityfield.units import ureg from tests.dummyapp.models import ( BigIntFieldSaveModel, CustomUregDecimalHayBale, CustomUregHayBale, DecimalFieldSaveModel, EmptyHayBaleBigInt, EmptyHayBaleDecimal, EmptyHayBaleFloat, EmptyHayBaleInt, FieldSaveModel, FloatFieldSaveModel, IntFieldSaveModel, ) Quantity = ureg.Quantity class BaseMixinTestFieldCreate: # The field that needs to be tested FIELD: Type[Union[Field, QuantityFieldMixin]] # Some fields, i.e. the decimal require default kwargs to work properly DEFAULT_KWARGS = {} def test_sets_units(self): test_grams = self.FIELD("gram", **self.DEFAULT_KWARGS) self.assertEqual(test_grams.units, ureg.gram) def test_fails_with_unknown_units(self): with self.assertRaises(UndefinedUnitError): test_crazy_units = self.FIELD( # noqa: F841 "zinghie", **self.DEFAULT_KWARGS ) def test_base_units_is_required(self): with self.assertRaises(TypeError): no_units = self.FIELD(**self.DEFAULT_KWARGS) # noqa: F841 def test_base_units_set_with_name(self): okay_units = self.FIELD(base_units="meter", **self.DEFAULT_KWARGS) # noqa: F841 def test_base_units_are_invalid(self): with self.assertRaises(ValueError): wrong_units = self.FIELD(None, **self.DEFAULT_KWARGS) # noqa: F841 def test_unit_choices_must_be_valid_units(self): with self.assertRaises(UndefinedUnitError): self.FIELD(base_units="mile", unit_choices=["gunzu"], **self.DEFAULT_KWARGS) def test_unit_choices_must_match_base_dimensionality(self): with self.assertRaises(DimensionalityError): self.FIELD( base_units="gram", unit_choices=["meter", "ounces"], **self.DEFAULT_KWARGS ) class TestFloatFieldCrate(BaseMixinTestFieldCreate, TestCase): FIELD = QuantityField class TestIntegerFieldCreate(BaseMixinTestFieldCreate, TestCase): FIELD = IntegerQuantityField class TestBigIntegerFieldCreate(BaseMixinTestFieldCreate, TestCase): FIELD = BigIntegerQuantityField class TestDecimalFieldCreate(BaseMixinTestFieldCreate, TestCase): FIELD = DecimalQuantityField DEFAULT_KWARGS = {"max_digits": 10, "decimal_places": 2} @pytest.mark.parametrize( "max_digits, decimal_places, error", [ (None, None, "Invalid initialization.*expect.*integers.*"), (10, None, "Invalid initialization.*expect.*integers.*"), (None, 2, "Invalid initialization.*expect.*integers.*"), (-1, 2, "Invalid initialization.*positive.*larger than decimal_places.*"), (2, -1, "Invalid initialization.*positive.*larger than decimal_places.*"), (2, 3, "Invalid initialization.*positive.*larger than decimal_places.*"), ], ) def test_decimal_init_fail(max_digits, decimal_places, error): with pytest.raises(ValueError, match=error): DecimalQuantityField( "meter", max_digits=max_digits, decimal_places=decimal_places ) @pytest.mark.parametrize("max_digits, decimal_places", [(2, 0), (2, 2), (1, 0)]) def decimal_init_success(max_digits, decimal_places): DecimalQuantityField("meter", max_digits=max_digits, decimal_places=decimal_places) @pytest.mark.django_db class TestCustomDecimalUreg(TestCase): def setUp(self): # Custom Values are fined in confest.py CustomUregDecimalHayBale.objects.create(custom_decimal=Decimal("5")) CustomUregDecimalHayBale.objects.create( custom_decimal=Decimal("5") * ureg.kilocustom, ) def tearDown(self): CustomUregHayBale.objects.all().delete() def test_custom_ureg_decimal(self): obj = CustomUregDecimalHayBale.objects.first() self.assertEqual(str(obj.custom_decimal), "5.00 custom") obj = CustomUregDecimalHayBale.objects.last() self.assertEqual(str(obj.custom_decimal), "5000.00 custom") @pytest.mark.django_db class TestCustomUreg(TestCase): def setUp(self): # Custom Values are fined in confest.py CustomUregHayBale.objects.create(custom=5, custom_int=5, custom_bigint=5) CustomUregHayBale.objects.create( custom=5 * ureg.kilocustom, custom_int=5 * ureg.kilocustom, custom_bigint=5 * ureg.kilocustom, ) def tearDown(self): CustomUregHayBale.objects.all().delete() def test_custom_ureg_float(self): obj = CustomUregHayBale.objects.first() self.assertIsInstance(obj.custom, ureg.Quantity) self.assertEqual(str(obj.custom), "5.0 custom") obj = CustomUregHayBale.objects.last() self.assertEqual(str(obj.custom), "5000.0 custom") def test_custom_ureg_int(self): obj = CustomUregHayBale.objects.first() self.assertIsInstance(obj.custom_int, ureg.Quantity) self.assertEqual(str(obj.custom_int), "5 custom") obj = CustomUregHayBale.objects.last() self.assertEqual(str(obj.custom_int), "5000 custom") def test_custom_ureg_bigint(self): obj = CustomUregHayBale.objects.first() self.assertIsInstance(obj.custom_int, ureg.Quantity) self.assertEqual(str(obj.custom_bigint), "5 custom") obj = CustomUregHayBale.objects.last() self.assertEqual(str(obj.custom_bigint), "5000 custom") class BaseMixinNullAble: EMPTY_MODEL: Type[Model] FLOAT_SET_STR = "707.7" FLOAT_SET = float(FLOAT_SET_STR) DB_FLOAT_VALUE_EXPECTED = 707.7 def setUp(self): self.EMPTY_MODEL.objects.create(name="Empty") def tearDown(self) -> None: self.EMPTY_MODEL.objects.all().delete() def test_accepts_assigned_null(self): new = self.EMPTY_MODEL() new.weight = None new.name = "Test" new.save() self.assertIsNone(new.weight) # Also get it from database to verify from_db = self.EMPTY_MODEL.objects.last() self.assertIsNone(from_db.weight) def test_accepts_auto_null(self): empty = self.EMPTY_MODEL.objects.first() self.assertIsNone(empty.weight, None) def test_accepts_default_pint_unit(self): new = self.EMPTY_MODEL(name="DefaultPintUnitTest") units = UnitRegistry() new.weight = 5 * units.kilogram # Different Registers so we expect a warning! with self.assertWarns(RuntimeWarning): new.save() obj = self.EMPTY_MODEL.objects.last() self.assertEqual(obj.name, "DefaultPintUnitTest") self.assertEqual(obj.weight.units, "gram") self.assertEqual(obj.weight.magnitude, 5000) def test_accepts_default_app_unit(self): new = self.EMPTY_MODEL(name="DefaultAppUnitTest") new.weight = 5 * ureg.kilogram # Make sure that the correct argument does not raise a warning with warnings.catch_warnings(record=True) as w: new.save() assert len(w) == 0 obj = self.EMPTY_MODEL.objects.last() self.assertEqual(obj.name, "DefaultAppUnitTest") self.assertEqual(obj.weight.units, "gram") self.assertEqual(obj.weight.magnitude, 5000) def test_accepts_assigned_whole_number(self): new = self.EMPTY_MODEL(name="WholeNumber") new.weight = 707 new.save() obj = self.EMPTY_MODEL.objects.last() self.assertEqual(obj.name, "WholeNumber") self.assertEqual(obj.weight.units, "gram") self.assertEqual(obj.weight.magnitude, 707) def test_accepts_assigned_float_number(self): new = self.EMPTY_MODEL(name="FloatNumber") new.weight = self.FLOAT_SET new.save() obj = self.EMPTY_MODEL.objects.last() self.assertEqual(obj.name, "FloatNumber") self.assertEqual(obj.weight.units, "gram") # We expect the database to deliver the correct type, at least # for postgresql this is true self.assertEqual(obj.weight.magnitude, self.DB_FLOAT_VALUE_EXPECTED) self.assertIsInstance(obj.weight.magnitude, type(self.DB_FLOAT_VALUE_EXPECTED)) def test_serialisation(self): serialized = serialize( "json", [ self.EMPTY_MODEL.objects.first(), ], ) deserialized = json.loads(serialized) obj = deserialized[0]["fields"] self.assertEqual(obj["name"], "Empty") self.assertIsNone(obj["weight"]) obj_generator = deserialize("json", serialized, ignorenonexistent=True) obj_back = next(obj_generator) self.assertEqual(obj_back.object.name, "Empty") self.assertIsNone(obj_back.object.weight) @pytest.mark.django_db class TestNullableFloat(BaseMixinNullAble, TestCase): EMPTY_MODEL = EmptyHayBaleFloat @pytest.mark.django_db class TestNullableInt(BaseMixinNullAble, TestCase): EMPTY_MODEL = EmptyHayBaleInt DB_FLOAT_VALUE_EXPECTED = int(BaseMixinNullAble.FLOAT_SET) @pytest.mark.django_db class TestNullableBigInt(BaseMixinNullAble, TestCase): EMPTY_MODEL = EmptyHayBaleBigInt DB_FLOAT_VALUE_EXPECTED = int(BaseMixinNullAble.FLOAT_SET) @pytest.mark.django_db class TestNullableDecimal(BaseMixinNullAble, TestCase): EMPTY_MODEL = EmptyHayBaleDecimal DB_FLOAT_VALUE_EXPECTED = Decimal(BaseMixinNullAble.FLOAT_SET_STR) def test_with_default_implementation(self): new = self.EMPTY_MODEL(name="FloatNumber") new.weight = self.FLOAT_SET new.compare = self.FLOAT_SET new.save() obj = self.EMPTY_MODEL.objects.last() self.assertEqual(obj.name, "FloatNumber") self.assertEqual(obj.weight.units, "gram") # We compare with the reference implementation of django, this should # be always true no matter which database is used self.assertEqual(obj.weight.magnitude, obj.compare) self.assertIsInstance(obj.weight.magnitude, type(obj.compare)) def test_with_decimal(self): new = self.EMPTY_MODEL(name="FloatNumber") new.weight = Decimal(self.FLOAT_SET_STR) new.compare = Decimal(self.FLOAT_SET_STR) new.save() obj = self.EMPTY_MODEL.objects.last() self.assertEqual(obj.name, "FloatNumber") self.assertEqual(obj.weight.units, "gram") # We compare with the reference implementation of django, this should # be always true no matter which database is used self.assertEqual(obj.weight.magnitude, obj.compare) self.assertIsInstance(obj.weight.magnitude, type(obj.compare)) # But we also expect (at least for postgresql) that this a Decimal self.assertEqual(obj.weight.magnitude, self.DB_FLOAT_VALUE_EXPECTED) self.assertIsInstance(obj.weight.magnitude, Decimal) class FieldSaveTestBase: MODEL: Type[FieldSaveModel] EXPECTED_TYPE: Type = float DEFAULT_WEIGHT = 100 DEFAULT_WEIGHT_STR = "100.0" DEFAULT_WEIGHT_QUANTITY_STR = "100.0 gram" HEAVIEST = 1000 LIGHTEST = 1 OUNCE_VALUE = 3.52739619496 COMPARE_QUANTITY = Quantity(0.8 * ureg.ounce) # 1 ounce = 28.34 grams def setUp(self): self.MODEL.objects.create( weight=self.DEFAULT_WEIGHT, name="grams", ) self.lightest = self.MODEL.objects.create(weight=self.LIGHTEST, name="lightest") self.heaviest = self.MODEL.objects.create(weight=self.HEAVIEST, name="heaviest") def tearDown(self): self.MODEL.objects.all().delete() def test_fails_with_incompatible_units(self): # we have to wrap this in a transaction # fixing a unit test problem # http://stackoverflow.com/questions/21458387/transactionmanagementerror-you-cant-execute-queries-until-the-end-of-the-atom metres = Quantity(100 * ureg.meter) with transaction.atomic(): with self.assertRaises(DimensionalityError): self.MODEL.objects.create(weight=metres, name="Should Fail") def test_value_stored_as_quantity(self): obj = self.MODEL.objects.first() self.assertIsInstance(obj.weight, Quantity) self.assertEqual(str(obj.weight), self.DEFAULT_WEIGHT_QUANTITY_STR) def test_value_stored_as_correct_magnitude_type(self): obj = self.MODEL.objects.first() self.assertIsInstance(obj.weight, Quantity) self.assertIsInstance(obj.weight.magnitude, self.EXPECTED_TYPE) def test_value_conversion(self): obj = self.MODEL.objects.first() ounces = obj.weight.to(ureg.ounce) self.assertAlmostEqual(ounces.magnitude, self.OUNCE_VALUE) self.assertEqual(ounces.units, ureg.ounce) def test_order_by(self): qs = list(self.MODEL.objects.all().order_by("weight")) self.assertEqual(qs[0].name, "lightest") self.assertEqual(qs[-1].name, "heaviest") self.assertEqual(qs[0], self.lightest) self.assertEqual(qs[-1], self.heaviest) def test_comparison_with_number(self): qs = self.MODEL.objects.filter(weight__gt=2) self.assertNotIn(self.lightest, qs) def test_comparison_with_quantity(self): weight = Quantity(20 * ureg.gram) qs = self.MODEL.objects.filter(weight__gt=weight) self.assertNotIn(self.lightest, qs) def test_comparison_with_quantity_respects_units(self): qs = self.MODEL.objects.filter(weight__gt=self.COMPARE_QUANTITY) self.assertNotIn(self.lightest, qs) def test_comparison_is_actually_numeric(self): qs = self.MODEL.objects.filter(weight__gt=1.0) self.assertNotIn(self.lightest, qs) def test_serialisation(self): serialized = serialize( "json", [ self.MODEL.objects.first(), ], ) deserialized = json.loads(serialized) obj = deserialized[0]["fields"] self.assertEqual(obj["weight"], self.DEFAULT_WEIGHT_STR) class FloatLikeFieldSaveTestBase(FieldSaveTestBase): OUNCES = Quantity(10 * ureg.ounce) OUNCES_IN_GRAM = 283.49523125 def test_stores_value_in_base_units(self): self.MODEL.objects.create(weight=self.OUNCES, name="ounce") item = self.MODEL.objects.get(name="ounce") self.assertEqual(item.weight.units, "gram") self.assertAlmostEqual(item.weight.magnitude, self.OUNCES_IN_GRAM) class TestFloatFieldSave(FloatLikeFieldSaveTestBase, TestCase): MODEL = FloatFieldSaveModel class TestDecimalFieldSave(FloatLikeFieldSaveTestBase, TestCase): MODEL = DecimalFieldSaveModel DEFAULT_WEIGHT_STR = "100.00" DEFAULT_WEIGHT_QUANTITY_STR = "100.00 gram" OUNCES = Decimal("10") * ureg.ounce OUNCE_VALUE = Decimal("3.52739619496") OUNCES_IN_GRAM = Decimal("283.50") EXPECTED_TYPE = Decimal class IntLikeFieldSaveTestBase(FieldSaveTestBase): DEFAULT_WEIGHT_STR = "100" DEFAULT_WEIGHT_QUANTITY_STR = "100 gram" EXPECTED_TYPE = int # 1 ounce = 28.34 grams -> we use something that can be stored as int COMPARE_QUANTITY = Quantity(28 * 1000 * ureg.milligram) @pytest.mark.xfail(reason="Not anymore supported") def test_store_integer_loss_of_precision(self): # We don't support this anymore, as it introduces to many edge cases # Also the normal int field accepts floats, so this should be handled # by the forms! with transaction.atomic(): with self.assertRaisesRegex(ValueError, "loss of precision"): self.MODEL(name="x", weight=Quantity(10 * ureg.ounce)).save() class TestIntFieldSave(IntLikeFieldSaveTestBase, TestCase): MODEL = IntFieldSaveModel class TestBigIntFieldSave(IntLikeFieldSaveTestBase, TestCase): MODEL = BigIntFieldSaveModel django-pint-0.6.3/tests/test_helper.py000066400000000000000000000016701415077357600200070ustar00rootroot00000000000000from django.test import TestCase from pint import DimensionalityError import quantityfield.fields as fields import quantityfield.helper as helper from quantityfield.units import ureg class TestMatchingUnitDimensionsHelper(TestCase): def test_valid_choices(self): helper.check_matching_unit_dimension(ureg, "meter", ["mile", "foot", "cm"]) def test_invalid_choices(self): with self.assertRaises(DimensionalityError): helper.check_matching_unit_dimension( ureg, "meter", ["mile", "foot", "cm", "kg"] ) class TestEdgeCases(TestCase): def test_fix_unit_registry(self): field = fields.IntegerQuantityField("meter") with self.assertRaises(ValueError): field.fix_unit_registry(1) def test_get_prep_value(self): field = fields.IntegerQuantityField("meter") with self.assertRaises(ValueError): field.get_prep_value("foobar") django-pint-0.6.3/tests/test_integration.py000066400000000000000000000066471415077357600210640ustar00rootroot00000000000000import pytest from django import forms from django.test import TestCase from decimal import Decimal from tests.dummyapp.forms import ( DefaultFormBigInt, DefaultFormDecimal, DefaultFormFieldsBigInt, DefaultFormFieldsDecimal, DefaultFormFieldsFloat, DefaultFormFieldsInt, DefaultFormFloat, DefaultFormInt, DefaultWidgetsFormBigInt, DefaultWidgetsFormDecimal, DefaultWidgetsFormFloat, DefaultWidgetsFormInt, ) class IntegrationTestBase: DEFAULT_FORM = DefaultFormFloat DEFAULT_FIELDS_FORM = DefaultFormFieldsFloat DEFAULT_WIDGET_FORM = DefaultWidgetsFormFloat INPUT_STR = "10.3" OUTPUT_MAGNITUDE = 10.3 def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Make sure we did no mistake creating the tests assert self.DEFAULT_FORM.Meta.model == self.DEFAULT_FIELDS_FORM.Meta.model assert self.DEFAULT_FORM.Meta.model == self.DEFAULT_WIDGET_FORM.Meta.model def _check_form_and_saved_object(self, form: forms.ModelForm, has_magnitude: bool): self.assertTrue(form.is_valid()) if has_magnitude: self.assertAlmostEqual( form.cleaned_data["weight"].magnitude, self.OUTPUT_MAGNITUDE ) self.assertEqual(str(form.cleaned_data["weight"].units), "gram") else: self.assertAlmostEqual(form.cleaned_data["weight"], self.OUTPUT_MAGNITUDE) form.save() obj = form.Meta.model.objects.last() self.assertEqual(str(obj.weight.units), "gram") if type(self.OUTPUT_MAGNITUDE) == float: self.assertAlmostEqual(obj.weight.magnitude, self.OUTPUT_MAGNITUDE) else: self.assertEqual(obj.weight.magnitude, self.OUTPUT_MAGNITUDE) self.assertIsInstance(obj.weight.magnitude, type(self.OUTPUT_MAGNITUDE)) @pytest.mark.django_db def test_widget_valid_inputs_with_units(self): form = self.DEFAULT_FORM( data={ "name": "testing", "weight_0": self.INPUT_STR, "weight_1": "gram", } ) self._check_form_and_saved_object(form, True) @pytest.mark.django_db def test_widget_single_inputs_with_units_and_default_form_fields(self): """ Test with default form fields, will still create the correct database entries """ form = self.DEFAULT_FIELDS_FORM( data={ "name": "testing", "weight": self.INPUT_STR, } ) self._check_form_and_saved_object(form, False) class TestFloatFieldWidgetIntegration(IntegrationTestBase, TestCase): pass class TestDecimalFieldWidgetIntegration(IntegrationTestBase, TestCase): DEFAULT_FORM = DefaultFormDecimal DEFAULT_FIELDS_FORM = DefaultFormFieldsDecimal DEFAULT_WIDGET_FORM = DefaultWidgetsFormDecimal INPUT_STR = "10" OUTPUT_MAGNITUDE = Decimal("10") class IntegrationTestBaseInt(IntegrationTestBase): INPUT_STR = "10" OUTPUT_MAGNITUDE = 10 class TestIntFiledWidgetIntegration(IntegrationTestBaseInt, TestCase): DEFAULT_FORM = DefaultFormInt DEFAULT_FIELDS_FORM = DefaultFormFieldsInt DEFAULT_WIDGET_FORM = DefaultWidgetsFormInt class TestBigIntFiledWidgetIntegration(IntegrationTestBaseInt, TestCase): DEFAULT_FORM = DefaultFormBigInt DEFAULT_FIELDS_FORM = DefaultFormFieldsBigInt DEFAULT_WIDGET_FORM = DefaultWidgetsFormBigInt django-pint-0.6.3/tests/test_widget.py000066400000000000000000000200371415077357600200110ustar00rootroot00000000000000# flake8: noqa: F841 from django import forms from django.test import TestCase from pint import DimensionalityError, UndefinedUnitError from quantityfield.fields import IntegerQuantityFormField, QuantityFormField from quantityfield.units import ureg from quantityfield.widgets import QuantityWidget from tests.dummyapp.models import ( ChoicesDefinedInModel, ChoicesDefinedInModelInt, HayBale, ) Quantity = ureg.Quantity class HayBaleForm(forms.ModelForm): weight = QuantityFormField(base_units="gram", unit_choices=["ounce", "gram"]) weight_int = IntegerQuantityFormField( base_units="gram", unit_choices=["ounce", "gram", "kilogram"] ) class Meta: model = HayBale exclude = ["weight_bigint"] class HayBaleFormDefaultWidgets(forms.ModelForm): weight = QuantityFormField( base_units="gram", unit_choices=["ounce", "gram"], widget=forms.NumberInput ) weight_int = IntegerQuantityFormField( base_units="gram", unit_choices=["ounce", "gram"], widget=forms.NumberInput ) class Meta: model = HayBale exclude = ["weight_bigint"] class UnitChoicesDefinedInModelFieldModelForm(forms.ModelForm): class Meta: model = ChoicesDefinedInModel fields = ["weight"] class UnitChoicesDefinedInModelFieldModelFormInt(forms.ModelForm): class Meta: model = ChoicesDefinedInModelInt fields = ["weight"] class NullableWeightForm(forms.Form): weight = QuantityFormField(base_units="gram", required=False) class UnitChoicesForm(forms.Form): distance = QuantityFormField( base_units="kilometer", unit_choices=["mile", "kilometer", "yard", "feet"] ) class TestWidgets(TestCase): def test_creates_correct_widget_for_modelform(self): form = HayBaleForm() self.assertIsInstance(form.fields["weight"], QuantityFormField) self.assertIsInstance(form.fields["weight"].widget, QuantityWidget) def test_displays_initial_data_correctly(self): form = HayBaleForm( initial={"weight": Quantity(100 * ureg.gram), "name": "test"} ) def test_clean_yields_quantity(self): form = HayBaleForm( data={ "weight_0": 100.0, "weight_1": "gram", "weight_int_0": 100, "weight_int_1": "gram", "name": "test", } ) self.assertTrue(form.is_valid()) self.assertIsInstance(form.cleaned_data["weight"], Quantity) def test_clean_yields_quantity_in_correct_units(self): form = HayBaleForm( data={ "weight_0": 1.0, "weight_1": "ounce", "weight_int_0": 1, "weight_int_1": "kilogram", "name": "test", } ) self.assertTrue(form.is_valid()) self.assertEqual(str(form.cleaned_data["weight"].units), "gram") self.assertAlmostEqual(form.cleaned_data["weight"].magnitude, 28.349523125) self.assertEqual(str(form.cleaned_data["weight_int"].units), "gram") self.assertAlmostEqual(form.cleaned_data["weight_int"].magnitude, 1000) def test_precision_lost(self): def test_clean_yields_quantity_in_correct_units(self): form = HayBaleForm( data={ "weight_0": 1.0, "weight_1": "ounce", "weight_int_0": 1, "weight_int_1": "onuce", "name": "test", } ) self.assertFalse(form.is_valid()) def test_base_units_is_required_for_form_field(self): with self.assertRaises(ValueError): field = QuantityFormField() # noqa: F841 def test_quantityfield_can_be_null(self): form = NullableWeightForm(data={"weight_0": None, "weight_1": None}) self.assertTrue(form.is_valid()) def test_validate_units(self): form = UnitChoicesForm(data={"distance_0": 100, "distance_1": "ounce"}) self.assertFalse(form.is_valid()) def test_base_units_is_included_by_default(self): field = QuantityFormField(base_units="mile", unit_choices=["meters", "feet"]) self.assertIn("mile", field.units) def test_widget_field_displays_unit_choices(self): form = UnitChoicesForm() self.assertListEqual( [ ("mile", "mile"), ("kilometer", "kilometer"), ("yard", "yard"), ("feet", "feet"), ], form.fields["distance"].widget.widgets[1].choices, ) def test_widget_field_displays_unit_choices_for_model_field_propagation(self): form = UnitChoicesDefinedInModelFieldModelForm() self.assertListEqual( [ ("milligram", "milligram"), ("pounds", "pounds"), ("kilogram", "kilogram"), ], form.fields["weight"].widget.widgets[1].choices, ) def test_widget_int_field_displays_unit_choices_for_model_field_propagation(self): form = UnitChoicesDefinedInModelFieldModelFormInt() self.assertListEqual( [ ("milligram", "milligram"), ("pounds", "pounds"), ("kilogram", "kilogram"), ], form.fields["weight"].widget.widgets[1].choices, ) def test_unit_choices_must_be_valid_units(self): with self.assertRaises(UndefinedUnitError): field = QuantityFormField( base_units="mile", unit_choices=["gunzu"] ) # noqa: F841 def test_unit_choices_must_match_base_dimensionality(self): with self.assertRaises(DimensionalityError): field = QuantityFormField( base_units="gram", unit_choices=["meter", "ounces"] ) # noqa: F841 def test_widget_display(self): # TODO Move to integration test bale = HayBale.objects.create(name="Fritz", weight=20) form = HayBaleForm(instance=bale) html = str(form) self.assertIn( '', html, ) self.assertIn('', html) def test_widget_invalid_float(self): form = HayBaleForm( data={ "name": "testing", "weight_0": "a", "weight_1": "gram", "weight_int_0": "10", "weight_int_1": "gram", } ) self.assertFalse(form.is_valid()) self.assertIn("weight", form.errors) def test_widget_missing_required_input(self): form = HayBaleForm( data={ "name": "testing", "weight_int_0": "10", "weight_int_1": "gram", } ) self.assertFalse(form.is_valid()) self.assertIn("weight", form.errors) def test_widget_empty_value_for_required_input(self): form = HayBaleForm( data={ "name": "testing", "weight_0": "", "weight_1": "gram", "weight_int_0": "10", "weight_int_1": "gram", } ) self.assertFalse(form.is_valid()) self.assertIn("weight", form.errors) def test_widget_none_value_set_for_required_input(self): form = HayBaleForm( data={ "name": "testing", "weight_0": None, "weight_1": "gram", "weight_int_0": "10", "weight_int_1": "gram", } ) self.assertFalse(form.is_valid()) self.assertIn("weight", form.errors) def test_widget_int_precision_loss(self): form = HayBaleFormDefaultWidgets( data={ "name": "testing", "weight": "10", "weight_int": "10.3", } ) self.assertFalse(form.is_valid()) self.assertTrue(form.has_error("weight_int")) django-pint-0.6.3/tox.ini000066400000000000000000000012101415077357600152560ustar00rootroot00000000000000[tox] minversion = 3.15 isolated_build = True envlist = {py36,py37,py38}-django22, {py36,py37,py38}-django30, {py36,py37,py38,py39}-django31 {py36,py37,py38,py39,py310}-django32 [travis:env] DJANGO = 2.2: django22 3.0: django30 3.1: django31 3.2: django31 [testenv] passenv = TRAVIS TRAVIS_* setenv = DJANGO_SETTINGS_MODULE=tests.settings TOXINIDIR = {toxinidir} deps = django22: Django>=2.2,<2.3 django30: Django>=3.0,<3.1 django31: Django>=3.1,<3.2 django32: Django>=3.2,<3.3 psycopg2-binary pytest pytest-cov pytest-django commands = pytest -x {posargs}