From 9736d02165a2ee9d8611366f88eceb5dc16784b6 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Fri, 4 Jul 2025 12:58:51 -0700 Subject: [PATCH 1/8] Update project configuration to pyproject.toml --- pyproject.toml | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++ setup.cfg | 20 ---------------- setup.py | 33 -------------------------- 3 files changed, 64 insertions(+), 53 deletions(-) create mode 100644 pyproject.toml delete mode 100644 setup.cfg delete mode 100644 setup.py diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..288daf3 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,64 @@ +[build-system] +requires = ["setuptools>=77"] +build-backend = "setuptools.build_meta" + + +[project] +name = "jsonfield" +version = "3.1.0" + +dependencies = ["django >= 4.2"] +requires-python = ">=3.10" + +authors = [{name = "Brad Jasper", email = "contact@bradjasper.com"}] +maintainers = [{name = "Ryan P Kilby", email = "kilbyr@gmail.com"}] +description = "A reusable Django field that allows you to store validated JSON in your model." +readme = "README.rst" +license = "MIT" +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Web Environment", + "Framework :: Django", + "Framework :: Django :: 4.2", + "Framework :: Django :: 5.0", + "Framework :: Django :: 5.1", + "Framework :: Django :: 5.2", + "Intended Audience :: Developers", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", +] + +[project.urls] +Repository = "https://github.com/rpkilby/jsonfield" +Issues = "https://github.com/rpkilby/jsonfield/issues" +Changelog = "https://github.com/rpkilby/jsonfield/blob/master/CHANGELOG" + + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.coverage.run] +branch = true +include = ["src/*", "tests/*"] +omit = ["src/jsonfield/encoder.py"] + +[tool.coverage.report] +show_missing = true + +[tool.flake8] +max-line-length = 120 +max-complexity = 10 + +[tool.isort] +profile = "black" +atomic = true +line_length = 120 +combine_as_imports = true +lines_after_imports = 2 +known_first_party = ["jsonfield", "tests"] +known_third_party = ["django"] +src_paths = ["src", "tests"] diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index ea461e8..0000000 --- a/setup.cfg +++ /dev/null @@ -1,20 +0,0 @@ -[flake8] -max_line_length = 120 -max_complexity = 10 -exclude = migrations - -[isort] -skip = .tox,migrations -atomic = true -line_length = 120 -multi_line_output = 3 -include_trailing_comma = true -known_third_party = django -known_first_party = jsonfield - -[coverage:run] -branch = true -source = jsonfield -omit = - src/jsonfield/encoder.py - tests diff --git a/setup.py b/setup.py deleted file mode 100644 index dce3828..0000000 --- a/setup.py +++ /dev/null @@ -1,33 +0,0 @@ -from setuptools import find_packages, setup - - -setup( - name='jsonfield', - version='3.1.0', - license='MIT', - include_package_data=True, - 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(), - packages=find_packages('src'), - package_dir={'': 'src'}, - install_requires=['Django >= 2.2'], - python_requires='>=3.6', - 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.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - ], -) From a546861a8bfcb1b365d58522302b045f7778f8ed Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Fri, 4 Jul 2025 12:59:04 -0700 Subject: [PATCH 2/8] Update tox config, migrate circleci => github actions --- .circleci/config.yml | 102 ------------------------------------- .github/workflows/main.yml | 67 ++++++++++++++++++++++++ tox.ini | 46 ++++++++++------- 3 files changed, 95 insertions(+), 120 deletions(-) delete mode 100644 .circleci/config.yml create mode 100644 .github/workflows/main.yml diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 0e19b31..0000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,102 +0,0 @@ -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.8 - - dist: - steps: - - checkout - - *environ - - *install - - run: | - python setup.py bdist_wheel - tox -e dist --installpkg ./dist/jsonfield-*.whl - tox -e dist - docker: - - image: circleci/python:3.8 - - test-py38: - <<: *test-steps - docker: - - image: circleci/python:3.8 - environment: - TOXFACTOR: py38 - - 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 - - -workflows: - version: 2 - commit: &test-workflow - jobs: - - lint - - dist: - requires: - - lint - - - test-py38: - requires: - - lint - - - test-py37: - requires: - - lint - - - test-py36: - 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/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..a2cf132 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,67 @@ +name: CI + +on: + push: + branches: + - master + pull_request: + + +jobs: + checks: + name: Run ${{ matrix.toxenv }} check + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.13"] + toxenv: ["dist", "lint", "warnings"] + continue-on-error: ${{ matrix.toxenv == 'warnings' }} + + steps: + - uses: actions/checkout@v4 + + - name: Setup Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + + - name: Install tox + run: python -m pip install tox + + - name: Run check (${{ matrix.toxenv }}) + run: tox -e ${{ matrix.toxenv }} + + + tests: + name: Test on Python ${{ matrix.python-version }} + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13"] + + steps: + - uses: actions/checkout@v4 + + - name: Setup Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + + - name: Install job dependencies + run: python -m pip install tox "coverage[toml]" + + - name: Run tests (${{ matrix.python-version }}) + run: tox run-parallel -f $(echo py${{ matrix.python-version }} | tr -d .) + + - name: Combine coverage + run: coverage combine + + - name: Report coverage + run: coverage report + + - name: Upload coverage + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/tox.ini b/tox.ini index 93aa33e..b7c8e9c 100644 --- a/tox.ini +++ b/tox.ini @@ -1,35 +1,45 @@ [tox] envlist = - py{36,37,38}-django22, - py{36,37,38}-django30, - isort,lint,dist,warnings, + py310-django{42,50,51,52}, + py311-django{42,50,51,52}, + py312-django{42,50,51,52}, + py313-django{51,52}, + dist,lint,warnings [testenv] -commands = coverage run --parallel-mode manage.py test {posargs} +commands = coverage run -p manage.py test {posargs: --no-input -v 2} usedevelop = True -envdir={toxworkdir}/v/{envname} setenv = - PYTHONDONTWRITEBYTECODE=1 + PYTHONDONTWRITEBYTECODE = 1 deps = - coverage - django22: Django~=2.2.0 - django30: Django~=3.0.0 + coverage[toml] + django42: Django~=4.2.0 + django50: Django~=5.0.0 + django51: Django~=5.1.0 + django52: Django~=5.2.0 -[testenv:isort] -commands = isort --check-only --recursive jsonfield tests {posargs:--diff} +[testenv:dist] +commands = + python manage.py test {posargs: --no-input -v 2} + python -m build + twine check dist/* +usedevelop = False +setenv = + PYTHONDONTWRITEBYTECODE = deps = - isort + build + twine [testenv:lint] -commands = flake8 jsonfield tests {posargs} +commands = + isort src tests --check-only --diff + flake8 src tests deps = + isort flake8 - -[testenv:dist] -commands = python manage.py test {posargs} -usedevelop = False + flake8-pyproject [testenv:warnings] -commands = python -Werror manage.py test {posargs} +commands = python -Werror manage.py test {posargs: --no-input -v 2} deps = https://github.com/django/django/archive/master.tar.gz From 77e4f93e5e4fabbd6045335025a34f96790378cb Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Fri, 4 Jul 2025 13:04:12 -0700 Subject: [PATCH 3/8] Appease linter, remove unneeded test migrations --- src/jsonfield/__init__.py | 1 + src/jsonfield/fields.py | 1 + tests/migrations/0001_initial.py | 110 ------------------------------- tests/migrations/__init__.py | 0 tests/settings.py | 1 + 5 files changed, 3 insertions(+), 110 deletions(-) delete mode 100644 tests/migrations/0001_initial.py delete mode 100644 tests/migrations/__init__.py diff --git a/src/jsonfield/__init__.py b/src/jsonfield/__init__.py index 1a0121d..3ad1c1f 100644 --- a/src/jsonfield/__init__.py +++ b/src/jsonfield/__init__.py @@ -1,3 +1,4 @@ from .fields import JSONCharField, JSONField + __all__ = ['JSONCharField', 'JSONField'] diff --git a/src/jsonfield/fields.py b/src/jsonfield/fields.py index 717acda..8fcb6ab 100644 --- a/src/jsonfield/fields.py +++ b/src/jsonfield/fields.py @@ -10,6 +10,7 @@ from .encoder import JSONEncoder from .json import JSONString, checked_loads + DEFAULT_DUMP_KWARGS = { 'cls': JSONEncoder, } diff --git a/tests/migrations/0001_initial.py b/tests/migrations/0001_initial.py deleted file mode 100644 index ad7794c..0000000 --- a/tests/migrations/0001_initial.py +++ /dev/null @@ -1,110 +0,0 @@ -# Generated by Django 3.0.3 on 2020-02-22 04:39 - -import collections -from django.db import migrations, models -import django.db.models.deletion -import jsonfield.fields -import tests.models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('contenttypes', '0002_remove_content_type_name'), - ] - - operations = [ - migrations.CreateModel( - name='CallableDefaultModel', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('json', jsonfield.fields.JSONField(default=tests.models.default)), - ], - ), - 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(max_length=100)), - ('default_json', jsonfield.fields.JSONCharField(default={'check': 34}, 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()), - ('default_json', jsonfield.fields.JSONField(default={'check': 12})), - ('complex_default_json', jsonfield.fields.JSONField(default=[{'checkcheck': 1212}])), - ('empty_default', jsonfield.fields.JSONField(blank=True, default={})), - ], - ), - 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, null=True)), - ], - ), - migrations.CreateModel( - name='JSONRequiredModel', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('json', jsonfield.fields.JSONField()), - ], - ), - migrations.CreateModel( - name='MTIParentModel', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('parent_data', jsonfield.fields.JSONField()), - ], - ), - migrations.CreateModel( - name='OrderedJSONModel', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('json', jsonfield.fields.JSONField(load_kwargs={'object_pairs_hook': collections.OrderedDict})), - ], - ), - migrations.CreateModel( - name='MTIChildModel', - fields=[ - ('mtiparentmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.MTIParentModel')), - ('child_data', jsonfield.fields.JSONField()), - ], - bases=('tests.mtiparentmodel',), - ), - 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')), - ], - ), - migrations.CreateModel( - name='JSONModelWithForeignKey', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('json', jsonfield.fields.JSONField(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 deleted file mode 100644 index e69de29..0000000 diff --git a/tests/settings.py b/tests/settings.py index 42f1cf3..aa00582 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -1,5 +1,6 @@ import os + BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) From 31fd978ede57ee95e1bb408b2598301d25914c2d Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Fri, 4 Jul 2025 13:11:22 -0700 Subject: [PATCH 4/8] Update readme badges, remove version support section --- README.rst | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/README.rst b/README.rst index d9d23b0..ac5a43a 100644 --- a/README.rst +++ b/README.rst @@ -1,13 +1,15 @@ 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 +.. image:: https://github.com/rpkilby/jsonfield/actions/workflows/main.yml/badge.svg + :target: https://github.com/rpkilby/jsonfield/actions/workflows/main.yml +.. image:: https://codecov.io/gh/rpkilby/jsonfield/graph/badge.svg :target: https://codecov.io/gh/rpkilby/jsonfield +.. image:: https://img.shields.io/pypi/l/jsonfield.svg + :target: https://pypi.org/project/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 +.. image:: https://img.shields.io/pypi/pyversions/jsonfield.svg :target: https://pypi.org/project/jsonfield **jsonfield** is a reusable model field that allows you to store validated JSON, automatically handling @@ -21,17 +23,6 @@ serialization to and from the database. To use, add ``jsonfield.JSONField`` to o .. _introduced: https://docs.djangoproject.com/en/stable/releases/3.1/#jsonfield-for-all-supported-database-backends -Requirements ------------- - -**jsonfield** aims to support all current `versions of Django`_, however the explicitly tested versions are: - -* **Python:** 3.6, 3.7, 3.8 -* **Django:** 2.2, 3.0 - -.. _versions of Django: https://www.djangoproject.com/download/#supported-versions - - Installation ------------ From 94ba21ee1bf11b95f3a1ce25b179640a750eb16f Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Fri, 4 Jul 2025 13:11:34 -0700 Subject: [PATCH 5/8] Update changelog --- CHANGES.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 2a039b3..326b7cd 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,13 @@ Changes ------- +Unreleased +^^^^^^^^^^ + +- Set Python version support to 3.10 through 3.13 +- Set Django version support to 4.2 through 5.2 +- Modernize packaging to use `pyproject.toml` + v3.1.0 02/22/2020 ^^^^^^^^^^^^^^^^^ - Handle loading invalid JSON from db From 1577f35ec99c1bdefecbaac99c70fed597d775b9 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Fri, 4 Jul 2025 13:16:04 -0700 Subject: [PATCH 6/8] Update test settings --- tests/settings.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/settings.py b/tests/settings.py index aa00582..4237135 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -24,6 +24,9 @@ } +DEFAULT_AUTO_FIELD = "django.db.models.AutoField" + + # Internationalization # https://docs.djangoproject.com/en/dev/topics/i18n/ From 1088b229da5c084cad6159660277b166204cd562 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Fri, 4 Jul 2025 13:22:41 -0700 Subject: [PATCH 7/8] Update readme tox example --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index ac5a43a..ae2a7d1 100644 --- a/README.rst +++ b/README.rst @@ -119,11 +119,11 @@ Then, run the ``tox`` command, which will run all test jobs. $ tox -Or, to test just one job (for example Django 2.0 on Python 3.6): +Or, to test just one job (for example Django 5.2 on Python 3.13): .. code-block:: shell - $ tox -e py36-django20 + $ tox -e py313-django52 Release Process From 64d53cac54a6290ed60b7134e6e973a4efd71e61 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Fri, 4 Jul 2025 13:49:28 -0700 Subject: [PATCH 8/8] Remove unused pathways in complex encoder tests --- tests/models.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/tests/models.py b/tests/models.py index 61fd34f..df57f95 100644 --- a/tests/models.py +++ b/tests/models.py @@ -9,21 +9,16 @@ 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 default(self, o): + return { + '__complex__': True, + 'real': o.real, + 'imag': o.imag, + } def as_complex(dct): - if '__complex__' in dct: - return complex(dct['real'], dct['imag']) - return dct + return complex(dct['real'], dct['imag']) class GenericForeignKeyObj(models.Model):