-
Notifications
You must be signed in to change notification settings - Fork 23
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
📝 Closes #65 -- document migration path to native choices
* Extensively documented (and tested some) equivalent usage for the public API * Removed bulk of quickstart information from README to coerce users to upgrade * Added notices to _please_ migrate to vanilla enums
- Loading branch information
1 parent
8c8ef8c
commit 7ee1f36
Showing
5 changed files
with
203 additions
and
76 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import unittest | ||
|
||
from django.db.models import TextChoices | ||
|
||
from djchoices import ChoiceItem, DjangoChoices | ||
|
||
|
||
class ToMigrate(DjangoChoices): | ||
option_1 = ChoiceItem("option_1") | ||
option_2 = ChoiceItem("option_2", "Option 2") | ||
|
||
|
||
class Native(TextChoices): | ||
option_1 = "option_1", "option 1" | ||
option_2 = "option_2", "Option 2" | ||
|
||
|
||
class NativeChoicesEquivalenceTests(unittest.TestCase): | ||
|
||
def test_labels(self): | ||
labels = ToMigrate.labels | ||
native = dict(zip(Native.names, Native.labels)) | ||
|
||
self.assertEqual(native, labels) | ||
|
||
def test_values(self): | ||
values = ToMigrate.values | ||
native = dict(zip(Native.values, Native.labels)) | ||
|
||
self.assertEqual(native, values) | ||
|
||
def test_attributes(self): | ||
attributes = ToMigrate.attributes | ||
native = dict(zip(Native.values, Native.names)) | ||
|
||
self.assertEqual(native, attributes) | ||
|
||
def test_get_choice(self): | ||
a_choice = ToMigrate.get_choice(ToMigrate.option_2) | ||
native = Native[Native.option_2] | ||
|
||
self.assertEqual(native.value, a_choice.value) | ||
self.assertEqual(native.label, a_choice.label) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
.. _migration: | ||
|
||
Migrating to native Django Choices | ||
================================== | ||
|
||
Since version 3.0, Django offers native choices enums (mostly equivalent) to the | ||
functionality that this library offers. See the `django docs`_ for more details. | ||
|
||
We provide some automated tooling to facilitate migrating and instructions for possible | ||
hurdles. | ||
|
||
Generating equivalent native code | ||
--------------------------------- | ||
|
||
For trivial usage where you just define the choices as a constant, there is a management | ||
command since version 2.0 to generate the equivalent code. It supports ``str`` and ``int`` | ||
for values types. | ||
|
||
#. Ensure you have ``djchoices`` added to your ``INSTALLED_APPS`` setting. | ||
#. Run the command ``python manage.py generate_native_django_choices`` | ||
|
||
The command essentially discovers all subclasses of ``DjangoChoices`` and introspects | ||
them to generate the equivalent native choices code. This depends on the classes being | ||
imported during Django's initialization phase. This should be the case for almost all | ||
usages, as the choices need to be imported to be picked up by models. | ||
|
||
Possible options for the command: | ||
|
||
* ``--no-wrap-gettext``: do not wrap the choice labels in a function call to mark them | ||
translatable. | ||
|
||
* ``--gettext-alias``: when wrapping the labels, you can specify the name of the | ||
function call/alias to wrap with, e.g. ``gettext_lazy``. It defaults to the common | ||
pattern of ``_``. You need to ensure the necessary imports are present in the module. | ||
|
||
Public API | ||
---------- | ||
|
||
* The ``choices`` class attribute behaves the same in native choices. | ||
|
||
Migrating non-trivial usage | ||
--------------------------- | ||
|
||
Django-choices offered some class attributes that need to be updated too when migrating | ||
to native choices. | ||
|
||
|
||
``DjangoChoices.labels`` | ||
^^^^^^^^^^^^^^^^^^^^^^^^ | ||
|
||
This is roughly equivalent to: | ||
|
||
.. code-block:: python | ||
dict(zip(Native.names, Native.labels)) | ||
Notable differences: | ||
|
||
* for a value like ``'option1'`` without explicit label, Django Choices produces | ||
``'option1'`` as label, while Django produces ``'option 1'``. A value like | ||
``'option_1'`` results in the same label. | ||
* It may not play nice with the empty option in native choices. | ||
|
||
|
||
``ChoiceItem.order`` | ||
^^^^^^^^^^^^^^^^^^^^ | ||
|
||
The management command emits the generated native choices in the configured order. | ||
|
||
If you need access to the order, you can leverage ``enumerate(Native.values)`` to loop | ||
over tuples of ``(order, value)``. | ||
|
||
``DjangoChoices.values`` | ||
^^^^^^^^^^^^^^^^^^^^^^^^ | ||
|
||
This is equivalent to: | ||
|
||
.. code-block:: python | ||
dict(zip(Native.values, Native.labels)) | ||
``DjangoChoices.validator`` | ||
^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||
|
||
Django has been performing this out of the box on model fields since at least | ||
Django 1.3 - you don't need it. | ||
|
||
``DjangoChoices.attributes`` | ||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||
|
||
This is roughly equivalent to: | ||
|
||
.. code-block:: python | ||
native = dict(zip(Native.values, Native.names)) | ||
Remarks: | ||
|
||
* It may not play nice with the empty option in native choices. | ||
|
||
``DjangoChoices.get_choice`` | ||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||
|
||
There is no direct equivalent, however you can access the enum instance and look up | ||
properties: | ||
|
||
.. code-block:: python | ||
an_enum_value = Native[Native.some_value] | ||
print(an_enum_value.value) | ||
print(an_enum_value.label) | ||
``DjangoChoices.get_order_expression`` | ||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||
|
||
There is no equivalent, but you should easily be able to add this as your own | ||
class method/mixin: | ||
|
||
.. code-block:: python | ||
from django.db.models import Case, IntegerField, Value, When | ||
@classmethod | ||
def get_order_expression(cls, field_name): | ||
whens = [] | ||
for order, value in enumerate(cls.values()): | ||
whens.append( | ||
When(**{field_name: value, "then": Value(order)}) | ||
) | ||
return Case(*whens, output_field=IntegerField()) | ||
Custom attributes | ||
^^^^^^^^^^^^^^^^^ | ||
|
||
It's recommended to keep a separate dictionary with a mapping of choice values to the | ||
additional attributes. You could consider dataclasses to model this too. | ||
|
||
|
||
.. _django docs: https://docs.djangoproject.com/en/3.2/ref/models/fields/#enumeration-types |