Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 131 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,22 @@
[![Codecov](https://codecov.io/gh/lukeburden/django-konst/branch/master/graph/badge.svg)](https://codecov.io/gh/lukeburden/django-konst)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)

## Deprecated

**`django-konst` is no longer actively maintained.** The core problem it solves -- named, labelled
constants with `choices` support -- is now covered by Django's own
[`models.IntegerChoices` / `models.TextChoices`](https://docs.djangoproject.com/en/stable/ref/models/fields/#enumeration-types)
(built on Python's `enum.Enum`, available since Django 3.0), so there's no need for a third-party
library for that. It's a trade-off rather than a strict replacement, though: you lose some
syntactic sugar -- `apple.purpose.cooking` and group-membership checks like `apple.purpose.culinary`
become explicit `==`/`in` checks -- in exchange for attributes that are visible to static analysis
and IDE autocomplete (see [below](#migrating-away-from-django-konst)).

**4.0.0 is the final planned release.** It exists to give existing dependents a version that works
with currently-supported Python (3.10-3.14) and Django (5.2 LTS, 6.0) before you migrate away, not
to signal ongoing feature development. See [Migrating away from django-konst](#migrating-away-from-django-konst)
below for the modern equivalent of each feature. No further releases, bug fixes, or new Django/Python
version support are planned; the repository will be archived at some point after this release.

## django-konst

Expand Down Expand Up @@ -270,6 +286,121 @@ apple.save()
Regardless of whether you're using `django-konst` for your constants, it's good practice to not hard-code constant values in order to avoid subtle mistakes and to ease changes in the future.


## Migrating away from django-konst

Django's built-in [`models.IntegerChoices` / `models.TextChoices`](https://docs.djangoproject.com/en/stable/ref/models/fields/#enumeration-types)
cover nearly everything below, with better IDE/type-checker support than `django-konst`'s dynamic
attribute lookups, and no third-party dependency.

On that last point specifically: `django-konst` resolves things like `Apple.purposes.cooking` and
`apple.purpose.culinary` via `__getattr__` and runtime `setattr` calls -- those attributes don't
exist until a `Constants(...)` call executes, so no static analyzer or IDE (mypy, pyright/Pylance,
PyCharm) can see them, and you get no autocomplete and no "unknown attribute" errors on typos.
`IntegerChoices`/`TextChoices` members are real, declared class attributes (`Purpose.COOKING` is
just an enum member), so every type checker and IDE understands them for free, with no plugin or
extra tooling required.

### Defining constants

```python
from django.db import models
from django.utils.translation import gettext_lazy as _

class Purpose(models.IntegerChoices):
COOKING = 0, _("Cook me!")
EATING = 1, _("Eat me!")
JUICING = 2, _("Juice me!")
ORNAMENTAL = 3, _("Just look how pretty I am!")

class Colour(models.TextChoices):
RED = "FF0000", _("red")
GREEN = "00FF00", _("green")
YELLOW = "FFFF80", _("yellow")
WHITE = "FFFFFF", _("white")
```

### Model fields

No custom field class needed — `IntegerChoices`/`TextChoices` members *are* `int`/`str` instances,
so a plain field with `choices=` does the job, and you get `get_purpose_display()` for free:

```python
class Apple(models.Model):
purpose = models.IntegerField(choices=Purpose.choices)
colour = models.CharField(choices=Colour.choices, max_length=6)
```

### Using constants

```python
apple.purpose == Apple.Purpose.COOKING # rather than apple.purpose.cooking
apple.purpose = Apple.Purpose.EATING
apple.get_purpose_display() # "Cook me!" -- Django provides this automatically
```

There's no built-in equivalent of `ConstantGroup`; a plain set (optionally as a classmethod or
module-level constant) covers it:

```python
CULINARY_PURPOSES = {Purpose.COOKING, Purpose.EATING, Purpose.JUICING}

apple.purpose in CULINARY_PURPOSES # rather than apple.purpose.culinary
```

### Forms

Usually nothing to do — `ModelForm` generates the right field automatically from `choices=`. For a
bare (non-model) form field: `forms.TypedChoiceField(choices=Purpose.choices, coerce=Purpose)`.

### JSON serialization

Nothing to do — `IntegerChoices`/`TextChoices` members are plain `int`/`str` subclasses, so
`json.dumps()` handles them natively. `django-konst`'s custom encoder isn't needed.

### DRF: exposing the member name instead of the raw value

The one place stock DRF doesn't do quite what `konst.extras.drf.fields.ConstantChoiceField` did —
expose `"cooking"` over the API rather than the raw `0` — is exposing an enum by its *name*. DRF
doesn't ship a field for this, but it's a small enough one to own directly rather than depend on a
library for. Copy this into your own codebase and adapt as needed:

```python
from rest_framework.fields import Field

class EnumNameField(Field):
"""Expose a Django Choices enum member by its name (e.g. "cooking") rather
than its underlying value (e.g. 0)."""

default_error_messages = {"invalid_choice": '"{input}" is not a valid choice.'}

def __init__(self, choices_enum, **kwargs):
self.choices_enum = choices_enum
super().__init__(**kwargs)

def to_internal_value(self, data):
try:
return self.choices_enum[str(data).upper()]
except KeyError:
self.fail("invalid_choice", input=data)

def to_representation(self, value):
try:
return self.choices_enum(value).name.lower()
except ValueError:
return None
```

```python
class AppleSerializer(serializers.ModelSerializer):
purpose = EnumNameField(Purpose)
colour = EnumNameField(Colour)

class Meta:
model = Apple
fields = ("name", "purpose", "colour")
```


## Contribute

`django-konst` follows the [supported Python](https://endoflife.date/python) and [supported Django](https://www.djangoproject.com/download/) release lines, currently Python 3.10-3.14 against Django 5.2 (LTS) and 6.0. [GitHub Actions](https://github.com/lukeburden/django-konst/actions) runs the full matrix on every push and pull request, plus an allowed-to-fail job against Django's `main` branch to surface upcoming breakage early.
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ build-backend = "setuptools.build_meta"

[project]
name = "django-konst"
version = "3.0.0"
version = "4.0.0"
description = "Convenient constants fields for Django"
readme = "README.md"
license = { text = "MIT" }
authors = [{ name = "Luke Burden", email = "lukeburden@gmail.com" }]
requires-python = ">=3.10"
dependencies = ["django>=5.2"]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Development Status :: 7 - Inactive",
"Environment :: Web Environment",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
Expand Down
Loading