From 7d9888ba40fd1d638d2119ca42a8c81229255f38 Mon Sep 17 00:00:00 2001 From: Nicholas Bunn Date: Wed, 14 Aug 2024 15:26:25 +0100 Subject: [PATCH 01/44] Update README to reference the original repository Just a back reference to the original author and repo --- README.md | 42 ++++++++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index cf5adf0..a63ccb3 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ -# djangorestframework-recursive +# Django Rest Framework Recursive (Fork) + +This repository is the friendly fork of the original [django-rest-framework-recursive](https://github.com/heywbj/django-rest-framework-recursive) by [heywbj](https://github.com/heywbj). As the original repo is no longer being actively maintained, we've friendly forked it here to undertake on maintenance for modern versions of Python, Django, and Django Rest Framework. [![build-status-image]][travis] [![pypi-version]][pypi] @@ -8,11 +10,13 @@ Recursive Serialization for Django REST framework This package provides a `RecursiveField` that enables you to serialize a tree, -linked list, or even a directed acyclic graph. Also supports validation, +linked list, or even a directed acyclic graph. It also supports validation, deserialization, ModelSerializers, and multi-step recursive structures. -## Example +## Examples + +### Tree Recursion ```python from rest_framework import serializers @@ -23,8 +27,18 @@ class TreeSerializer(serializers.Serializer): children = serializers.ListField(child=RecursiveField()) ``` -see [**here**][tests] for more usage examples +### Linked List Recursion +```python +from rest_framework import serializers +from rest_framework_recursive.fields import RecursiveField + +class LinkSerializer(serializers.Serializer): + name = serializers.CharField(max_length=25) + next = RecursiveField(allow_null=True) +``` + +Further use cases are documented in the tests, see [**here**][tests] for more usage examples ## Requirements @@ -37,8 +51,8 @@ see [**here**][tests] for more usage examples Install using `pip`... -```bash -$ pip install djangorestframework-recursive +``` +pip install django-rest-framework-recursive ``` ## Release notes @@ -53,20 +67,20 @@ $ pip install djangorestframework-recursive Install testing requirements. -```bash -$ pip install -r requirements.txt +``` +pip install -r requirements.txt ``` Run with runtests. -```bash -$ ./runtests.py +``` +./runtests.py ``` -You can also use the excellent [tox](http://tox.readthedocs.org/en/latest/) testing tool to run the tests against all supported versions of Python and Django. Install tox globally, and then simply run: +You can also use [tox](http://tox.readthedocs.org/en/latest/) to run the tests against all supported versions of Python and Django. Install tox globally with `pip install tox`, and then simply run: -```bash -$ tox +``` +tox ``` @@ -74,4 +88,4 @@ $ tox [travis]: http://travis-ci.org/heywbj/django-rest-framework-recursive?branch=master [pypi-version]: https://img.shields.io/pypi/v/djangorestframework-recursive.svg [pypi]: https://pypi.python.org/pypi/djangorestframework-recursive -[tests]: https://github.com/heywbj/django-rest-framework-recursive/blob/master/tests/test_recursive.py +[tests]: https://github.com/heywbj/django-rest-framework-recursive/blob/master/tests/test_recursive.py \ No newline at end of file From 750a50f94a6a96131ae70f083a48437abcf03914 Mon Sep 17 00:00:00 2001 From: Nicholas Bunn Date: Wed, 14 Aug 2024 16:00:18 +0100 Subject: [PATCH 02/44] Remove Python 2 support Testing that the branch protection is in place by removing Python 2 support --- tox.ini | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 190f159..7301dea 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,6 @@ [tox] envlist = - {py27,py34,py35,py36}-django{1.8,1.9,1.10}-drf{3.3,3.4,3.5,3.6} - + {py34,py35,py36}-django{1.8,1.9,1.10}-drf{3.3,3.4,3.5,3.6} [testenv] commands = ./runtests.py --fast setenv = From 982818925697316f950acbfc9bdf4356836c7d5a Mon Sep 17 00:00:00 2001 From: Nicholas Bunn Date: Wed, 14 Aug 2024 16:01:45 +0100 Subject: [PATCH 03/44] Pin requirements Pin requirements for version 3.6 and update the tox configuration to make it easier to update to Python 10. --- requirements.txt | 13 ++++++++----- tox.ini | 23 ++++++++++++----------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/requirements.txt b/requirements.txt index e766772..0b39558 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,11 +1,14 @@ # Minimum Django and REST framework version -Django>=1.6 -djangorestframework>=3.0.0 +Django>=1.6; python_version <= '3.6' + +djangorestframework>=3.0.0; python_version <= '3.6' # Test requirements -pytest-django==2.9.1 -pytest==2.8.5 +pytest-django==2.9.1; python_version <= '3.6' + +pytest==2.8.5; python_version <= '3.6' + pytest-cov==2.2.0 # wheel for PyPI installs -wheel==0.24.0 +wheel==0.24.0 \ No newline at end of file diff --git a/tox.ini b/tox.ini index 7301dea..60ef637 100644 --- a/tox.ini +++ b/tox.ini @@ -1,17 +1,18 @@ [tox] envlist = - {py34,py35,py36}-django{1.8,1.9,1.10}-drf{3.3,3.4,3.5,3.6} + py{34,35,36}-django{1.8,1.9,1.10}-drf{3.3,3.4,3.5,3.6} [testenv] -commands = ./runtests.py --fast +allowlist_externals = python +commands = python ./runtests.py --fast setenv = PYTHONDONTWRITEBYTECODE=1 deps = - django1.8: Django>=1.8,<1.9 - django1.9: Django>=1.9,<1.10 - django1.10: Django>=1.10,<1.11 - django1.11: Django>=1.11,<2.0 - drf3.3: djangorestframework>=3.3,<3.4 - drf3.4: djangorestframework>=3.4,<3.5 - drf3.5: djangorestframework>=3.5,<3.6 - drf3.6: djangorestframework>=3.6,<3.7 - pytest-django==3.1.2 + django1.8: Django>=1.8,<1.9; python_version <= '3.6' + django1.9: Django>=1.9,<1.10; python_version <= '3.6' + django1.10: Django>=1.10,<1.11; python_version <= '3.6' + django1.11: Django>=1.11,<2.0; python_version <= '3.6' + drf3.3: djangorestframework>=3.3,<3.4; python_version <= '3.6' + drf3.4: djangorestframework>=3.4,<3.5; python_version <= '3.6' + drf3.5: djangorestframework>=3.5,<3.6; python_version <= '3.6' + drf3.6: djangorestframework>=3.6,<3.7; python_version <= '3.6' + pytest-django==3.1.2; python_version <= '3.6' \ No newline at end of file From 1bda770229e4424374eedbed042eaf66a2aa0776 Mon Sep 17 00:00:00 2001 From: Nicholas Bunn Date: Wed, 14 Aug 2024 16:02:47 +0100 Subject: [PATCH 04/44] Add support for 3.10 Adds support in the tests for 3.10, and updates the configuration to support updates in newer versions of DRF where null fields are treated more consistently --- requirements.txt | 9 +- tests/test_recursive.py | 233 +++++++++++++++++++++++----------------- tox.ini | 8 +- 3 files changed, 147 insertions(+), 103 deletions(-) diff --git a/requirements.txt b/requirements.txt index 0b39558..cf04c55 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,14 +1,15 @@ # Minimum Django and REST framework version Django>=1.6; python_version <= '3.6' +Django>=4.0; python_version >= '3.10' djangorestframework>=3.0.0; python_version <= '3.6' +djangorestframework>=3.15.0; python_version >= '3.10' # Test requirements pytest-django==2.9.1; python_version <= '3.6' +pytest-django==4.7.0; python_version >= '3.7' and python_version <= '3.10' pytest==2.8.5; python_version <= '3.6' +pytest==8.3.2; python_version >= '3.8' and python_version <= '3.10' -pytest-cov==2.2.0 - -# wheel for PyPI installs -wheel==0.24.0 \ No newline at end of file +pytest-cov==2.2.0 \ No newline at end of file diff --git a/tests/test_recursive.py b/tests/test_recursive.py index ec7d530..8132159 100644 --- a/tests/test_recursive.py +++ b/tests/test_recursive.py @@ -1,3 +1,6 @@ +import sys +from copy import deepcopy + from django.db import models from rest_framework import serializers from rest_framework_recursive.fields import RecursiveField @@ -20,7 +23,7 @@ class ManyNullSerializer(serializers.Serializer): class PingSerializer(serializers.Serializer): ping_id = serializers.IntegerField() - pong = RecursiveField('PongSerializer', required=False) + pong = RecursiveField("PongSerializer", required=False) class PongSerializer(serializers.Serializer): @@ -29,19 +32,16 @@ class PongSerializer(serializers.Serializer): class SillySerializer(serializers.Serializer): - name = RecursiveField( - 'rest_framework.fields.CharField', max_length=5) - blankable = RecursiveField( - 'rest_framework.fields.CharField', allow_blank=True) - nullable = RecursiveField( - 'rest_framework.fields.CharField', allow_null=True) - links = RecursiveField('LinkSerializer') + name = RecursiveField("rest_framework.fields.CharField", max_length=5) + blankable = RecursiveField("rest_framework.fields.CharField", allow_blank=True) + nullable = RecursiveField("rest_framework.fields.CharField", allow_null=True) + links = RecursiveField("LinkSerializer") self = RecursiveField(required=False) class RecursiveModel(models.Model): name = models.CharField(max_length=255) - parent = models.ForeignKey('self', null=True, on_delete=models.CASCADE) + parent = models.ForeignKey("self", null=True, on_delete=models.CASCADE) class RecursiveModelSerializer(serializers.ModelSerializer): @@ -49,36 +49,75 @@ class RecursiveModelSerializer(serializers.ModelSerializer): class Meta: model = RecursiveModel - fields = ('name', 'parent') + fields = ("name", "parent") class TestRecursiveField: @staticmethod def serialize(serializer_class, value): + def _recursively_populate_nullable_field(serializer, instance_type): + """ + Recursively populate the nullable fields with None, this allows + us to test for newer versions of DRF. + """ + for key, val in instance_type().get_fields().items(): + # If the field is recursive, step further + if isinstance(val, RecursiveField): + if val.to is not None: + pass + elif serializer.get(key) is None: + # If the field is not set, explicitly set it to None + serializer[key] = None + elif isinstance(serializer[key], list): + serializer[key] = [ + _recursively_populate_nullable_field( + serializer_item, instance_type + ) + for serializer_item in serializer[key] + ] + else: + # Step further until we get to a leaf + serializer[key] = _recursively_populate_nullable_field( + serializer[key], instance_type + ) + + return serializer + serializer = serializer_class(value) - assert serializer.data == value, \ - 'serialized data does not match input' + # We need to be able to populate nullable fields with None in the payload + # this needs to be done recursively + if sys.version_info <= (3, 6): + assert serializer.data == value, "serialized data does not match input" + else: + updated_payload = _recursively_populate_nullable_field( + value, serializer_class + ) + assert ( + serializer.data == updated_payload + ), "serialized data does not match input" @staticmethod def deserialize(serializer_class, data): serializer = serializer_class(data=data) - assert serializer.is_valid(), \ - 'cannot validate on deserialization: %s' % dict(serializer.errors) - assert serializer.validated_data == data, \ - 'deserialized data does not match input' + assert serializer.is_valid(), "cannot validate on deserialization: %s" % dict( + serializer.errors + ) + assert ( + serializer.validated_data == data + ), "deserialized data does not match input" def test_link_serializer(self): value = { - 'name': 'first', - 'next': { - 'name': 'second', - 'next': { - 'name': 'third', - 'next': None, - } - } + "name": "first", + "next": { + "name": "second", + "next": { + "name": "third", + "next": None, + }, + }, } self.serialize(LinkSerializer, value) @@ -86,14 +125,17 @@ def test_link_serializer(self): def test_node_serializer(self): value = { - 'name': 'root', - 'children': [{ - 'name': 'first child', - 'children': [], - }, { - 'name': 'second child', - 'children': [], - }] + "name": "root", + "children": [ + { + "name": "first child", + "children": [], + }, + { + "name": "second child", + "children": [], + }, + ], } self.serialize(NodeSerializer, value) @@ -103,20 +145,18 @@ def test_many_null_serializer(self): """Test that allow_null is propagated when many=True""" # Children is omitted from the root node - value = { - 'name': 'root' - } + value = {"name": "root"} self.serialize(ManyNullSerializer, value) self.deserialize(ManyNullSerializer, value) # Children is omitted from the child nodes value2 = { - 'name': 'root', - 'children':[ - {'name': 'child1'}, - {'name': 'child2'}, - ] + "name": "root", + "children": [ + {"name": "child1"}, + {"name": "child2"}, + ], } self.serialize(ManyNullSerializer, value2) @@ -124,13 +164,13 @@ def test_many_null_serializer(self): def test_ping_pong(self): pong = { - 'pong_id': 4, - 'ping': { - 'ping_id': 3, - 'pong': { - 'pong_id': 2, - 'ping': { - 'ping_id': 1, + "pong_id": 4, + "ping": { + "ping_id": 3, + "pong": { + "pong_id": 2, + "ping": { + "ping_id": 1, }, }, }, @@ -140,73 +180,72 @@ def test_ping_pong(self): def test_validation(self): value = { - 'name': 'good', - 'blankable': '', - 'nullable': None, - 'links': { - 'name': 'something', - 'next': { - 'name': 'inner something', - 'next': None, - } - } + "name": "good", + "blankable": "", + "nullable": None, + "links": { + "name": "something", + "next": { + "name": "inner something", + "next": None, + }, + }, } - self.serialize(SillySerializer, value) + self.serialize(SillySerializer, deepcopy(value)) self.deserialize(SillySerializer, value) max_length = { - 'name': 'too long', - 'blankable': 'not blank', - 'nullable': 'not null', - 'links': { - 'name': 'something', - 'next': None, - } + "name": "too long", + "blankable": "not blank", + "nullable": "not null", + "links": { + "name": "something", + "next": None, + }, } serializer = SillySerializer(data=max_length) - assert not serializer.is_valid(), \ - 'validation should fail due to name too long' + assert not serializer.is_valid(), "validation should fail due to name too long" nulled_out = { - 'name': 'good', - 'blankable': None, - 'nullable': 'not null', - 'links': { - 'name': 'something', - 'next': None, - } + "name": "good", + "blankable": None, + "nullable": "not null", + "links": { + "name": "something", + "next": None, + }, } serializer = SillySerializer(data=nulled_out) - assert not serializer.is_valid(), \ - 'validation should fail due to null field' + assert not serializer.is_valid(), "validation should fail due to null field" way_too_long = { - 'name': 'good', - 'blankable': '', - 'nullable': None, - 'links': { - 'name': 'something', - 'next': { - 'name': 'inner something that is much too long', - 'next': None, - } - } + "name": "good", + "blankable": "", + "nullable": None, + "links": { + "name": "something", + "next": { + "name": "inner something that is much too long", + "next": None, + }, + }, } serializer = SillySerializer(data=way_too_long) - assert not serializer.is_valid(), \ - 'validation should fail on inner link validation' + assert ( + not serializer.is_valid() + ), "validation should fail on inner link validation" def test_model_serializer(self): - one = RecursiveModel(name='one') - two = RecursiveModel(name='two', parent=one) + one = RecursiveModel(name="one") + two = RecursiveModel(name="two", parent=one) # serialization representation = { - 'name': 'two', - 'parent': { - 'name': 'one', - 'parent': None, - } + "name": "two", + "parent": { + "name": "one", + "parent": None, + }, } s = RecursiveModelSerializer(two) @@ -219,5 +258,5 @@ def test_super_kwargs(self): """RecursiveField.__init__ introspect the parent constructor to pass kwargs properly. default is used used here to verify that the argument is properly passed to the super Field.""" - field = RecursiveField(default='a default value') - assert field.default == 'a default value' + field = RecursiveField(default="a default value") + assert field.default == "a default value" diff --git a/tox.ini b/tox.ini index 60ef637..4d90e51 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,7 @@ [tox] envlist = - py{34,35,36}-django{1.8,1.9,1.10}-drf{3.3,3.4,3.5,3.6} + py{27,34,35,36}-django{1.8,1.9,1.10}-drf{3.3,3.4,3.5,3.6} + py{310}-django{4}-drf{3.15} [testenv] allowlist_externals = python commands = python ./runtests.py --fast @@ -11,8 +12,11 @@ deps = django1.9: Django>=1.9,<1.10; python_version <= '3.6' django1.10: Django>=1.10,<1.11; python_version <= '3.6' django1.11: Django>=1.11,<2.0; python_version <= '3.6' + django4: Django>=4.0,<5.0; python_version >= '3.10' drf3.3: djangorestframework>=3.3,<3.4; python_version <= '3.6' drf3.4: djangorestframework>=3.4,<3.5; python_version <= '3.6' drf3.5: djangorestframework>=3.5,<3.6; python_version <= '3.6' drf3.6: djangorestframework>=3.6,<3.7; python_version <= '3.6' - pytest-django==3.1.2; python_version <= '3.6' \ No newline at end of file + drf3.15: djangorestframework>=3.15,<3.16; python_version >= '3.10' + pytest-django==3.1.2; python_version <= '3.6' + pytest-django==4.7.0; python_version >= '3.10' \ No newline at end of file From 60719bb07f9356fad385cf7bc8cd4d73b2a8d9d8 Mon Sep 17 00:00:00 2001 From: Nicholas Bunn Date: Wed, 14 Aug 2024 16:04:22 +0100 Subject: [PATCH 05/44] Remove support for Python versions below 3.10 Stripping support for older versions of Python, and related Django & DRF configurations --- requirements.txt | 8 ++------ tests/test_recursive.py | 22 +++++++--------------- tox.ini | 10 ---------- 3 files changed, 9 insertions(+), 31 deletions(-) diff --git a/requirements.txt b/requirements.txt index cf04c55..fa51a5e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,15 +1,11 @@ # Minimum Django and REST framework version -Django>=1.6; python_version <= '3.6' Django>=4.0; python_version >= '3.10' -djangorestframework>=3.0.0; python_version <= '3.6' djangorestframework>=3.15.0; python_version >= '3.10' # Test requirements -pytest-django==2.9.1; python_version <= '3.6' -pytest-django==4.7.0; python_version >= '3.7' and python_version <= '3.10' +pytest-django==4.7.0; python_version <= '3.10' -pytest==2.8.5; python_version <= '3.6' -pytest==8.3.2; python_version >= '3.8' and python_version <= '3.10' +pytest==8.3.2; python_version <= '3.10' pytest-cov==2.2.0 \ No newline at end of file diff --git a/tests/test_recursive.py b/tests/test_recursive.py index 8132159..bfeebf4 100644 --- a/tests/test_recursive.py +++ b/tests/test_recursive.py @@ -1,4 +1,3 @@ -import sys from copy import deepcopy from django.db import models @@ -55,7 +54,7 @@ class Meta: class TestRecursiveField: @staticmethod def serialize(serializer_class, value): - def _recursively_populate_nullable_field(serializer, instance_type): + def _recursively_populate_nullable_fields(serializer, instance_type): """ Recursively populate the nullable fields with None, this allows us to test for newer versions of DRF. @@ -70,14 +69,14 @@ def _recursively_populate_nullable_field(serializer, instance_type): serializer[key] = None elif isinstance(serializer[key], list): serializer[key] = [ - _recursively_populate_nullable_field( + _recursively_populate_nullable_fields( serializer_item, instance_type ) for serializer_item in serializer[key] ] else: # Step further until we get to a leaf - serializer[key] = _recursively_populate_nullable_field( + serializer[key] = _recursively_populate_nullable_fields( serializer[key], instance_type ) @@ -85,17 +84,10 @@ def _recursively_populate_nullable_field(serializer, instance_type): serializer = serializer_class(value) - # We need to be able to populate nullable fields with None in the payload - # this needs to be done recursively - if sys.version_info <= (3, 6): - assert serializer.data == value, "serialized data does not match input" - else: - updated_payload = _recursively_populate_nullable_field( - value, serializer_class - ) - assert ( - serializer.data == updated_payload - ), "serialized data does not match input" + updated_payload = _recursively_populate_nullable_fields(value, serializer_class) + assert ( + serializer.data == updated_payload + ), "serialized data does not match input" @staticmethod def deserialize(serializer_class, data): diff --git a/tox.ini b/tox.ini index 4d90e51..49cd950 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,5 @@ [tox] envlist = - py{27,34,35,36}-django{1.8,1.9,1.10}-drf{3.3,3.4,3.5,3.6} py{310}-django{4}-drf{3.15} [testenv] allowlist_externals = python @@ -8,15 +7,6 @@ commands = python ./runtests.py --fast setenv = PYTHONDONTWRITEBYTECODE=1 deps = - django1.8: Django>=1.8,<1.9; python_version <= '3.6' - django1.9: Django>=1.9,<1.10; python_version <= '3.6' - django1.10: Django>=1.10,<1.11; python_version <= '3.6' - django1.11: Django>=1.11,<2.0; python_version <= '3.6' django4: Django>=4.0,<5.0; python_version >= '3.10' - drf3.3: djangorestframework>=3.3,<3.4; python_version <= '3.6' - drf3.4: djangorestframework>=3.4,<3.5; python_version <= '3.6' - drf3.5: djangorestframework>=3.5,<3.6; python_version <= '3.6' - drf3.6: djangorestframework>=3.6,<3.7; python_version <= '3.6' drf3.15: djangorestframework>=3.15,<3.16; python_version >= '3.10' - pytest-django==3.1.2; python_version <= '3.6' pytest-django==4.7.0; python_version >= '3.10' \ No newline at end of file From 720db6b683fbdef2cb9b7b174be71ce44e91cbca Mon Sep 17 00:00:00 2001 From: Nicholas Bunn Date: Wed, 14 Aug 2024 16:04:22 +0100 Subject: [PATCH 06/44] Remove support for Python versions below 3.10 Stripping support for older versions of Python, and related Django & DRF configurations --- requirements.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index fa51a5e..b53fcbb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,11 +1,11 @@ # Minimum Django and REST framework version -Django>=4.0; python_version >= '3.10' +Django>=3.2; python_version >= '3.10' -djangorestframework>=3.15.0; python_version >= '3.10' +djangorestframework>=3.12.0; python_version >= '3.10' # Test requirements -pytest-django==4.7.0; python_version <= '3.10' +pytest-django==4.7.0; python_version >= '3.10' -pytest==8.3.2; python_version <= '3.10' +pytest==8.3.2; python_version >= '3.10' pytest-cov==2.2.0 \ No newline at end of file From da57288012bdcd875869e85ce886bd84426a3823 Mon Sep 17 00:00:00 2001 From: Nicholas Bunn Date: Wed, 14 Aug 2024 16:13:46 +0100 Subject: [PATCH 07/44] Add full Python 3.10 support Parametrised the tox file to run tests for all valid combinations of Python 3.10, Django, and Django Rest Framework --- tox.ini | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/tox.ini b/tox.ini index 49cd950..53e91ab 100644 --- a/tox.ini +++ b/tox.ini @@ -1,12 +1,26 @@ [tox] -envlist = - py{310}-django{4}-drf{3.15} +envlist = + py310-django{32}-drf{312} # DRF 3.12 is not supported beyond Django 3.2 so it's getting it's own configuration here + py310-django{32,40,41,42}-drf{313,314,315} + py310-django{5}-drf{314,315} # Django 5 is not supported for DRF < 3.13 so it's getting it's own configuration here + [testenv] allowlist_externals = python commands = python ./runtests.py --fast setenv = PYTHONDONTWRITEBYTECODE=1 +basepython = + py310: python3.10 deps = - django4: Django>=4.0,<5.0; python_version >= '3.10' - drf3.15: djangorestframework>=3.15,<3.16; python_version >= '3.10' - pytest-django==4.7.0; python_version >= '3.10' \ No newline at end of file + django32: Django>=3.2,<3.3 + django40: Django>=4.0,<4.1 + django41: Django>=4.1,<4.2 + django42: Django>=4.2,<4.3 + django5: Django>=5.0,<5.1 + + drf312: djangorestframework>=3.12,<3.13 + drf313: djangorestframework>=3.13,<3.14 + drf314: djangorestframework>=3.14,<3.15 + drf315: djangorestframework>=3.15,<3.16 + + pytest-django==4.7.0 \ No newline at end of file From c82a2f72c2caaae6f670b87130518ac44c9eca99 Mon Sep 17 00:00:00 2001 From: Nicholas Bunn Date: Wed, 14 Aug 2024 16:30:13 +0100 Subject: [PATCH 08/44] Add codeowners --- CODEOWNERS | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 CODEOWNERS diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..e2a6df4 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,4 @@ +# Example of a CODEOWNERS file + +# Specify owners for the entire repository +* @nicholasbunn @isabellanorris \ No newline at end of file From 6bf51ccdbf1370f3289d14d3e1bab1cf73cbcb41 Mon Sep 17 00:00:00 2001 From: Nicholas Bunn Date: Tue, 20 Aug 2024 09:12:31 +0100 Subject: [PATCH 09/44] Move files into expected dirs Move the actual package code into a src directory following the Kraken open source package convention, update the test directory structure to support our test tooling --- rest_framework_recursive/__init__.py | 1 - .../__init__.py | 6 ++ .../fields.py | 44 +++++----- .../django_rest_framework_recursive/py.typed | 0 tests/conftest.py | 86 ------------------- .../__init__.py | 0 .../conftest.py | 81 +++++++++++++++++ .../test_package.py} | 19 ++-- 8 files changed, 113 insertions(+), 124 deletions(-) delete mode 100644 rest_framework_recursive/__init__.py create mode 100644 src/kraken/django_rest_framework_recursive/__init__.py rename {rest_framework_recursive => src/kraken/django_rest_framework_recursive}/fields.py (81%) rename tests/models.py => src/kraken/django_rest_framework_recursive/py.typed (100%) delete mode 100644 tests/conftest.py create mode 100644 tests/django_rest_framework_recursive/__init__.py create mode 100644 tests/django_rest_framework_recursive/conftest.py rename tests/{test_recursive.py => django_rest_framework_recursive/test_package.py} (93%) diff --git a/rest_framework_recursive/__init__.py b/rest_framework_recursive/__init__.py deleted file mode 100644 index 10939f0..0000000 --- a/rest_framework_recursive/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = '0.1.2' diff --git a/src/kraken/django_rest_framework_recursive/__init__.py b/src/kraken/django_rest_framework_recursive/__init__.py new file mode 100644 index 0000000..df89e9b --- /dev/null +++ b/src/kraken/django_rest_framework_recursive/__init__.py @@ -0,0 +1,6 @@ +""" +This package provides a `RecursiveField` that allows you to serialize a tree, +linked list, or even a directed acyclic graph, where the children may be the +same type as the parent. It also supports validation, deserialization, +ModelSerializers, and multi-step recursive structures. +""" diff --git a/rest_framework_recursive/fields.py b/src/kraken/django_rest_framework_recursive/fields.py similarity index 81% rename from rest_framework_recursive/fields.py rename to src/kraken/django_rest_framework_recursive/fields.py index 6d717f2..2f15944 100644 --- a/rest_framework_recursive/fields.py +++ b/src/kraken/django_rest_framework_recursive/fields.py @@ -1,5 +1,6 @@ -import inspect import importlib +import inspect + from rest_framework.fields import Field from rest_framework.serializers import BaseSerializer @@ -39,19 +40,18 @@ class ListSerializer(self): # `rest_framework.serializers` calls to on a field object PROXIED_ATTRS = ( # methods - 'get_value', - 'get_initial', - 'run_validation', - 'get_attribute', - 'to_representation', - + "get_value", + "get_initial", + "run_validation", + "get_attribute", + "to_representation", # attributes - 'field_name', - 'source', - 'read_only', - 'default', - 'source_attrs', - 'write_only', + "field_name", + "source", + "read_only", + "default", + "source_attrs", + "write_only", ) def __init__(self, to=None, **kwargs): @@ -68,9 +68,7 @@ def __init__(self, to=None, **kwargs): # need to call super-constructor to support ModelSerializer super_kwargs = dict( - (key, kwargs[key]) - for key in kwargs - if key in _signature_parameters(Field.__init__) + (key, kwargs[key]) for key in kwargs if key in _signature_parameters(Field.__init__) ) super(RecursiveField, self).__init__(**super_kwargs) @@ -85,7 +83,7 @@ def proxied(self): if self.bind_args: field_name, parent = self.bind_args - if hasattr(parent, 'child') and parent.child is self: + if hasattr(parent, "child") and parent.child is self: # RecursiveField nested inside of a ListField parent_class = parent.parent.__class__ else: @@ -98,28 +96,26 @@ def proxied(self): proxied_class = parent_class else: try: - module_name, class_name = self.to.rsplit('.', 1) + module_name, class_name = self.to.rsplit(".", 1) except ValueError: module_name, class_name = parent_class.__module__, self.to try: - proxied_class = getattr( - importlib.import_module(module_name), class_name) + proxied_class = getattr(importlib.import_module(module_name), class_name) except Exception as e: - raise ImportError( - 'could not locate serializer %s' % self.to, e) + raise ImportError("could not locate serializer %s" % self.to, e) # Create a new serializer instance and proxy it proxied = proxied_class(**self.init_kwargs) proxied.bind(field_name, parent) self._proxied = proxied - + return self._proxied def __getattribute__(self, name): if name in RecursiveField.PROXIED_ATTRS: try: - proxied = object.__getattribute__(self, 'proxied') + proxied = object.__getattribute__(self, "proxied") return getattr(proxied, name) except AttributeError: pass diff --git a/tests/models.py b/src/kraken/django_rest_framework_recursive/py.typed similarity index 100% rename from tests/models.py rename to src/kraken/django_rest_framework_recursive/py.typed diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index 31142ea..0000000 --- a/tests/conftest.py +++ /dev/null @@ -1,86 +0,0 @@ -def pytest_configure(): - from django.conf import settings - - settings.configure( - DEBUG_PROPAGATE_EXCEPTIONS=True, - DATABASES={'default': {'ENGINE': 'django.db.backends.sqlite3', - 'NAME': ':memory:'}}, - SITE_ID=1, - SECRET_KEY='not very secret in tests', - USE_I18N=True, - USE_L10N=True, - STATIC_URL='/static/', - ROOT_URLCONF='tests.urls', - TEMPLATE_LOADERS=( - 'django.template.loaders.filesystem.Loader', - 'django.template.loaders.app_directories.Loader', - ), - MIDDLEWARE_CLASSES=( - 'django.middleware.common.CommonMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - ), - INSTALLED_APPS=( - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.sites', - 'django.contrib.messages', - 'django.contrib.staticfiles', - - 'rest_framework', - 'rest_framework.authtoken', - 'tests', - ), - PASSWORD_HASHERS=( - 'django.contrib.auth.hashers.SHA1PasswordHasher', - 'django.contrib.auth.hashers.PBKDF2PasswordHasher', - 'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher', - 'django.contrib.auth.hashers.BCryptPasswordHasher', - 'django.contrib.auth.hashers.MD5PasswordHasher', - 'django.contrib.auth.hashers.CryptPasswordHasher', - ), - ) - - try: - import oauth_provider # NOQA - import oauth2 # NOQA - except ImportError: - pass - else: - settings.INSTALLED_APPS += ( - 'oauth_provider', - ) - - try: - import provider # NOQA - except ImportError: - pass - else: - settings.INSTALLED_APPS += ( - 'provider', - 'provider.oauth2', - ) - - # guardian is optional - try: - import guardian # NOQA - except ImportError: - pass - else: - settings.ANONYMOUS_USER_ID = -1 - settings.AUTHENTICATION_BACKENDS = ( - 'django.contrib.auth.backends.ModelBackend', - 'guardian.backends.ObjectPermissionBackend', - ) - settings.INSTALLED_APPS += ( - 'guardian', - ) - - try: - import django - django.setup() - except AttributeError: - pass diff --git a/tests/django_rest_framework_recursive/__init__.py b/tests/django_rest_framework_recursive/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/django_rest_framework_recursive/conftest.py b/tests/django_rest_framework_recursive/conftest.py new file mode 100644 index 0000000..439b6a2 --- /dev/null +++ b/tests/django_rest_framework_recursive/conftest.py @@ -0,0 +1,81 @@ +def pytest_configure(): + from django.conf import settings + + settings.configure( + DEBUG_PROPAGATE_EXCEPTIONS=True, + DATABASES={"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}}, + SITE_ID=1, + SECRET_KEY="not very secret in tests", + USE_I18N=True, + USE_L10N=True, + STATIC_URL="/static/", + ROOT_URLCONF="tests.urls", + TEMPLATE_LOADERS=( + "django.template.loaders.filesystem.Loader", + "django.template.loaders.app_directories.Loader", + ), + MIDDLEWARE_CLASSES=( + "django.middleware.common.CommonMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + ), + INSTALLED_APPS=( + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.sites", + "django.contrib.messages", + "django.contrib.staticfiles", + "rest_framework", + "rest_framework.authtoken", + "tests", + ), + PASSWORD_HASHERS=( + "django.contrib.auth.hashers.SHA1PasswordHasher", + "django.contrib.auth.hashers.PBKDF2PasswordHasher", + "django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher", + "django.contrib.auth.hashers.BCryptPasswordHasher", + "django.contrib.auth.hashers.MD5PasswordHasher", + "django.contrib.auth.hashers.CryptPasswordHasher", + ), + ) + + try: + import oauth_provider # NOQA + import oauth2 # NOQA + except ImportError: + pass + else: + settings.INSTALLED_APPS += ("oauth_provider",) + + try: + import provider # NOQA + except ImportError: + pass + else: + settings.INSTALLED_APPS += ( + "provider", + "provider.oauth2", + ) + + # guardian is optional + try: + import guardian # NOQA + except ImportError: + pass + else: + settings.ANONYMOUS_USER_ID = -1 + settings.AUTHENTICATION_BACKENDS = ( + "django.contrib.auth.backends.ModelBackend", + "guardian.backends.ObjectPermissionBackend", + ) + settings.INSTALLED_APPS += ("guardian",) + + try: + import django + + django.setup() + except AttributeError: + pass diff --git a/tests/test_recursive.py b/tests/django_rest_framework_recursive/test_package.py similarity index 93% rename from tests/test_recursive.py rename to tests/django_rest_framework_recursive/test_package.py index bfeebf4..a877dc1 100644 --- a/tests/test_recursive.py +++ b/tests/django_rest_framework_recursive/test_package.py @@ -2,7 +2,8 @@ from django.db import models from rest_framework import serializers -from rest_framework_recursive.fields import RecursiveField + +from kraken.django_rest_framework_recursive.fields import RecursiveField class LinkSerializer(serializers.Serializer): @@ -69,9 +70,7 @@ def _recursively_populate_nullable_fields(serializer, instance_type): serializer[key] = None elif isinstance(serializer[key], list): serializer[key] = [ - _recursively_populate_nullable_fields( - serializer_item, instance_type - ) + _recursively_populate_nullable_fields(serializer_item, instance_type) for serializer_item in serializer[key] ] else: @@ -85,9 +84,7 @@ def _recursively_populate_nullable_fields(serializer, instance_type): serializer = serializer_class(value) updated_payload = _recursively_populate_nullable_fields(value, serializer_class) - assert ( - serializer.data == updated_payload - ), "serialized data does not match input" + assert serializer.data == updated_payload, "serialized data does not match input" @staticmethod def deserialize(serializer_class, data): @@ -96,9 +93,7 @@ def deserialize(serializer_class, data): assert serializer.is_valid(), "cannot validate on deserialization: %s" % dict( serializer.errors ) - assert ( - serializer.validated_data == data - ), "deserialized data does not match input" + assert serializer.validated_data == data, "deserialized data does not match input" def test_link_serializer(self): value = { @@ -223,9 +218,7 @@ def test_validation(self): }, } serializer = SillySerializer(data=way_too_long) - assert ( - not serializer.is_valid() - ), "validation should fail on inner link validation" + assert not serializer.is_valid(), "validation should fail on inner link validation" def test_model_serializer(self): one = RecursiveModel(name="one") From d4bfb10b666259b015a345261b44c9d55fa56984 Mon Sep 17 00:00:00 2001 From: Nicholas Bunn Date: Tue, 20 Aug 2024 09:15:11 +0100 Subject: [PATCH 10/44] Add makefile --- makefile | 91 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 makefile diff --git a/makefile b/makefile new file mode 100644 index 0000000..eeb8ecc --- /dev/null +++ b/makefile @@ -0,0 +1,91 @@ +PIP_VERSION=24.0 +SHELL=/bin/bash + +# If we're running in CI then store Pytest output in a format which CircleCI can parse +ifdef CIRCLECI +MYPY_ARGS=--junit-xml=test-results/mypy.xml +endif + +# Standard entry points +# ===================== + +.PHONY:dev +dev: install_python_packages .git/hooks/pre-commit + +.PHONY:test +test: + pytest + +.PHONY:matrix_test +matrix_test: + nox + +.PHONY:lint +lint: ruff_format ruff_lint mypy + +.PHONY:ruff_format +ruff_format: + ruff format --check . + +.PHONY:ruff_lint +ruff_lint: + ruff check . + +.PHONY:mypy +mypy: + mypy $(MYPY_ARGS) + +.PHONY:format +format: + ruff format . + ruff check --fix . + +.PHONY:update +update: + uv pip compile pyproject.toml -q --upgrade --extra=dev --output-file=requirements/development.txt + +.PHONY:package +package: + python -m build + +.PHONY:version_major +version_major: + bump-my-version bump major + +.PHONY:version_minor +version_minor: + bump-my-version bump minor + +.PHONY:version_patch +version_patch: + bump-my-version bump patch + + +# Implementation details +# ====================== + +# Pip install all required Python packages +.PHONY:install_python_packages +install_python_packages: install_prerequisites requirements/development.txt + uv pip sync requirements/development.txt requirements/firstparty.txt + +# This target _could_ run `uv pip install` unconditionally because `uv pip +# install` is idempotent if versions have not changed. The benefits of checking +# the version number before installing are that if there's nothing to do then +# (a) it's faster and (b) it produces less noisy output. +.PHONY:install_prerequisites +install_prerequisites: + @if [ `uv pip show pip 2>/dev/null | awk '/^Version:/ {print $$2}'` != "$(PIP_VERSION)" ]; then \ + uv pip install pip==$(PIP_VERSION); \ + fi + +# Add new dependencies to requirements/development.txt whenever pyproject.toml changes +requirements/development.txt: pyproject.toml + uv pip compile pyproject.toml -q --extra=dev --output-file=requirements/development.txt + +.git/hooks/pre-commit: + @if type pre-commit >/dev/null 2>&1; then \ + pre-commit install; \ + else \ + echo "WARNING: pre-commit not installed." > /dev/stderr; \ + fi From 1eccd1d91da0d9461420cf070d5a8d4c0cf318df Mon Sep 17 00:00:00 2001 From: Nicholas Bunn Date: Tue, 20 Aug 2024 09:15:46 +0100 Subject: [PATCH 11/44] Add pre-commit setup file --- .pre-commit-config.yaml | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..d7c0b75 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,39 @@ +# This file contains configuration for the pre-commit (https://pre-commit.com/) tool. + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: check-added-large-files # Prevent giant files from being committed + - id: check-case-conflict # Checks for files that conflict in case-insensitive filesystems + - id: check-json # Attempts to load all json files to verify syntax + - id: check-merge-conflict # Check for files that contain merge conflict strings + - id: check-symlinks # Checks for symlinks which do not point to anything + - id: check-xml # Attempts to load all xml files to verify syntax + - id: check-yaml # Attempts to load all yaml files to verify syntax + - id: debug-statements # Check for debugger imports and breakpoint() calls in python code + - id: end-of-file-fixer # Makes sure files end in a newline and only a newline + - id: no-commit-to-branch + # Protect 'main' branch from direct commits and also ensure branch names are lowercase to + # avoid clashes on case-insensitive filesystems + args: ['-p', '.*[^0-9a-z-_/.=].*'] + - id: trailing-whitespace # Trims trailing whitespace + + - repo: local + # We prefer to use local hooks as much as possible for formatting and linting checks. We + # install these tools locally anyway so editors can run them on a pre-save hook. Using local + # tools here ensures the versions used by the editor, pre-commit and CI all stay in sync. + hooks: + - id: ruff-lint + name: "lint Python code with ruff" + entry: "ruff check" + language: system + types: [python] + require_serial: true + + - id: ruff-format + name: "check Python formatting with ruff" + entry: "ruff format --check" + language: system + types: [python] + require_serial: true From 9d88e10d3ea8a97942584945c7ea35ff3d05ad4f Mon Sep 17 00:00:00 2001 From: Nicholas Bunn Date: Tue, 20 Aug 2024 09:15:59 +0100 Subject: [PATCH 12/44] Update package requirements Replace the existing requirements file with auto-generated ones and the necessary pyproject.toml to generate the files --- pyproject.toml | 168 +++++++++++++++++++++++++++++++++++ requirements.txt | 11 --- requirements/development.txt | 114 ++++++++++++++++++++++++ requirements/firstparty.txt | 2 + 4 files changed, 284 insertions(+), 11 deletions(-) create mode 100644 pyproject.toml delete mode 100644 requirements.txt create mode 100644 requirements/development.txt create mode 100644 requirements/firstparty.txt diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..940e389 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,168 @@ +# Packaging +# --------- + +[build-system] +requires = ["setuptools>=67.0"] +build-backend = "setuptools.build_meta" + +[tool.setuptools] +# This is the default but we include it to be explicit. +include-package-data = true + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.setuptools.package-data] +# Include the root-package `py.typed` file so Mypy uses inline type annotations. +"kraken.django_rest_framework_recursive" = [ + "kraken/django_rest_framework_recursive/py.typed", +] + +# Project +# ------- + +[project] +name = "django-rest-framework-recursive" + +# Do not manually edit the version, use `make version_{type}` instead. +# This should match the version in the [tool.bumpversion] section. +version = "0.1.0" +dependencies = [ + "Django>=3.2", + "djangorestframework>=3.13.0", + "pytest-django==4.7.0", + "pytest==8.3.2", + "pytest-cov", +] + +[project.urls] +# See https://daniel.feldroy.com/posts/2023-08-pypi-project-urls-cheatsheet for +# additional URLs that can be included here. +repository = "https://github.com/octoenergy/django-rest-framework-recursive" +changelog = "https://github.com/octoenergy/django-rest-framework-recursive/blob/main/CHANGELOG.md" + +[project.optional-dependencies] +dev = [ + # Testing + "pytest", + "nox", # Install in virtualenv so Mypy has access to the package types. + + # Linting + "ruff", + "mypy", + + # Packaging + "build", + + # Versioning + "bump-my-version", +] + +# Ruff +# ---- + +[tool.ruff] +line-length = 99 + +[tool.ruff.lint] +select = [ + # pycodestyle + "E", + # pyflakes + "F", + # isort + "I", +] +ignore = [ + # Ruff's formatter will try to respect the `line-length` setting + # but doesn't guarantee it - so we ignore the possible line length + # errors that the checker might raise. + "E501", +] + +[tool.ruff.lint.per-file-ignores] +# Allow unused imports in `__init__.py` files as these are convenience imports. +"**/__init__.py" = ["F401"] + +[tool.ruff.lint.isort] +known-first-party = ["kraken"] +lines-after-imports = 2 +section-order = [ + "future", + "standard-library", + "third-party", + "first-party", + "project", + "local-folder", +] + +[tool.ruff.lint.isort.sections] +"project" = ["kraken.django_rest_framework_recursive", "tests"] + +# Mypy +# ---- + +[tool.mypy] +files = "." +exclude = "build/" +# Mypy doesn't understand how to handle namespace packages unless we run it with +# the `explicit_package_bases` flag and specify where all the packages are +# located. The "$MYPY_CONFIG_FILE_DIR/src" path allows Mypy to find the "kraken" +# namespace package. The "$MYPY_CONFIG_FILE_DIR" path allows Mypy to find the +# "tests" package and noxfile.py. +explicit_package_bases = true +mypy_path = ["$MYPY_CONFIG_FILE_DIR/src", "$MYPY_CONFIG_FILE_DIR"] + +# Use strict defaults +strict = true +warn_unreachable = true +warn_no_return = true + +[[tool.mypy.overrides]] +# Don't require test functions to include types +module = "tests.*" +allow_untyped_defs = true +disable_error_code = "attr-defined" + +# Pytest +# ------ + +[tool.pytest.ini_options] +# Ensure error warnings are converted into test errors. +filterwarnings = "error" + +# Bump My Version +# --------------- + +[tool.bumpversion] +# Do not manually edit the version, use `make version_{type}` instead. +# This should match the version in the [project] section. +current_version = "0.1.0" +parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)" +serialize = ["{major}.{minor}.{patch}"] +search = "{current_version}" +replace = "{new_version}" +regex = false +ignore_missing_version = false +tag = false +sign_tags = false +tag_name = "v{new_version}" +tag_message = "Bump version: {current_version} → {new_version}" +allow_dirty = false +commit = false +message = "Bump version: {current_version} → {new_version}" +commit_args = "" + +# Relabel the Unreleased section of the changelog and add a new unreleased section +# as a reminder to add to it. +[[tool.bumpversion.files]] +filename = "CHANGELOG.md" +search = "## [Unreleased]" +replace = "## [Unreleased]\n\n## [{new_version}] - {now:%Y-%m-%d}" + +# Update the project version. +[[tool.bumpversion.files]] +filename = "pyproject.toml" +regex = true +search = "^version = \"{current_version}\"" +replace = "version = \"{new_version}\"" diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index b53fcbb..0000000 --- a/requirements.txt +++ /dev/null @@ -1,11 +0,0 @@ -# Minimum Django and REST framework version -Django>=3.2; python_version >= '3.10' - -djangorestframework>=3.12.0; python_version >= '3.10' - -# Test requirements -pytest-django==4.7.0; python_version >= '3.10' - -pytest==8.3.2; python_version >= '3.10' - -pytest-cov==2.2.0 \ No newline at end of file diff --git a/requirements/development.txt b/requirements/development.txt new file mode 100644 index 0000000..afb4143 --- /dev/null +++ b/requirements/development.txt @@ -0,0 +1,114 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile pyproject.toml --extra=dev --output-file=requirements/development.txt +annotated-types==0.7.0 + # via pydantic +argcomplete==3.5.0 + # via nox +asgiref==3.8.1 + # via django +bracex==2.5 + # via wcmatch +build==1.2.1 + # via django-rest-framework-recursive (pyproject.toml) +bump-my-version==0.26.0 + # via django-rest-framework-recursive (pyproject.toml) +click==8.1.7 + # via + # bump-my-version + # rich-click +colorlog==6.8.2 + # via nox +coverage==7.6.1 + # via pytest-cov +distlib==0.3.8 + # via virtualenv +django==5.1 + # via + # django-rest-framework-recursive (pyproject.toml) + # djangorestframework +djangorestframework==3.15.2 + # via django-rest-framework-recursive (pyproject.toml) +exceptiongroup==1.2.2 + # via pytest +filelock==3.15.4 + # via virtualenv +iniconfig==2.0.0 + # via pytest +markdown-it-py==3.0.0 + # via rich +mdurl==0.1.2 + # via markdown-it-py +mypy==1.11.1 + # via django-rest-framework-recursive (pyproject.toml) +mypy-extensions==1.0.0 + # via mypy +nox==2024.4.15 + # via django-rest-framework-recursive (pyproject.toml) +packaging==24.1 + # via + # build + # nox + # pytest +platformdirs==4.2.2 + # via virtualenv +pluggy==1.5.0 + # via pytest +prompt-toolkit==3.0.36 + # via questionary +pydantic==2.8.2 + # via + # bump-my-version + # pydantic-settings +pydantic-core==2.20.1 + # via pydantic +pydantic-settings==2.4.0 + # via bump-my-version +pygments==2.18.0 + # via rich +pyproject-hooks==1.1.0 + # via build +pytest==8.3.2 + # via + # django-rest-framework-recursive (pyproject.toml) + # pytest-cov + # pytest-django +pytest-cov==5.0.0 + # via django-rest-framework-recursive (pyproject.toml) +pytest-django==4.7.0 + # via django-rest-framework-recursive (pyproject.toml) +python-dotenv==1.0.1 + # via pydantic-settings +questionary==2.0.1 + # via bump-my-version +rich==13.7.1 + # via + # bump-my-version + # rich-click +rich-click==1.8.3 + # via bump-my-version +ruff==0.6.1 + # via django-rest-framework-recursive (pyproject.toml) +sqlparse==0.5.1 + # via django +tomli==2.0.1 + # via + # build + # coverage + # mypy + # nox + # pytest +tomlkit==0.13.2 + # via bump-my-version +typing-extensions==4.12.2 + # via + # asgiref + # mypy + # pydantic + # pydantic-core + # rich-click +virtualenv==20.26.3 + # via nox +wcmatch==9.0 + # via bump-my-version +wcwidth==0.2.13 + # via prompt-toolkit diff --git a/requirements/firstparty.txt b/requirements/firstparty.txt new file mode 100644 index 0000000..872af65 --- /dev/null +++ b/requirements/firstparty.txt @@ -0,0 +1,2 @@ +# Install this package in editable mode. +-e . From 59cf4154e8ced72974635d60b71fb01e9b61fe00 Mon Sep 17 00:00:00 2001 From: Nicholas Bunn Date: Tue, 20 Aug 2024 09:16:17 +0100 Subject: [PATCH 13/44] Update git ignore --- .gitignore | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index ae73f83..b077a81 100644 --- a/.gitignore +++ b/.gitignore @@ -1,20 +1,29 @@ -*.pyc -*.db -*~ -.* +# Python byte-code +__pycache__/ +*.py[cod] -html/ -htmlcov/ -coverage/ +# Distribution / packaging build/ dist/ *.egg-info/ MANIFEST +*.whl +# Linting +.mypy_cache/ +.import_linter_cache/ + +# Testing +.pytest_cache/ + +# Environments +.venv/ +.direnv/ +.envrc bin/ include/ lib/ local/ -!.gitignore -!.travis.yml +# Environment variables +.env From c22937e863006c31fd954a1de6eb56842a3588bc Mon Sep 17 00:00:00 2001 From: Nicholas Bunn Date: Tue, 20 Aug 2024 09:16:34 +0100 Subject: [PATCH 14/44] Update README --- README.md | 226 ++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 185 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index a63ccb3..9b8fd90 100644 --- a/README.md +++ b/README.md @@ -1,51 +1,47 @@ -# Django Rest Framework Recursive (Fork) +****# Django REST Framework Recursive (Fork) -This repository is the friendly fork of the original [django-rest-framework-recursive](https://github.com/heywbj/django-rest-framework-recursive) by [heywbj](https://github.com/heywbj). As the original repo is no longer being actively maintained, we've friendly forked it here to undertake on maintenance for modern versions of Python, Django, and Django Rest Framework. +This repository is the friendly fork of the original [django-rest-framework-recursive](https://github.com/heywbj/django-rest-framework-recursive) by [heywbj](https://github.com/heywbj). As the original repo is no longer being actively maintained, we've friendly forked it here to undertake maintenance for modern versions of Python, Django, and Django Rest Framework. -[![build-status-image]][travis] -[![pypi-version]][pypi] +This **package** provides a `RecursiveField` that enables you to serailize a tree, linked list, or even a directed acyclic graph. It also supports validation, deserialization, ModeSerializer, and multi-step recursive structures. -## Overview +### Example Usage -Recursive Serialization for Django REST framework - -This package provides a `RecursiveField` that enables you to serialize a tree, -linked list, or even a directed acyclic graph. It also supports validation, -deserialization, ModelSerializers, and multi-step recursive structures. - - -## Examples - -### Tree Recursion +#### Tree Recursion ```python from rest_framework import serializers -from rest_framework_recursive.fields import RecursiveField +from django_rest_framework_recursive.fields import RecursiveField class TreeSerializer(serializers.Serializer): name = serializers.CharField() children = serializers.ListField(child=RecursiveField()) ``` -### Linked List Recursion +### #Linked List Recursion ```python from rest_framework import serializers -from rest_framework_recursive.fields import RecursiveField +from django_rest_framework_recursive.fields import RecursiveField class LinkSerializer(serializers.Serializer): name = serializers.CharField(max_length=25) next = RecursiveField(allow_null=True) ``` - Further use cases are documented in the tests, see [**here**][tests] for more usage examples -## Requirements -* Python (Tested on 2.7, 3.4, 3.6) -* Django (Tested on 1.8, 1.9, 2.0) -* Django REST Framework (Tested on 3.3, 3.7) +## Prerequisites +This package supports: + +- Python 3.10 +- Django 3.2, 4.0, 4.1, 4.2 +- Django Rest Framework 3.13, 3.14, 3.15 + +During development you will also need: + +- `uv` installed as a system package. +- pre-commit 3 _(Optional, but strongly recommended)_ ## Installation @@ -55,37 +51,185 @@ Install using `pip`... pip install django-rest-framework-recursive ``` -## Release notes +## Local development + +When making changes please remember to update the `CHANGELOG.md`, which follows the guidelines at +[keepachangelog]. Add your changes to the `[Unreleased]` section when you create your PR. + +[keepachangelog]: https://keepachangelog.com/ + +### Installation + +Ensure one of the above Pythons is installed and used by the `python` executable:**** + +```sh +python --version +Python 3.10.13 # or any of the supported versions +``` + +Ensure `uv` is installed as a system package. This can be done with `pipx` or Homebrew. + +Then create and activate a virtual environment. If you don't have any other way of managing virtual +environments this can be done by running: -### 0.1.2 -* This is the first release to include release notes. -* Use inspect.signature when available. This avoids emitting deprecation warnings on Python 3. -* Updated CI versions. djangorestframework-recursive is now tested against DRF - 3.3-3.6, Python 2.7 and 3.6 and Django 1.8-1.11. +```sh +uv venv +source .venv/bin/activate +``` -## Testing +You could also use [virtualenvwrapper], [direnv] or any similar tool to help manage your virtual +environments. -Install testing requirements. +Once you are in an active virtual environment run +```sh +make dev ``` -pip install -r requirements.txt + +This will set up your local development environment, installing all development dependencies. + +[virtualenvwrapper]: https://virtualenvwrapper.readthedocs.io/ +[direnv]: https://direnv.net + +### Testing (single Python version) + +To run the test suite using the Python version of your virtual environment, run: + +```sh +make test ``` -Run with runtests. +### Testing (all supported Python versions) + +To test against multiple Python (and package) versions, we need to: + +- Have [`nox`][nox] installed outside of the virtualenv. This is best done using `pipx`: + + ```sh + pipx install nox + ``` + +- Ensure that all supported Python versions are installed and available on your system (as e.g. + `python3.10`, `python3.11` etc). This can be done with `pyenv`. + +Then run `nox` with: +```sh +nox ``` -./runtests.py + +Nox will create a separate virtual environment for each combination of Python and package versions +defined in `noxfile.py`. + +To list the available sessions, run: + +```sh +nox --list-sessions ``` -You can also use [tox](http://tox.readthedocs.org/en/latest/) to run the tests against all supported versions of Python and Django. Install tox globally with `pip install tox`, and then simply run: +To run the test suite in a specific Nox session, use: +```sh +nox -s $SESSION_NAME ``` -tox + +[nox]: https://nox.thea.codes/en/stable/ + +### Static analysis + +Run all static analysis tools with: + +```sh +make lint ``` +### Auto formatting + +Reformat code to conform with our conventions using: + +```sh +make format +``` + +### Dependencies + +Package dependencies are declared in `pyproject.toml`. + +- _package_ dependencies in the `dependencies` array in the `[project]` section. +- _development_ dependencies in the `dev` array in the `[project.optional-dependencies]` section. + +For local development, the dependencies declared in `pyproject.toml` are pinned to specific +versions using the `requirements/development.txt` lock file. + +#### Adding a new dependency + +To install a new Python dependency add it to the appropriate section in `pyproject.toml` and then +run: + +```sh +make dev +``` + +This will: + +1. Build a new version of the `requirements/development.txt` lock file containing the newly added + package. +2. Sync your installed packages with those pinned in `requirements/development.txt`. + +This will not change the pinned versions of any packages already in any requirements file unless +needed by the new packages, even if there are updated versions of those packages available. + +Remember to commit your changed `requirements/development.txt` files alongside the changed +`pyproject.toml`. + +#### Removing a dependency + +Removing Python dependencies works exactly the same way: edit `pyproject.toml` and then run +`make dev`. + +#### Updating all Python packages + +To update the pinned versions of all packages simply run: + +```sh +make update +``` + +This will update the pinned versions of every package in the `requirements/development.txt` lock +file to the latest version which is compatible with the constraints in `pyproject.toml`. + +You can then run: + +```sh +make dev +``` + +to sync your installed packages with the updated versions pinned in `requirements/development.txt`. + +#### Updating individual Python packages + +Upgrade a single development dependency with: + +```sh +uv pip compile -P $PACKAGE==$VERSION pyproject.toml --extra=dev --output-file=requirements/development.txt +``` + +You can then run: + +```sh +make dev +``` + +to sync your installed packages with the updated versions pinned in `requirements/development.txt`. + +## Versioning + +This project uses [SemVer] for versioning with no additional suffix after the version number. When +it is time for a new release, run the command `make version_{type}` where `{type}` should be +replaced with one of `major`, `minor`, `patch` depending on the type of changes in the release. + +The command will update the version in `pyproject.toml` and move the changes from the "Unreleased" +section of the changelog to a versioned section and create a new "Unreleased" section for future +improvements. -[build-status-image]: https://secure.travis-ci.org/heywbj/django-rest-framework-recursive.png?branch=master -[travis]: http://travis-ci.org/heywbj/django-rest-framework-recursive?branch=master -[pypi-version]: https://img.shields.io/pypi/v/djangorestframework-recursive.svg -[pypi]: https://pypi.python.org/pypi/djangorestframework-recursive -[tests]: https://github.com/heywbj/django-rest-framework-recursive/blob/master/tests/test_recursive.py \ No newline at end of file +[semver]: https://semver.org/ From 7b07a648786d01fd8d839c4f1629e0aa973eed87 Mon Sep 17 00:00:00 2001 From: Nicholas Bunn Date: Tue, 20 Aug 2024 09:24:19 +0100 Subject: [PATCH 15/44] Swap Tox for Nox Switched the testing framework from Tox to Nox. In doing this, we've dropped support for DRF 3.12 because I'm still getting my head around Nox and want to get this PR out. We've also removed the travis setup here as we'll be using CircleCI instead." --- .travis.yml | 40 ---------------------- noxfile.py | 88 ++++++++++++++++++++++++++++++++++++++++++++++++ runtests.py | 71 --------------------------------------- setup.cfg | 2 -- setup.py | 96 ----------------------------------------------------- tox.ini | 26 --------------- 6 files changed, 88 insertions(+), 235 deletions(-) delete mode 100644 .travis.yml create mode 100644 noxfile.py delete mode 100755 runtests.py delete mode 100644 setup.cfg delete mode 100644 setup.py delete mode 100644 tox.ini diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index e53508b..0000000 --- a/.travis.yml +++ /dev/null @@ -1,40 +0,0 @@ -language: python -python: - - "3.6" - -sudo: false - -env: - - TOX_ENV=py27-django1.8-drf3.3 - - TOX_ENV=py27-django1.8-drf3.4 - - TOX_ENV=py27-django1.8-drf3.5 - - TOX_ENV=py27-django1.9-drf3.3 - - TOX_ENV=py27-django1.9-drf3.4 - - TOX_ENV=py27-django1.9-drf3.5 - - TOX_ENV=py27-django1.10-drf3.3 - - TOX_ENV=py27-django1.10-drf3.4 - - TOX_ENV=py27-django1.10-drf3.5 - - TOX_ENV=py36-django1.8-drf3.3 - - TOX_ENV=py36-django1.8-drf3.4 - - TOX_ENV=py36-django1.8-drf3.5 - - TOX_ENV=py36-django1.9-drf3.3 - - TOX_ENV=py36-django1.9-drf3.4 - - TOX_ENV=py36-django1.9-drf3.5 - - TOX_ENV=py36-django1.9-drf3.6 - - TOX_ENV=py36-django1.10-drf3.3 - - TOX_ENV=py36-django1.10-drf3.4 - - TOX_ENV=py36-django1.10-drf3.5 - - TOX_ENV=py36-django1.10-drf3.6 - - TOX_ENV=py36-django1.11-drf3.3 - - TOX_ENV=py36-django1.11-drf3.4 - - TOX_ENV=py36-django1.11-drf3.5 - - TOX_ENV=py36-django1.11-drf3.6 - -matrix: - fast_finish: true - -install: - - pip install tox - -script: - - tox -e $TOX_ENV diff --git a/noxfile.py b/noxfile.py new file mode 100644 index 0000000..d5ad682 --- /dev/null +++ b/noxfile.py @@ -0,0 +1,88 @@ +""" +This `noxfile.py` is configured to run the test suite with multiple versions of Python and multiple +versions of Django (used as an example). +""" + +import contextlib +import os +import tempfile +from typing import IO, Generator + +import nox + + +# Use uv to manage venvs. +nox.options.default_venv_backend = "uv" + + +@contextlib.contextmanager +def temp_constraints_file() -> Generator[IO[str], None, None]: + with tempfile.NamedTemporaryFile(mode="w", prefix="constraints.", suffix=".txt") as f: + yield f + + +@contextlib.contextmanager +def temp_lock_file() -> Generator[IO[str], None, None]: + with tempfile.NamedTemporaryFile(mode="w", prefix="packages.", suffix=".txt") as f: + yield f + + +@nox.session() +@nox.parametrize( + "drf_version", + [ + nox.param("djangorestframework>=3.13,<3.14", id="drf=3.13"), + nox.param("djangorestframework>=3.14,<3.15", id="drf=3.14"), + nox.param("djangorestframework>=3.15,<3.16", id="drf=3.15"), + ], +) +@nox.parametrize( + "django_version", + [ + nox.param("django>=3.2,django<3.3", id="django=3.2.X"), + nox.param("django>=4.0,django<4.1", id="django=4.0.X"), + nox.param("django>=4.1,django<4.2", id="django=4.1.X"), + nox.param("django>=4.2,django<4.3", id="django=4.2.X"), + ], +) +@nox.parametrize( + "python", + [ + nox.param("3.10", id="python3.10"), + ], +) +def tests(session: nox.Session, django_version: str, drf_version: str) -> None: + """ + Run the test suite. + """ + with temp_constraints_file() as constraints_file, temp_lock_file() as lock_file: + # Create a constraints file with the parameterized package versions. + # It's easy to add more constraints here if needed. + constraints_file.write(f"{django_version}\n") + constraints_file.write(f"{drf_version}\n") + + # Compile a new development lock file with the additional package constraints from this + # session. Use a unique lock file name to avoid session pollution. + session.run( + "uv", + "pip", + "compile", + "--quiet", + "--strip-extras", + "--extra=dev", + "pyproject.toml", + "--constraint", + constraints_file.name, + "--output-file", + lock_file.name, + ) + + # Install the dependencies from the newly compiled lockfile and main package. + session.install("-r", lock_file.name, ".") + + # When run in CircleCI, create JUnit XML test results. + commands = ["pytest"] + if "CIRCLECI" in os.environ: + commands.append(f"--junitxml=test-results/junit.{session.name}.xml") + + session.run(*commands, *session.posargs) diff --git a/runtests.py b/runtests.py deleted file mode 100755 index e0067a9..0000000 --- a/runtests.py +++ /dev/null @@ -1,71 +0,0 @@ -#! /usr/bin/env python -from __future__ import print_function - -import pytest -import sys -import os -import subprocess - - -PYTEST_ARGS = { - 'default': ['tests'], - 'fast': ['tests', '-q'], -} - -sys.path.append(os.path.dirname(__file__)) - - -def exit_on_failure(ret, message=None): - if ret: - sys.exit(ret) - - -def split_class_and_function(string): - class_string, function_string = string.split('.', 1) - return "%s and %s" % (class_string, function_string) - - -def is_function(string): - # `True` if it looks like a test function is included in the string. - return string.startswith('test_') or '.test_' in string - - -def is_class(string): - # `True` if first character is uppercase - assume it's a class name. - return string[0] == string[0].upper() - - -if __name__ == "__main__": - try: - sys.argv.remove('--lintonly') - except ValueError: - run_tests = True - else: - run_tests = False - - try: - sys.argv.remove('--fast') - except ValueError: - style = 'default' - else: - style = 'fast' - - if len(sys.argv) > 1: - pytest_args = sys.argv[1:] - first_arg = pytest_args[0] - if first_arg.startswith('-'): - # `runtests.py [flags]` - pytest_args = ['tests'] + pytest_args - elif is_class(first_arg) and is_function(first_arg): - # `runtests.py TestCase.test_function [flags]` - expression = split_class_and_function(first_arg) - pytest_args = ['tests', '-k', expression] + pytest_args[1:] - elif is_class(first_arg) or is_function(first_arg): - # `runtests.py TestCase [flags]` - # `runtests.py test_function [flags]` - pytest_args = ['tests', '-k', pytest_args[0]] + pytest_args[1:] - else: - pytest_args = PYTEST_ARGS[style] - - if run_tests: - exit_on_failure(pytest.main(pytest_args)) diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 5e40900..0000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[wheel] -universal = 1 diff --git a/setup.py b/setup.py deleted file mode 100644 index f3d4982..0000000 --- a/setup.py +++ /dev/null @@ -1,96 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -import re -import os -import sys -from setuptools import setup - - -name = 'djangorestframework-recursive' -package = 'rest_framework_recursive' -description = 'Recursive Serialization for Django REST framework' -url = 'https://github.com/heywbj/django-rest-framework-recursive' -author = 'Warren Jin' -author_email = 'jinwarren@gmail.com' -license = 'BSD' - - -def get_version(package): - """ - Return package version as listed in `__version__` in `init.py`. - """ - init_py = open(os.path.join(package, '__init__.py')).read() - return re.search("^__version__ = ['\"]([^'\"]+)['\"]", - init_py, re.MULTILINE).group(1) - - -def get_packages(package): - """ - Return root package and all sub-packages. - """ - return [dirpath - for dirpath, dirnames, filenames in os.walk(package) - if os.path.exists(os.path.join(dirpath, '__init__.py'))] - - -def get_package_data(package): - """ - Return all files under the root package, that are not in a - package themselves. - """ - walk = [(dirpath.replace(package + os.sep, '', 1), filenames) - for dirpath, dirnames, filenames in os.walk(package) - if not os.path.exists(os.path.join(dirpath, '__init__.py'))] - - filepaths = [] - for base, filenames in walk: - filepaths.extend([os.path.join(base, filename) - for filename in filenames]) - return {package: filepaths} - - -version = get_version(package) - - -if sys.argv[-1] == 'publish': - if os.system("pip freeze | grep wheel"): - print("wheel not installed.\nUse `pip install wheel`.\nExiting.") - sys.exit() - os.system("python setup.py sdist upload") - os.system("python setup.py bdist_wheel upload") - print("You probably want to also tag the version now:") - print(" git tag -a {0} -m 'version {0}'".format(version)) - print(" git push --tags") - sys.exit() - - -setup( - name=name, - version=version, - url=url, - license=license, - description=description, - author=author, - author_email=author_email, - packages=get_packages(package), - package_data=get_package_data(package), - install_requires=[ - 'Django', - 'djangorestframework >= 3.0' - ], - classifiers=[ - 'Development Status :: 2 - Pre-Alpha', - 'Environment :: Web Environment', - 'Framework :: Django', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: BSD License', - 'Operating System :: OS Independent', - 'Natural Language :: English', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: 3.4', - 'Topic :: Internet :: WWW/HTTP', - ] -) diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 53e91ab..0000000 --- a/tox.ini +++ /dev/null @@ -1,26 +0,0 @@ -[tox] -envlist = - py310-django{32}-drf{312} # DRF 3.12 is not supported beyond Django 3.2 so it's getting it's own configuration here - py310-django{32,40,41,42}-drf{313,314,315} - py310-django{5}-drf{314,315} # Django 5 is not supported for DRF < 3.13 so it's getting it's own configuration here - -[testenv] -allowlist_externals = python -commands = python ./runtests.py --fast -setenv = - PYTHONDONTWRITEBYTECODE=1 -basepython = - py310: python3.10 -deps = - django32: Django>=3.2,<3.3 - django40: Django>=4.0,<4.1 - django41: Django>=4.1,<4.2 - django42: Django>=4.2,<4.3 - django5: Django>=5.0,<5.1 - - drf312: djangorestframework>=3.12,<3.13 - drf313: djangorestframework>=3.13,<3.14 - drf314: djangorestframework>=3.14,<3.15 - drf315: djangorestframework>=3.15,<3.16 - - pytest-django==4.7.0 \ No newline at end of file From b30e3bbcdf3a01dd260077a8bf81fbf65a712036 Mon Sep 17 00:00:00 2001 From: Nicholas Bunn Date: Tue, 20 Aug 2024 09:25:19 +0100 Subject: [PATCH 16/44] Remove manifest --- MANIFEST.in | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 MANIFEST.in diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index ca446cc..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,2 +0,0 @@ -recursive-exclude * __pycache__ -recursive-exclude * *.py[co] From 8df48051f2e3219ad9f3848d4ecdf6613ac22548 Mon Sep 17 00:00:00 2001 From: Nicholas Bunn Date: Tue, 20 Aug 2024 09:25:32 +0100 Subject: [PATCH 17/44] Add changelog --- CHANGELOG.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..7b6466c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,28 @@ +# Changelog and Versioning + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project +adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added + +- Tested support for Python 3.10 +- Tested support for Django versions 3.2 -> 4.2 +- Tested support for Django REST Framework versions 3.13 -> 3.15 +- Development support using MyPy, Ruff, and UV +- Pre-commit set-up to format before code is pushed + +### Changed + +- Formatting to follow Kraken open-source conventions +- Matrix testing runner from Tox to Nox + +### Removed + +- Tested support for all Python versions < 3.10. +- Tested support for all Django versions < 3.2 +- Tested support for all Django REST Framework versions < 3.13. +- CI configuration on Travis From 2b00670ee0f319036d07939ec10b2c7a3e05a726 Mon Sep 17 00:00:00 2001 From: Nicholas Bunn Date: Tue, 20 Aug 2024 10:45:18 +0100 Subject: [PATCH 18/44] Add CircleCI config Note that we've disabled MyPy here, this is because mypy is currently failing due to this project being untyped. I want to set CircleCI up to run after merging this PR and don't want it to fail on master. I plan to add typing and re-enable MyPy in a subsequent PR --- .circleci/config.yml | 87 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 .circleci/config.yml diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..8fe0d3c --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,87 @@ +version: 2.1 + +jobs: + lint: + docker: + # When upgrading the Docker image, make sure the cache key is updated too. + - image: cimg/python:3.11.9 + resource_class: small + steps: + - checkout + - run: + name: Install system Python packages + command: pipx install uv pre-commit + # Installing packages into a virtualenv is useful as it provides an easier target to cache. + # Note that this step needs including in all jobs that install Python packages. + - run: + name: Create virtualenv + command: | + uv venv /home/circleci/venv/ + echo "source /home/circleci/venv/bin/activate" >> $BASH_ENV + - restore_cache: + keys: + - &cache-key python-3.11.9-packages-v1-{{ checksum "requirements/development.txt" }} + - &cache-key-prefix python-3.11.9-packages-v1- + - run: + name: Install dependencies + command: make dev + - save_cache: + key: *cache-key + paths: + - "~/venv/" + - "~/.cache/pip" + - run: + name: Run ruff formatter + command: make ruff_format + when: always + - run: + name: Run ruff linter + command: make ruff_lint + when: always + # - run: + # name: Run Mypy + # command: make mypy + # when: always + - run: + name: Run pre-commit hooks + environment: + # Don't run pre-commit checks which have a dedicated CI step. + # Also, don't complain about commits on the main branch in CI. + SKIP: ruff-lint,ruff-format,no-commit-to-branch + command: pre-commit run --all-files + when: always + - store_test_results: + path: test-results + + test: + docker: + # When upgrading the Docker image, make sure the cache key is updated too. + - image: cimg/python:3.11.9 + resource_class: small + steps: + - checkout + - run: + name: Install additional Python versions and system Python packages + command: | + # Install additional Python versions that Nox needs. Note the + # `cimg/python:3.11` image already has Python 3.10 installed. + pyenv install 3.12.3 + pyenv global 3.11.9 3.12.3 + pipx install uv nox + when: always + - run: + name: Run tests + # To start with, it's cost-effective to run all matrix tests in one + # CI job. But as the test suite grows, it will make more sense to + # split the matrix sessions across multiple CI jobs. This can be done + # using CircleCI's matrix jobs functionality to pass in the Nox session name to run. + command: make matrix_test + when: always + - store_test_results: + path: test-results + +workflows: + test-build: + jobs: + - lint + - test From a2abf66d6f0d444a4285a0d21822b0b58c87b913 Mon Sep 17 00:00:00 2001 From: Nicholas Bunn Date: Tue, 20 Aug 2024 10:48:27 +0100 Subject: [PATCH 19/44] Format CODEOWNERS Just so that pre-commit check doesn't fail on CI --- CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index e2a6df4..48795db 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,4 +1,4 @@ # Example of a CODEOWNERS file # Specify owners for the entire repository -* @nicholasbunn @isabellanorris \ No newline at end of file +* @nicholasbunn @isabellanorris From 09f30240e5896e00e99e7e9bd416dff88c5d8b3a Mon Sep 17 00:00:00 2001 From: Nicholas Bunn Date: Wed, 21 Aug 2024 10:13:19 +0100 Subject: [PATCH 20/44] Refactor Nox parametrization Updated the way we parametrize our matrix testing to enable specific environment combinations. Also adds support for DRF 3.12 back in. --- README.md | 8 +++++--- noxfile.py | 44 ++++++++++++++++++++------------------------ pyproject.toml | 4 ++-- 3 files changed, 27 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 9b8fd90..3af7158 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -****# Django REST Framework Recursive (Fork) +# Django REST Framework Recursive (Fork) This repository is the friendly fork of the original [django-rest-framework-recursive](https://github.com/heywbj/django-rest-framework-recursive) by [heywbj](https://github.com/heywbj). As the original repo is no longer being actively maintained, we've friendly forked it here to undertake maintenance for modern versions of Python, Django, and Django Rest Framework. @@ -17,7 +17,7 @@ class TreeSerializer(serializers.Serializer): children = serializers.ListField(child=RecursiveField()) ``` -### #Linked List Recursion +### Linked List Recursion ```python from rest_framework import serializers from django_rest_framework_recursive.fields import RecursiveField @@ -36,7 +36,9 @@ This package supports: - Python 3.10 - Django 3.2, 4.0, 4.1, 4.2 -- Django Rest Framework 3.13, 3.14, 3.15 +- Django Rest Framework 3.12, 3.13, 3.14, 3.15 + +For an exact list of tested version combinations, see the `valid_version_combinations` set in the [noxfile](./noxfile.py) During development you will also need: diff --git a/noxfile.py b/noxfile.py index d5ad682..4bc30a8 100644 --- a/noxfile.py +++ b/noxfile.py @@ -27,30 +27,20 @@ def temp_lock_file() -> Generator[IO[str], None, None]: yield f +valid_version_combinations = [ + ("3.10", "django>=3.2,<3.3", "djangorestframework>=3.12,<3.13"), + ("3.10", "django>=3.2,<3.3", "djangorestframework>=3.13,<3.14"), + ("3.10", "django>=3.2,<3.3", "djangorestframework>=3.14,<3.15"), + ("3.10", "django>=4.0,<4.1", "djangorestframework>=3.13,<3.14"), + ("3.10", "django>=4.0,<4.1", "djangorestframework>=3.14,<3.15"), + ("3.10", "django>=4.1,<4.2", "djangorestframework>=3.14,<3.15"), + ("3.10", "django>=4.2,<4.3", "djangorestframework>=3.14,<3.15"), + ("3.10", "django>=4.2,<4.3", "djangorestframework>=3.15,<3.16"), +] + + @nox.session() -@nox.parametrize( - "drf_version", - [ - nox.param("djangorestframework>=3.13,<3.14", id="drf=3.13"), - nox.param("djangorestframework>=3.14,<3.15", id="drf=3.14"), - nox.param("djangorestframework>=3.15,<3.16", id="drf=3.15"), - ], -) -@nox.parametrize( - "django_version", - [ - nox.param("django>=3.2,django<3.3", id="django=3.2.X"), - nox.param("django>=4.0,django<4.1", id="django=4.0.X"), - nox.param("django>=4.1,django<4.2", id="django=4.1.X"), - nox.param("django>=4.2,django<4.3", id="django=4.2.X"), - ], -) -@nox.parametrize( - "python", - [ - nox.param("3.10", id="python3.10"), - ], -) +@nox.parametrize("python, django_version, drf_version", valid_version_combinations) def tests(session: nox.Session, django_version: str, drf_version: str) -> None: """ Run the test suite. @@ -60,6 +50,9 @@ def tests(session: nox.Session, django_version: str, drf_version: str) -> None: # It's easy to add more constraints here if needed. constraints_file.write(f"{django_version}\n") constraints_file.write(f"{drf_version}\n") + constraints_file.write("pytest-django==4.7.0\n") + constraints_file.write("pytest==8.3.2\n") + constraints_file.flush() # Compile a new development lock file with the additional package constraints from this # session. Use a unique lock file name to avoid session pollution. @@ -85,4 +78,7 @@ def tests(session: nox.Session, django_version: str, drf_version: str) -> None: if "CIRCLECI" in os.environ: commands.append(f"--junitxml=test-results/junit.{session.name}.xml") - session.run(*commands, *session.posargs) + session.run( + *commands, + *session.posargs, + ) diff --git a/pyproject.toml b/pyproject.toml index 940e389..b97ec00 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,8 +28,8 @@ name = "django-rest-framework-recursive" # This should match the version in the [tool.bumpversion] section. version = "0.1.0" dependencies = [ - "Django>=3.2", - "djangorestframework>=3.13.0", + "django>=3.2", + "djangorestframework>=3.12.0", "pytest-django==4.7.0", "pytest==8.3.2", "pytest-cov", From 67e41190ab4fa42eab35b9f5c5bae16b866770da Mon Sep 17 00:00:00 2001 From: Nicholas Bunn Date: Wed, 21 Aug 2024 20:08:14 +0100 Subject: [PATCH 21/44] Conditionally set USE_L10N This setting has been deprecated staring from Django 4, so the setting has been removed if this is run on Django 4.0+ --- tests/django_rest_framework_recursive/conftest.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/django_rest_framework_recursive/conftest.py b/tests/django_rest_framework_recursive/conftest.py index 439b6a2..965da5a 100644 --- a/tests/django_rest_framework_recursive/conftest.py +++ b/tests/django_rest_framework_recursive/conftest.py @@ -1,4 +1,5 @@ def pytest_configure(): + import django from django.conf import settings settings.configure( @@ -7,7 +8,6 @@ def pytest_configure(): SITE_ID=1, SECRET_KEY="not very secret in tests", USE_I18N=True, - USE_L10N=True, STATIC_URL="/static/", ROOT_URLCONF="tests.urls", TEMPLATE_LOADERS=( @@ -42,6 +42,10 @@ def pytest_configure(): ), ) + if django.get_version() < "4.0": + # This setting has been deprecated for Django 4.0 + settings.USE_L10N = True + try: import oauth_provider # NOQA import oauth2 # NOQA From 10998d4f439fc6069640085d8889234949f9c834 Mon Sep 17 00:00:00 2001 From: Nicholas Bunn Date: Wed, 21 Aug 2024 20:21:50 +0100 Subject: [PATCH 22/44] Add support for 3.11 --- README.md | 4 ++-- noxfile.py | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3af7158..a7b99bf 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,8 @@ Further use cases are documented in the tests, see [**here**][tests] for more us This package supports: -- Python 3.10 -- Django 3.2, 4.0, 4.1, 4.2 +- Python 3.10, 3.11 +- Django 3.2, 4.0, 4.1, 4.2, 5.0 - Django Rest Framework 3.12, 3.13, 3.14, 3.15 For an exact list of tested version combinations, see the `valid_version_combinations` set in the [noxfile](./noxfile.py) diff --git a/noxfile.py b/noxfile.py index 4bc30a8..4a5cfca 100644 --- a/noxfile.py +++ b/noxfile.py @@ -28,6 +28,7 @@ def temp_lock_file() -> Generator[IO[str], None, None]: valid_version_combinations = [ + # Python 3.10 ("3.10", "django>=3.2,<3.3", "djangorestframework>=3.12,<3.13"), ("3.10", "django>=3.2,<3.3", "djangorestframework>=3.13,<3.14"), ("3.10", "django>=3.2,<3.3", "djangorestframework>=3.14,<3.15"), @@ -36,6 +37,13 @@ def temp_lock_file() -> Generator[IO[str], None, None]: ("3.10", "django>=4.1,<4.2", "djangorestframework>=3.14,<3.15"), ("3.10", "django>=4.2,<4.3", "djangorestframework>=3.14,<3.15"), ("3.10", "django>=4.2,<4.3", "djangorestframework>=3.15,<3.16"), + # Python 3.11 + ("3.11", "django>=4.1,<4.2", "djangorestframework>=3.14,<3.15"), + ("3.11", "django>=4.1,<4.2", "djangorestframework>=3.15,<3.16"), + ("3.11", "django>=4.2,<4.3", "djangorestframework>=3.14,<3.15"), + ("3.11", "django>=4.2,<4.3", "djangorestframework>=3.15,<3.16"), + ("3.11", "django>=5.0,<5.1", "djangorestframework>=3.14,<3.15"), + ("3.11", "django>=5.0,<5.1", "djangorestframework>=3.15,<3.16"), ] From 07628ecf1324ebd17c4e340876c0786759c8812d Mon Sep 17 00:00:00 2001 From: Nicholas Bunn Date: Wed, 21 Aug 2024 20:38:01 +0100 Subject: [PATCH 23/44] Update CHANGELOG --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b6466c..b6af44e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,9 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Added -- Tested support for Python 3.10 -- Tested support for Django versions 3.2 -> 4.2 -- Tested support for Django REST Framework versions 3.13 -> 3.15 +- Tested support for Python 3.10 -> 3.11 +- Tested support for Django versions 3.2 -> 5.0 +- Tested support for Django REST Framework versions 3.12 -> 3.15 - Development support using MyPy, Ruff, and UV - Pre-commit set-up to format before code is pushed From bf47ff61e3fcfa28bb788be18911b8a9c5f16f4f Mon Sep 17 00:00:00 2001 From: Nicholas Bunn Date: Thu, 22 Aug 2024 14:10:56 +0100 Subject: [PATCH 24/44] Add workflow script for publishing to PyPI --- .github/workflows/build_and_publish.yaml | 56 ++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 .github/workflows/build_and_publish.yaml diff --git a/.github/workflows/build_and_publish.yaml b/.github/workflows/build_and_publish.yaml new file mode 100644 index 0000000..d0595c2 --- /dev/null +++ b/.github/workflows/build_and_publish.yaml @@ -0,0 +1,56 @@ +name: Publish Python 🐍 distribution 📦 to PyPI and TestPyPI + +on: + release: + types: [created] + +jobs: + build: + name: Build distribution 📦 + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + - name: Install pypa/build + run: >- + python3 -m + pip install + build + --user + - name: Build a binary wheel and a source tarball + run: python3 -m build + - name: Store the distribution packages + uses: actions/upload-artifact@v4 + with: + name: python-package-distributions + path: dist/ + + publish-to-pypi: + name: >- + Publish Python 🐍 distribution 📦 to PyPI + if: github.repository_owner == 'kraken-tech' && github.ref_type == 'tag' + needs: + - build + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/django-rest-framework-recursive + timeout-minutes: 5 + + permissions: + id-token: write # IMPORTANT: mandatory for trusted publishing + + steps: + - name: Download all the dists + uses: actions/download-artifact@v4 + with: + name: python-package-distributions + path: dist/ + - name: Publish distribution 📦 to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + repository-url: https://pypi.org/legacy/ From 2008069c3656865f4d81d8f0f4762043ca98e0b6 Mon Sep 17 00:00:00 2001 From: Nicholas Bunn Date: Thu, 22 Aug 2024 14:11:18 +0100 Subject: [PATCH 25/44] Bump to v0.2.0 --- CHANGELOG.md | 2 ++ pyproject.toml | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6af44e..2e5d46d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [0.2.0] - 2024-08-22 + ### Added - Tested support for Python 3.10 -> 3.11 diff --git a/pyproject.toml b/pyproject.toml index b97ec00..d4edfdd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,7 @@ name = "django-rest-framework-recursive" # Do not manually edit the version, use `make version_{type}` instead. # This should match the version in the [tool.bumpversion] section. -version = "0.1.0" +version = "0.2.0" dependencies = [ "django>=3.2", "djangorestframework>=3.12.0", @@ -137,7 +137,7 @@ filterwarnings = "error" [tool.bumpversion] # Do not manually edit the version, use `make version_{type}` instead. # This should match the version in the [project] section. -current_version = "0.1.0" +current_version = "0.2.0" parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)" serialize = ["{major}.{minor}.{patch}"] search = "{current_version}" From 4a090a3790b399005d5cf5945fc3106c1ac54f55 Mon Sep 17 00:00:00 2001 From: Nicholas Bunn Date: Fri, 23 Aug 2024 11:37:42 +0100 Subject: [PATCH 26/44] Change package name from django-rest-framework-recursive to drf-recursive This is because the deployment to PyPi failed due to name similarity --- .github/workflows/build_and_publish.yaml | 2 +- README.md | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build_and_publish.yaml b/.github/workflows/build_and_publish.yaml index d0595c2..ce0dc09 100644 --- a/.github/workflows/build_and_publish.yaml +++ b/.github/workflows/build_and_publish.yaml @@ -38,7 +38,7 @@ jobs: runs-on: ubuntu-latest environment: name: pypi - url: https://pypi.org/p/django-rest-framework-recursive + url: https://pypi.org/p/drf-recursive timeout-minutes: 5 permissions: diff --git a/README.md b/README.md index a7b99bf..2bdd848 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ During development you will also need: Install using `pip`... ``` -pip install django-rest-framework-recursive +pip install drf-recursive ``` ## Local development diff --git a/pyproject.toml b/pyproject.toml index d4edfdd..f696258 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ where = ["src"] # ------- [project] -name = "django-rest-framework-recursive" +name = "drf-recursive" # Do not manually edit the version, use `make version_{type}` instead. # This should match the version in the [tool.bumpversion] section. From 924f016e294db8247fd4e74aa23f754751d3873f Mon Sep 17 00:00:00 2001 From: Nicholas Bunn Date: Fri, 23 Aug 2024 12:12:20 +0100 Subject: [PATCH 27/44] Update the pyproject.toml to specify readme file If this isn't specified it seems to look for an RST file which caused an error --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index f696258..13be709 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,7 @@ where = ["src"] [project] name = "drf-recursive" +readme = "README.md" # Do not manually edit the version, use `make version_{type}` instead. # This should match the version in the [tool.bumpversion] section. From 5b9fcc6c697812a277b77e30e44ef544a6730b2a Mon Sep 17 00:00:00 2001 From: Nicholas Bunn Date: Fri, 23 Aug 2024 12:12:47 +0100 Subject: [PATCH 28/44] Update README link Made this ref absolute so that it renders on PyPi correctly --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2bdd848..932fd70 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ This package supports: - Django 3.2, 4.0, 4.1, 4.2, 5.0 - Django Rest Framework 3.12, 3.13, 3.14, 3.15 -For an exact list of tested version combinations, see the `valid_version_combinations` set in the [noxfile](./noxfile.py) +For an exact list of tested version combinations, see the `valid_version_combinations` set in the [noxfile](https://github.com/kraken-tech/django-rest-framework-recursive/blob/master/noxfile.py) During development you will also need: From 13da6d63c8b3418ed4dc028f0b878f65a889abe8 Mon Sep 17 00:00:00 2001 From: Nicholas Bunn Date: Fri, 23 Aug 2024 13:54:34 +0100 Subject: [PATCH 29/44] Remove repository_url for publishing to PyPi Legacy looks like it's been deprecated on PyPi (even though it's active on TestPyPi). So removing this should force it to use whatever the current default specified in the gh-action-pypi-publish release is --- .github/workflows/build_and_publish.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/build_and_publish.yaml b/.github/workflows/build_and_publish.yaml index ce0dc09..cbdf672 100644 --- a/.github/workflows/build_and_publish.yaml +++ b/.github/workflows/build_and_publish.yaml @@ -52,5 +52,3 @@ jobs: path: dist/ - name: Publish distribution 📦 to PyPI uses: pypa/gh-action-pypi-publish@release/v1 - with: - repository-url: https://pypi.org/legacy/ From 6f362e50e288111c154b8408d38ea349019616ed Mon Sep 17 00:00:00 2001 From: Nicholas Bunn Date: Fri, 23 Aug 2024 15:21:49 +0100 Subject: [PATCH 30/44] Move CODEOWNERS Just a bit of housekeeping, move CODEOWNERS into .github folder --- CODEOWNERS => .github/workflows/CODEOWNERS | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename CODEOWNERS => .github/workflows/CODEOWNERS (100%) diff --git a/CODEOWNERS b/.github/workflows/CODEOWNERS similarity index 100% rename from CODEOWNERS rename to .github/workflows/CODEOWNERS From ff62ac1c4a6ea84166f335fbc1ab20e92513a4e7 Mon Sep 17 00:00:00 2001 From: Nicholas Bunn Date: Fri, 23 Aug 2024 15:23:34 +0100 Subject: [PATCH 31/44] Update directory structure Updated the directory structure so that the import path is django_rest_framework_recursive rather than kraken.django_rest_framework_recursive --- pyproject.toml | 8 +++----- .../django_rest_framework_recursive/__init__.py | 0 .../django_rest_framework_recursive/fields.py | 0 src/{kraken => }/django_rest_framework_recursive/py.typed | 0 tests/django_rest_framework_recursive/test_package.py | 2 +- 5 files changed, 4 insertions(+), 6 deletions(-) rename src/{kraken => }/django_rest_framework_recursive/__init__.py (100%) rename src/{kraken => }/django_rest_framework_recursive/fields.py (100%) rename src/{kraken => }/django_rest_framework_recursive/py.typed (100%) diff --git a/pyproject.toml b/pyproject.toml index 13be709..516734a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,9 +14,7 @@ where = ["src"] [tool.setuptools.package-data] # Include the root-package `py.typed` file so Mypy uses inline type annotations. -"kraken.django_rest_framework_recursive" = [ - "kraken/django_rest_framework_recursive/py.typed", -] +"django_rest_framework_recursive" = ["django_rest_framework_recursive/py.typed"] # Project # ------- @@ -40,7 +38,7 @@ dependencies = [ # See https://daniel.feldroy.com/posts/2023-08-pypi-project-urls-cheatsheet for # additional URLs that can be included here. repository = "https://github.com/octoenergy/django-rest-framework-recursive" -changelog = "https://github.com/octoenergy/django-rest-framework-recursive/blob/main/CHANGELOG.md" +changelog = "https://github.com/octoenergy/django-rest-framework-recursive/blob/master/CHANGELOG.md" [project.optional-dependencies] dev = [ @@ -98,7 +96,7 @@ section-order = [ ] [tool.ruff.lint.isort.sections] -"project" = ["kraken.django_rest_framework_recursive", "tests"] +"project" = ["django_rest_framework_recursive", "tests"] # Mypy # ---- diff --git a/src/kraken/django_rest_framework_recursive/__init__.py b/src/django_rest_framework_recursive/__init__.py similarity index 100% rename from src/kraken/django_rest_framework_recursive/__init__.py rename to src/django_rest_framework_recursive/__init__.py diff --git a/src/kraken/django_rest_framework_recursive/fields.py b/src/django_rest_framework_recursive/fields.py similarity index 100% rename from src/kraken/django_rest_framework_recursive/fields.py rename to src/django_rest_framework_recursive/fields.py diff --git a/src/kraken/django_rest_framework_recursive/py.typed b/src/django_rest_framework_recursive/py.typed similarity index 100% rename from src/kraken/django_rest_framework_recursive/py.typed rename to src/django_rest_framework_recursive/py.typed diff --git a/tests/django_rest_framework_recursive/test_package.py b/tests/django_rest_framework_recursive/test_package.py index a877dc1..5da465a 100644 --- a/tests/django_rest_framework_recursive/test_package.py +++ b/tests/django_rest_framework_recursive/test_package.py @@ -3,7 +3,7 @@ from django.db import models from rest_framework import serializers -from kraken.django_rest_framework_recursive.fields import RecursiveField +from django_rest_framework_recursive.fields import RecursiveField class LinkSerializer(serializers.Serializer): From d993ba0a77ff3466b461c3168fdb1ca13a8d5377 Mon Sep 17 00:00:00 2001 From: Nicholas Bunn Date: Fri, 23 Aug 2024 15:24:04 +0100 Subject: [PATCH 32/44] Update link reference to kraken-tech rather than octopus --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 516734a..b5dac2d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,8 +37,8 @@ dependencies = [ [project.urls] # See https://daniel.feldroy.com/posts/2023-08-pypi-project-urls-cheatsheet for # additional URLs that can be included here. -repository = "https://github.com/octoenergy/django-rest-framework-recursive" -changelog = "https://github.com/octoenergy/django-rest-framework-recursive/blob/master/CHANGELOG.md" +repository = "https://github.com/kraken-tech/django-rest-framework-recursive" +changelog = "https://github.com/kraken-tech/django-rest-framework-recursive/blob/master/CHANGELOG.md" [project.optional-dependencies] dev = [ From 62eb5592e45bbe70724957cc30a354fc172e9246 Mon Sep 17 00:00:00 2001 From: Nicholas Bunn Date: Tue, 27 Aug 2024 09:15:37 +0100 Subject: [PATCH 33/44] Update Pytest-Django constraint Updates the pytest-django constraint to be a minimum rather than an exact one --- noxfile.py | 4 ++-- requirements/development.txt | 20 ++++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/noxfile.py b/noxfile.py index 4a5cfca..fe5253e 100644 --- a/noxfile.py +++ b/noxfile.py @@ -58,8 +58,8 @@ def tests(session: nox.Session, django_version: str, drf_version: str) -> None: # It's easy to add more constraints here if needed. constraints_file.write(f"{django_version}\n") constraints_file.write(f"{drf_version}\n") - constraints_file.write("pytest-django==4.7.0\n") - constraints_file.write("pytest==8.3.2\n") + constraints_file.write("pytest-django>=4.7.0\n") + constraints_file.write("pytest>=8.3.2\n") constraints_file.flush() # Compile a new development lock file with the additional package constraints from this diff --git a/requirements/development.txt b/requirements/development.txt index afb4143..0b26c7c 100644 --- a/requirements/development.txt +++ b/requirements/development.txt @@ -9,9 +9,9 @@ asgiref==3.8.1 bracex==2.5 # via wcmatch build==1.2.1 - # via django-rest-framework-recursive (pyproject.toml) + # via drf-recursive (pyproject.toml) bump-my-version==0.26.0 - # via django-rest-framework-recursive (pyproject.toml) + # via drf-recursive (pyproject.toml) click==8.1.7 # via # bump-my-version @@ -24,10 +24,10 @@ distlib==0.3.8 # via virtualenv django==5.1 # via - # django-rest-framework-recursive (pyproject.toml) + # drf-recursive (pyproject.toml) # djangorestframework djangorestframework==3.15.2 - # via django-rest-framework-recursive (pyproject.toml) + # via drf-recursive (pyproject.toml) exceptiongroup==1.2.2 # via pytest filelock==3.15.4 @@ -39,11 +39,11 @@ markdown-it-py==3.0.0 mdurl==0.1.2 # via markdown-it-py mypy==1.11.1 - # via django-rest-framework-recursive (pyproject.toml) + # via drf-recursive (pyproject.toml) mypy-extensions==1.0.0 # via mypy nox==2024.4.15 - # via django-rest-framework-recursive (pyproject.toml) + # via drf-recursive (pyproject.toml) packaging==24.1 # via # build @@ -69,13 +69,13 @@ pyproject-hooks==1.1.0 # via build pytest==8.3.2 # via - # django-rest-framework-recursive (pyproject.toml) + # drf-recursive (pyproject.toml) # pytest-cov # pytest-django pytest-cov==5.0.0 - # via django-rest-framework-recursive (pyproject.toml) + # via drf-recursive (pyproject.toml) pytest-django==4.7.0 - # via django-rest-framework-recursive (pyproject.toml) + # via drf-recursive (pyproject.toml) python-dotenv==1.0.1 # via pydantic-settings questionary==2.0.1 @@ -87,7 +87,7 @@ rich==13.7.1 rich-click==1.8.3 # via bump-my-version ruff==0.6.1 - # via django-rest-framework-recursive (pyproject.toml) + # via drf-recursive (pyproject.toml) sqlparse==0.5.1 # via django tomli==2.0.1 From 15e82ebc8f41da5f0bb06bbef63e40c72515b9e4 Mon Sep 17 00:00:00 2001 From: Nicholas Bunn Date: Tue, 27 Aug 2024 09:25:33 +0100 Subject: [PATCH 34/44] Update CHANGELOG --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e5d46d..c71d350 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added + +### Changed + +- The package import semantics from `kraken.django_rest_framework_recursive` to `django_rest_framework_recursive`. +- Links in the README.md file to be to the correct files in the repository. +- The `pytest-django` version to be *minimum* `4.7.0` rather than *exactly* `4.7.0`. + +### Removed + ## [0.2.0] - 2024-08-22 ### Added From 8057cdc5ed7d4497b60ca20da81cdea6676552f9 Mon Sep 17 00:00:00 2001 From: Nicholas Bunn Date: Wed, 28 Aug 2024 14:04:56 +0100 Subject: [PATCH 35/44] Bump to v0.3.0 --- CHANGELOG.md | 2 ++ pyproject.toml | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c71d350..fd11434 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [0.3.0] - 2024-08-28 + ### Added ### Changed diff --git a/pyproject.toml b/pyproject.toml index b5dac2d..e8e827d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ readme = "README.md" # Do not manually edit the version, use `make version_{type}` instead. # This should match the version in the [tool.bumpversion] section. -version = "0.2.0" +version = "0.3.0" dependencies = [ "django>=3.2", "djangorestframework>=3.12.0", @@ -136,7 +136,7 @@ filterwarnings = "error" [tool.bumpversion] # Do not manually edit the version, use `make version_{type}` instead. # This should match the version in the [project] section. -current_version = "0.2.0" +current_version = "0.3.0" parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)" serialize = ["{major}.{minor}.{patch}"] search = "{current_version}" From e65f1d45f21a845f521314f19fdaef01c6cd7f78 Mon Sep 17 00:00:00 2001 From: Nicholas Bunn Date: Tue, 3 Sep 2024 09:10:20 +0100 Subject: [PATCH 36/44] Move test dependencies to be optional --- CHANGELOG.md | 8 ++++++++ pyproject.toml | 12 ++++-------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd11434..69059de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added + +### Changed + +- Moved the dependencies for test packages (`pytest`, `pytest-django`, `pytest-cov`) into dev dependencies. + +### Removed + ## [0.3.0] - 2024-08-28 ### Added diff --git a/pyproject.toml b/pyproject.toml index e8e827d..33e3e4f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,13 +26,7 @@ readme = "README.md" # Do not manually edit the version, use `make version_{type}` instead. # This should match the version in the [tool.bumpversion] section. version = "0.3.0" -dependencies = [ - "django>=3.2", - "djangorestframework>=3.12.0", - "pytest-django==4.7.0", - "pytest==8.3.2", - "pytest-cov", -] +dependencies = ["django>=3.2", "djangorestframework>=3.12.0"] [project.urls] # See https://daniel.feldroy.com/posts/2023-08-pypi-project-urls-cheatsheet for @@ -44,7 +38,9 @@ changelog = "https://github.com/kraken-tech/django-rest-framework-recursive/blob dev = [ # Testing "pytest", - "nox", # Install in virtualenv so Mypy has access to the package types. + "pytest-django>=4.7.0", + "pytest-cov", + "nox", # Install in virtualenv so Mypy has access to the package types. # Linting "ruff", From 758009a6748d8c64f6251222758a359e7b9cea29 Mon Sep 17 00:00:00 2001 From: Nicholas Bunn Date: Tue, 3 Sep 2024 09:11:53 +0100 Subject: [PATCH 37/44] Bump to v0.3.1 --- CHANGELOG.md | 2 ++ pyproject.toml | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 69059de..5f83d1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [0.3.1] - 2024-09-03 + ### Added ### Changed diff --git a/pyproject.toml b/pyproject.toml index 33e3e4f..050b8a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ readme = "README.md" # Do not manually edit the version, use `make version_{type}` instead. # This should match the version in the [tool.bumpversion] section. -version = "0.3.0" +version = "0.3.1" dependencies = ["django>=3.2", "djangorestframework>=3.12.0"] [project.urls] @@ -132,7 +132,7 @@ filterwarnings = "error" [tool.bumpversion] # Do not manually edit the version, use `make version_{type}` instead. # This should match the version in the [project] section. -current_version = "0.3.0" +current_version = "0.3.1" parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)" serialize = ["{major}.{minor}.{patch}"] search = "{current_version}" From bf8ec523380c151f2e0a965c11c27ecd9b4e114e Mon Sep 17 00:00:00 2001 From: Nicholas Bunn Date: Mon, 23 Sep 2024 16:01:22 +0100 Subject: [PATCH 38/44] Add support for Django 5.1 Adds tested support for Django 5.1 --- README.md | 2 +- noxfile.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 932fd70..5e91d61 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ Further use cases are documented in the tests, see [**here**][tests] for more us This package supports: - Python 3.10, 3.11 -- Django 3.2, 4.0, 4.1, 4.2, 5.0 +- Django 3.2, 4.0, 4.1, 4.2, 5.0, 5.1 - Django Rest Framework 3.12, 3.13, 3.14, 3.15 For an exact list of tested version combinations, see the `valid_version_combinations` set in the [noxfile](https://github.com/kraken-tech/django-rest-framework-recursive/blob/master/noxfile.py) diff --git a/noxfile.py b/noxfile.py index fe5253e..49b4f9b 100644 --- a/noxfile.py +++ b/noxfile.py @@ -44,6 +44,7 @@ def temp_lock_file() -> Generator[IO[str], None, None]: ("3.11", "django>=4.2,<4.3", "djangorestframework>=3.15,<3.16"), ("3.11", "django>=5.0,<5.1", "djangorestframework>=3.14,<3.15"), ("3.11", "django>=5.0,<5.1", "djangorestframework>=3.15,<3.16"), + ("3.11", "django>=5.1,<5.2", "djangorestframework>=3.15,<3.16"), ] From e165df0218ed0013bfb9c3977ca3f64b969063ce Mon Sep 17 00:00:00 2001 From: Nicholas Bunn Date: Mon, 23 Sep 2024 16:01:40 +0100 Subject: [PATCH 39/44] Add support for Python 3.12 --- README.md | 2 +- noxfile.py | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5e91d61..d8c5976 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ Further use cases are documented in the tests, see [**here**][tests] for more us This package supports: -- Python 3.10, 3.11 +- Python 3.10, 3.11, 3.12 - Django 3.2, 4.0, 4.1, 4.2, 5.0, 5.1 - Django Rest Framework 3.12, 3.13, 3.14, 3.15 diff --git a/noxfile.py b/noxfile.py index 49b4f9b..ecea008 100644 --- a/noxfile.py +++ b/noxfile.py @@ -45,6 +45,15 @@ def temp_lock_file() -> Generator[IO[str], None, None]: ("3.11", "django>=5.0,<5.1", "djangorestframework>=3.14,<3.15"), ("3.11", "django>=5.0,<5.1", "djangorestframework>=3.15,<3.16"), ("3.11", "django>=5.1,<5.2", "djangorestframework>=3.15,<3.16"), + # Python 3.12 + ("3.12", "django>=4.1,<4.2", "djangorestframework>=3.14,<3.15"), + ("3.12", "django>=4.1,<4.2", "djangorestframework>=3.15,<3.16"), + ("3.12", "django>=4.2,<4.3", "djangorestframework>=3.14,<3.15"), + ("3.12", "django>=4.2,<4.3", "djangorestframework>=3.15,<3.16"), + ("3.12", "django>=5.0,<5.1", "djangorestframework>=3.14,<3.15"), + ("3.12", "django>=5.0,<5.1", "djangorestframework>=3.15,<3.16"), + ("3.12", "django>=5.1,<5.2", "djangorestframework>=3.14,<3.15"), + ("3.12", "django>=5.1,<5.2", "djangorestframework>=3.15,<3.16"), ] From fb0db5fa31cd23ab80aad605df1da85fb4d86c1a Mon Sep 17 00:00:00 2001 From: Nicholas Bunn Date: Thu, 3 Oct 2024 16:15:20 +0100 Subject: [PATCH 40/44] Update CHANGELOG --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f83d1e..d7cbfc7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added + +- Tested support for Python 3.12 +- Tested support for Django versions 5.1 + +### Changed + +### Removed + ## [0.3.1] - 2024-09-03 ### Added From 230eba83b728de26191eaac69e1a70ec1eabe1b1 Mon Sep 17 00:00:00 2001 From: Nicholas Bunn Date: Thu, 3 Oct 2024 16:21:07 +0100 Subject: [PATCH 41/44] Bump to v0.4.0 --- CHANGELOG.md | 2 ++ pyproject.toml | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d7cbfc7..791ad45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [0.4.0] - 2024-10-03 + ### Added - Tested support for Python 3.12 diff --git a/pyproject.toml b/pyproject.toml index 050b8a2..dee76e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ readme = "README.md" # Do not manually edit the version, use `make version_{type}` instead. # This should match the version in the [tool.bumpversion] section. -version = "0.3.1" +version = "0.4.0" dependencies = ["django>=3.2", "djangorestframework>=3.12.0"] [project.urls] @@ -132,7 +132,7 @@ filterwarnings = "error" [tool.bumpversion] # Do not manually edit the version, use `make version_{type}` instead. # This should match the version in the [project] section. -current_version = "0.3.1" +current_version = "0.4.0" parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)" serialize = ["{major}.{minor}.{patch}"] search = "{current_version}" From d21de8cd676e99c6dd64f6d68ad991811dec8cad Mon Sep 17 00:00:00 2001 From: Nicholas Bunn Date: Fri, 2 May 2025 09:54:50 +1000 Subject: [PATCH 42/44] Add updated Django support for Python 3.11 Adds support for Django 5.2 and Django Rest Framework 3.17 to Python 3.11 --- noxfile.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/noxfile.py b/noxfile.py index ecea008..0d0facd 100644 --- a/noxfile.py +++ b/noxfile.py @@ -45,6 +45,9 @@ def temp_lock_file() -> Generator[IO[str], None, None]: ("3.11", "django>=5.0,<5.1", "djangorestframework>=3.14,<3.15"), ("3.11", "django>=5.0,<5.1", "djangorestframework>=3.15,<3.16"), ("3.11", "django>=5.1,<5.2", "djangorestframework>=3.15,<3.16"), + ("3.11", "django>=5.1,<5.2", "djangorestframework>=3.16,<3.17"), + ("3.11", "django>=5.2,<5.3", "djangorestframework>=3.15,<3.16"), + ("3.11", "django>=5.2,<5.3", "djangorestframework>=3.16,<3.17"), # Python 3.12 ("3.12", "django>=4.1,<4.2", "djangorestframework>=3.14,<3.15"), ("3.12", "django>=4.1,<4.2", "djangorestframework>=3.15,<3.16"), From 5acb08f61389a145d49684c71c1a13cc1c7a0645 Mon Sep 17 00:00:00 2001 From: Nicholas Bunn Date: Fri, 2 May 2025 09:55:10 +1000 Subject: [PATCH 43/44] Add updated Django support for Python 3.12 Adds support for Django 5.2 and Django Rest Framework 3.17 to Python 3.12 --- noxfile.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/noxfile.py b/noxfile.py index 0d0facd..a3b3903 100644 --- a/noxfile.py +++ b/noxfile.py @@ -57,6 +57,10 @@ def temp_lock_file() -> Generator[IO[str], None, None]: ("3.12", "django>=5.0,<5.1", "djangorestframework>=3.15,<3.16"), ("3.12", "django>=5.1,<5.2", "djangorestframework>=3.14,<3.15"), ("3.12", "django>=5.1,<5.2", "djangorestframework>=3.15,<3.16"), + ("3.12", "django>=5.1,<5.2", "djangorestframework>=3.16,<3.17"), + ("3.12", "django>=5.2,<5.3", "djangorestframework>=3.14,<3.15"), + ("3.12", "django>=5.2,<5.3", "djangorestframework>=3.15,<3.16"), + ("3.12", "django>=5.2,<5.3", "djangorestframework>=3.16,<3.17"), ] From 0887860cf857a9c08079688f689bbd5c99c75baa Mon Sep 17 00:00:00 2001 From: Nicholas Bunn Date: Fri, 2 May 2025 09:55:27 +1000 Subject: [PATCH 44/44] Add updated Django support for Python 3.13 Adds support for Django 5.2 and Django Rest Framework 3.17 to Python 3.13 --- noxfile.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index a3b3903..27450eb 100644 --- a/noxfile.py +++ b/noxfile.py @@ -10,7 +10,6 @@ import nox - # Use uv to manage venvs. nox.options.default_venv_backend = "uv" @@ -61,6 +60,19 @@ def temp_lock_file() -> Generator[IO[str], None, None]: ("3.12", "django>=5.2,<5.3", "djangorestframework>=3.14,<3.15"), ("3.12", "django>=5.2,<5.3", "djangorestframework>=3.15,<3.16"), ("3.12", "django>=5.2,<5.3", "djangorestframework>=3.16,<3.17"), + # Python 3.13 + ("3.13", "django>=4.1,<4.2", "djangorestframework>=3.14,<3.15"), + ("3.13", "django>=4.1,<4.2", "djangorestframework>=3.15,<3.16"), + ("3.13", "django>=4.2,<4.3", "djangorestframework>=3.14,<3.15"), + ("3.13", "django>=4.2,<4.3", "djangorestframework>=3.15,<3.16"), + ("3.13", "django>=5.0,<5.1", "djangorestframework>=3.14,<3.15"), + ("3.13", "django>=5.0,<5.1", "djangorestframework>=3.15,<3.16"), + ("3.13", "django>=5.1,<5.2", "djangorestframework>=3.14,<3.15"), + ("3.13", "django>=5.1,<5.2", "djangorestframework>=3.15,<3.16"), + ("3.13", "django>=5.1,<5.2", "djangorestframework>=3.16,<3.17"), + ("3.13", "django>=5.2,<5.3", "djangorestframework>=3.14,<3.15"), + ("3.13", "django>=5.2,<5.3", "djangorestframework>=3.15,<3.16"), + ("3.13", "django>=5.2,<5.3", "djangorestframework>=3.16,<3.17"), ]