Skip to content

Commit

Permalink
Merge pull request #29 from CarliJoy/improve_testing
Browse files Browse the repository at this point in the history
Improve testing
  • Loading branch information
CarliJoy committed Mar 10, 2021
2 parents 15c2167 + 9f130f6 commit e855d1b
Show file tree
Hide file tree
Showing 17 changed files with 517 additions and 125 deletions.
4 changes: 4 additions & 0 deletions .env.example
@@ -0,0 +1,4 @@
POSTGRES_HOST=postgres
POSTGRES_PASSWORD=django_pint
POSTGRES_USER=django_pint
POSTGRES_DB=django_pint
3 changes: 3 additions & 0 deletions .gitignore
Expand Up @@ -14,6 +14,8 @@ __pycache__/*
.*.swp
*/.ipynb_checkpoints/*
.DS_Store
.env
local.py

# Project files
.ropeproject
Expand Down Expand Up @@ -47,6 +49,7 @@ docs/_rst/*
docs/_build/*
cover/*
MANIFEST
tests/local.py

# Per-project virtualenvs
.venv*/
4 changes: 4 additions & 0 deletions .travis.yml
Expand Up @@ -4,6 +4,7 @@ virt: lxd
dist: focal
group: edge
language: python
services: postgresql
cache:
- directories:
- $HOME/.cache/pre-commit
Expand Down Expand Up @@ -45,6 +46,9 @@ jobs:
install:
- travis_retry pip install tox tox-travis

before_script:
- bash ci_setup_postgres.sh

script: tox

after_success:
Expand Down
20 changes: 20 additions & 0 deletions Dockerfile
@@ -0,0 +1,20 @@
FROM 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]'
28 changes: 28 additions & 0 deletions README.md
@@ -1,5 +1,7 @@

