Skip to content

Commit

Permalink
Updates for compatibility with Django3 and dropping support for Python2.
Browse files Browse the repository at this point in the history
Updates for compatibility with Django3 and dropping support for Python2.
  • Loading branch information
lukeburden authored Feb 29, 2020
2 parents 17720b9 + beef1c9 commit cbf5b47
Show file tree
Hide file tree
Showing 7 changed files with 111 additions and 165 deletions.
148 changes: 32 additions & 116 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,160 +32,76 @@ jobs:
lint:
<<: *common
docker:
- image: circleci/python:3.7
- image: circleci/python:3.8
environment:
- TOXENV=checkqa
- UPLOAD_COVERAGE=0
py27dj18:
<<: *common
docker:
- image: circleci/python:2.7
environment:
TOXENV=py27-dj18
py27dj110:
<<: *common
docker:
- image: circleci/python:2.7
environment:
TOXENV=py27-dj110
py27dj111:
<<: *common
docker:
- image: circleci/python:2.7
environment:
TOXENV=py27-dj111
py34dj18:
<<: *common
docker:
- image: circleci/python:3.4
environment:
TOXENV=py34-dj18
py34dj110:
<<: *common
docker:
- image: circleci/python:3.4
environment:
TOXENV=py34-dj110
py34dj111:
<<: *common
docker:
- image: circleci/python:3.4
environment:
TOXENV=py34-dj111
py34dj20:
<<: *common
docker:
- image: circleci/python:3.4
environment:
TOXENV=py34-dj20
py35dj18:
<<: *common
docker:
- image: circleci/python:3.5
environment:
TOXENV=py35-dj18
py35dj110:
<<: *common
docker:
- image: circleci/python:3.5
environment:
TOXENV=py35-dj110
py35dj111:
<<: *common
docker:
- image: circleci/python:3.5
environment:
TOXENV=py35-dj111
py35dj20:
<<: *common
docker:
- image: circleci/python:3.5
environment:
TOXENV=py35-dj20
py35dj21:
<<: *common
docker:
- image: circleci/python:3.5
environment:
TOXENV=py35-dj21
py35dj22:
py36dj22:
<<: *common
docker:
- image: circleci/python:3.5
- image: circleci/python:3.6
environment:
TOXENV=py35-dj22
py36dj111:
TOXENV=py36-dj22
py36dj30:
<<: *common
docker:
- image: circleci/python:3.6
environment:
TOXENV=py36-dj111
py36dj20:
TOXENV=py36-dj30
py36djmaster:
<<: *common
docker:
- image: circleci/python:3.6
environment:
TOXENV=py36-dj20
py36dj21:
TOXENV=py36-djmaster
py37dj22:
<<: *common
docker:
- image: circleci/python:3.6
- image: circleci/python:3.7
environment:
TOXENV=py36-dj21
py36dj22:
TOXENV=py37-dj22
py37dj30:
<<: *common
docker:
- image: circleci/python:3.6
- image: circleci/python:3.7
environment:
TOXENV=py36-dj22
py37dj111:
TOXENV=py37-dj30
py37djmaster:
<<: *common
docker:
- image: circleci/python:3.7
environment:
TOXENV=py37-dj111
py37dj20:
TOXENV=py37-djmaster
py38dj22:
<<: *common
docker:
- image: circleci/python:3.7
- image: circleci/python:3.8
environment:
TOXENV=py37-dj20
py37dj21:
TOXENV=py38-dj22
py38dj30:
<<: *common
docker:
- image: circleci/python:3.7
- image: circleci/python:3.8
environment:
TOXENV=py37-dj21
py37dj22:
TOXENV=py38-dj30
py38djmaster:
<<: *common
docker:
- image: circleci/python:3.7
- image: circleci/python:3.8
environment:
TOXENV=py37-dj22
TOXENV=py38-djmaster

workflows:
version: 2
test:
jobs:
- lint
- py27dj18
- py27dj110
- py27dj111
- py34dj18
- py34dj110
- py34dj111
- py34dj20
- py35dj18
- py35dj110
- py35dj111
- py35dj20
- py35dj21
- py35dj22
- py36dj111
- py36dj20
- py36dj21
- py36dj22
- py37dj111
- py37dj20
- py37dj21
- py36dj30
- py36djmaster
- py37dj22
- py37dj30
- py37djmaster
- py38dj22
- py38dj30
- py38djmaster
77 changes: 59 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,50 @@ AppleSerializer(instance=instance).data

```

## Gotchas

### Setting a field on a model to the value of a constant rather than the Constant object

When the Django ORM instantiates a model instance from the database, it will ensure that any `ConstantChoiceField` or `ConstantChoiceCharField` fields have values set to `Constant` instances.

If you create or modify a model instance using a raw, hard-coded constant value, you're likely to hit errors along the lines of "AttributeError: 'int' object has no attribute 'attribute-name'".

Take the example below where the caller sets the colour of a model instance to the underlying value for the `Constant`:

```python
apple = Apple.objects.get(name='Granny Smith')
apple.purpose = 1 # constant for `eating` .. I know, who on earth would eat a granny smith straight up?!
apple.save()
```

Django will happily persist the data correctly, but let's say you have a `post_save` signal (or any other code, really) that does something further with the instance:

```python
from django.db.models.signals import post_save
from django.dispatch import receiver

