From 1985d1b36cb827fbe76b805f2e425b41fa8230d6 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Mon, 7 May 2018 14:44:16 -0400 Subject: [PATCH 01/75] Remove docker files --- .dockerignore | 91 ---------------------------------------------- Dockerfile | 3 -- docker-compose.yml | 4 -- 3 files changed, 98 deletions(-) delete mode 100644 .dockerignore delete mode 100644 Dockerfile delete mode 100644 docker-compose.yml diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 4da5a92..0000000 --- a/.dockerignore +++ /dev/null @@ -1,91 +0,0 @@ -# Git -.git -.gitignore - -# CI -.codeclimate.yml -.travis.yml - -# Docker -docker-compose.yml - -# Byte-compiled / optimized / DLL files -__pycache__/ -*/__pycache__/ -*/*/__pycache__/ -*/*/*/__pycache__/ -*.py[cod] -*/*.py[cod] -*/*/*.py[cod] -*/*/*/*.py[cod] - -# C extensions -*.so - -# Distribution / packaging -.Python -env/ -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -*.egg-info/ -.installed.cfg -*.egg - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.coverage -.cache -nosetests.xml -coverage.xml - -# Translations -*.mo -*.pot - -# Django stuff: -*.log - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# Virtual environment -.env/ -.venv/ -venv/ - -# PyCharm -.idea - -# Python mode for VIM -.ropeproject -*/.ropeproject -*/*/.ropeproject -*/*/*/.ropeproject - -# Vim swap files -*.swp -*/*.swp -*/*/*.swp -*/*/*/*.swp diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index f5555e6..0000000 --- a/Dockerfile +++ /dev/null @@ -1,3 +0,0 @@ -FROM themattrix/tox - -MAINTAINER Dan Koch diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index ddd84c9..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,4 +0,0 @@ -tox: - build: . - volumes: - - ".:/src:ro" From 887447ab13a2bf680ce12b1a0d7f8ecdcf3db429 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Mon, 7 May 2018 14:52:32 -0400 Subject: [PATCH 02/75] Move tests out of package --- tests/__init__.py | 0 jsonfield/tests.py => tests/test_jsonfield.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/__init__.py rename jsonfield/tests.py => tests/test_jsonfield.py (100%) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/jsonfield/tests.py b/tests/test_jsonfield.py similarity index 100% rename from jsonfield/tests.py rename to tests/test_jsonfield.py From 5f5c2de29f9d4dc66970ab3e5b096ce2bd83a810 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Mon, 7 May 2018 14:58:59 -0400 Subject: [PATCH 03/75] Use manage.py to run tests --- manage.py | 15 +++++++++++++++ setup.py | 25 +------------------------ tests/settings.py | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 24 deletions(-) create mode 100644 manage.py create mode 100644 tests/settings.py diff --git a/manage.py b/manage.py new file mode 100644 index 0000000..e9e7fdd --- /dev/null +++ b/manage.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.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) diff --git a/setup.py b/setup.py index 9a349ae..ad3a1b1 100644 --- a/setup.py +++ b/setup.py @@ -1,28 +1,5 @@ -from distutils.core import Command -from setuptools import setup - - -class TestCommand(Command): - user_options = [] - - def initialize_options(self): - pass - def finalize_options(self): - pass - - def run(self): - from django.conf import settings - settings.configure( - DATABASES={'default': {'NAME': ':memory:', 'ENGINE': 'django.db.backends.sqlite3'}}, - INSTALLED_APPS=('jsonfield', 'django.contrib.contenttypes') - ) - from django.core.management import call_command - import django - - if django.VERSION[:2] >= (1, 7): - django.setup() - call_command('test', 'jsonfield') +from setuptools import setup setup( diff --git a/tests/settings.py b/tests/settings.py new file mode 100644 index 0000000..5e98ff4 --- /dev/null +++ b/tests/settings.py @@ -0,0 +1,37 @@ +import os + +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +SECRET_KEY = 'not-a-secret' + +DEBUG = True + +INSTALLED_APPS = [ + 'django.contrib.contenttypes', + 'jsonfield', +] + +ROOT_URLCONF = [] + + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': ':memory:', + }, +} + + +# Internationalization +# https://docs.djangoproject.com/en/dev/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True From 5b5674a239d2a79977c693d754e634ece61399ec Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Mon, 7 May 2018 14:59:19 -0400 Subject: [PATCH 04/75] Make tests runnable outside of app --- tests/test_jsonfield.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/tests/test_jsonfield.py b/tests/test_jsonfield.py index 2d0b980..8dd54bf 100644 --- a/tests/test_jsonfield.py +++ b/tests/test_jsonfield.py @@ -12,7 +12,7 @@ except ImportError: from django.utils import simplejson as json -from .fields import JSONField, JSONCharField +from jsonfield.fields import JSONField, JSONCharField try: from django.forms.utils import ValidationError except ImportError: @@ -29,10 +29,16 @@ class JsonModel(models.Model): complex_default_json = JSONField(default=[{"checkcheck": 1212}]) empty_default = JSONField(default={}) + class Meta: + app_label = 'jsonfield' + class GenericForeignKeyObj(models.Model): name = models.CharField('Foreign Obj', max_length=255, null=True) + class Meta: + app_label = 'jsonfield' + class JSONModelWithForeignKey(models.Model): json = JSONField(null=True) @@ -41,11 +47,17 @@ class JSONModelWithForeignKey(models.Model): content_type = models.ForeignKey(ContentType, blank=True, null=True, on_delete=models.CASCADE) + class Meta: + app_label = 'jsonfield' + class JsonCharModel(models.Model): json = JSONCharField(max_length=100) default_json = JSONCharField(max_length=100, default={"check": 34}) + class Meta: + app_label = 'jsonfield' + class ComplexEncoder(json.JSONEncoder): def default(self, obj): @@ -72,6 +84,9 @@ class JSONModelCustomEncoders(models.Model): load_kwargs={'object_hook': as_complex}, ) + class Meta: + app_label = 'jsonfield' + class JSONModelWithForeignKeyTestCase(TestCase): def test_object_create(self): @@ -281,6 +296,9 @@ class JSONCharFieldTest(JSONFieldTest): class OrderedJsonModel(models.Model): json = JSONField(load_kwargs={'object_pairs_hook': OrderedDict}) + class Meta: + app_label = 'jsonfield' + class OrderedDictSerializationTest(TestCase): def setUp(self): @@ -316,6 +334,9 @@ def test_load_kwargs_hook_does_not_lose_sort_order(self): class JsonNotRequiredModel(models.Model): json = JSONField(blank=True, null=True) + class Meta: + app_label = 'jsonfield' + class JsonNotRequiredForm(forms.ModelForm): class Meta: From 03a4261ca4a5e054a54cc932a87723720b8de551 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Mon, 7 May 2018 15:17:49 -0400 Subject: [PATCH 05/75] Update CI --- .travis.yml | 62 +++++++++++++++++++++-------------------------------- tox.ini | 56 ++++++++++++++++++++++++----------------------- 2 files changed, 53 insertions(+), 65 deletions(-) diff --git a/.travis.yml b/.travis.yml index 20be682..b84ff89 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,46 +1,32 @@ -language: python sudo: false + +language: python + python: - "2.7" - - "3.3" - "3.4" - "3.5" - "3.6" -env: - - DJANGO=1.8 - - DJANGO=1.9 - - DJANGO=1.10 - - DJANGO=1.11 - - DJANGO=master -matrix: - exclude: - - python: "2.7" - env: DJANGO=master - - python: "3.3" - env: DJANGO=1.9 - - python: "3.3" - env: DJANGO=1.10 - - python: "3.3" - env: DJANGO=1.11 - - python: "3.3" - env: DJANGO=master - - python: "3.4" - env: DJANGO=master - - python: "3.6" - env: DJANGO=1.8 - - python: "3.6" - env: DJANGO=1.9 - - python: "3.6" - env: DJANGO=1.10 + install: - - pip install tox coveralls + - pip install -U pip setuptools wheel + - pip install tox tox-travis tox-venv + - python setup.py bdist_wheel + script: - - tox -e py${TRAVIS_PYTHON_VERSION//[.]/}-$DJANGO -after_success: - - coveralls -notifications: - email: - recipients: - - bjasper@gmail.com - - paltman@gmail.com - - dkoch@mm.st + - tox + +matrix: + include: + - python: 3.6 + env: TOXENV="isort,lint" + + - python: 3.6 + env: TOXENV="coverage" + after_success: + - pip install codecov && codecov + + - python: 3.6 + env: TOXENV="warnings" + allow_failures: + - env: TOXENV="warnings" diff --git a/tox.ini b/tox.ini index b774d21..8cf6c20 100644 --- a/tox.ini +++ b/tox.ini @@ -1,33 +1,35 @@ [tox] envlist = - py27-{1.8,1.9,1.10,1.11}, - py33-{1.8}, - py34-{1.8,1.9,1.10,1.11}, - py35-{1.8,1.9,1.10,1.11,master}, - py36-{1.11,master} -skipsdist = {env:TOXBUILD:false} + py{27}-django{111}, + py{34,35,36}-django{111,20}, + isort,lint,coverage,warnings, [testenv] -deps = - coverage==4.3.4 - flake8==3.3.0 - 1.8: Django>=1.8,<1.9 - 1.9: Django>=1.9,<1.10 - 1.10: Django>=1.10,<1.11 - 1.11: Django>=1.11,<2.0 - master: https://github.com/django/django/tarball/master +commands = python manage.py test {posargs} setenv = - LANG=en_US.UTF-8 - LANGUAGE=en_US:en - LC_ALL=en_US.UTF-8 -whitelist_externals = - true -commands = - {env:TOXBUILD:flake8 jsonfield} - {env:TOXBUILD:coverage run setup.py test} + PYTHONDONTWRITEBYTECODE=1 +deps = + django111: Django~=1.11 + django20: Django~=2.0 -[flake8] -# ignore = E265,E501 -max-line-length = 115 -max-complexity = 10 -# exclude = migrations/*,docs/* +[testenv:isort] +commands = isort --check-only --recursive jsonfield tests {posargs} +deps = + isort + +[testenv:lint] +commands = flake8 jsonfield tests {posargs} +deps = + flake8 + +[testenv:coverage] +commands = coverage run manage.py test {posargs} +usedevelop = True +deps = + coverage + django + +[testenv:warnings] +commands = python -Werror manage.py test {posargs} +deps = + https://github.com/django/django/archive/master.tar.gz From e133144e725b9b475e215d5f4011e9bf4270d37a Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Mon, 7 May 2018 15:30:01 -0400 Subject: [PATCH 06/75] Update JSONEncoder from DRF --- jsonfield/encoder.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/jsonfield/encoder.py b/jsonfield/encoder.py index 4923a90..07d8dbb 100644 --- a/jsonfield/encoder.py +++ b/jsonfield/encoder.py @@ -17,13 +17,11 @@ class JSONEncoder(json.JSONEncoder): """ def default(self, obj): # noqa # For Date Time string spec, see ECMA 262 - # http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.15 + # https://ecma-international.org/ecma-262/5.1/#sec-15.9.1.15 if isinstance(obj, Promise): return force_text(obj) elif isinstance(obj, datetime.datetime): representation = obj.isoformat() - if obj.microsecond: - representation = representation[:23] + representation[26:] if representation.endswith('+00:00'): representation = representation[:-6] + 'Z' return representation @@ -33,8 +31,6 @@ def default(self, obj): # noqa if timezone and timezone.is_aware(obj): raise ValueError("JSON can't represent timezone-aware times.") representation = obj.isoformat() - if obj.microsecond: - representation = representation[:12] return representation elif isinstance(obj, datetime.timedelta): return six.text_type(obj.total_seconds()) @@ -45,13 +41,16 @@ def default(self, obj): # noqa return six.text_type(obj) elif isinstance(obj, QuerySet): return tuple(obj) + elif isinstance(obj, six.binary_type): + # Best-effort for binary blobs. See #4187. + return obj.decode('utf-8') elif hasattr(obj, 'tolist'): # Numpy arrays and array scalars. return obj.tolist() elif hasattr(obj, '__getitem__'): try: return dict(obj) - except: + except Exception: pass elif hasattr(obj, '__iter__'): return tuple(item for item in obj) From acf71dad69a723b18e4f77f95cfccf5d332bcc7f Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Mon, 7 May 2018 15:30:40 -0400 Subject: [PATCH 07/75] Add lint/isort config, fix complaints --- .coveragerc | 7 ------- jsonfield/encoder.py | 11 ++++++----- jsonfield/fields.py | 21 +++++---------------- setup.cfg | 19 +++++++++++++++++++ tests/test_jsonfield.py | 24 ++++++++---------------- 5 files changed, 38 insertions(+), 44 deletions(-) delete mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 902d25c..0000000 --- a/.coveragerc +++ /dev/null @@ -1,7 +0,0 @@ -[run] -source = jsonfield -omit = jsonfield/tests.py -branch = 1 - -[report] -omit = jsonfield/tests.py diff --git a/jsonfield/encoder.py b/jsonfield/encoder.py index 07d8dbb..3326b05 100644 --- a/jsonfield/encoder.py +++ b/jsonfield/encoder.py @@ -1,12 +1,13 @@ -from django.db.models.query import QuerySet -from django.utils import six, timezone -from django.utils.encoding import force_text -from django.utils.functional import Promise import datetime import decimal import json import uuid +from django.db.models.query import QuerySet +from django.utils import six, timezone +from django.utils.encoding import force_text +from django.utils.functional import Promise + class JSONEncoder(json.JSONEncoder): """ @@ -15,7 +16,7 @@ class JSONEncoder(json.JSONEncoder): Taken from https://github.com/tomchristie/django-rest-framework/blob/master/rest_framework/utils/encoders.py """ - def default(self, obj): # noqa + def default(self, obj): # noqa: C901 # For Date Time string spec, see ECMA 262 # https://ecma-international.org/ecma-262/5.1/#sec-15.9.1.15 if isinstance(obj, Promise): diff --git a/jsonfield/fields.py b/jsonfield/fields.py index 4150bbf..80bc528 100644 --- a/jsonfield/fields.py +++ b/jsonfield/fields.py @@ -1,24 +1,13 @@ import copy +import json + from django.db import models +from django.forms import ValidationError, fields +from django.utils import six from django.utils.translation import ugettext_lazy as _ -try: - from django.utils import six -except ImportError: - import six - -try: - import json -except ImportError: - from django.utils import simplejson as json -from django.forms import fields -try: - from django.forms.utils import ValidationError -except ImportError: - from django.forms.util import ValidationError - -from .subclassing import SubfieldBase from .encoder import JSONEncoder +from .subclassing import SubfieldBase class JSONFormFieldBase(object): diff --git a/setup.cfg b/setup.cfg index 2a9acf1..e0aa720 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,21 @@ [bdist_wheel] universal = 1 + +[flake8] +max_line_length = 120 +max_complexity = 10 + +[isort] +skip = .tox +atomic = true +line_length = 120 +multi_line_output = 3 +known_third_party = django +known_first_party = jsonfield + +[coverage:run] +branch = true +source = jsonfield + +[coverage:report] +omit = tests diff --git a/tests/test_jsonfield.py b/tests/test_jsonfield.py index 8dd54bf..ca4d250 100644 --- a/tests/test_jsonfield.py +++ b/tests/test_jsonfield.py @@ -1,26 +1,18 @@ +import json +from collections import OrderedDict from decimal import Decimal + import django -from django import forms -from django.core.serializers import deserialize, serialize -from django.core.serializers.base import DeserializationError from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType +from django.core.serializers import deserialize, serialize +from django.core.serializers.base import DeserializationError from django.db import models +from django.forms import ModelForm, ValidationError from django.test import TestCase -try: - import json -except ImportError: - from django.utils import simplejson as json - -from jsonfield.fields import JSONField, JSONCharField -try: - from django.forms.utils import ValidationError -except ImportError: - from django.forms.util import ValidationError - from django.utils.six import string_types -from collections import OrderedDict +from jsonfield.fields import JSONCharField, JSONField class JsonModel(models.Model): @@ -338,7 +330,7 @@ class Meta: app_label = 'jsonfield' -class JsonNotRequiredForm(forms.ModelForm): +class JsonNotRequiredForm(ModelForm): class Meta: model = JsonNotRequiredModel fields = '__all__' From 02b07d184c56fd43412bb32d7bbbeed53abbfa85 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Mon, 7 May 2018 15:33:24 -0400 Subject: [PATCH 08/75] Drop py2k support --- .travis.yml | 1 - tox.ini | 1 - 2 files changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index b84ff89..a751950 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,6 @@ sudo: false language: python python: - - "2.7" - "3.4" - "3.5" - "3.6" diff --git a/tox.ini b/tox.ini index 8cf6c20..66ec66e 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,5 @@ [tox] envlist = - py{27}-django{111}, py{34,35,36}-django{111,20}, isort,lint,coverage,warnings, From 8ce228e59f664f833bb098a6b8232f5a6c952daa Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Mon, 7 May 2018 15:34:02 -0400 Subject: [PATCH 09/75] Update distribution info --- setup.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/setup.py b/setup.py index ad3a1b1..19cce81 100644 --- a/setup.py +++ b/setup.py @@ -1,21 +1,18 @@ - from setuptools import setup setup( - name='jsonfield', + name='jsonfield2', version='2.0.2', packages=['jsonfield'], license='MIT', include_package_data=True, - author='Dan Koch', - author_email='dmkoch@gmail.com', - url='https://github.com/dmkoch/django-jsonfield/', + author='Ryan P Kilby', + author_email='rpkilby@ncsu.edu', + url='https://github.com/rpkilby/jsonfield2/', description='A reusable Django field that allows you to store validated JSON in your model.', long_description=open("README.rst").read(), - install_requires=['Django >= 1.8.0'], - tests_require=['Django >= 1.8.0'], - cmdclass={'test': TestCommand}, + install_requires=['Django >= 1.11'], classifiers=[ 'Environment :: Web Environment', 'Intended Audience :: Developers', @@ -23,7 +20,6 @@ 'Programming Language :: Python', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', From 149cac46a12675f6f300add7336a9df49f39b990 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Mon, 7 May 2018 17:27:18 -0400 Subject: [PATCH 10/75] Remove south migration handling --- jsonfield/fields.py | 57 ++++++++---------------------------- jsonfield/subclassing.py | 62 ---------------------------------------- 2 files changed, 12 insertions(+), 107 deletions(-) delete mode 100644 jsonfield/subclassing.py diff --git a/jsonfield/fields.py b/jsonfield/fields.py index 80bc528..16eb962 100644 --- a/jsonfield/fields.py +++ b/jsonfield/fields.py @@ -7,7 +7,6 @@ from django.utils.translation import ugettext_lazy as _ from .encoder import JSONEncoder -from .subclassing import SubfieldBase class JSONFormFieldBase(object): @@ -43,7 +42,7 @@ class JSONCharFormField(JSONFormFieldBase, fields.CharField): pass -class JSONFieldBase(six.with_metaclass(SubfieldBase, models.Field)): +class JSONFieldBase(models.Field): def __init__(self, *args, **kwargs): self.dump_kwargs = kwargs.pop('dump_kwargs', { @@ -54,37 +53,19 @@ def __init__(self, *args, **kwargs): super(JSONFieldBase, self).__init__(*args, **kwargs) - def pre_init(self, value, obj): - """Convert a string value to JSON only if it needs to be deserialized. - - SubfieldBase metaclass has been modified to call this method instead of - to_python so that we can check the obj state and determine if it needs to be - deserialized""" + def to_python(self, value): + if self.null and value is None: + return None try: - if obj._state.adding: - # Make sure the primary key actually exists on the object before - # checking if it's empty. This is a special case for South datamigrations - # see: https://github.com/bradjasper/django-jsonfield/issues/52 - if getattr(obj, "pk", None) is not None: - if isinstance(value, six.string_types): - try: - return json.loads(value, **self.load_kwargs) - except ValueError: - raise ValidationError(_("Enter valid JSON")) - - except AttributeError: - # south fake meta class doesn't create proper attributes - # see this: - # https://github.com/bradjasper/django-jsonfield/issues/52 - pass - - return value + return json.loads(value, **self.load_kwargs) + except ValueError: + raise ValidationError(_("Enter valid JSON")) - def to_python(self, value): - """The SubfieldBase metaclass calls pre_init instead of to_python, however to_python - is still necessary for Django's deserializer""" - return value + def from_db_value(self, value, expression, connection): + if self.null and value is None: + return None + return json.loads(value, **self.load_kwargs) def get_prep_value(self, value): """Convert JSON object to a string""" @@ -92,17 +73,10 @@ def get_prep_value(self, value): return None return json.dumps(value, **self.dump_kwargs) - def value_to_string(self, obj): - value = self.value_from_object(obj, dump=False) - return self.get_db_prep_value(value, None) - - def value_from_object(self, obj, dump=True): + def value_from_object(self, obj): value = super(JSONFieldBase, self).value_from_object(obj) if self.null and value is None: return None - return self.dumps_for_display(value) if dump else value - - def dumps_for_display(self, value): return json.dumps(value, **self.dump_kwargs) def formfield(self, **kwargs): @@ -155,10 +129,3 @@ class JSONCharField(JSONFieldBase, models.CharField): stored in the database like a CharField, which enables it to be used e.g. in unique keys""" form_class = JSONCharFormField - - -try: - from south.modelsinspector import add_introspection_rules - add_introspection_rules([], ["^jsonfield\.fields\.(JSONField|JSONCharField)"]) -except ImportError: - pass diff --git a/jsonfield/subclassing.py b/jsonfield/subclassing.py deleted file mode 100644 index 49e30e1..0000000 --- a/jsonfield/subclassing.py +++ /dev/null @@ -1,62 +0,0 @@ -# This file was copied from django.db.models.fields.subclassing so that we could -# change the Creator.__set__ behavior. Read the comment below for full details. - -""" -Convenience routines for creating non-trivial Field subclasses, as well as -backwards compatibility utilities. - -Add SubfieldBase as the __metaclass__ for your Field subclass, implement -to_python() and the other necessary methods and everything will work seamlessly. -""" - - -class SubfieldBase(type): - """ - A metaclass for custom Field subclasses. This ensures the model's attribute - has the descriptor protocol attached to it. - """ - def __new__(cls, name, bases, attrs): - new_class = super(SubfieldBase, cls).__new__(cls, name, bases, attrs) - new_class.contribute_to_class = make_contrib( - new_class, attrs.get('contribute_to_class') - ) - return new_class - - -class Creator(object): - """ - A placeholder class that provides a way to set the attribute on the model. - """ - def __init__(self, field): - self.field = field - - def __get__(self, obj, type=None): - if obj is None: - return self - return obj.__dict__[self.field.name] - - def __set__(self, obj, value): - # Usually this would call to_python, but we've changed it to pre_init - # so that we can tell which state we're in. By passing an obj, - # we can definitively tell if a value has already been deserialized - # More: https://github.com/bradjasper/django-jsonfield/issues/33 - obj.__dict__[self.field.name] = self.field.pre_init(value, obj) - - -def make_contrib(superclass, func=None): - """ - Returns a suitable contribute_to_class() method for the Field subclass. - - If 'func' is passed in, it is the existing contribute_to_class() method on - the subclass and it is called before anything else. It is assumed in this - case that the existing contribute_to_class() calls all the necessary - superclass methods. - """ - def contribute_to_class(self, cls, name): - if func: - func(self, cls, name) - else: - super(superclass, self).contribute_to_class(cls, name) - setattr(cls, self.name, Creator(self)) - - return contribute_to_class From 2fa46aeb1766b34463be18cb6a10ffd97ff3b5cd Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Mon, 7 May 2018 17:28:03 -0400 Subject: [PATCH 11/75] Make optional arguments keyword-only --- jsonfield/fields.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/jsonfield/fields.py b/jsonfield/fields.py index 16eb962..23b8bae 100644 --- a/jsonfield/fields.py +++ b/jsonfield/fields.py @@ -44,12 +44,12 @@ class JSONCharFormField(JSONFormFieldBase, fields.CharField): class JSONFieldBase(models.Field): - def __init__(self, *args, **kwargs): - self.dump_kwargs = kwargs.pop('dump_kwargs', { + def __init__(self, *args, dump_kwargs=None, load_kwargs=None, **kwargs): + self.dump_kwargs = dump_kwargs if dump_kwargs is not None else { 'cls': JSONEncoder, 'separators': (',', ':') - }) - self.load_kwargs = kwargs.pop('load_kwargs', {}) + } + self.load_kwargs = load_kwargs if load_kwargs is not None else {} super(JSONFieldBase, self).__init__(*args, **kwargs) From 0d915839907c86ae6dea639cb7f506af4d238baa Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Mon, 7 May 2018 17:28:36 -0400 Subject: [PATCH 12/75] Add field deconstruction --- jsonfield/fields.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/jsonfield/fields.py b/jsonfield/fields.py index 23b8bae..f95ef51 100644 --- a/jsonfield/fields.py +++ b/jsonfield/fields.py @@ -53,6 +53,15 @@ def __init__(self, *args, dump_kwargs=None, load_kwargs=None, **kwargs): super(JSONFieldBase, self).__init__(*args, **kwargs) + def deconstruct(self): + name, path, args, kwargs = super().deconstruct() + + if self.dump_kwargs is not None: + kwargs['dump_kwargs'] = self.dump_kwargs + if self.load_kwargs is not None: + kwargs['load_kwargs'] = self.load_kwargs + return name, path, args, kwargs + def to_python(self, value): if self.null and value is None: return None From 2ea5211d7c1671fdd202af3b58964be4640cd85d Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Mon, 7 May 2018 18:15:14 -0400 Subject: [PATCH 13/75] Small fixes --- jsonfield/fields.py | 2 +- tests/test_jsonfield.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/jsonfield/fields.py b/jsonfield/fields.py index f95ef51..b82f268 100644 --- a/jsonfield/fields.py +++ b/jsonfield/fields.py @@ -71,7 +71,7 @@ def to_python(self, value): except ValueError: raise ValidationError(_("Enter valid JSON")) - def from_db_value(self, value, expression, connection): + def from_db_value(self, value, expression, connection, context=None): if self.null and value is None: return None return json.loads(value, **self.load_kwargs) diff --git a/tests/test_jsonfield.py b/tests/test_jsonfield.py index ca4d250..d7cf51d 100644 --- a/tests/test_jsonfield.py +++ b/tests/test_jsonfield.py @@ -215,9 +215,10 @@ def test_invalid_json(self): '"fields": {"json": "{]", "default_json": "{]"}}]' with self.assertRaises(DeserializationError) as cm: next(deserialize('json', ser)) - # Django 2.0+ uses PEP 3134 exception chaining + # Django 2 does not reraise DeserializationError as another DeserializationError + # Changed in: https://github.com/django/django/pull/7878 if django.VERSION < (2, 0,): - inner = cm.exception.args[0] + inner = cm.exception.__context__.__context__ else: inner = cm.exception.__context__ self.assertTrue(isinstance(inner, ValidationError)) From 0014179b7b9b533654562901b2cbdfa427dad2a8 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Mon, 7 May 2018 18:30:48 -0400 Subject: [PATCH 14/75] Move form fields into separate module --- jsonfield/fields.py | 43 +++++-------------------------------------- jsonfield/forms.py | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 38 deletions(-) create mode 100644 jsonfield/forms.py diff --git a/jsonfield/fields.py b/jsonfield/fields.py index b82f268..165ef19 100644 --- a/jsonfield/fields.py +++ b/jsonfield/fields.py @@ -2,46 +2,13 @@ import json from django.db import models -from django.forms import ValidationError, fields -from django.utils import six +from django.forms import ValidationError from django.utils.translation import ugettext_lazy as _ +from . import forms from .encoder import JSONEncoder -class JSONFormFieldBase(object): - def __init__(self, *args, **kwargs): - self.load_kwargs = kwargs.pop('load_kwargs', {}) - super(JSONFormFieldBase, self).__init__(*args, **kwargs) - - def to_python(self, value): - if isinstance(value, six.string_types) and value: - try: - return json.loads(value, **self.load_kwargs) - except ValueError: - raise ValidationError(_("Enter valid JSON")) - return value - - def clean(self, value): - - if not value and not self.required: - return None - - # Trap cleaning errors & bubble them up as JSON errors - try: - return super(JSONFormFieldBase, self).clean(value) - except TypeError: - raise ValidationError(_("Enter valid JSON")) - - -class JSONFormField(JSONFormFieldBase, fields.CharField): - pass - - -class JSONCharFormField(JSONFormFieldBase, fields.CharField): - pass - - class JSONFieldBase(models.Field): def __init__(self, *args, dump_kwargs=None, load_kwargs=None, **kwargs): @@ -95,7 +62,7 @@ def formfield(self, **kwargs): field = super(JSONFieldBase, self).formfield(**kwargs) - if isinstance(field, JSONFormFieldBase): + if isinstance(field, forms.JSONFieldBase): field.load_kwargs = self.load_kwargs if not field.help_text: @@ -125,7 +92,7 @@ def get_default(self): class JSONField(JSONFieldBase, models.TextField): """JSONField is a generic textfield that serializes/deserializes JSON objects""" - form_class = JSONFormField + form_class = forms.JSONField def dumps_for_display(self, value): kwargs = {"indent": 2} @@ -137,4 +104,4 @@ class JSONCharField(JSONFieldBase, models.CharField): """JSONCharField is a generic textfield that serializes/deserializes JSON objects, stored in the database like a CharField, which enables it to be used e.g. in unique keys""" - form_class = JSONCharFormField + form_class = forms.JSONCharField diff --git a/jsonfield/forms.py b/jsonfield/forms.py new file mode 100644 index 0000000..e72029a --- /dev/null +++ b/jsonfield/forms.py @@ -0,0 +1,38 @@ +import json + +from django.forms import ValidationError, fields +from django.utils import six +from django.utils.translation import ugettext_lazy as _ + + +class JSONFieldBase(object): + def __init__(self, *args, **kwargs): + self.load_kwargs = kwargs.pop('load_kwargs', {}) + super(JSONFieldBase, self).__init__(*args, **kwargs) + + def to_python(self, value): + if isinstance(value, six.string_types) and value: + try: + return json.loads(value, **self.load_kwargs) + except ValueError: + raise ValidationError(_("Enter valid JSON")) + return value + + def clean(self, value): + + if not value and not self.required: + return None + + # Trap cleaning errors & bubble them up as JSON errors + try: + return super(JSONFieldBase, self).clean(value) + except TypeError: + raise ValidationError(_("Enter valid JSON")) + + +class JSONField(JSONFieldBase, fields.CharField): + pass + + +class JSONCharField(JSONFieldBase, fields.CharField): + pass From bf256cd16cc72bf173bbd2e5fa6eb81da4b06a27 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Mon, 7 May 2018 18:31:04 -0400 Subject: [PATCH 15/75] Combine travis builds --- .travis.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index a751950..af9cd9d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,10 +18,7 @@ script: matrix: include: - python: 3.6 - env: TOXENV="isort,lint" - - - python: 3.6 - env: TOXENV="coverage" + env: TOXENV="isort,lint,coverage" after_success: - pip install codecov && codecov From c797e76a3da9becc462cddd10c943a8ab34dc5ae Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Mon, 7 May 2018 18:38:56 -0400 Subject: [PATCH 16/75] Ignore coverage for encoder copied from DRF --- jsonfield/encoder.py | 2 +- setup.cfg | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/jsonfield/encoder.py b/jsonfield/encoder.py index 3326b05..d93f476 100644 --- a/jsonfield/encoder.py +++ b/jsonfield/encoder.py @@ -14,7 +14,7 @@ class JSONEncoder(json.JSONEncoder): JSONEncoder subclass that knows how to encode date/time/timedelta, decimal types, generators and other basic python objects. - Taken from https://github.com/tomchristie/django-rest-framework/blob/master/rest_framework/utils/encoders.py + Taken from https://github.com/tomchristie/django-rest-framework/blob/3.8.2/rest_framework/utils/encoders.py """ def default(self, obj): # noqa: C901 # For Date Time string spec, see ECMA 262 diff --git a/setup.cfg b/setup.cfg index e0aa720..5940488 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,6 +16,6 @@ known_first_party = jsonfield [coverage:run] branch = true source = jsonfield - -[coverage:report] -omit = tests +omit = + jsonfield/encoder.py + tests From 6aafd5a2abe29bc2f94602a11fc2d8368460ce4c Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Mon, 7 May 2018 19:26:30 -0400 Subject: [PATCH 17/75] Remove unneeded form field, remove old method --- jsonfield/fields.py | 8 +------- jsonfield/forms.py | 4 ---- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/jsonfield/fields.py b/jsonfield/fields.py index 165ef19..1d14f0d 100644 --- a/jsonfield/fields.py +++ b/jsonfield/fields.py @@ -10,6 +10,7 @@ class JSONFieldBase(models.Field): + form_class = forms.JSONField def __init__(self, *args, dump_kwargs=None, load_kwargs=None, **kwargs): self.dump_kwargs = dump_kwargs if dump_kwargs is not None else { @@ -92,16 +93,9 @@ def get_default(self): class JSONField(JSONFieldBase, models.TextField): """JSONField is a generic textfield that serializes/deserializes JSON objects""" - form_class = forms.JSONField - - def dumps_for_display(self, value): - kwargs = {"indent": 2} - kwargs.update(self.dump_kwargs) - return json.dumps(value, ensure_ascii=False, **kwargs) class JSONCharField(JSONFieldBase, models.CharField): """JSONCharField is a generic textfield that serializes/deserializes JSON objects, stored in the database like a CharField, which enables it to be used e.g. in unique keys""" - form_class = forms.JSONCharField diff --git a/jsonfield/forms.py b/jsonfield/forms.py index e72029a..1c6df07 100644 --- a/jsonfield/forms.py +++ b/jsonfield/forms.py @@ -32,7 +32,3 @@ def clean(self, value): class JSONField(JSONFieldBase, fields.CharField): pass - - -class JSONCharField(JSONFieldBase, fields.CharField): - pass From bb85034c118c7f052cb5ad52cc79530d15e1c82c Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Mon, 7 May 2018 20:07:24 -0400 Subject: [PATCH 18/75] Renamed JSONFieldBase => JSONFieldMixin --- jsonfield/__init__.py | 5 ++++- jsonfield/fields.py | 20 +++++++++----------- jsonfield/forms.py | 8 ++++---- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/jsonfield/__init__.py b/jsonfield/__init__.py index 54360e2..3ad1c1f 100644 --- a/jsonfield/__init__.py +++ b/jsonfield/__init__.py @@ -1 +1,4 @@ -from .fields import JSONField, JSONCharField # noqa +from .fields import JSONCharField, JSONField + + +__all__ = ['JSONCharField', 'JSONField'] diff --git a/jsonfield/fields.py b/jsonfield/fields.py index 1d14f0d..4a28517 100644 --- a/jsonfield/fields.py +++ b/jsonfield/fields.py @@ -9,7 +9,7 @@ from .encoder import JSONEncoder -class JSONFieldBase(models.Field): +class JSONFieldMixin(models.Field): form_class = forms.JSONField def __init__(self, *args, dump_kwargs=None, load_kwargs=None, **kwargs): @@ -19,7 +19,7 @@ def __init__(self, *args, dump_kwargs=None, load_kwargs=None, **kwargs): } self.load_kwargs = load_kwargs if load_kwargs is not None else {} - super(JSONFieldBase, self).__init__(*args, **kwargs) + super(JSONFieldMixin, self).__init__(*args, **kwargs) def deconstruct(self): name, path, args, kwargs = super().deconstruct() @@ -51,7 +51,7 @@ def get_prep_value(self, value): return json.dumps(value, **self.dump_kwargs) def value_from_object(self, obj): - value = super(JSONFieldBase, self).value_from_object(obj) + value = super(JSONFieldMixin, self).value_from_object(obj) if self.null and value is None: return None return json.dumps(value, **self.dump_kwargs) @@ -61,9 +61,9 @@ def formfield(self, **kwargs): if "form_class" not in kwargs: kwargs["form_class"] = self.form_class - field = super(JSONFieldBase, self).formfield(**kwargs) + field = super(JSONFieldMixin, self).formfield(**kwargs) - if isinstance(field, forms.JSONFieldBase): + if isinstance(field, forms.JSONFieldMixin): field.load_kwargs = self.load_kwargs if not field.help_text: @@ -88,14 +88,12 @@ def get_default(self): return self.default() return copy.deepcopy(self.default) # If the field doesn't have a default, then we punt to models.Field. - return super(JSONFieldBase, self).get_default() + return super(JSONFieldMixin, self).get_default() -class JSONField(JSONFieldBase, models.TextField): +class JSONField(JSONFieldMixin, models.TextField): """JSONField is a generic textfield that serializes/deserializes JSON objects""" -class JSONCharField(JSONFieldBase, models.CharField): - """JSONCharField is a generic textfield that serializes/deserializes JSON objects, - stored in the database like a CharField, which enables it to be used - e.g. in unique keys""" +class JSONCharField(JSONFieldMixin, models.CharField): + """JSONCharField is a generic textfield that serializes/deserializes JSON objects""" diff --git a/jsonfield/forms.py b/jsonfield/forms.py index 1c6df07..97f4b65 100644 --- a/jsonfield/forms.py +++ b/jsonfield/forms.py @@ -5,10 +5,10 @@ from django.utils.translation import ugettext_lazy as _ -class JSONFieldBase(object): +class JSONFieldMixin(object): def __init__(self, *args, **kwargs): self.load_kwargs = kwargs.pop('load_kwargs', {}) - super(JSONFieldBase, self).__init__(*args, **kwargs) + super(JSONFieldMixin, self).__init__(*args, **kwargs) def to_python(self, value): if isinstance(value, six.string_types) and value: @@ -25,10 +25,10 @@ def clean(self, value): # Trap cleaning errors & bubble them up as JSON errors try: - return super(JSONFieldBase, self).clean(value) + return super(JSONFieldMixin, self).clean(value) except TypeError: raise ValidationError(_("Enter valid JSON")) -class JSONField(JSONFieldBase, fields.CharField): +class JSONField(JSONFieldMixin, fields.CharField): pass From 895e5da595f33be9fb7ebf7cbf3b17483f37f568 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Mon, 7 May 2018 20:14:58 -0400 Subject: [PATCH 19/75] Update README --- README.rst | 109 +++++++++++++++++++++++++++++------------------------ 1 file changed, 59 insertions(+), 50 deletions(-) diff --git a/README.rst b/README.rst index 3ef6595..5f8c592 100644 --- a/README.rst +++ b/README.rst @@ -1,25 +1,42 @@ -django-jsonfield ----------------- +jsonfield2 +========== -django-jsonfield is a reusable Django field that allows you to store validated JSON in your model. +.. image:: https://travis-ci.org/rpkilby/jsonfield2.svg?branch=master + :target: https://travis-ci.org/rpkilby/jsonfield2 +.. image:: https://codecov.io/gh/rpkilby/jsonfield2/branch/master/graph/badge.svg + :target: https://codecov.io/gh/rpkilby/jsonfield2 +.. image:: https://img.shields.io/pypi/v/jsonfield2.svg + :target: https://pypi.org/project/jsonfield2 +.. image:: https://img.shields.io/pypi/l/jsonfield2.svg + :target: https://pypi.org/project/jsonfield2 -It silently takes care of serialization. To use, simply add the field to one of your models. +A modern fork of `django-jsonfield`_, compatible with the latest versions of Django. -Python 3 & Django 1.8 through 1.11 supported! +.. _django-jsonfield: https://github.com/dmkoch/django-jsonfield -**Use PostgreSQL?** 1.0.0 introduced a breaking change to the underlying data type, so if you were using < 1.0.0 please read https://github.com/dmkoch/django-jsonfield/issues/57 before upgrading. Also, consider switching to Django's native JSONField that was added in Django 1.9. +----- + +**jsonfield2** is a reusable model field that allows you to store validated JSON, automatically handling +serialization to and from the database. To use, add ``jsonfield.JSONField`` to one of your models. + +**Note:** `django.contrib.postgres`_ now supports PostgreSQL's jsonb type, which includes extended querying +capabilities. If you're an end user of PostgreSQL and want full-featured JSON support, then it is +recommended that you use the built-in JSONField. However, jsonfield2 is still useful when your app +needs to be database-agnostic, or when the built-in JSONField's extended querying is not being leveraged. +e.g., a configuration field. + +.. _django.contrib.postgres: https://docs.djangoproject.com/en/dev/ref/contrib/postgres/fields/#jsonfield -**Note:** There are a couple of third-party add-on JSONFields for Django. This project is django-jsonfield here on GitHub but is named `jsonfield on PyPI`_. There is another `django-jsonfield on Bitbucket`_, but that one is `django-jsonfield on PyPI`_. I realize this naming conflict is confusing and I am open to merging the two projects. -.. _jsonfield on PyPI: https://pypi.python.org/pypi/jsonfield -.. _django-jsonfield on Bitbucket: https://bitbucket.org/schinckel/django-jsonfield -.. _django-jsonfield on PyPI: https://pypi.python.org/pypi/django-jsonfield +Requirements +------------ -**Note:** Django 1.9 added native PostgreSQL JSON support in `django.contrib.postgres.fields.JSONField`_. This module is still useful if you need to support JSON in databases other than PostgreSQL or are creating a third-party module that needs to be database-agnostic. But if you're an end user using PostgreSQL and want full-featured JSON support, I recommend using the built-in JSONField from Django instead of this module. +jsonfield2 aims to support all current `versions of Django`_, however the explicity tested versions are: -.. _django.contrib.postgres.fields.JSONField: https://docs.djangoproject.com/en/dev/ref/contrib/postgres/fields/#jsonfield +* **Python:** 3.4, 3.5, 3.6 +* **Django:** 1.11, 2.0 -**Note:** Semver is followed after the 1.0 release. +.. _versions of Django: https://www.djangoproject.com/download/#supported-versions Installation @@ -27,7 +44,7 @@ Installation .. code-block:: python - pip install jsonfield + pip install jsonfield2 Usage @@ -39,20 +56,22 @@ Usage from jsonfield import JSONField class MyModel(models.Model): - json = JSONField() + json = JSONField() + Advanced Usage -------------- -By default python deserializes json into dict objects. This behavior differs from the standard json behavior because python dicts do not have ordered keys. - -To overcome this limitation and keep the sort order of OrderedDict keys the deserialisation can be adjusted on model initialisation: +By default python deserializes json into dict objects. This behavior differs from the standard json +behavior because python dicts do not have ordered keys. To overcome this limitation and keep the +sort order of OrderedDict keys the deserialisation can be adjusted on model initialisation: .. code-block:: python import collections + class MyModel(models.Model): - json = JSONField(load_kwargs={'object_pairs_hook': collections.OrderedDict}) + json = JSONField(load_kwargs={'object_pairs_hook': collections.OrderedDict}) Other Fields @@ -60,60 +79,50 @@ Other Fields **jsonfield.JSONCharField** -If you need to use your JSON field in an index or other constraint, you can use **JSONCharField** which subclasses **CharField** instead of **TextField**. You'll also need to specify a **max_length** parameter if you use this field. +Subclasses **models.CharField** instead of **models.TextField**. -Compatibility --------------- - -django-jsonfield aims to support the same versions of Django currently maintained by the main Django project. See `Django supported versions`_, currently: +Running the tests +----------------- - * Django 1.8 (LTS) with Python 2.7, 3.3, 3.4, or 3.5 - * Django 1.9 with Python 2.7, 3.4, or 3.5 - * Django 1.10 with Python 2.7, 3.4, or 3.5 - * Django 1.11 (LTS) with Python 2.7, 3.4, 3.5 or 3.6 +The test suite requires ``tox`` and ``tox-venv``. -.. _Django supported versions: https://www.djangoproject.com/download/#supported-versions +.. code-block:: shell + $ pip install tox tox-venv -Testing django-jsonfield Locally --------------------------------- -To test against all supported versions of Django: +To test against all supported versions of Django, install and run ``tox``: .. code-block:: shell - $ docker-compose build && docker-compose up + $ tox -Or just one version (for example Django 1.10 on Python 3.5): +Or, to test just one version (for example Django 2.0 on Python 3.6): .. code-block:: shell - $ docker-compose build && docker-compose run tox tox -e py35-1.10 - + $ tox -e py36-django20 -Travis CI ---------- -.. image:: https://travis-ci.org/dmkoch/django-jsonfield.svg?branch=master - :target: https://travis-ci.org/dmkoch/django-jsonfield +Release Process +--------------- -Contact -------- -Web: http://bradjasper.com - -Twitter: `@bradjasper`_ - -Email: `contact@bradjasper.com`_ +* Update changelog +* Update package version in setup.py +* Create git tag for version +* Upload release to PyPI +.. code-block:: shell + $ pip install -U pip setuptools wheel + $ rm -rf dist/ build/ + $ python setup.py bdist_wheel upload -.. _contact@bradjasper.com: mailto:contact@bradjasper.com -.. _@bradjasper: https://twitter.com/bradjasper Changes ------- Take a look at the `changelog`_. -.. _changelog: https://github.com/dmkoch/django-jsonfield/blob/master/CHANGES.rst +.. _changelog: https://github.com/rpkilby/jsonfield2/blob/master/CHANGES.rst From 16c77bcb6965f0d5791645b30f758657413c1049 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Mon, 7 May 2018 20:27:44 -0400 Subject: [PATCH 20/75] Update changelog --- CHANGES.rst | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 808d645..c178838 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,5 +1,20 @@ Changes ------- + +v3.0.0 05/07/2017 +^^^^^^^^^^^^^^^^^ +- Add Django 2.0 support +- Drop Django 1.8, 1.9, and 1.10 support +- Drop Python 2.7 and 3.3 support +- Rework field serialization/deserialization +- Remove support for South +- Rename JSONFieldBase to JSONFieldMixin +- Move form fields into separate module +- Rename JSONFormFieldBase to forms.JSONFieldMixin +- Rename JSONFormField to forms.JSONField +- Remove JSONCharFormField +- Update JSONEncoder from DRF + v2.0.2, 6/18/2017 ^^^^^^^^^^^^^^^^^ - Fixed issue with GenericForeignKey field @@ -27,7 +42,6 @@ v1.0.1, 2/2/2015 v1.0.0, 9/4/2014 ^^^^^^^^^^^^^^^^ - - Removed native JSON datatype support for PostgreSQL (breaking change) & added Python 3.4 to tests v0.9.23, 9/3/2014 From 4dd07538de72ef3a55ff2d8dadc76958d59756dc Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Mon, 7 May 2018 20:28:08 -0400 Subject: [PATCH 21/75] Bump major version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 19cce81..2e786f1 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='jsonfield2', - version='2.0.2', + version='3.0.0', packages=['jsonfield'], license='MIT', include_package_data=True, From 87121ca9b8b349c58b36c62cfd1b261d69ab2a2a Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Mon, 21 May 2018 16:33:56 -0400 Subject: [PATCH 22/75] Refactor test models --- setup.cfg | 1 + tests/models.py | 65 +++++++++++++++++++ tests/settings.py | 1 + tests/test_jsonfield.py | 134 ++++++++++------------------------------ 4 files changed, 98 insertions(+), 103 deletions(-) create mode 100644 tests/models.py diff --git a/setup.cfg b/setup.cfg index 5940488..8d7ed89 100644 --- a/setup.cfg +++ b/setup.cfg @@ -10,6 +10,7 @@ skip = .tox atomic = true line_length = 120 multi_line_output = 3 +include_trailing_comma = true known_third_party = django known_first_party = jsonfield diff --git a/tests/models.py b/tests/models.py new file mode 100644 index 0000000..6432b38 --- /dev/null +++ b/tests/models.py @@ -0,0 +1,65 @@ +import json +from collections import OrderedDict + +from django.contrib.contenttypes.fields import GenericForeignKey +from django.contrib.contenttypes.models import ContentType +from django.db import models + +from jsonfield import JSONCharField, JSONField + + +class ComplexEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, complex): + return { + '__complex__': True, + 'real': obj.real, + 'imag': obj.imag, + } + + return json.JSONEncoder.default(self, obj) + + +def as_complex(dct): + if '__complex__' in dct: + return complex(dct['real'], dct['imag']) + return dct + + +class GenericForeignKeyObj(models.Model): + name = models.CharField('Foreign Obj', max_length=255, null=True) + + +class JSONCharModel(models.Model): + json = JSONCharField(max_length=100) + default_json = JSONCharField(max_length=100, default={"check": 34}) + + +class JSONModel(models.Model): + json = JSONField() + default_json = JSONField(default={"check": 12}) + complex_default_json = JSONField(default=[{"checkcheck": 1212}]) + empty_default = JSONField(default={}) + + +class JSONModelCustomEncoders(models.Model): + # A JSON field that can store complex numbers + json = JSONField( + dump_kwargs={'cls': ComplexEncoder, "indent": 4}, + load_kwargs={'object_hook': as_complex}, + ) + + +class JSONModelWithForeignKey(models.Model): + json = JSONField(null=True) + foreign_obj = GenericForeignKey() + object_id = models.PositiveIntegerField(blank=True, null=True, db_index=True) + content_type = models.ForeignKey(ContentType, blank=True, null=True, on_delete=models.CASCADE) + + +class JSONNotRequiredModel(models.Model): + json = JSONField(blank=True, null=True) + + +class OrderedJSONModel(models.Model): + json = JSONField(load_kwargs={'object_pairs_hook': OrderedDict}) diff --git a/tests/settings.py b/tests/settings.py index 5e98ff4..78064bd 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -10,6 +10,7 @@ INSTALLED_APPS = [ 'django.contrib.contenttypes', 'jsonfield', + 'tests', ] ROOT_URLCONF = [] diff --git a/tests/test_jsonfield.py b/tests/test_jsonfield.py index d7cf51d..a7eb564 100644 --- a/tests/test_jsonfield.py +++ b/tests/test_jsonfield.py @@ -3,81 +3,23 @@ from decimal import Decimal import django -from django.contrib.contenttypes.fields import GenericForeignKey -from django.contrib.contenttypes.models import ContentType from django.core.serializers import deserialize, serialize from django.core.serializers.base import DeserializationError -from django.db import models from django.forms import ModelForm, ValidationError from django.test import TestCase from django.utils.six import string_types -from jsonfield.fields import JSONCharField, JSONField +from jsonfield.fields import JSONField - -class JsonModel(models.Model): - json = JSONField() - default_json = JSONField(default={"check": 12}) - complex_default_json = JSONField(default=[{"checkcheck": 1212}]) - empty_default = JSONField(default={}) - - class Meta: - app_label = 'jsonfield' - - -class GenericForeignKeyObj(models.Model): - name = models.CharField('Foreign Obj', max_length=255, null=True) - - class Meta: - app_label = 'jsonfield' - - -class JSONModelWithForeignKey(models.Model): - json = JSONField(null=True) - foreign_obj = GenericForeignKey() - object_id = models.PositiveIntegerField(blank=True, null=True, db_index=True) - content_type = models.ForeignKey(ContentType, blank=True, null=True, - on_delete=models.CASCADE) - - class Meta: - app_label = 'jsonfield' - - -class JsonCharModel(models.Model): - json = JSONCharField(max_length=100) - default_json = JSONCharField(max_length=100, default={"check": 34}) - - class Meta: - app_label = 'jsonfield' - - -class ComplexEncoder(json.JSONEncoder): - def default(self, obj): - if isinstance(obj, complex): - return { - '__complex__': True, - 'real': obj.real, - 'imag': obj.imag, - } - - return json.JSONEncoder.default(self, obj) - - -def as_complex(dct): - if '__complex__' in dct: - return complex(dct['real'], dct['imag']) - return dct - - -class JSONModelCustomEncoders(models.Model): - # A JSON field that can store complex numbers - json = JSONField( - dump_kwargs={'cls': ComplexEncoder, "indent": 4}, - load_kwargs={'object_hook': as_complex}, - ) - - class Meta: - app_label = 'jsonfield' +from .models import ( + GenericForeignKeyObj, + JSONCharModel, + JSONModel, + JSONModelCustomEncoders, + JSONModelWithForeignKey, + JSONNotRequiredModel, + OrderedJSONModel, +) class JSONModelWithForeignKeyTestCase(TestCase): @@ -89,7 +31,7 @@ def test_object_create(self): class JSONFieldTest(TestCase): """JSONField Wrapper Tests""" - json_model = JsonModel + json_model = JSONModel def test_json_field_create(self): """Test saving a JSON object in our JSONField""" @@ -201,7 +143,7 @@ def test_django_serializers(self): def test_default_parameters(self): """Test providing a default value to the model""" - model = JsonModel() + model = JSONModel() model.json = {"check": 12} self.assertEqual(model.json, {"check": 12}) self.assertEqual(type(model.json), dict) @@ -211,7 +153,7 @@ def test_default_parameters(self): def test_invalid_json(self): # invalid json data {] in the json and default_json fields - ser = '[{"pk": 1, "model": "jsonfield.jsoncharmodel", ' \ + ser = '[{"pk": 1, "model": "tests.jsoncharmodel", ' \ '"fields": {"json": "{]", "default_json": "{]"}}]' with self.assertRaises(DeserializationError) as cm: next(deserialize('json', ser)) @@ -221,7 +163,7 @@ def test_invalid_json(self): inner = cm.exception.__context__.__context__ else: inner = cm.exception.__context__ - self.assertTrue(isinstance(inner, ValidationError)) + self.assertIsInstance(inner, ValidationError) self.assertEqual('Enter valid JSON', inner.messages[0]) def test_integer_in_string_in_json_field(self): @@ -242,7 +184,7 @@ def test_boolean_in_string_in_json_field(self): def test_pass_by_reference_pollution(self): """Make sure the default parameter is copied rather than passed by reference""" - model = JsonModel() + model = JSONModel() model.default_json["check"] = 144 model.complex_default_json[0]["checkcheck"] = 144 self.assertEqual(model.default_json["check"], 144) @@ -250,32 +192,32 @@ def test_pass_by_reference_pollution(self): # Make sure when we create a new model, it resets to the default value # and not to what we just set it to (it would be if it were passed by reference) - model = JsonModel() + model = JSONModel() self.assertEqual(model.default_json["check"], 12) self.assertEqual(model.complex_default_json[0]["checkcheck"], 1212) def test_normal_regex_filter(self): """Make sure JSON model can filter regex""" - JsonModel.objects.create(json={"boom": "town"}) - JsonModel.objects.create(json={"move": "town"}) - JsonModel.objects.create(json={"save": "town"}) + JSONModel.objects.create(json={"boom": "town"}) + JSONModel.objects.create(json={"move": "town"}) + JSONModel.objects.create(json={"save": "town"}) - self.assertEqual(JsonModel.objects.count(), 3) + self.assertEqual(JSONModel.objects.count(), 3) - self.assertEqual(JsonModel.objects.filter(json__regex=r"boom").count(), 1) - self.assertEqual(JsonModel.objects.filter(json__regex=r"town").count(), 3) + self.assertEqual(JSONModel.objects.filter(json__regex=r"boom").count(), 1) + self.assertEqual(JSONModel.objects.filter(json__regex=r"town").count(), 3) def test_save_blank_object(self): """Test that JSON model can save a blank object as none""" - model = JsonModel() + model = JSONModel() self.assertEqual(model.empty_default, {}) model.save() self.assertEqual(model.empty_default, {}) - model1 = JsonModel(empty_default={"hey": "now"}) + model1 = JSONModel(empty_default={"hey": "now"}) self.assertEqual(model1.empty_default, {"hey": "now"}) model1.save() @@ -283,14 +225,7 @@ def test_save_blank_object(self): class JSONCharFieldTest(JSONFieldTest): - json_model = JsonCharModel - - -class OrderedJsonModel(models.Model): - json = JSONField(load_kwargs={'object_pairs_hook': OrderedDict}) - - class Meta: - app_label = 'jsonfield' + json_model = JSONCharModel class OrderedDictSerializationTest(TestCase): @@ -310,34 +245,27 @@ def test_ordered_dict_differs_from_normal_dict(self): self.assertNotEqual(dict(self.ordered_dict).keys(), self.expected_key_order) def test_default_behaviour_loses_sort_order(self): - mod = JsonModel.objects.create(json=self.ordered_dict) + mod = JSONModel.objects.create(json=self.ordered_dict) self.assertEqual(list(mod.json.keys()), self.expected_key_order) - mod_from_db = JsonModel.objects.get(id=mod.id) + mod_from_db = JSONModel.objects.get(id=mod.id) # mod_from_db lost ordering information during json.loads() self.assertNotEqual(mod_from_db.json.keys(), self.expected_key_order) def test_load_kwargs_hook_does_not_lose_sort_order(self): - mod = OrderedJsonModel.objects.create(json=self.ordered_dict) + mod = OrderedJSONModel.objects.create(json=self.ordered_dict) self.assertEqual(list(mod.json.keys()), self.expected_key_order) - mod_from_db = OrderedJsonModel.objects.get(id=mod.id) + mod_from_db = OrderedJSONModel.objects.get(id=mod.id) self.assertEqual(list(mod_from_db.json.keys()), self.expected_key_order) -class JsonNotRequiredModel(models.Model): - json = JSONField(blank=True, null=True) - - class Meta: - app_label = 'jsonfield' - - class JsonNotRequiredForm(ModelForm): class Meta: - model = JsonNotRequiredModel + model = JSONNotRequiredModel fields = '__all__' -class JsonModelFormTest(TestCase): +class JSONModelFormTest(TestCase): def test_blank_form(self): form = JsonNotRequiredForm(data={'json': ''}) self.assertFalse(form.has_changed()) From 794986f4015fd9a75542f7160df105261a309cf2 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Mon, 21 May 2018 16:37:39 -0400 Subject: [PATCH 23/75] Move form into test casse setup --- tests/test_jsonfield.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/tests/test_jsonfield.py b/tests/test_jsonfield.py index a7eb564..cb909db 100644 --- a/tests/test_jsonfield.py +++ b/tests/test_jsonfield.py @@ -259,19 +259,21 @@ def test_load_kwargs_hook_does_not_lose_sort_order(self): self.assertEqual(list(mod_from_db.json.keys()), self.expected_key_order) -class JsonNotRequiredForm(ModelForm): - class Meta: - model = JSONNotRequiredModel - fields = '__all__' +class JSONModelFormTest(TestCase): + def setUp(self): + class JSONNotRequiredForm(ModelForm): + class Meta: + model = JSONNotRequiredModel + fields = '__all__' + self.form_class = JSONNotRequiredForm -class JSONModelFormTest(TestCase): def test_blank_form(self): - form = JsonNotRequiredForm(data={'json': ''}) + form = self.form_class(data={'json': ''}) self.assertFalse(form.has_changed()) def test_form_with_data(self): - form = JsonNotRequiredForm(data={'json': '{}'}) + form = self.form_class(data={'json': '{}'}) self.assertTrue(form.has_changed()) From a7d908ceabc0289e16ffbd52a2c6ff3dc7064033 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Mon, 21 May 2018 16:40:05 -0400 Subject: [PATCH 24/75] Remove unnecessary models file --- jsonfield/models.py | 1 - tests/settings.py | 1 - 2 files changed, 2 deletions(-) delete mode 100644 jsonfield/models.py diff --git a/jsonfield/models.py b/jsonfield/models.py deleted file mode 100644 index e5faf1b..0000000 --- a/jsonfield/models.py +++ /dev/null @@ -1 +0,0 @@ -# Django needs this to see it as a project diff --git a/tests/settings.py b/tests/settings.py index 78064bd..42f1cf3 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -9,7 +9,6 @@ INSTALLED_APPS = [ 'django.contrib.contenttypes', - 'jsonfield', 'tests', ] From ecc02066bd72f77de7f89152c0c2fd0f66e5ef94 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Mon, 21 May 2018 16:47:35 -0400 Subject: [PATCH 25/75] Make error message a sentence --- jsonfield/fields.py | 5 ++--- jsonfield/forms.py | 5 ++--- tests/test_jsonfield.py | 2 +- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/jsonfield/fields.py b/jsonfield/fields.py index 4a28517..3590bde 100644 --- a/jsonfield/fields.py +++ b/jsonfield/fields.py @@ -37,7 +37,7 @@ def to_python(self, value): try: return json.loads(value, **self.load_kwargs) except ValueError: - raise ValidationError(_("Enter valid JSON")) + raise ValidationError(_("Enter valid JSON.")) def from_db_value(self, value, expression, connection, context=None): if self.null and value is None: @@ -57,7 +57,6 @@ def value_from_object(self, obj): return json.dumps(value, **self.dump_kwargs) def formfield(self, **kwargs): - if "form_class" not in kwargs: kwargs["form_class"] = self.form_class @@ -67,7 +66,7 @@ def formfield(self, **kwargs): field.load_kwargs = self.load_kwargs if not field.help_text: - field.help_text = "Enter valid JSON" + field.help_text = "Enter valid JSON." return field diff --git a/jsonfield/forms.py b/jsonfield/forms.py index 97f4b65..3634479 100644 --- a/jsonfield/forms.py +++ b/jsonfield/forms.py @@ -15,11 +15,10 @@ def to_python(self, value): try: return json.loads(value, **self.load_kwargs) except ValueError: - raise ValidationError(_("Enter valid JSON")) + raise ValidationError(_("Enter valid JSON.")) return value def clean(self, value): - if not value and not self.required: return None @@ -27,7 +26,7 @@ def clean(self, value): try: return super(JSONFieldMixin, self).clean(value) except TypeError: - raise ValidationError(_("Enter valid JSON")) + raise ValidationError(_("Enter valid JSON.")) class JSONField(JSONFieldMixin, fields.CharField): diff --git a/tests/test_jsonfield.py b/tests/test_jsonfield.py index cb909db..ae36974 100644 --- a/tests/test_jsonfield.py +++ b/tests/test_jsonfield.py @@ -164,7 +164,7 @@ def test_invalid_json(self): else: inner = cm.exception.__context__ self.assertIsInstance(inner, ValidationError) - self.assertEqual('Enter valid JSON', inner.messages[0]) + self.assertEqual('Enter valid JSON.', inner.messages[0]) def test_integer_in_string_in_json_field(self): """Test saving the Python string '123' in our JSONField""" From 168f2e28fcf87e335dfce1b2b2e7170ac9ed779e Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Mon, 21 May 2018 16:54:16 -0400 Subject: [PATCH 26/75] Fix model full_clean behavior --- jsonfield/fields.py | 3 +++ tests/models.py | 2 +- tests/test_jsonfield.py | 15 +++++++++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/jsonfield/fields.py b/jsonfield/fields.py index 3590bde..8b6a8d4 100644 --- a/jsonfield/fields.py +++ b/jsonfield/fields.py @@ -34,6 +34,9 @@ def to_python(self, value): if self.null and value is None: return None + if not isinstance(value, (str, bytes, bytearray)): + return value + try: return json.loads(value, **self.load_kwargs) except ValueError: diff --git a/tests/models.py b/tests/models.py index 6432b38..084b9ee 100644 --- a/tests/models.py +++ b/tests/models.py @@ -39,7 +39,7 @@ class JSONModel(models.Model): json = JSONField() default_json = JSONField(default={"check": 12}) complex_default_json = JSONField(default=[{"checkcheck": 1212}]) - empty_default = JSONField(default={}) + empty_default = JSONField(default={}, blank=True) class JSONModelCustomEncoders(models.Model): diff --git a/tests/test_jsonfield.py b/tests/test_jsonfield.py index ae36974..7513b18 100644 --- a/tests/test_jsonfield.py +++ b/tests/test_jsonfield.py @@ -223,6 +223,17 @@ def test_save_blank_object(self): model1.save() self.assertEqual(model1.empty_default, {"hey": "now"}) + def test_model_full_clean(self): + instances = [ + JSONNotRequiredModel(), + JSONModel(json={'a': 'b'}), + ] + + for instance in instances: + with self.subTest(instance=instance): + instance.full_clean() + instance.save() + class JSONCharFieldTest(JSONFieldTest): json_model = JSONCharModel @@ -276,6 +287,10 @@ def test_form_with_data(self): form = self.form_class(data={'json': '{}'}) self.assertTrue(form.has_changed()) + def test_form_save(self): + form = self.form_class(data={'json': ''}) + form.save() + class TestFieldAPIMethods(TestCase): def test_get_db_prep_value_method_with_null(self): From baf7ed00d963b3ef47aba131a5f66e9bc822d054 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Mon, 21 May 2018 17:12:36 -0400 Subject: [PATCH 27/75] Update changelog --- CHANGES.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index c178838..4d640e5 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,10 @@ Changes ------- +v3.0.1 05/21/2017 +^^^^^^^^^^^^^^^^^ +- Fix model full_clean behavior + v3.0.0 05/07/2017 ^^^^^^^^^^^^^^^^^ - Add Django 2.0 support From 2565ee0af5d3720e615e2e29038887bc9f5a8eea Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Mon, 21 May 2018 17:12:47 -0400 Subject: [PATCH 28/75] Drop universal wheel build --- setup.cfg | 3 --- 1 file changed, 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index 8d7ed89..49511b7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,3 @@ -[bdist_wheel] -universal = 1 - [flake8] max_line_length = 120 max_complexity = 10 From aed087e87abcf3e189dcca3048bf57d960e8dd0d Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Mon, 21 May 2018 17:13:04 -0400 Subject: [PATCH 29/75] Bump version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 2e786f1..aac2d9f 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='jsonfield2', - version='3.0.0', + version='3.0.1', packages=['jsonfield'], license='MIT', include_package_data=True, From 45be8122c73454fe432c9f9ee380d053b0ba0cfc Mon Sep 17 00:00:00 2001 From: Yoz Grahame <173848+yozlet@users.noreply.github.com> Date: Fri, 13 Jul 2018 14:34:29 -0700 Subject: [PATCH 30/75] Fix date typos in changelog (#3) --- CHANGES.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 4d640e5..5a6b69e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,11 +1,11 @@ Changes ------- -v3.0.1 05/21/2017 +v3.0.1 05/21/2018 ^^^^^^^^^^^^^^^^^ - Fix model full_clean behavior -v3.0.0 05/07/2017 +v3.0.0 05/07/2018 ^^^^^^^^^^^^^^^^^ - Add Django 2.0 support - Drop Django 1.8, 1.9, and 1.10 support From 05ed04cdedef283945c039223b1e3ab19c7034f9 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Mon, 24 Sep 2018 14:06:32 -0700 Subject: [PATCH 31/75] Add Python 3.7 & Django 2.1 support (#6) * Add Python 3.7 & Django 2.1 to CI matrix * Test warning for Django 2.0 * Fix warning for Django 2.0 --- .travis.yml | 4 ++++ README.rst | 4 ++-- jsonfield/fields.py | 15 +++++++++++---- tests/test_jsonfield.py | 14 +++++++++++++- tox.ini | 4 +++- 5 files changed, 33 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index af9cd9d..a8e882b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,6 +17,10 @@ script: matrix: include: + - python: 3.7 + sudo: true + dist: xenial + - python: 3.6 env: TOXENV="isort,lint,coverage" after_success: diff --git a/README.rst b/README.rst index 5f8c592..fac4293 100644 --- a/README.rst +++ b/README.rst @@ -33,8 +33,8 @@ Requirements jsonfield2 aims to support all current `versions of Django`_, however the explicity tested versions are: -* **Python:** 3.4, 3.5, 3.6 -* **Django:** 1.11, 2.0 +* **Python:** 3.4, 3.5, 3.6, 3.7 +* **Django:** 1.11, 2.0, 2.1 .. _versions of Django: https://www.djangoproject.com/download/#supported-versions diff --git a/jsonfield/fields.py b/jsonfield/fields.py index 8b6a8d4..78ca08b 100644 --- a/jsonfield/fields.py +++ b/jsonfield/fields.py @@ -1,6 +1,7 @@ import copy import json +import django from django.db import models from django.forms import ValidationError from django.utils.translation import ugettext_lazy as _ @@ -42,10 +43,16 @@ def to_python(self, value): except ValueError: raise ValidationError(_("Enter valid JSON.")) - def from_db_value(self, value, expression, connection, context=None): - if self.null and value is None: - return None - return json.loads(value, **self.load_kwargs) + if django.VERSION < (2, 0): + def from_db_value(self, value, expression, connection, context=None): + if self.null and value is None: + return None + return json.loads(value, **self.load_kwargs) + else: + def from_db_value(self, value, expression, connection): + if self.null and value is None: + return None + return json.loads(value, **self.load_kwargs) def get_prep_value(self, value): """Convert JSON object to a string""" diff --git a/tests/test_jsonfield.py b/tests/test_jsonfield.py index 7513b18..a37ea58 100644 --- a/tests/test_jsonfield.py +++ b/tests/test_jsonfield.py @@ -1,4 +1,5 @@ import json +import warnings from collections import OrderedDict from decimal import Decimal @@ -132,7 +133,7 @@ def test_django_serializers(self): 'dict': {'k': 'v'}}]: obj = self.json_model.objects.create(json=json_obj) new_obj = self.json_model.objects.get(id=obj.id) - self.assert_(new_obj) + self.assertTrue(new_obj) queryset = self.json_model.objects.all() ser = serialize('json', queryset) @@ -349,3 +350,14 @@ def test_get_prep_value_can_return_none_if_null(self): self.assertDictEqual(value, json.loads(json.loads(double_prepared_value))) self.assertIs(json_field_instance.get_prep_value(None), None) + + def test_from_db_value_deprecation_warning(self): + # Compatibility for Django 1.11 and earlier + # Django 2.0+ drops the `context` argument + JSONModel.objects.create(json='{}') + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter('always') + JSONModel.objects.get() + + self.assertEqual(w, []) diff --git a/tox.ini b/tox.ini index 66ec66e..8865e5b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,8 @@ [tox] envlist = - py{34,35,36}-django{111,20}, + py{34,35,36}-django111, + py{34,35,36}-django20, + py{36,36,37}-django21, isort,lint,coverage,warnings, [testenv] From 527e41dd87e8379632e7f0984dc53c6bf3b77413 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Fri, 21 Dec 2018 10:41:12 -0800 Subject: [PATCH 32/75] Update changelog --- CHANGES.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 5a6b69e..de6f896 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,10 @@ Changes ------- +v3.0.2 12/21/2018 +^^^^^^^^^^^^^^^^^ +- Add Python 3.7 & Django 2.1 support + v3.0.1 05/21/2018 ^^^^^^^^^^^^^^^^^ - Fix model full_clean behavior From 4f656c1a8c9e64bc290f834aaae8672b2088a1e4 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Fri, 21 Dec 2018 10:41:21 -0800 Subject: [PATCH 33/75] Bump version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index aac2d9f..14cd378 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='jsonfield2', - version='3.0.1', + version='3.0.2', packages=['jsonfield'], license='MIT', include_package_data=True, From e912a055de696fb7cae2e2872e673163e9895ca1 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Wed, 27 Mar 2019 17:50:27 -0700 Subject: [PATCH 34/75] Testing updates (#13) --- .gitignore | 10 ++--- .travis.yml | 20 +++++---- README.rst | 2 +- jsonfield/__init__.py | 1 - setup.cfg | 3 +- tests/migrations/0001_initial.py | 75 ++++++++++++++++++++++++++++++++ tests/migrations/__init__.py | 0 tox.ini | 22 +++++----- 8 files changed, 106 insertions(+), 27 deletions(-) create mode 100644 tests/migrations/0001_initial.py create mode 100644 tests/migrations/__init__.py diff --git a/.gitignore b/.gitignore index bf5c760..0bd81c6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,11 @@ -.coverage +.coverage* .tox htmlcov *.pyc +*.egg-info/ build/ dist/ -MANIFEST -.project -.pydevproject .DS_Store -.idea/* +.env +.env/ +.venv/ diff --git a/.travis.yml b/.travis.yml index a8e882b..9d00101 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,32 +1,34 @@ sudo: false +dist: xenial language: python - python: - "3.4" - "3.5" - "3.6" + - "3.7" install: - pip install -U pip setuptools wheel - - pip install tox tox-travis tox-venv + - pip install tox~=3.7.0 tox-travis tox-venv - python setup.py bdist_wheel script: - tox +after_success: + - pip install codecov && codecov + matrix: include: - python: 3.7 - sudo: true - dist: xenial + env: TOXENV="isort,lint" - - python: 3.6 - env: TOXENV="isort,lint,coverage" - after_success: - - pip install codecov && codecov + - python: 3.7 + env: TOXENV="dist" - - python: 3.6 + - python: 3.7 env: TOXENV="warnings" + allow_failures: - env: TOXENV="warnings" diff --git a/README.rst b/README.rst index fac4293..40e847c 100644 --- a/README.rst +++ b/README.rst @@ -34,7 +34,7 @@ Requirements jsonfield2 aims to support all current `versions of Django`_, however the explicity tested versions are: * **Python:** 3.4, 3.5, 3.6, 3.7 -* **Django:** 1.11, 2.0, 2.1 +* **Django:** 1.11, 2.0, 2.1, 2.2b1 .. _versions of Django: https://www.djangoproject.com/download/#supported-versions diff --git a/jsonfield/__init__.py b/jsonfield/__init__.py index 3ad1c1f..1a0121d 100644 --- a/jsonfield/__init__.py +++ b/jsonfield/__init__.py @@ -1,4 +1,3 @@ from .fields import JSONCharField, JSONField - __all__ = ['JSONCharField', 'JSONField'] diff --git a/setup.cfg b/setup.cfg index 49511b7..dc85308 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,9 +1,10 @@ [flake8] max_line_length = 120 max_complexity = 10 +exclude = migrations [isort] -skip = .tox +skip = .tox,migrations atomic = true line_length = 120 multi_line_output = 3 diff --git a/tests/migrations/0001_initial.py b/tests/migrations/0001_initial.py new file mode 100644 index 0000000..bd333e8 --- /dev/null +++ b/tests/migrations/0001_initial.py @@ -0,0 +1,75 @@ +# Generated by Django 2.2rc1 on 2019-03-27 22:51 + +import collections +from django.db import migrations, models +import django.db.models.deletion +import jsonfield.encoder +import jsonfield.fields +import tests.models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ] + + operations = [ + migrations.CreateModel( + name='GenericForeignKeyObj', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, null=True, verbose_name='Foreign Obj')), + ], + ), + migrations.CreateModel( + name='JSONCharModel', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('json', jsonfield.fields.JSONCharField(dump_kwargs={'cls': jsonfield.encoder.JSONEncoder, 'separators': (',', ':')}, load_kwargs={}, max_length=100)), + ('default_json', jsonfield.fields.JSONCharField(default={'check': 34}, dump_kwargs={'cls': jsonfield.encoder.JSONEncoder, 'separators': (',', ':')}, load_kwargs={}, max_length=100)), + ], + ), + migrations.CreateModel( + name='JSONModel', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('json', jsonfield.fields.JSONField(dump_kwargs={'cls': jsonfield.encoder.JSONEncoder, 'separators': (',', ':')}, load_kwargs={})), + ('default_json', jsonfield.fields.JSONField(default={'check': 12}, dump_kwargs={'cls': jsonfield.encoder.JSONEncoder, 'separators': (',', ':')}, load_kwargs={})), + ('complex_default_json', jsonfield.fields.JSONField(default=[{'checkcheck': 1212}], dump_kwargs={'cls': jsonfield.encoder.JSONEncoder, 'separators': (',', ':')}, load_kwargs={})), + ('empty_default', jsonfield.fields.JSONField(blank=True, default={}, dump_kwargs={'cls': jsonfield.encoder.JSONEncoder, 'separators': (',', ':')}, load_kwargs={})), + ], + ), + migrations.CreateModel( + name='JSONModelCustomEncoders', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('json', jsonfield.fields.JSONField(dump_kwargs={'cls': tests.models.ComplexEncoder, 'indent': 4}, load_kwargs={'object_hook': tests.models.as_complex})), + ], + ), + migrations.CreateModel( + name='JSONNotRequiredModel', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('json', jsonfield.fields.JSONField(blank=True, dump_kwargs={'cls': jsonfield.encoder.JSONEncoder, 'separators': (',', ':')}, load_kwargs={}, null=True)), + ], + ), + migrations.CreateModel( + name='OrderedJSONModel', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('json', jsonfield.fields.JSONField(dump_kwargs={'cls': jsonfield.encoder.JSONEncoder, 'separators': (',', ':')}, load_kwargs={'object_pairs_hook': collections.OrderedDict})), + ], + ), + migrations.CreateModel( + name='JSONModelWithForeignKey', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('json', jsonfield.fields.JSONField(dump_kwargs={'cls': jsonfield.encoder.JSONEncoder, 'separators': (',', ':')}, load_kwargs={}, null=True)), + ('object_id', models.PositiveIntegerField(blank=True, db_index=True, null=True)), + ('content_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), + ], + ), + ] diff --git a/tests/migrations/__init__.py b/tests/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tox.ini b/tox.ini index 8865e5b..5cd5744 100644 --- a/tox.ini +++ b/tox.ini @@ -2,19 +2,24 @@ envlist = py{34,35,36}-django111, py{34,35,36}-django20, - py{36,36,37}-django21, - isort,lint,coverage,warnings, + py{35,36,37}-django21, + py{35,36,37}-django22, + isort,lint,dist,warnings, [testenv] -commands = python manage.py test {posargs} +commands = coverage run --parallel-mode manage.py test {posargs} +usedevelop = True setenv = PYTHONDONTWRITEBYTECODE=1 deps = + coverage django111: Django~=1.11 django20: Django~=2.0 + django21: Django~=2.1 + django22: Django~=2.2b1 [testenv:isort] -commands = isort --check-only --recursive jsonfield tests {posargs} +commands = isort --check-only --recursive jsonfield tests {posargs:--diff} deps = isort @@ -23,12 +28,9 @@ commands = flake8 jsonfield tests {posargs} deps = flake8 -[testenv:coverage] -commands = coverage run manage.py test {posargs} -usedevelop = True -deps = - coverage - django +[testenv:dist] +commands = python manage.py test {posargs} +usedevelop = False [testenv:warnings] commands = python -Werror manage.py test {posargs} From a533f856b98ff97ffa993270a9fd0fdfe0e42653 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Wed, 17 Apr 2019 17:58:22 -0700 Subject: [PATCH 35/75] Move to CircleCI (#14) * Fix Django version ranges in tox * Move from Travis CI to CircleCI * Combine coverage before reporting * Replace Travis CI w/ CircleCI badge --- .circleci/config.yml | 113 +++++++++++++++++++++++++++++++++++++++++++ .travis.yml | 34 ------------- README.rst | 6 +-- tox.ini | 8 +-- 4 files changed, 120 insertions(+), 41 deletions(-) create mode 100644 .circleci/config.yml delete mode 100644 .travis.yml diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..eb64b7e --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,113 @@ +version: 2.1 + +aliases: + - &environ + run: + name: setup virtual environment + # The below ensures the venv is activated for every subsequent step + command: | + virtualenv venv + echo "source /home/circleci/project/venv/bin/activate" >> $BASH_ENV + + - &install + run: + name: install dependencies + command: | + pip install -U pip setuptools wheel tox tox-factor codecov + + - &test-steps + steps: + - checkout + - *environ + - *install + - run: tox + - run: coverage combine + - run: coverage report + - run: codecov + +jobs: + lint: + steps: + - checkout + - *environ + - *install + - run: tox -e isort,lint + docker: + - image: circleci/python:3.7 + + dist: + steps: + - checkout + - *environ + - *install + - run: | + python setup.py bdist_wheel + tox -e dist --installpkg ./dist/jsonfield2-*.whl + tox -e dist + docker: + - image: circleci/python:3.7 + + test-py37: + <<: *test-steps + docker: + - image: circleci/python:3.7 + environment: + TOXFACTOR: py37 + + test-py36: + <<: *test-steps + docker: + - image: circleci/python:3.6 + environment: + TOXFACTOR: py36 + + test-py35: + <<: *test-steps + docker: + - image: circleci/python:3.5 + environment: + TOXFACTOR: py35 + + test-py34: + <<: *test-steps + docker: + - image: circleci/python:3.4 + environment: + TOXFACTOR: py34 + + +workflows: + version: 2 + commit: &test-workflow + jobs: + - lint + - dist: + requires: + - lint + + - test-py37: + requires: + - lint + + - test-py36: + requires: + - lint + + - test-py35: + requires: + - lint + + - test-py34: + requires: + - lint + + weekly: + <<: *test-workflow + triggers: + - schedule: + # 8/9 AM PST/PDT every Monday + cron: "0 16 * * 1" + filters: + branches: + only: + - master diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 9d00101..0000000 --- a/.travis.yml +++ /dev/null @@ -1,34 +0,0 @@ -sudo: false -dist: xenial - -language: python -python: - - "3.4" - - "3.5" - - "3.6" - - "3.7" - -install: - - pip install -U pip setuptools wheel - - pip install tox~=3.7.0 tox-travis tox-venv - - python setup.py bdist_wheel - -script: - - tox - -after_success: - - pip install codecov && codecov - -matrix: - include: - - python: 3.7 - env: TOXENV="isort,lint" - - - python: 3.7 - env: TOXENV="dist" - - - python: 3.7 - env: TOXENV="warnings" - - allow_failures: - - env: TOXENV="warnings" diff --git a/README.rst b/README.rst index 40e847c..e19a46f 100644 --- a/README.rst +++ b/README.rst @@ -1,8 +1,8 @@ jsonfield2 ========== -.. image:: https://travis-ci.org/rpkilby/jsonfield2.svg?branch=master - :target: https://travis-ci.org/rpkilby/jsonfield2 +.. image:: https://circleci.com/gh/rpkilby/jsonfield2.svg?style=shield + :target: https://circleci.com/gh/rpkilby/jsonfield2 .. image:: https://codecov.io/gh/rpkilby/jsonfield2/branch/master/graph/badge.svg :target: https://codecov.io/gh/rpkilby/jsonfield2 .. image:: https://img.shields.io/pypi/v/jsonfield2.svg @@ -34,7 +34,7 @@ Requirements jsonfield2 aims to support all current `versions of Django`_, however the explicity tested versions are: * **Python:** 3.4, 3.5, 3.6, 3.7 -* **Django:** 1.11, 2.0, 2.1, 2.2b1 +* **Django:** 1.11, 2.0, 2.1, 2.2 .. _versions of Django: https://www.djangoproject.com/download/#supported-versions diff --git a/tox.ini b/tox.ini index 5cd5744..070af67 100644 --- a/tox.ini +++ b/tox.ini @@ -13,10 +13,10 @@ setenv = PYTHONDONTWRITEBYTECODE=1 deps = coverage - django111: Django~=1.11 - django20: Django~=2.0 - django21: Django~=2.1 - django22: Django~=2.2b1 + django111: Django~=1.11.0 + django20: Django~=2.0.0 + django21: Django~=2.1.0 + django22: Django~=2.2.0 [testenv:isort] commands = isort --check-only --recursive jsonfield tests {posargs:--diff} From 2a8081f5c65568937eca587258f71239e2ceebec Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Wed, 7 Aug 2019 17:42:53 -0700 Subject: [PATCH 36/75] Fix tox dist name conflict (#15) --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 070af67..4da27d7 100644 --- a/tox.ini +++ b/tox.ini @@ -9,6 +9,7 @@ envlist = [testenv] commands = coverage run --parallel-mode manage.py test {posargs} usedevelop = True +envdir={toxworkdir}/v/{envname} setenv = PYTHONDONTWRITEBYTECODE=1 deps = From f468e7594600f5f35cf7a511f16c2334e68e6933 Mon Sep 17 00:00:00 2001 From: John Carter Date: Thu, 24 Oct 2019 14:23:12 +1300 Subject: [PATCH 37/75] Add Django 3.0 support (#16) * Add Django 3.0 & Python 3.8 support * Drop Python 3.4 support --- .circleci/config.yml | 22 +++++++++++----------- CHANGES.rst | 5 +++++ README.rst | 2 +- jsonfield/encoder.py | 8 ++++---- jsonfield/forms.py | 3 +-- setup.py | 4 ++-- tests/test_jsonfield.py | 9 ++++----- tox.ini | 6 ++++-- 8 files changed, 32 insertions(+), 27 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index eb64b7e..19179e6 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -47,6 +47,13 @@ jobs: docker: - image: circleci/python:3.7 + test-py38: + <<: *test-steps + docker: + - image: circleci/python:3.8 + environment: + TOXFACTOR: py38 + test-py37: <<: *test-steps docker: @@ -68,13 +75,6 @@ jobs: environment: TOXFACTOR: py35 - test-py34: - <<: *test-steps - docker: - - image: circleci/python:3.4 - environment: - TOXFACTOR: py34 - workflows: version: 2 @@ -85,19 +85,19 @@ workflows: requires: - lint - - test-py37: + - test-py38: requires: - lint - - test-py36: + - test-py37: requires: - lint - - test-py35: + - test-py36: requires: - lint - - test-py34: + - test-py35: requires: - lint diff --git a/CHANGES.rst b/CHANGES.rst index de6f896..a261468 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,11 @@ Changes ------- +v3.0.3 (unreleased) +^^^^^^^^^^^^^^^^^^^ +- Add Python 3.8 & Django 3.0 support +- Drop Python 3.4 support + v3.0.2 12/21/2018 ^^^^^^^^^^^^^^^^^ - Add Python 3.7 & Django 2.1 support diff --git a/README.rst b/README.rst index e19a46f..aa78883 100644 --- a/README.rst +++ b/README.rst @@ -33,7 +33,7 @@ Requirements jsonfield2 aims to support all current `versions of Django`_, however the explicity tested versions are: -* **Python:** 3.4, 3.5, 3.6, 3.7 +* **Python:** 3.5, 3.6, 3.7, 3.8 * **Django:** 1.11, 2.0, 2.1, 2.2 .. _versions of Django: https://www.djangoproject.com/download/#supported-versions diff --git a/jsonfield/encoder.py b/jsonfield/encoder.py index d93f476..8f21307 100644 --- a/jsonfield/encoder.py +++ b/jsonfield/encoder.py @@ -4,7 +4,7 @@ import uuid from django.db.models.query import QuerySet -from django.utils import six, timezone +from django.utils import timezone from django.utils.encoding import force_text from django.utils.functional import Promise @@ -34,15 +34,15 @@ def default(self, obj): # noqa: C901 representation = obj.isoformat() return representation elif isinstance(obj, datetime.timedelta): - return six.text_type(obj.total_seconds()) + return str(obj.total_seconds()) elif isinstance(obj, decimal.Decimal): # Serializers will coerce decimals to strings by default. return float(obj) elif isinstance(obj, uuid.UUID): - return six.text_type(obj) + return str(obj) elif isinstance(obj, QuerySet): return tuple(obj) - elif isinstance(obj, six.binary_type): + elif isinstance(obj, bytes): # Best-effort for binary blobs. See #4187. return obj.decode('utf-8') elif hasattr(obj, 'tolist'): diff --git a/jsonfield/forms.py b/jsonfield/forms.py index 3634479..4bb980f 100644 --- a/jsonfield/forms.py +++ b/jsonfield/forms.py @@ -1,7 +1,6 @@ import json from django.forms import ValidationError, fields -from django.utils import six from django.utils.translation import ugettext_lazy as _ @@ -11,7 +10,7 @@ def __init__(self, *args, **kwargs): super(JSONFieldMixin, self).__init__(*args, **kwargs) def to_python(self, value): - if isinstance(value, six.string_types) and value: + if isinstance(value, str) and value: try: return json.loads(value, **self.load_kwargs) except ValueError: diff --git a/setup.py b/setup.py index 14cd378..1d01b59 100644 --- a/setup.py +++ b/setup.py @@ -18,11 +18,11 @@ 'Intended Audience :: Developers', 'Operating System :: OS Independent', 'Programming Language :: Python', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', 'Framework :: Django', ], ) diff --git a/tests/test_jsonfield.py b/tests/test_jsonfield.py index a37ea58..c9c9f0d 100644 --- a/tests/test_jsonfield.py +++ b/tests/test_jsonfield.py @@ -8,7 +8,6 @@ from django.core.serializers.base import DeserializationError from django.forms import ModelForm, ValidationError from django.test import TestCase -from django.utils.six import string_types from jsonfield.fields import JSONField @@ -299,7 +298,7 @@ def test_get_db_prep_value_method_with_null(self): value = {'a': 1} prepared_value = json_field_instance.get_db_prep_value( value, connection=None, prepared=False) - self.assertIsInstance(prepared_value, string_types) + self.assertIsInstance(prepared_value, str) self.assertDictEqual(value, json.loads(prepared_value)) self.assertIs(json_field_instance.get_db_prep_value( None, connection=None, prepared=True), None) @@ -311,7 +310,7 @@ def test_get_db_prep_value_method_with_not_null(self): value = {'a': 1} prepared_value = json_field_instance.get_db_prep_value( value, connection=None, prepared=False) - self.assertIsInstance(prepared_value, string_types) + self.assertIsInstance(prepared_value, str) self.assertDictEqual(value, json.loads(prepared_value)) self.assertIs(json_field_instance.get_db_prep_value( None, connection=None, prepared=True), None) @@ -329,7 +328,7 @@ def test_get_prep_value_always_json_dumps_if_not_null(self): json_field_instance = JSONField(null=False) value = {'a': 1} prepared_value = json_field_instance.get_prep_value(value) - self.assertIsInstance(prepared_value, string_types) + self.assertIsInstance(prepared_value, str) self.assertDictEqual(value, json.loads(prepared_value)) already_json = json.dumps(value) double_prepared_value = json_field_instance.get_prep_value( @@ -342,7 +341,7 @@ def test_get_prep_value_can_return_none_if_null(self): json_field_instance = JSONField(null=True) value = {'a': 1} prepared_value = json_field_instance.get_prep_value(value) - self.assertIsInstance(prepared_value, string_types) + self.assertIsInstance(prepared_value, str) self.assertDictEqual(value, json.loads(prepared_value)) already_json = json.dumps(value) double_prepared_value = json_field_instance.get_prep_value( diff --git a/tox.ini b/tox.ini index 4da27d7..f30a77e 100644 --- a/tox.ini +++ b/tox.ini @@ -1,9 +1,10 @@ [tox] envlist = - py{34,35,36}-django111, - py{34,35,36}-django20, + py{35,36}-django111, + py{35,36}-django20, py{35,36,37}-django21, py{35,36,37}-django22, + py{36,37,38}-django30, isort,lint,dist,warnings, [testenv] @@ -18,6 +19,7 @@ deps = django20: Django~=2.0.0 django21: Django~=2.1.0 django22: Django~=2.2.0 + django30: Django>=3.0b1,<3.1 [testenv:isort] commands = isort --check-only --recursive jsonfield tests {posargs:--diff} From 54160d65d494aa7f60f3e1687f67b0aab85dbd89 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Wed, 23 Oct 2019 18:30:05 -0700 Subject: [PATCH 38/75] Bump version --- CHANGES.rst | 4 ++-- setup.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index a261468..0b4dd6b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,8 +1,8 @@ Changes ------- -v3.0.3 (unreleased) -^^^^^^^^^^^^^^^^^^^ +v3.0.3 10/23/2019 +^^^^^^^^^^^^^^^^^ - Add Python 3.8 & Django 3.0 support - Drop Python 3.4 support diff --git a/setup.py b/setup.py index 1d01b59..a448706 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='jsonfield2', - version='3.0.2', + version='3.0.3', packages=['jsonfield'], license='MIT', include_package_data=True, From 4e1a6bb686662290bdde7e705b236e38ac94800b Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Wed, 4 Dec 2019 18:35:35 -0800 Subject: [PATCH 39/75] Drop unsupported versions (#21) - Drop Python 3.5 support - Drop Django 2.1 (and below) support --- .circleci/config.yml | 15 +------ README.rst | 2 +- jsonfield/fields.py | 15 ++----- setup.py | 6 ++- tests/test_fields.py | 78 +++++++++++++++++++++++++++++++++ tests/test_jsonfield.py | 96 +++-------------------------------------- tox.ini | 7 +-- 7 files changed, 98 insertions(+), 121 deletions(-) create mode 100644 tests/test_fields.py diff --git a/.circleci/config.yml b/.circleci/config.yml index 19179e6..f712394 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -33,7 +33,7 @@ jobs: - *install - run: tox -e isort,lint docker: - - image: circleci/python:3.7 + - image: circleci/python:3.8 dist: steps: @@ -45,7 +45,7 @@ jobs: tox -e dist --installpkg ./dist/jsonfield2-*.whl tox -e dist docker: - - image: circleci/python:3.7 + - image: circleci/python:3.8 test-py38: <<: *test-steps @@ -68,13 +68,6 @@ jobs: environment: TOXFACTOR: py36 - test-py35: - <<: *test-steps - docker: - - image: circleci/python:3.5 - environment: - TOXFACTOR: py35 - workflows: version: 2 @@ -97,10 +90,6 @@ workflows: requires: - lint - - test-py35: - requires: - - lint - weekly: <<: *test-workflow triggers: diff --git a/README.rst b/README.rst index aa78883..20430cb 100644 --- a/README.rst +++ b/README.rst @@ -33,7 +33,7 @@ Requirements jsonfield2 aims to support all current `versions of Django`_, however the explicity tested versions are: -* **Python:** 3.5, 3.6, 3.7, 3.8 +* **Python:** 3.6, 3.7, 3.8 * **Django:** 1.11, 2.0, 2.1, 2.2 .. _versions of Django: https://www.djangoproject.com/download/#supported-versions diff --git a/jsonfield/fields.py b/jsonfield/fields.py index 78ca08b..11856f2 100644 --- a/jsonfield/fields.py +++ b/jsonfield/fields.py @@ -1,7 +1,6 @@ import copy import json -import django from django.db import models from django.forms import ValidationError from django.utils.translation import ugettext_lazy as _ @@ -43,16 +42,10 @@ def to_python(self, value): except ValueError: raise ValidationError(_("Enter valid JSON.")) - if django.VERSION < (2, 0): - def from_db_value(self, value, expression, connection, context=None): - if self.null and value is None: - return None - return json.loads(value, **self.load_kwargs) - else: - def from_db_value(self, value, expression, connection): - if self.null and value is None: - return None - return json.loads(value, **self.load_kwargs) + def from_db_value(self, value, expression, connection): + if self.null and value is None: + return None + return json.loads(value, **self.load_kwargs) def get_prep_value(self, value): """Convert JSON object to a string""" diff --git a/setup.py b/setup.py index a448706..262686f 100644 --- a/setup.py +++ b/setup.py @@ -12,14 +12,16 @@ url='https://github.com/rpkilby/jsonfield2/', description='A reusable Django field that allows you to store validated JSON in your model.', long_description=open("README.rst").read(), - install_requires=['Django >= 1.11'], + install_requires=['Django >= 2.2'], classifiers=[ 'Environment :: Web Environment', + 'Framework :: Django', + 'Framework :: Django :: 2.2', + 'Framework :: Django :: 3.0', 'Intended Audience :: Developers', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', diff --git a/tests/test_fields.py b/tests/test_fields.py new file mode 100644 index 0000000..932eb23 --- /dev/null +++ b/tests/test_fields.py @@ -0,0 +1,78 @@ +import json +import warnings + +from django.test import TestCase + +from jsonfield.fields import JSONField + +from .models import JSONModel + + +class TestFieldAPIMethods(TestCase): + def test_get_db_prep_value_method_with_null(self): + json_field_instance = JSONField(null=True) + value = {'a': 1} + prepared_value = json_field_instance.get_db_prep_value( + value, connection=None, prepared=False) + self.assertIsInstance(prepared_value, str) + self.assertDictEqual(value, json.loads(prepared_value)) + self.assertIs(json_field_instance.get_db_prep_value( + None, connection=None, prepared=True), None) + self.assertIs(json_field_instance.get_db_prep_value( + None, connection=None, prepared=False), None) + + def test_get_db_prep_value_method_with_not_null(self): + json_field_instance = JSONField(null=False) + value = {'a': 1} + prepared_value = json_field_instance.get_db_prep_value( + value, connection=None, prepared=False) + self.assertIsInstance(prepared_value, str) + self.assertDictEqual(value, json.loads(prepared_value)) + self.assertIs(json_field_instance.get_db_prep_value( + None, connection=None, prepared=True), None) + self.assertEqual(json_field_instance.get_db_prep_value( + None, connection=None, prepared=False), 'null') + + def test_get_db_prep_value_method_skips_prepared_values(self): + json_field_instance = JSONField(null=False) + value = {'a': 1} + prepared_value = json_field_instance.get_db_prep_value( + value, connection=None, prepared=True) + self.assertIs(prepared_value, value) + + def test_get_prep_value_always_json_dumps_if_not_null(self): + json_field_instance = JSONField(null=False) + value = {'a': 1} + prepared_value = json_field_instance.get_prep_value(value) + self.assertIsInstance(prepared_value, str) + self.assertDictEqual(value, json.loads(prepared_value)) + already_json = json.dumps(value) + double_prepared_value = json_field_instance.get_prep_value( + already_json) + self.assertDictEqual(value, + json.loads(json.loads(double_prepared_value))) + self.assertEqual(json_field_instance.get_prep_value(None), 'null') + + def test_get_prep_value_can_return_none_if_null(self): + json_field_instance = JSONField(null=True) + value = {'a': 1} + prepared_value = json_field_instance.get_prep_value(value) + self.assertIsInstance(prepared_value, str) + self.assertDictEqual(value, json.loads(prepared_value)) + already_json = json.dumps(value) + double_prepared_value = json_field_instance.get_prep_value( + already_json) + self.assertDictEqual(value, + json.loads(json.loads(double_prepared_value))) + self.assertIs(json_field_instance.get_prep_value(None), None) + + def test_from_db_value_deprecation_warning(self): + # Compatibility for Django 1.11 and earlier + # Django 2.0+ drops the `context` argument + JSONModel.objects.create(json='{}') + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter('always') + JSONModel.objects.get() + + self.assertEqual(w, []) diff --git a/tests/test_jsonfield.py b/tests/test_jsonfield.py index c9c9f0d..5ff5b16 100644 --- a/tests/test_jsonfield.py +++ b/tests/test_jsonfield.py @@ -1,5 +1,3 @@ -import json -import warnings from collections import OrderedDict from decimal import Decimal @@ -9,8 +7,6 @@ from django.forms import ModelForm, ValidationError from django.test import TestCase -from jsonfield.fields import JSONField - from .models import ( GenericForeignKeyObj, JSONCharModel, @@ -251,23 +247,15 @@ def setUp(self): ]) self.expected_key_order = ['number', 'notes', 'alpha', 'romeo', 'juliet', 'bravo'] - def test_ordered_dict_differs_from_normal_dict(self): - self.assertEqual(list(self.ordered_dict.keys()), self.expected_key_order) - self.assertNotEqual(dict(self.ordered_dict).keys(), self.expected_key_order) - - def test_default_behaviour_loses_sort_order(self): - mod = JSONModel.objects.create(json=self.ordered_dict) - self.assertEqual(list(mod.json.keys()), self.expected_key_order) - mod_from_db = JSONModel.objects.get(id=mod.id) + self.instance = OrderedJSONModel.objects.create(json=self.ordered_dict) - # mod_from_db lost ordering information during json.loads() - self.assertNotEqual(mod_from_db.json.keys(), self.expected_key_order) + def test_load_kwargs_hook(self): + from_db = OrderedJSONModel.objects.get(id=self.instance.id) - def test_load_kwargs_hook_does_not_lose_sort_order(self): - mod = OrderedJSONModel.objects.create(json=self.ordered_dict) - self.assertEqual(list(mod.json.keys()), self.expected_key_order) - mod_from_db = OrderedJSONModel.objects.get(id=mod.id) - self.assertEqual(list(mod_from_db.json.keys()), self.expected_key_order) + # OrderedJSONModel explicitly sets `object_pairs_hook` to `OrderedDict` + self.assertEqual(list(self.instance.json), self.expected_key_order) + self.assertEqual(list(from_db.json), self.expected_key_order) + self.assertIsInstance(from_db.json, OrderedDict) class JSONModelFormTest(TestCase): @@ -290,73 +278,3 @@ def test_form_with_data(self): def test_form_save(self): form = self.form_class(data={'json': ''}) form.save() - - -class TestFieldAPIMethods(TestCase): - def test_get_db_prep_value_method_with_null(self): - json_field_instance = JSONField(null=True) - value = {'a': 1} - prepared_value = json_field_instance.get_db_prep_value( - value, connection=None, prepared=False) - self.assertIsInstance(prepared_value, str) - self.assertDictEqual(value, json.loads(prepared_value)) - self.assertIs(json_field_instance.get_db_prep_value( - None, connection=None, prepared=True), None) - self.assertIs(json_field_instance.get_db_prep_value( - None, connection=None, prepared=False), None) - - def test_get_db_prep_value_method_with_not_null(self): - json_field_instance = JSONField(null=False) - value = {'a': 1} - prepared_value = json_field_instance.get_db_prep_value( - value, connection=None, prepared=False) - self.assertIsInstance(prepared_value, str) - self.assertDictEqual(value, json.loads(prepared_value)) - self.assertIs(json_field_instance.get_db_prep_value( - None, connection=None, prepared=True), None) - self.assertEqual(json_field_instance.get_db_prep_value( - None, connection=None, prepared=False), 'null') - - def test_get_db_prep_value_method_skips_prepared_values(self): - json_field_instance = JSONField(null=False) - value = {'a': 1} - prepared_value = json_field_instance.get_db_prep_value( - value, connection=None, prepared=True) - self.assertIs(prepared_value, value) - - def test_get_prep_value_always_json_dumps_if_not_null(self): - json_field_instance = JSONField(null=False) - value = {'a': 1} - prepared_value = json_field_instance.get_prep_value(value) - self.assertIsInstance(prepared_value, str) - self.assertDictEqual(value, json.loads(prepared_value)) - already_json = json.dumps(value) - double_prepared_value = json_field_instance.get_prep_value( - already_json) - self.assertDictEqual(value, - json.loads(json.loads(double_prepared_value))) - self.assertEqual(json_field_instance.get_prep_value(None), 'null') - - def test_get_prep_value_can_return_none_if_null(self): - json_field_instance = JSONField(null=True) - value = {'a': 1} - prepared_value = json_field_instance.get_prep_value(value) - self.assertIsInstance(prepared_value, str) - self.assertDictEqual(value, json.loads(prepared_value)) - already_json = json.dumps(value) - double_prepared_value = json_field_instance.get_prep_value( - already_json) - self.assertDictEqual(value, - json.loads(json.loads(double_prepared_value))) - self.assertIs(json_field_instance.get_prep_value(None), None) - - def test_from_db_value_deprecation_warning(self): - # Compatibility for Django 1.11 and earlier - # Django 2.0+ drops the `context` argument - JSONModel.objects.create(json='{}') - - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter('always') - JSONModel.objects.get() - - self.assertEqual(w, []) diff --git a/tox.ini b/tox.ini index f30a77e..044d7f3 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,7 @@ envlist = py{35,36}-django111, py{35,36}-django20, py{35,36,37}-django21, - py{35,36,37}-django22, + py{36,37,38}-django22, py{36,37,38}-django30, isort,lint,dist,warnings, @@ -15,11 +15,8 @@ setenv = PYTHONDONTWRITEBYTECODE=1 deps = coverage - django111: Django~=1.11.0 - django20: Django~=2.0.0 - django21: Django~=2.1.0 django22: Django~=2.2.0 - django30: Django>=3.0b1,<3.1 + django30: Django~=3.0.0 [testenv:isort] commands = isort --check-only --recursive jsonfield tests {posargs:--diff} From 9982f0cadce8c47b931d3b0f000bc4a1a2781843 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Fri, 6 Dec 2019 11:59:21 -0800 Subject: [PATCH 40/75] Fix use with select_related (#22) --- jsonfield/fields.py | 2 +- tests/migrations/0001_initial.py | 7 +++++++ tests/models.py | 4 ++++ tests/test_fields.py | 14 -------------- tests/test_jsonfield.py | 15 +++++++++++++++ 5 files changed, 27 insertions(+), 15 deletions(-) diff --git a/jsonfield/fields.py b/jsonfield/fields.py index 11856f2..64a975a 100644 --- a/jsonfield/fields.py +++ b/jsonfield/fields.py @@ -43,7 +43,7 @@ def to_python(self, value): raise ValidationError(_("Enter valid JSON.")) def from_db_value(self, value, expression, connection): - if self.null and value is None: + if value is None: return None return json.loads(value, **self.load_kwargs) diff --git a/tests/migrations/0001_initial.py b/tests/migrations/0001_initial.py index bd333e8..bc71b85 100644 --- a/tests/migrations/0001_initial.py +++ b/tests/migrations/0001_initial.py @@ -72,4 +72,11 @@ class Migration(migrations.Migration): ('content_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), ], ), + migrations.CreateModel( + name='RemoteJSONModel', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('foreign', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='tests.JSONModel')), + ], + ), ] diff --git a/tests/models.py b/tests/models.py index 084b9ee..884991b 100644 --- a/tests/models.py +++ b/tests/models.py @@ -63,3 +63,7 @@ class JSONNotRequiredModel(models.Model): class OrderedJSONModel(models.Model): json = JSONField(load_kwargs={'object_pairs_hook': OrderedDict}) + + +class RemoteJSONModel(models.Model): + foreign = models.ForeignKey(JSONModel, blank=True, null=True, on_delete=models.CASCADE) diff --git a/tests/test_fields.py b/tests/test_fields.py index 932eb23..fa87967 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -1,12 +1,9 @@ import json -import warnings from django.test import TestCase from jsonfield.fields import JSONField -from .models import JSONModel - class TestFieldAPIMethods(TestCase): def test_get_db_prep_value_method_with_null(self): @@ -65,14 +62,3 @@ def test_get_prep_value_can_return_none_if_null(self): self.assertDictEqual(value, json.loads(json.loads(double_prepared_value))) self.assertIs(json_field_instance.get_prep_value(None), None) - - def test_from_db_value_deprecation_warning(self): - # Compatibility for Django 1.11 and earlier - # Django 2.0+ drops the `context` argument - JSONModel.objects.create(json='{}') - - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter('always') - JSONModel.objects.get() - - self.assertEqual(w, []) diff --git a/tests/test_jsonfield.py b/tests/test_jsonfield.py index 5ff5b16..c699b47 100644 --- a/tests/test_jsonfield.py +++ b/tests/test_jsonfield.py @@ -15,6 +15,7 @@ JSONModelWithForeignKey, JSONNotRequiredModel, OrderedJSONModel, + RemoteJSONModel, ) @@ -24,6 +25,20 @@ def test_object_create(self): JSONModelWithForeignKey.objects.create(foreign_obj=foreign_obj) +class RemoteJSONFieldTests(TestCase): + """Test JSON fields across a ForeignKey""" + + @classmethod + def setUpTestData(cls): + RemoteJSONModel.objects.create() + + def test_related_accessor(self): + RemoteJSONModel.objects.get().foreign + + def test_select_related(self): + RemoteJSONModel.objects.select_related('foreign').get() + + class JSONFieldTest(TestCase): """JSONField Wrapper Tests""" From 09b2482da7e212dfeb962570a0c29d23920d2b82 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Fri, 6 Dec 2019 12:20:34 -0800 Subject: [PATCH 41/75] Fix field deconstruction (#23) --- jsonfield/fields.py | 19 ++++++++++++------- tests/test_fields.py | 19 +++++++++++++++++++ 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/jsonfield/fields.py b/jsonfield/fields.py index 64a975a..5dacc3a 100644 --- a/jsonfield/fields.py +++ b/jsonfield/fields.py @@ -8,25 +8,30 @@ from . import forms from .encoder import JSONEncoder +DEFAULT_DUMP_KWARGS = { + 'cls': JSONEncoder, + 'separators': (',', ':'), +} + +DEFAULT_LOAD_KWARGS = {} + class JSONFieldMixin(models.Field): + form_class = forms.JSONField def __init__(self, *args, dump_kwargs=None, load_kwargs=None, **kwargs): - self.dump_kwargs = dump_kwargs if dump_kwargs is not None else { - 'cls': JSONEncoder, - 'separators': (',', ':') - } - self.load_kwargs = load_kwargs if load_kwargs is not None else {} + self.dump_kwargs = DEFAULT_DUMP_KWARGS if dump_kwargs is None else dump_kwargs + self.load_kwargs = DEFAULT_LOAD_KWARGS if load_kwargs is None else load_kwargs super(JSONFieldMixin, self).__init__(*args, **kwargs) def deconstruct(self): name, path, args, kwargs = super().deconstruct() - if self.dump_kwargs is not None: + if self.dump_kwargs != DEFAULT_DUMP_KWARGS: kwargs['dump_kwargs'] = self.dump_kwargs - if self.load_kwargs is not None: + if self.load_kwargs != DEFAULT_LOAD_KWARGS: kwargs['load_kwargs'] = self.load_kwargs return name, path, args, kwargs diff --git a/tests/test_fields.py b/tests/test_fields.py index fa87967..b994eb9 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -62,3 +62,22 @@ def test_get_prep_value_can_return_none_if_null(self): self.assertDictEqual(value, json.loads(json.loads(double_prepared_value))) self.assertIs(json_field_instance.get_prep_value(None), None) + + def test_deconstruct_default_kwargs(self): + field = JSONField() + + _, _, _, kwargs = field.deconstruct() + + self.assertNotIn('dump_kwargs', kwargs) + self.assertNotIn('load_kwargs', kwargs) + + def test_deconstruct_non_default_kwargs(self): + field = JSONField( + dump_kwargs={'separators': (',', ':')}, + load_kwargs={'object_pairs_hook': dict}, + ) + + _, _, _, kwargs = field.deconstruct() + + self.assertEqual(kwargs['dump_kwargs'], {'separators': (',', ':')}) + self.assertEqual(kwargs['load_kwargs'], {'object_pairs_hook': dict}) From 491694efd6d1fb0be87a4aaa37f2b7f6e805ef12 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Fri, 6 Dec 2019 12:23:17 -0800 Subject: [PATCH 42/75] Update release process --- README.rst | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 20430cb..837e563 100644 --- a/README.rst +++ b/README.rst @@ -111,13 +111,16 @@ Release Process * Update changelog * Update package version in setup.py * Create git tag for version -* Upload release to PyPI +* Upload release to PyPI test server +* Upload release to official PyPI server .. code-block:: shell - $ pip install -U pip setuptools wheel + $ pip install -U pip setuptools wheel twine $ rm -rf dist/ build/ - $ python setup.py bdist_wheel upload + $ python setup.py bdist_wheel + $ twine upload -r test dist/* + $ twine upload dist/* Changes From 891e4fd7d8356564469730a8052c54618a3f4e89 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Fri, 6 Dec 2019 12:27:13 -0800 Subject: [PATCH 43/75] Update changelog --- CHANGES.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 0b4dd6b..20e4d68 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,13 @@ Changes ------- +v3.1.0 12/06/2019 +^^^^^^^^^^^^^^^^^ +- Fix use with `select_related` across a foreign key +- Fix field deconstruction +- Drop Python 3.5 support +- Drop Django 2.1 (and below) support + v3.0.3 10/23/2019 ^^^^^^^^^^^^^^^^^ - Add Python 3.8 & Django 3.0 support From d330b1bb6764277e1f54b9036311e13520d5f110 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Fri, 6 Dec 2019 12:27:31 -0800 Subject: [PATCH 44/75] Bump version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 262686f..2dbd3c4 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='jsonfield2', - version='3.0.3', + version='3.1.0', packages=['jsonfield'], license='MIT', include_package_data=True, From 388dcfbf0fcdc46cbfddb785e267a7b8ab1d8854 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Mon, 9 Dec 2019 14:51:00 -0800 Subject: [PATCH 45/75] Fix readme --- README.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 837e563..ef72219 100644 --- a/README.rst +++ b/README.rst @@ -34,7 +34,7 @@ Requirements jsonfield2 aims to support all current `versions of Django`_, however the explicity tested versions are: * **Python:** 3.6, 3.7, 3.8 -* **Django:** 1.11, 2.0, 2.1, 2.2 +* **Django:** 2.2, 3.0 .. _versions of Django: https://www.djangoproject.com/download/#supported-versions @@ -110,6 +110,7 @@ Release Process * Update changelog * Update package version in setup.py +* Check supported versions in setup.py and readme * Create git tag for version * Upload release to PyPI test server * Upload release to official PyPI server From 8975b825aab91dce9479498ea0b85cc533062877 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Thu, 13 Feb 2020 15:35:41 -0800 Subject: [PATCH 46/75] Update encoder from DRF 3.11.0 --- jsonfield/encoder.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/jsonfield/encoder.py b/jsonfield/encoder.py index 8f21307..f87644a 100644 --- a/jsonfield/encoder.py +++ b/jsonfield/encoder.py @@ -5,7 +5,7 @@ from django.db.models.query import QuerySet from django.utils import timezone -from django.utils.encoding import force_text +from django.utils.encoding import force_str from django.utils.functional import Promise @@ -14,13 +14,13 @@ class JSONEncoder(json.JSONEncoder): JSONEncoder subclass that knows how to encode date/time/timedelta, decimal types, generators and other basic python objects. - Taken from https://github.com/tomchristie/django-rest-framework/blob/3.8.2/rest_framework/utils/encoders.py + Taken from https://github.com/tomchristie/django-rest-framework/blob/3.11.0/rest_framework/utils/encoders.py """ def default(self, obj): # noqa: C901 # For Date Time string spec, see ECMA 262 # https://ecma-international.org/ecma-262/5.1/#sec-15.9.1.15 if isinstance(obj, Promise): - return force_text(obj) + return force_str(obj) elif isinstance(obj, datetime.datetime): representation = obj.isoformat() if representation.endswith('+00:00'): @@ -44,15 +44,16 @@ def default(self, obj): # noqa: C901 return tuple(obj) elif isinstance(obj, bytes): # Best-effort for binary blobs. See #4187. - return obj.decode('utf-8') + return obj.decode() elif hasattr(obj, 'tolist'): # Numpy arrays and array scalars. return obj.tolist() elif hasattr(obj, '__getitem__'): + cls = (list if isinstance(obj, (list, tuple)) else dict) try: - return dict(obj) + return cls(obj) except Exception: pass elif hasattr(obj, '__iter__'): return tuple(item for item in obj) - return super(JSONEncoder, self).default(obj) + return super().default(obj) From a1d6e58e1e4a20472c416d47faf7e47ca15606d6 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Thu, 13 Feb 2020 15:42:42 -0800 Subject: [PATCH 47/75] Don't use unicode gettext variants --- jsonfield/fields.py | 2 +- jsonfield/forms.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/jsonfield/fields.py b/jsonfield/fields.py index 5dacc3a..15f7f95 100644 --- a/jsonfield/fields.py +++ b/jsonfield/fields.py @@ -3,7 +3,7 @@ from django.db import models from django.forms import ValidationError -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from . import forms from .encoder import JSONEncoder diff --git a/jsonfield/forms.py b/jsonfield/forms.py index 4bb980f..b297dc3 100644 --- a/jsonfield/forms.py +++ b/jsonfield/forms.py @@ -1,7 +1,7 @@ import json from django.forms import ValidationError, fields -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ class JSONFieldMixin(object): From 5dbe816403af561614910be79d546f9f186962db Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Thu, 13 Feb 2020 16:20:10 -0800 Subject: [PATCH 48/75] Remove unnecessary form field mixin --- jsonfield/fields.py | 2 +- jsonfield/forms.py | 10 +++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/jsonfield/fields.py b/jsonfield/fields.py index 15f7f95..2943809 100644 --- a/jsonfield/fields.py +++ b/jsonfield/fields.py @@ -70,7 +70,7 @@ def formfield(self, **kwargs): field = super(JSONFieldMixin, self).formfield(**kwargs) - if isinstance(field, forms.JSONFieldMixin): + if isinstance(field, forms.JSONField): field.load_kwargs = self.load_kwargs if not field.help_text: diff --git a/jsonfield/forms.py b/jsonfield/forms.py index b297dc3..1829c2b 100644 --- a/jsonfield/forms.py +++ b/jsonfield/forms.py @@ -4,10 +4,10 @@ from django.utils.translation import gettext_lazy as _ -class JSONFieldMixin(object): +class JSONField(fields.CharField): def __init__(self, *args, **kwargs): self.load_kwargs = kwargs.pop('load_kwargs', {}) - super(JSONFieldMixin, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def to_python(self, value): if isinstance(value, str) and value: @@ -23,10 +23,6 @@ def clean(self, value): # Trap cleaning errors & bubble them up as JSON errors try: - return super(JSONFieldMixin, self).clean(value) + return super().clean(value) except TypeError: raise ValidationError(_("Enter valid JSON.")) - - -class JSONField(JSONFieldMixin, fields.CharField): - pass From 745e133db02e4ff6d80306d9c507e84fcf1bf2e9 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Thu, 13 Feb 2020 16:21:42 -0800 Subject: [PATCH 49/75] New-style super --- jsonfield/fields.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/jsonfield/fields.py b/jsonfield/fields.py index 2943809..fa08067 100644 --- a/jsonfield/fields.py +++ b/jsonfield/fields.py @@ -24,7 +24,7 @@ def __init__(self, *args, dump_kwargs=None, load_kwargs=None, **kwargs): self.dump_kwargs = DEFAULT_DUMP_KWARGS if dump_kwargs is None else dump_kwargs self.load_kwargs = DEFAULT_LOAD_KWARGS if load_kwargs is None else load_kwargs - super(JSONFieldMixin, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def deconstruct(self): name, path, args, kwargs = super().deconstruct() @@ -68,7 +68,7 @@ def formfield(self, **kwargs): if "form_class" not in kwargs: kwargs["form_class"] = self.form_class - field = super(JSONFieldMixin, self).formfield(**kwargs) + field = super().formfield(**kwargs) if isinstance(field, forms.JSONField): field.load_kwargs = self.load_kwargs @@ -95,7 +95,7 @@ def get_default(self): return self.default() return copy.deepcopy(self.default) # If the field doesn't have a default, then we punt to models.Field. - return super(JSONFieldMixin, self).get_default() + return super().get_default() class JSONField(JSONFieldMixin, models.TextField): From fe0519007111a1817d809847f514cc7ea7d20dde Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Thu, 13 Feb 2020 16:23:39 -0800 Subject: [PATCH 50/75] Fix form field kwargs handling Ensure dump/load kwargs are provided to the init method instead of setting them on the instance after initialization. --- jsonfield/fields.py | 12 +++++++----- jsonfield/forms.py | 6 ++++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/jsonfield/fields.py b/jsonfield/fields.py index fa08067..0c9ffa1 100644 --- a/jsonfield/fields.py +++ b/jsonfield/fields.py @@ -33,6 +33,7 @@ def deconstruct(self): kwargs['dump_kwargs'] = self.dump_kwargs if self.load_kwargs != DEFAULT_LOAD_KWARGS: kwargs['load_kwargs'] = self.load_kwargs + return name, path, args, kwargs def to_python(self, value): @@ -65,13 +66,14 @@ def value_from_object(self, obj): return json.dumps(value, **self.dump_kwargs) def formfield(self, **kwargs): - if "form_class" not in kwargs: - kwargs["form_class"] = self.form_class + if 'form_class' not in kwargs: + kwargs['form_class'] = self.form_class - field = super().formfield(**kwargs) + if issubclass(kwargs['form_class'], forms.JSONField): + kwargs.setdefault('dump_kwargs', self.dump_kwargs) + kwargs.setdefault('load_kwargs', self.load_kwargs) - if isinstance(field, forms.JSONField): - field.load_kwargs = self.load_kwargs + field = super().formfield(**kwargs) if not field.help_text: field.help_text = "Enter valid JSON." diff --git a/jsonfield/forms.py b/jsonfield/forms.py index 1829c2b..fe9c403 100644 --- a/jsonfield/forms.py +++ b/jsonfield/forms.py @@ -5,8 +5,10 @@ class JSONField(fields.CharField): - def __init__(self, *args, **kwargs): - self.load_kwargs = kwargs.pop('load_kwargs', {}) + def __init__(self, *args, dump_kwargs=None, load_kwargs=None, **kwargs): + self.dump_kwargs = dump_kwargs if dump_kwargs else {} + self.load_kwargs = load_kwargs if load_kwargs else {} + super().__init__(*args, **kwargs) def to_python(self, value): From 9dd6934545cb6e8a5702334fdbb04936beaa70b5 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Thu, 13 Feb 2020 16:27:50 -0800 Subject: [PATCH 51/75] Ensure help text supports translations --- jsonfield/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsonfield/fields.py b/jsonfield/fields.py index 0c9ffa1..48915e7 100644 --- a/jsonfield/fields.py +++ b/jsonfield/fields.py @@ -76,7 +76,7 @@ def formfield(self, **kwargs): field = super().formfield(**kwargs) if not field.help_text: - field.help_text = "Enter valid JSON." + field.help_text = _("Enter valid JSON.") return field From 4ddcd649c08d8644d51868c844e955a9e7b02887 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Thu, 13 Feb 2020 16:28:53 -0800 Subject: [PATCH 52/75] Fix form field styling Readds indentation and adds spaces after separators --- jsonfield/fields.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/jsonfield/fields.py b/jsonfield/fields.py index 48915e7..009251c 100644 --- a/jsonfield/fields.py +++ b/jsonfield/fields.py @@ -10,7 +10,6 @@ DEFAULT_DUMP_KWARGS = { 'cls': JSONEncoder, - 'separators': (',', ':'), } DEFAULT_LOAD_KWARGS = {} @@ -103,6 +102,13 @@ def get_default(self): class JSONField(JSONFieldMixin, models.TextField): """JSONField is a generic textfield that serializes/deserializes JSON objects""" + def formfield(self, **kwargs): + field = super().formfield(**kwargs) + if isinstance(field, forms.JSONField): + # Note: TextField sets the Textarea widget + field.dump_kwargs.setdefault('indent', 4) + return field + class JSONCharField(JSONFieldMixin, models.CharField): """JSONCharField is a generic textfield that serializes/deserializes JSON objects""" From 8242a92e05fc60e6a0b1dfdb229f9a858f366a75 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Thu, 13 Feb 2020 16:29:50 -0800 Subject: [PATCH 53/75] Test form saving/rendering --- tests/test_jsonfield.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tests/test_jsonfield.py b/tests/test_jsonfield.py index c699b47..17f8efb 100644 --- a/tests/test_jsonfield.py +++ b/tests/test_jsonfield.py @@ -293,3 +293,40 @@ def test_form_with_data(self): def test_form_save(self): form = self.form_class(data={'json': ''}) form.save() + + def test_save_values(self): + values = [ + # (type, form input, db value) + ('object', '{"a": "b"}', {'a': 'b'}), + ('array', '[1, 2]', [1, 2]), + ('string', '"test"', 'test'), + ('number', '1.0', 1.0), + ('bool', 'true', True), + ('null', 'null', None), + ] + + for vtype, form_value, db_value in values: + with self.subTest(type=vtype, input=form_value, db=db_value): + form = self.form_class(data={'json': form_value}) + self.assertTrue(form.is_valid(), msg=form.errors) + + instance = form.save() + self.assertEqual(instance.json, db_value) + + def test_render_values(self): + values = [ + # (type, db value, form output) + ('object', {'a': 'b'}, '{\n "a": "b"\n}'), + ('array', [1, 2], "[\n 1,\n 2\n]"), + ('string', 'test', '"test"'), + ('number', 1.0, '1.0'), + ('bool', True, 'true'), + ('null', None, 'null'), + ] + + for vtype, db_value, form_value in values: + with self.subTest(type=vtype, db=db_value, output=form_value): + instance = JSONNotRequiredModel.objects.create(json=db_value) + + form = self.form_class(instance=instance) + self.assertEqual(form['json'].value(), form_value) From 92613991d76429c65bd35aebea3470c0baf26520 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Thu, 13 Feb 2020 16:33:05 -0800 Subject: [PATCH 54/75] Fix field value deserialization --- jsonfield/fields.py | 15 ++------------- jsonfield/forms.py | 23 +++++++++++------------ jsonfield/json.py | 22 ++++++++++++++++++++++ 3 files changed, 35 insertions(+), 25 deletions(-) create mode 100644 jsonfield/json.py diff --git a/jsonfield/fields.py b/jsonfield/fields.py index 009251c..55f5c45 100644 --- a/jsonfield/fields.py +++ b/jsonfield/fields.py @@ -7,6 +7,7 @@ from . import forms from .encoder import JSONEncoder +from .json import checked_loads DEFAULT_DUMP_KWARGS = { 'cls': JSONEncoder, @@ -36,14 +37,8 @@ def deconstruct(self): return name, path, args, kwargs def to_python(self, value): - if self.null and value is None: - return None - - if not isinstance(value, (str, bytes, bytearray)): - return value - try: - return json.loads(value, **self.load_kwargs) + return checked_loads(value, **self.load_kwargs) except ValueError: raise ValidationError(_("Enter valid JSON.")) @@ -58,12 +53,6 @@ def get_prep_value(self, value): return None return json.dumps(value, **self.dump_kwargs) - def value_from_object(self, obj): - value = super(JSONFieldMixin, self).value_from_object(obj) - if self.null and value is None: - return None - return json.dumps(value, **self.dump_kwargs) - def formfield(self, **kwargs): if 'form_class' not in kwargs: kwargs['form_class'] = self.form_class diff --git a/jsonfield/forms.py b/jsonfield/forms.py index fe9c403..ab30b63 100644 --- a/jsonfield/forms.py +++ b/jsonfield/forms.py @@ -3,6 +3,8 @@ from django.forms import ValidationError, fields from django.utils.translation import gettext_lazy as _ +from .json import checked_loads + class JSONField(fields.CharField): def __init__(self, *args, dump_kwargs=None, load_kwargs=None, **kwargs): @@ -12,19 +14,16 @@ def __init__(self, *args, dump_kwargs=None, load_kwargs=None, **kwargs): super().__init__(*args, **kwargs) def to_python(self, value): - if isinstance(value, str) and value: - try: - return json.loads(value, **self.load_kwargs) - except ValueError: - raise ValidationError(_("Enter valid JSON.")) - return value - - def clean(self, value): - if not value and not self.required: + if self.disabled: + return value + + if value in self.empty_values: return None - # Trap cleaning errors & bubble them up as JSON errors try: - return super().clean(value) - except TypeError: + return checked_loads(value, **self.load_kwargs) + except json.JSONDecodeError: raise ValidationError(_("Enter valid JSON.")) + + def prepare_value(self, value): + return json.dumps(value, **self.dump_kwargs) diff --git a/jsonfield/json.py b/jsonfield/json.py new file mode 100644 index 0000000..86f1736 --- /dev/null +++ b/jsonfield/json.py @@ -0,0 +1,22 @@ +import json + + +class JSONString(str): + pass + + +def checked_loads(value, **kwargs): + """ + Ensure that values aren't loaded twice, resulting in an encoding error. + + Loaded strings are wrapped in JSONString, as it is otherwise not possible + to differentiate between a loaded and unloaded string. + """ + if isinstance(value, (list, dict, int, float, JSONString, type(None))): + return value + + value = json.loads(value, **kwargs) + if isinstance(value, str): + value = JSONString(value) + + return value From b41b53315f24e3f0730767991593a1c282105e39 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Thu, 13 Feb 2020 20:43:05 -0800 Subject: [PATCH 55/75] Use named error messages --- jsonfield/forms.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/jsonfield/forms.py b/jsonfield/forms.py index ab30b63..0628cf6 100644 --- a/jsonfield/forms.py +++ b/jsonfield/forms.py @@ -7,6 +7,10 @@ class JSONField(fields.CharField): + default_error_messages = { + 'invalid': _('"%(value)s" value must be valid JSON.'), + } + def __init__(self, *args, dump_kwargs=None, load_kwargs=None, **kwargs): self.dump_kwargs = dump_kwargs if dump_kwargs else {} self.load_kwargs = load_kwargs if load_kwargs else {} @@ -23,7 +27,11 @@ def to_python(self, value): try: return checked_loads(value, **self.load_kwargs) except json.JSONDecodeError: - raise ValidationError(_("Enter valid JSON.")) + raise ValidationError( + self.error_messages['invalid'], + code='invalid', + params={'value': value}, + ) def prepare_value(self, value): return json.dumps(value, **self.dump_kwargs) From a7e3f211f4c2a83eb9056ea77dde9367c488d88f Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Thu, 13 Feb 2020 20:44:26 -0800 Subject: [PATCH 56/75] Fix invalid value handling --- jsonfield/forms.py | 14 ++++++++++++++ tests/test_jsonfield.py | 9 +++++++++ 2 files changed, 23 insertions(+) diff --git a/jsonfield/forms.py b/jsonfield/forms.py index 0628cf6..646af43 100644 --- a/jsonfield/forms.py +++ b/jsonfield/forms.py @@ -6,6 +6,10 @@ from .json import checked_loads +class InvalidJSONInput(str): + pass + + class JSONField(fields.CharField): default_error_messages = { 'invalid': _('"%(value)s" value must be valid JSON.'), @@ -33,5 +37,15 @@ def to_python(self, value): params={'value': value}, ) + def bound_data(self, data, initial): + if self.disabled: + return initial + try: + return json.loads(data, **self.load_kwargs) + except json.JSONDecodeError: + return InvalidJSONInput(data) + def prepare_value(self, value): + if isinstance(value, InvalidJSONInput): + return value return json.dumps(value, **self.dump_kwargs) diff --git a/tests/test_jsonfield.py b/tests/test_jsonfield.py index 17f8efb..ba9cbae 100644 --- a/tests/test_jsonfield.py +++ b/tests/test_jsonfield.py @@ -330,3 +330,12 @@ def test_render_values(self): form = self.form_class(instance=instance) self.assertEqual(form['json'].value(), form_value) + + def test_invalid_value(self): + form = self.form_class(data={'json': 'foo'}) + + self.assertFalse(form.is_valid()) + self.assertEqual(form.errors, { + 'json': ['"foo" value must be valid JSON.'], + }) + self.assertEqual(form['json'].value(), 'foo') From 2cf0215890276d2f5d7016f30186efa5a09e3ae3 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Thu, 13 Feb 2020 20:45:12 -0800 Subject: [PATCH 57/75] Fix field serialization --- jsonfield/fields.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/jsonfield/fields.py b/jsonfield/fields.py index 55f5c45..ea97a5a 100644 --- a/jsonfield/fields.py +++ b/jsonfield/fields.py @@ -53,6 +53,10 @@ def get_prep_value(self, value): return None return json.dumps(value, **self.dump_kwargs) + def value_to_string(self, obj): + value = self.value_from_object(obj) + return json.dumps(value, **self.dump_kwargs) + def formfield(self, **kwargs): if 'form_class' not in kwargs: kwargs['form_class'] = self.form_class From a5897a02fdce15d20635df0dd628063cadba2050 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Fri, 14 Feb 2020 11:58:36 -0800 Subject: [PATCH 58/75] Differentiate int/float value tests --- tests/test_jsonfield.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_jsonfield.py b/tests/test_jsonfield.py index ba9cbae..e802775 100644 --- a/tests/test_jsonfield.py +++ b/tests/test_jsonfield.py @@ -300,7 +300,8 @@ def test_save_values(self): ('object', '{"a": "b"}', {'a': 'b'}), ('array', '[1, 2]', [1, 2]), ('string', '"test"', 'test'), - ('number', '1.0', 1.0), + ('float', '1.2', 1.2), + ('int', '1234', 1234), ('bool', 'true', True), ('null', 'null', None), ] @@ -319,7 +320,8 @@ def test_render_values(self): ('object', {'a': 'b'}, '{\n "a": "b"\n}'), ('array', [1, 2], "[\n 1,\n 2\n]"), ('string', 'test', '"test"'), - ('number', 1.0, '1.0'), + ('float', 1.2, '1.2'), + ('int', 1234, '1234'), ('bool', True, 'true'), ('null', None, 'null'), ] From 5ed2d3b70f04836ce5ec01b7238316f197ed96a7 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Fri, 14 Feb 2020 14:17:44 -0800 Subject: [PATCH 59/75] Move forms tests to separate module --- tests/test_forms.py | 74 +++++++++++++++++++++++++++++++++++++++++ tests/test_jsonfield.py | 72 +-------------------------------------- 2 files changed, 75 insertions(+), 71 deletions(-) create mode 100644 tests/test_forms.py diff --git a/tests/test_forms.py b/tests/test_forms.py new file mode 100644 index 0000000..ce507f0 --- /dev/null +++ b/tests/test_forms.py @@ -0,0 +1,74 @@ +from django.forms import ModelForm +from django.test import TestCase + +from .models import JSONNotRequiredModel + + +class JSONModelFormTest(TestCase): + def setUp(self): + class JSONNotRequiredForm(ModelForm): + class Meta: + model = JSONNotRequiredModel + fields = '__all__' + + self.form_class = JSONNotRequiredForm + + def test_blank_form(self): + form = self.form_class(data={'json': ''}) + self.assertFalse(form.has_changed()) + + def test_form_with_data(self): + form = self.form_class(data={'json': '{}'}) + self.assertTrue(form.has_changed()) + + def test_form_save(self): + form = self.form_class(data={'json': ''}) + form.save() + + def test_save_values(self): + values = [ + # (type, form input, db value) + ('object', '{"a": "b"}', {'a': 'b'}), + ('array', '[1, 2]', [1, 2]), + ('string', '"test"', 'test'), + ('float', '1.2', 1.2), + ('int', '1234', 1234), + ('bool', 'true', True), + ('null', 'null', None), + ] + + for vtype, form_value, db_value in values: + with self.subTest(type=vtype, input=form_value, db=db_value): + form = self.form_class(data={'json': form_value}) + self.assertTrue(form.is_valid(), msg=form.errors) + + instance = form.save() + self.assertEqual(instance.json, db_value) + + def test_render_values(self): + values = [ + # (type, db value, form output) + ('object', {'a': 'b'}, '{\n "a": "b"\n}'), + ('array', [1, 2], "[\n 1,\n 2\n]"), + ('string', 'test', '"test"'), + ('float', 1.2, '1.2'), + ('int', 1234, '1234'), + ('bool', True, 'true'), + ('null', None, 'null'), + ] + + for vtype, db_value, form_value in values: + with self.subTest(type=vtype, db=db_value, output=form_value): + instance = JSONNotRequiredModel.objects.create(json=db_value) + + form = self.form_class(instance=instance) + self.assertEqual(form['json'].value(), form_value) + + def test_invalid_value(self): + form = self.form_class(data={'json': 'foo'}) + + self.assertFalse(form.is_valid()) + self.assertEqual(form.errors, { + 'json': ['"foo" value must be valid JSON.'], + }) + self.assertEqual(form['json'].value(), 'foo') diff --git a/tests/test_jsonfield.py b/tests/test_jsonfield.py index e802775..18be627 100644 --- a/tests/test_jsonfield.py +++ b/tests/test_jsonfield.py @@ -4,7 +4,7 @@ import django from django.core.serializers import deserialize, serialize from django.core.serializers.base import DeserializationError -from django.forms import ModelForm, ValidationError +from django.forms import ValidationError from django.test import TestCase from .models import ( @@ -271,73 +271,3 @@ def test_load_kwargs_hook(self): self.assertEqual(list(self.instance.json), self.expected_key_order) self.assertEqual(list(from_db.json), self.expected_key_order) self.assertIsInstance(from_db.json, OrderedDict) - - -class JSONModelFormTest(TestCase): - def setUp(self): - class JSONNotRequiredForm(ModelForm): - class Meta: - model = JSONNotRequiredModel - fields = '__all__' - - self.form_class = JSONNotRequiredForm - - def test_blank_form(self): - form = self.form_class(data={'json': ''}) - self.assertFalse(form.has_changed()) - - def test_form_with_data(self): - form = self.form_class(data={'json': '{}'}) - self.assertTrue(form.has_changed()) - - def test_form_save(self): - form = self.form_class(data={'json': ''}) - form.save() - - def test_save_values(self): - values = [ - # (type, form input, db value) - ('object', '{"a": "b"}', {'a': 'b'}), - ('array', '[1, 2]', [1, 2]), - ('string', '"test"', 'test'), - ('float', '1.2', 1.2), - ('int', '1234', 1234), - ('bool', 'true', True), - ('null', 'null', None), - ] - - for vtype, form_value, db_value in values: - with self.subTest(type=vtype, input=form_value, db=db_value): - form = self.form_class(data={'json': form_value}) - self.assertTrue(form.is_valid(), msg=form.errors) - - instance = form.save() - self.assertEqual(instance.json, db_value) - - def test_render_values(self): - values = [ - # (type, db value, form output) - ('object', {'a': 'b'}, '{\n "a": "b"\n}'), - ('array', [1, 2], "[\n 1,\n 2\n]"), - ('string', 'test', '"test"'), - ('float', 1.2, '1.2'), - ('int', 1234, '1234'), - ('bool', True, 'true'), - ('null', None, 'null'), - ] - - for vtype, db_value, form_value in values: - with self.subTest(type=vtype, db=db_value, output=form_value): - instance = JSONNotRequiredModel.objects.create(json=db_value) - - form = self.form_class(instance=instance) - self.assertEqual(form['json'].value(), form_value) - - def test_invalid_value(self): - form = self.form_class(data={'json': 'foo'}) - - self.assertFalse(form.is_valid()) - self.assertEqual(form.errors, { - 'json': ['"foo" value must be valid JSON.'], - }) - self.assertEqual(form['json'].value(), 'foo') From 194e7d3c0f6225e44a505706849722586393aa09 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Fri, 14 Feb 2020 15:15:11 -0800 Subject: [PATCH 60/75] Add bound vs unbound rendering tests --- jsonfield/forms.py | 13 +++++++++++++ tests/test_forms.py | 19 ++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/jsonfield/forms.py b/jsonfield/forms.py index 646af43..02cd563 100644 --- a/jsonfield/forms.py +++ b/jsonfield/forms.py @@ -38,6 +38,19 @@ def to_python(self, value): ) def bound_data(self, data, initial): + # Note: This is a bit confusing, as there are multiple things occurring. + # First, the `initial` value is the *unencoded* python object provided + # via the form instance, while `data` is the *encoded* form input. The + # outgoing value needs to be uniform, so we decode `data` here. + # + # Second, it may seem counterintuitive to encode data, just to decode + # it in `prepare_value`. Why not just decode `initial` here? This is + # due to `BoundField.value()`, which only calls `bound_data` when the + # form is bound. If unbound, the `initial` value is provided directly + # to `prepare_value`, and the value would still need to be encoded. + # + # Lastly, we don't want to run `checked_loads` here, since we *know* + # that the input `data` isn't a decoded value (e.g., via `to_python`). if self.disabled: return initial try: diff --git a/tests/test_forms.py b/tests/test_forms.py index ce507f0..8ed8af7 100644 --- a/tests/test_forms.py +++ b/tests/test_forms.py @@ -45,7 +45,7 @@ def test_save_values(self): instance = form.save() self.assertEqual(instance.json, db_value) - def test_render_values(self): + def test_render_initial_values(self): values = [ # (type, db value, form output) ('object', {'a': 'b'}, '{\n "a": "b"\n}'), @@ -64,6 +64,23 @@ def test_render_values(self): form = self.form_class(instance=instance) self.assertEqual(form['json'].value(), form_value) + def test_render_bound_values(self): + values = [ + # (type, db value, form input, form output) + ('object', '{"a": "b"}', '{\n "a": "b"\n}'), + ('array', '[1, 2]', "[\n 1,\n 2\n]"), + ('string', '"test"', '"test"'), + ('float', '1.2', '1.2'), + ('int', '1234', '1234'), + ('bool', 'true', 'true'), + ('null', 'null', 'null'), + ] + + for vtype, form_input, form_output in values: + with self.subTest(type=vtype, input=form_input, output=form_output): + form = self.form_class(data={'json': form_input}) + self.assertEqual(form['json'].value(), form_output) + def test_invalid_value(self): form = self.form_class(data={'json': 'foo'}) From 9aea78fb4894c86d0c1360615418e37094c72d5a Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Fri, 14 Feb 2020 15:15:41 -0800 Subject: [PATCH 61/75] Make test variables more consistent --- tests/test_forms.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_forms.py b/tests/test_forms.py index 8ed8af7..6bf6e36 100644 --- a/tests/test_forms.py +++ b/tests/test_forms.py @@ -37,9 +37,9 @@ def test_save_values(self): ('null', 'null', None), ] - for vtype, form_value, db_value in values: - with self.subTest(type=vtype, input=form_value, db=db_value): - form = self.form_class(data={'json': form_value}) + for vtype, form_input, db_value in values: + with self.subTest(type=vtype, input=form_input, db=db_value): + form = self.form_class(data={'json': form_input}) self.assertTrue(form.is_valid(), msg=form.errors) instance = form.save() @@ -57,12 +57,12 @@ def test_render_initial_values(self): ('null', None, 'null'), ] - for vtype, db_value, form_value in values: - with self.subTest(type=vtype, db=db_value, output=form_value): + for vtype, db_value, form_output in values: + with self.subTest(type=vtype, db=db_value, output=form_output): instance = JSONNotRequiredModel.objects.create(json=db_value) form = self.form_class(instance=instance) - self.assertEqual(form['json'].value(), form_value) + self.assertEqual(form['json'].value(), form_output) def test_render_bound_values(self): values = [ From 63c1d35e776a0accb9bd9a812b73ab55057bb897 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Fri, 14 Feb 2020 15:57:04 -0800 Subject: [PATCH 62/75] Add test for disabled field --- tests/test_forms.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/test_forms.py b/tests/test_forms.py index 6bf6e36..44b3015 100644 --- a/tests/test_forms.py +++ b/tests/test_forms.py @@ -89,3 +89,15 @@ def test_invalid_value(self): 'json': ['"foo" value must be valid JSON.'], }) self.assertEqual(form['json'].value(), 'foo') + + def test_disabled_field(self): + instance = JSONNotRequiredModel.objects.create(json=100) + + form = self.form_class(data={'json': '{"foo": "bar"}'}, instance=instance) + form.fields['json'].disabled = True + + self.assertTrue(form.is_valid()) + self.assertEqual(form.cleaned_data, {'json': 100}) + + # rendered value + self.assertEqual(form['json'].value(), '100') From 808a3cfdb31890c13e1c5fad603a021e6ef116b7 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Fri, 14 Feb 2020 15:57:38 -0800 Subject: [PATCH 63/75] Ensure loaded value is check-aware --- jsonfield/fields.py | 2 +- tests/test_fields.py | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/jsonfield/fields.py b/jsonfield/fields.py index ea97a5a..05bad44 100644 --- a/jsonfield/fields.py +++ b/jsonfield/fields.py @@ -45,7 +45,7 @@ def to_python(self, value): def from_db_value(self, value, expression, connection): if value is None: return None - return json.loads(value, **self.load_kwargs) + return checked_loads(value, **self.load_kwargs) def get_prep_value(self, value): """Convert JSON object to a string""" diff --git a/tests/test_fields.py b/tests/test_fields.py index b994eb9..724744a 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -3,6 +3,7 @@ from django.test import TestCase from jsonfield.fields import JSONField +from jsonfield.json import JSONString class TestFieldAPIMethods(TestCase): @@ -81,3 +82,21 @@ def test_deconstruct_non_default_kwargs(self): self.assertEqual(kwargs['dump_kwargs'], {'separators': (',', ':')}) self.assertEqual(kwargs['load_kwargs'], {'object_pairs_hook': dict}) + + def test_from_db_value_loaded_types(self): + values = [ + # (label, db value, loaded type) + ('object', '{"a": "b"}', dict), + ('array', '[1, 2]', list), + ('string', '"test"', JSONString), + ('float', '1.2', float), + ('int', '1234', int), + ('bool', 'true', bool), + ('null', 'null', type(None)), + ] + + for label, db_value, inst_type in values: + with self.subTest(type=label, db_value=db_value): + value = JSONField().from_db_value(db_value, None, None) + + self.assertIsInstance(value, inst_type) From 46ae0934b19f95b671ca6c8dec3ba7e675f3fc8f Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Fri, 14 Feb 2020 16:27:18 -0800 Subject: [PATCH 64/75] Remove old tests --- tests/test_fields.py | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/tests/test_fields.py b/tests/test_fields.py index 724744a..76c132d 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -7,36 +7,6 @@ class TestFieldAPIMethods(TestCase): - def test_get_db_prep_value_method_with_null(self): - json_field_instance = JSONField(null=True) - value = {'a': 1} - prepared_value = json_field_instance.get_db_prep_value( - value, connection=None, prepared=False) - self.assertIsInstance(prepared_value, str) - self.assertDictEqual(value, json.loads(prepared_value)) - self.assertIs(json_field_instance.get_db_prep_value( - None, connection=None, prepared=True), None) - self.assertIs(json_field_instance.get_db_prep_value( - None, connection=None, prepared=False), None) - - def test_get_db_prep_value_method_with_not_null(self): - json_field_instance = JSONField(null=False) - value = {'a': 1} - prepared_value = json_field_instance.get_db_prep_value( - value, connection=None, prepared=False) - self.assertIsInstance(prepared_value, str) - self.assertDictEqual(value, json.loads(prepared_value)) - self.assertIs(json_field_instance.get_db_prep_value( - None, connection=None, prepared=True), None) - self.assertEqual(json_field_instance.get_db_prep_value( - None, connection=None, prepared=False), 'null') - - def test_get_db_prep_value_method_skips_prepared_values(self): - json_field_instance = JSONField(null=False) - value = {'a': 1} - prepared_value = json_field_instance.get_db_prep_value( - value, connection=None, prepared=True) - self.assertIs(prepared_value, value) def test_get_prep_value_always_json_dumps_if_not_null(self): json_field_instance = JSONField(null=False) From eac62140c22026573d4966a07b833b593b54041a Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Fri, 14 Feb 2020 16:59:00 -0800 Subject: [PATCH 65/75] Use setdefault over 'in' condition --- jsonfield/fields.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/jsonfield/fields.py b/jsonfield/fields.py index 05bad44..d790fec 100644 --- a/jsonfield/fields.py +++ b/jsonfield/fields.py @@ -17,7 +17,6 @@ class JSONFieldMixin(models.Field): - form_class = forms.JSONField def __init__(self, *args, dump_kwargs=None, load_kwargs=None, **kwargs): @@ -58,9 +57,7 @@ def value_to_string(self, obj): return json.dumps(value, **self.dump_kwargs) def formfield(self, **kwargs): - if 'form_class' not in kwargs: - kwargs['form_class'] = self.form_class - + kwargs.setdefault('form_class', self.form_class) if issubclass(kwargs['form_class'], forms.JSONField): kwargs.setdefault('dump_kwargs', self.dump_kwargs) kwargs.setdefault('load_kwargs', self.load_kwargs) @@ -82,7 +79,6 @@ def get_default(self): without calling force_unicode on it. Note that if you set a callable as a default, the field will still call it. It will *not* try to pickle and encode it. - """ if self.has_default(): if callable(self.default): From f7d1224bb61a5db794f08a448f034fa55bced816 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Fri, 14 Feb 2020 17:02:08 -0800 Subject: [PATCH 66/75] Move help_text to form field Default help_text values are non-standard, and it should probably be removed. However, moving this to the form field allows subclasses to override or otherwise disable this. --- jsonfield/fields.py | 7 +------ jsonfield/forms.py | 1 + 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/jsonfield/fields.py b/jsonfield/fields.py index d790fec..c20e400 100644 --- a/jsonfield/fields.py +++ b/jsonfield/fields.py @@ -62,12 +62,7 @@ def formfield(self, **kwargs): kwargs.setdefault('dump_kwargs', self.dump_kwargs) kwargs.setdefault('load_kwargs', self.load_kwargs) - field = super().formfield(**kwargs) - - if not field.help_text: - field.help_text = _("Enter valid JSON.") - - return field + return super().formfield(**kwargs) def get_default(self): """ diff --git a/jsonfield/forms.py b/jsonfield/forms.py index 02cd563..9df9d58 100644 --- a/jsonfield/forms.py +++ b/jsonfield/forms.py @@ -19,6 +19,7 @@ def __init__(self, *args, dump_kwargs=None, load_kwargs=None, **kwargs): self.dump_kwargs = dump_kwargs if dump_kwargs else {} self.load_kwargs = load_kwargs if load_kwargs else {} + kwargs.setdefault('help_text', _("Enter valid JSON.")) super().__init__(*args, **kwargs) def to_python(self, value): From 500cb9e90c6dfdcbd132ca4e30c766d8daba5554 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Fri, 14 Feb 2020 17:39:33 -0800 Subject: [PATCH 67/75] Update manifest to include test suite --- MANIFEST.in | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 7545927..908c4f9 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,4 @@ -include CHANGES.rst -include LICENSE -include README.rst +include README.rst CHANGES.rst LICENSE + +include tox.ini +recursive-include tests *.py From 5a6726c1cafca1d0128e43f1a23f9dcd33dc5d3f Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Fri, 14 Feb 2020 17:39:51 -0800 Subject: [PATCH 68/75] Remove old Django versions from tox config --- tox.ini | 3 --- 1 file changed, 3 deletions(-) diff --git a/tox.ini b/tox.ini index 044d7f3..93aa33e 100644 --- a/tox.ini +++ b/tox.ini @@ -1,8 +1,5 @@ [tox] envlist = - py{35,36}-django111, - py{35,36}-django20, - py{35,36,37}-django21, py{36,37,38}-django22, py{36,37,38}-django30, isort,lint,dist,warnings, From 314bc9b839f4f83b3b2c72bfa738499a0e100a3b Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Fri, 14 Feb 2020 18:05:47 -0800 Subject: [PATCH 69/75] Update changelog --- CHANGES.rst | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 20e4d68..14e9b91 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,9 +1,23 @@ Changes ------- +v4.0.0 02/14/2020 <3 +^^^^^^^^^^^^^^^^^^^^ + +Note: This is the final release of ``jsonfield2``, as this fork is being merged +back into ``jsonfield``. + +- Add source distribution to release process +- Update ``JSONEncoder`` from DRF +- Fix re-rendering of invalid field inputs +- Fix form field cleaning of string inputs +- Fix indentation for ``Textarea`` widgets +- Allow form field error message to be overridden +- Obey form ``Field.disabled`` + v3.1.0 12/06/2019 ^^^^^^^^^^^^^^^^^ -- Fix use with `select_related` across a foreign key +- Fix use with ``select_related`` across a foreign key - Fix field deconstruction - Drop Python 3.5 support - Drop Django 2.1 (and below) support @@ -42,7 +56,7 @@ v2.0.2, 6/18/2017 v2.0.1, 3/8/2017 ^^^^^^^^^^^^^^^^ - Support upcoming Django 1.11 in test suite -- Renamed method `get_db_prep_value` to `get_prep_value` +- Renamed method ``get_db_prep_value`` to ``get_prep_value`` v2.0.0, 3/4/2017 ^^^^^^^^^^^^^^^^ From bcc9106aa512c2d184223bd0a131903fcfa6287c Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Fri, 14 Feb 2020 18:11:55 -0800 Subject: [PATCH 70/75] Add sdist to release process notes --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index ef72219..d85fbdd 100644 --- a/README.rst +++ b/README.rst @@ -119,7 +119,7 @@ Release Process $ pip install -U pip setuptools wheel twine $ rm -rf dist/ build/ - $ python setup.py bdist_wheel + $ python setup.py sdist bdist_wheel $ twine upload -r test dist/* $ twine upload dist/* From 91be5cd321fa299c620b176a237d087dc34ff27d Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Fri, 14 Feb 2020 18:12:07 -0800 Subject: [PATCH 71/75] Bump major version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 2dbd3c4..1b46385 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='jsonfield2', - version='3.1.0', + version='4.0.0', packages=['jsonfield'], license='MIT', include_package_data=True, From eb05235811eab3e3bf59a66c3c0d7dceb32e9c7c Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Fri, 14 Feb 2020 18:48:00 -0800 Subject: [PATCH 72/75] Revert jsonfield2 back to jsonfield --- .circleci/config.yml | 2 +- README.rst | 40 +++++++++++++++++----------------------- setup.py | 10 ++++++---- 3 files changed, 24 insertions(+), 28 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index f712394..0e19b31 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -42,7 +42,7 @@ jobs: - *install - run: | python setup.py bdist_wheel - tox -e dist --installpkg ./dist/jsonfield2-*.whl + tox -e dist --installpkg ./dist/jsonfield-*.whl tox -e dist docker: - image: circleci/python:3.8 diff --git a/README.rst b/README.rst index d85fbdd..811d12c 100644 --- a/README.rst +++ b/README.rst @@ -1,27 +1,21 @@ -jsonfield2 -========== - -.. image:: https://circleci.com/gh/rpkilby/jsonfield2.svg?style=shield - :target: https://circleci.com/gh/rpkilby/jsonfield2 -.. image:: https://codecov.io/gh/rpkilby/jsonfield2/branch/master/graph/badge.svg - :target: https://codecov.io/gh/rpkilby/jsonfield2 -.. image:: https://img.shields.io/pypi/v/jsonfield2.svg - :target: https://pypi.org/project/jsonfield2 -.. image:: https://img.shields.io/pypi/l/jsonfield2.svg - :target: https://pypi.org/project/jsonfield2 - -A modern fork of `django-jsonfield`_, compatible with the latest versions of Django. - -.. _django-jsonfield: https://github.com/dmkoch/django-jsonfield - ------ - -**jsonfield2** is a reusable model field that allows you to store validated JSON, automatically handling +jsonfield +========= + +.. image:: https://circleci.com/gh/rpkilby/jsonfield.svg?style=shield + :target: https://circleci.com/gh/rpkilby/jsonfield +.. image:: https://codecov.io/gh/rpkilby/jsonfield/branch/master/graph/badge.svg + :target: https://codecov.io/gh/rpkilby/jsonfield +.. image:: https://img.shields.io/pypi/v/jsonfield.svg + :target: https://pypi.org/project/jsonfield +.. image:: https://img.shields.io/pypi/l/jsonfield.svg + :target: https://pypi.org/project/jsonfield + +**jsonfield** is a reusable model field that allows you to store validated JSON, automatically handling serialization to and from the database. To use, add ``jsonfield.JSONField`` to one of your models. **Note:** `django.contrib.postgres`_ now supports PostgreSQL's jsonb type, which includes extended querying capabilities. If you're an end user of PostgreSQL and want full-featured JSON support, then it is -recommended that you use the built-in JSONField. However, jsonfield2 is still useful when your app +recommended that you use the built-in JSONField. However, jsonfield is still useful when your app needs to be database-agnostic, or when the built-in JSONField's extended querying is not being leveraged. e.g., a configuration field. @@ -31,7 +25,7 @@ e.g., a configuration field. Requirements ------------ -jsonfield2 aims to support all current `versions of Django`_, however the explicity tested versions are: +**jsonfield** aims to support all current `versions of Django`_, however the explicity tested versions are: * **Python:** 3.6, 3.7, 3.8 * **Django:** 2.2, 3.0 @@ -44,7 +38,7 @@ Installation .. code-block:: python - pip install jsonfield2 + pip install jsonfield Usage @@ -129,4 +123,4 @@ Changes Take a look at the `changelog`_. -.. _changelog: https://github.com/rpkilby/jsonfield2/blob/master/CHANGES.rst +.. _changelog: https://github.com/rpkilby/jsonfield/blob/master/CHANGES.rst diff --git a/setup.py b/setup.py index 1b46385..1afc7c1 100644 --- a/setup.py +++ b/setup.py @@ -2,14 +2,16 @@ setup( - name='jsonfield2', + name='jsonfield', version='4.0.0', packages=['jsonfield'], license='MIT', include_package_data=True, - author='Ryan P Kilby', - author_email='rpkilby@ncsu.edu', - url='https://github.com/rpkilby/jsonfield2/', + author='Brad Jasper', + author_email='contact@bradjasper.com', + maintainer='Ryan P Kilby', + maintainer_email='kilbyr@gmail.com', + url='https://github.com/rpkilby/jsonfield/', description='A reusable Django field that allows you to store validated JSON in your model.', long_description=open("README.rst").read(), install_requires=['Django >= 2.2'], From 08e2599a4b101924cc5228fa644cde0dfd3fb862 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Fri, 14 Feb 2020 18:48:20 -0800 Subject: [PATCH 73/75] Update changelog --- CHANGES.rst | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 14e9b91..bd3a2ba 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,11 +1,12 @@ Changes ------- -v4.0.0 02/14/2020 <3 -^^^^^^^^^^^^^^^^^^^^ +v3.0.0 02/14/2020 +^^^^^^^^^^^^^^^^^ -Note: This is the final release of ``jsonfield2``, as this fork is being merged -back into ``jsonfield``. +This release is a major rewrite of ``jsonfield``, merging in changes from the +``jsonfield2`` fork. Changelog entries for ``jsonfield2`` are included below +for completeness. - Add source distribution to release process - Update ``JSONEncoder`` from DRF @@ -15,28 +16,28 @@ back into ``jsonfield``. - Allow form field error message to be overridden - Obey form ``Field.disabled`` -v3.1.0 12/06/2019 -^^^^^^^^^^^^^^^^^ +jsonfield2 v3.1.0 12/06/2019 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - Fix use with ``select_related`` across a foreign key - Fix field deconstruction - Drop Python 3.5 support - Drop Django 2.1 (and below) support -v3.0.3 10/23/2019 -^^^^^^^^^^^^^^^^^ +jsonfield2 v3.0.3 10/23/2019 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - Add Python 3.8 & Django 3.0 support - Drop Python 3.4 support -v3.0.2 12/21/2018 -^^^^^^^^^^^^^^^^^ +jsonfield2 v3.0.2 12/21/2018 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - Add Python 3.7 & Django 2.1 support -v3.0.1 05/21/2018 -^^^^^^^^^^^^^^^^^ +jsonfield2 v3.0.1 05/21/2018 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - Fix model full_clean behavior -v3.0.0 05/07/2018 -^^^^^^^^^^^^^^^^^ +jsonfield2 v3.0.0 05/07/2018 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - Add Django 2.0 support - Drop Django 1.8, 1.9, and 1.10 support - Drop Python 2.7 and 3.3 support From 74d792364d5fd36da5ce24906ae37e28ef3d5eb7 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Fri, 14 Feb 2020 18:50:07 -0800 Subject: [PATCH 74/75] Move package under src directory --- setup.cfg | 2 +- setup.py | 5 +++-- {jsonfield => src/jsonfield}/__init__.py | 0 {jsonfield => src/jsonfield}/encoder.py | 0 {jsonfield => src/jsonfield}/fields.py | 0 {jsonfield => src/jsonfield}/forms.py | 0 {jsonfield => src/jsonfield}/json.py | 0 7 files changed, 4 insertions(+), 3 deletions(-) rename {jsonfield => src/jsonfield}/__init__.py (100%) rename {jsonfield => src/jsonfield}/encoder.py (100%) rename {jsonfield => src/jsonfield}/fields.py (100%) rename {jsonfield => src/jsonfield}/forms.py (100%) rename {jsonfield => src/jsonfield}/json.py (100%) diff --git a/setup.cfg b/setup.cfg index dc85308..ea461e8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,5 +16,5 @@ known_first_party = jsonfield branch = true source = jsonfield omit = - jsonfield/encoder.py + src/jsonfield/encoder.py tests diff --git a/setup.py b/setup.py index 1afc7c1..42caf3b 100644 --- a/setup.py +++ b/setup.py @@ -1,10 +1,9 @@ -from setuptools import setup +from setuptools import find_packages, setup setup( name='jsonfield', version='4.0.0', - packages=['jsonfield'], license='MIT', include_package_data=True, author='Brad Jasper', @@ -14,6 +13,8 @@ url='https://github.com/rpkilby/jsonfield/', description='A reusable Django field that allows you to store validated JSON in your model.', long_description=open("README.rst").read(), + packages=find_packages('src'), + package_dir={'': 'src'}, install_requires=['Django >= 2.2'], classifiers=[ 'Environment :: Web Environment', diff --git a/jsonfield/__init__.py b/src/jsonfield/__init__.py similarity index 100% rename from jsonfield/__init__.py rename to src/jsonfield/__init__.py diff --git a/jsonfield/encoder.py b/src/jsonfield/encoder.py similarity index 100% rename from jsonfield/encoder.py rename to src/jsonfield/encoder.py diff --git a/jsonfield/fields.py b/src/jsonfield/fields.py similarity index 100% rename from jsonfield/fields.py rename to src/jsonfield/fields.py diff --git a/jsonfield/forms.py b/src/jsonfield/forms.py similarity index 100% rename from jsonfield/forms.py rename to src/jsonfield/forms.py diff --git a/jsonfield/json.py b/src/jsonfield/json.py similarity index 100% rename from jsonfield/json.py rename to src/jsonfield/json.py From e05203c237205b13630afefd20ea17d2ef52df52 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Fri, 14 Feb 2020 19:01:23 -0800 Subject: [PATCH 75/75] Set major version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 42caf3b..ce2b9bc 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='jsonfield', - version='4.0.0', + version='3.0.0', license='MIT', include_package_data=True, author='Brad Jasper',