[![Build Status](https://api.travis-ci.com/CarliJoy/django-pint.svg?branch=master)](https://travis-ci.com/github/CarliJoy/django-pint)
[![Average time to resolve an issue](http://isitmaintained.com/badge/resolution/CarliJoy/django-pint.svg)](http://isitmaintained.com/project/CarliJoy/django-pint)
[![Percentage of issues still open](http://isitmaintained.com/badge/open/CarliJoy/django-pint.svg)](http://isitmaintained.com/project/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](https://img.shields.io/pypi/dm/django-pint.svg?maxAge=2592000?style=plastic)](https://pypi.org/project/django-pint/)
[![Python Versions](https://img.shields.io/pypi/pyversions/django-pint.svg)](https://pypi.org/project/django-pint/)
Expand Down Expand Up @@ -104,3 +106,29 @@ Please note that if you change the unit registry for an already created project
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 script 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/conftest.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.
9 changes: 9 additions & 0 deletions ci_setup_postgres.sh
@@ -0,0 +1,9 @@
psql -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
30 changes: 30 additions & 0 deletions docker-compose.yaml
@@ -0,0 +1,30 @@
version: '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
21 changes: 21 additions & 0 deletions manage.py
@@ -0,0 +1,21 @@
#!/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()
1 change: 1 addition & 0 deletions setup.cfg
Expand Up @@ -103,6 +103,7 @@ norecursedirs =
build
.tox
testpaths = tests
DJANGO_SETTINGS_MODULE = tests.settings

[aliases]
dists = bdist_wheel
Expand Down
47 changes: 38 additions & 9 deletions src/quantityfield/fields.py
Expand Up @@ -6,7 +6,7 @@

import warnings
from pint import Quantity
from typing import List, Optional
from typing import List, Optional, Type, Union

from quantityfield.exceptions import PrecisionLoss
from quantityfield.helper import check_matching_unit_dimension
Expand All @@ -15,10 +15,11 @@
from .widgets import QuantityWidget


def safe_to_int(value):
def safe_to_int(value: Union[float, str, int], exception: Type[Exception]) -> int:
"""
Check if a value is an int otherwise warn that it can't be converted
:param value:
:param exception: which kind of exception is needed to be raised
:return:
"""
float_value = float(value)
Expand All @@ -27,11 +28,24 @@ def safe_to_int(value):
if round(abs(int_value - float_value), 10) == 0:
return int_value
else:
raise PrecisionLoss(
_("Possible loss of precision converting value to integer: %s") % value
raise exception(
_(
"After unit conversation this leads to a loss of precision. "
"The converted value '%s' can not safely be stored as integer "
"without precision loss."
)
% value
)


def raise_validation_error_on_imprecise_int(value) -> int:
return safe_to_int(value, ValidationError)


def raise_precision_error_on_imprecise_int(value) -> int:
return safe_to_int(value, PrecisionLoss)


class QuantityFieldMixin(object):
to_number_type = NotImplemented

Expand Down Expand Up @@ -112,7 +126,7 @@ def from_db_value(self, value, *args, **kwargs):
return value
return self.ureg.Quantity(value * getattr(self.ureg, self.base_units))

def to_python(self, value):
def to_python(self, value) -> Quantity:
if isinstance(value, self.ureg.Quantity):
return value

Expand All @@ -121,6 +135,21 @@ def to_python(self, 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.get_prep_value(self.to_python(value))
self.validate(value, model_instance)
self.run_validators(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
Expand Down Expand Up @@ -214,7 +243,7 @@ def clean(self, value):

val = self.ureg.Quantity(val * getattr(self.ureg, units)).to(self.base_units)
self.validate(val.magnitude)
self.run_validators(val)
self.run_validators(val.magnitude)
return val


Expand All @@ -228,14 +257,14 @@ class QuantityField(QuantityFieldMixin, models.FloatField):


class IntegerQuantityFormField(QuantityFormFieldMixin, forms.IntegerField):
to_number_type = staticmethod(safe_to_int)
to_number_type = staticmethod(raise_validation_error_on_imprecise_int)


class IntegerQuantityField(QuantityFieldMixin, models.IntegerField):
form_field_class = IntegerQuantityFormField
to_number_type = staticmethod(safe_to_int)
to_number_type = staticmethod(raise_precision_error_on_imprecise_int)


class BigIntegerQuantityField(QuantityFieldMixin, models.BigIntegerField):
form_field_class = IntegerQuantityFormField
to_number_type = staticmethod(safe_to_int)
to_number_type = staticmethod(raise_precision_error_on_imprecise_int)
40 changes: 0 additions & 40 deletions tests/conftest.py
Expand Up @@ -6,43 +6,3 @@
Read more about conftest.py under:
https://pytest.org/latest/plugins.html
"""

import django

from pint import UnitRegistry


def pytest_configure(config):
from django.conf import settings

custom_ureg = UnitRegistry()
custom_ureg.define("custom = [custom]")
custom_ureg.define("kilocustom = 1000 * custom")

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",
"tests.dummyapp",
],
DJANGO_PINT_UNIT_REGISTER=custom_ureg,
)
django.setup()
20 changes: 18 additions & 2 deletions tests/dummyapp/models.py
Expand Up @@ -14,15 +14,31 @@ class HayBale(models.Model):
weight_bigint = BigIntegerQuantityField("gram", blank=True, null=True)


class EmptyHayBale(models.Model):
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 CustomUregHayBale(models.Model):
# Custom is defined in settings in conftest.py
custom = QuantityField("custom", null=True)
custom = QuantityField("custom")
custom_int = IntegerQuantityField("custom")
custom_bigint = BigIntegerQuantityField("custom")


class ChoicesDefinedInModel(models.Model):
weight = QuantityField("kilogram", unit_choices=["milligram", "pounds"])


class ChoicesDefinedInModelInt(models.Model):
weight = IntegerQuantityField("kilogram", unit_choices=["milligram", "pounds"])
6 changes: 6 additions & 0 deletions tests/local.py.docker-example
@@ -0,0 +1,6 @@
import 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')

0 comments on commit e855d1b

Please sign in to comment.