from .models import Apple

@receiver(post_save, sender=Apple)
def apple_updated_receiver(sender, instance, created, *args, **kwargs):
if not created and instance.purpose.eating:
print(f"Oh my, {instance.name} is now for eating!")

```

This code will sadly raise `AttributeError: 'int' object has no attribute 'purpose'`, as `instance.purpose` is just the integer we set it to before saving.

The good news is that you can avoid this by *always* setting `django-konst` fields on instances to `Constant` instances such that downstream code can happily handle the instance as if it'd come straight out of the database:

```python
apple = Apple.objects.get(name='Granny Smith')
apple.purpose = Apple.purposes.eating
apple.save()
```

Regardless of whether you're using `django-konst` for your constants, it's good practice to not hard-code constant values in order to avoid subtle mistakes and to ease changes in the future.


## Contribute

`django-konst` supports a variety of Python and Django versions. It's best if you test each one of these before committing. Our [Circle CI Integration](https://circleci.com) will test these when you push but knowing before you commit prevents from having to do a lot of extra commits to get the build to pass.
Expand All @@ -225,31 +269,28 @@ In order to easily test on all these Pythons and run the exact same thing that C

If you are on Mac OS X, it's recommended you use [brew](http://brew.sh/). After installing `brew` run:

```
$ brew install pyenv pyenv-virtualenv pyenv-virtualenvwrapper
```bash
brew install pyenv pyenv-virtualenv pyenv-virtualenvwrapper
```

Then:
Next, install the various python versions we want to test against and create a virtualenv specifically for `django-konst`:

```
pyenv install -s 2.7.14
pyenv install -s 3.4.7
pyenv install -s 3.5.4
pyenv install -s 3.6.3
pyenv virtualenv 2.7.14
pyenv virtualenv 3.4.7
pyenv virtualenv 3.5.4
pyenv virtualenv 3.6.3
pyenv global 2.7.14 3.4.7 3.5.4 3.6.3
```bash
pyenv install 3.6.10
pyenv install 3.7.6
pyenv install 3.8.1
pyenv virtualenv 3.8.1 konst
pyenv activate konst
pip install detox
pyenv shell konst 3.6.10 3.7.6
```

To run test suite:
Now ensure the `konst` virtualenv is activated, make the other python versions also on our path, and run the tests!

Make sure you are NOT inside a `virtualenv` and then:

```
$ detox
```bash
pyenv shell konst 3.6.10 3.7.6
detox
```

This will execute the testing matrix in parallel as defined in the `tox.ini`.
This will execute the test environments in parallel as defined in the `tox.ini`.
2 changes: 0 additions & 2 deletions konst/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,12 @@
from __future__ import absolute_import

from django.utils.deconstruct import deconstructible
from django.utils.encoding import python_2_unicode_compatible

# https://code.djangoproject.com/wiki/CookBookChoicesContantsClass
# modified to work with translation plus fields


@deconstructible
@python_2_unicode_compatible
class Constant(object):
def __init__(self, label=None, **kwargs):
self.constants = None
Expand Down
3 changes: 1 addition & 2 deletions konst/extras/drf/fields.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import

from django.utils import six
from django.utils.translation import ugettext_lazy as _

from rest_framework.fields import Field
Expand All @@ -26,7 +25,7 @@ def to_internal_value(self, data):
if data == "" and self.allow_blank:
return ""
try:
return self.constants.by_id[six.text_type(data)]
return self.constants.by_id[str(data)]
except KeyError:
self.fail("invalid_choice", input=data)

Expand Down
2 changes: 1 addition & 1 deletion konst/models/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def get_prep_value(self, value):
return value.v
return value

def from_db_value(self, value, expression, connection, *args):
def from_db_value(self, value, expression, connection):
# print "from_db_value: {}: {}".format(type(value), value)
if value is None:
return value
Expand Down
11 changes: 7 additions & 4 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
with open("README.md", "r") as fh:
long_description = fh.read()

tests_require = ["pytest", "pytest-django", "djangorestframework>=3.4.7"]
tests_require = ["pytest", "pytest-django", "djangorestframework>=3.10"]

setup(
name=name,
Expand All @@ -20,7 +20,7 @@
description=description,
long_description=long_description,
long_description_content_type="text/markdown",
version="1.0.2",
version="2.0.0",
license="MIT",
url=url,
packages=find_packages(),
Expand All @@ -31,11 +31,14 @@
"License :: OSI Approved :: BSD License",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Framework :: Django",
],
install_requires=["django>=1.8"],
install_requires=["django>=2.2"],
test_suite="runtests.runtests",
tests_require=tests_require,
zip_safe=False,
Expand Down
Loading

0 comments on commit cbf5b47

Please sign in to comment.