Merge pull request #661 from bckohan/docs

refresh docs, add missing intersphinx references, update CONTRIBUTING
This commit is contained in:
Brian Kohan 2025-08-26 14:25:25 -07:00 committed by GitHub
commit 7c1a73c5cd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 652 additions and 447 deletions

View File

@ -111,40 +111,40 @@ just release x.x.x
## Just Recipes
```bash
build # build docs and package
build-docs # build the docs
build-docs-html # build html documentation
build-docs-pdf # build pdf documentation
check # run all static checks
check-docs # lint the documentation
check-docs-links # check the documentation links for broken links
check-format # check if the code needs formatting
check-lint # lint the code
check-package # run package checks
check-readme # check that the readme renders
check-types # run static type checking
clean # remove all non repository artifacts
clean-docs # remove doc build artifacts
clean-env # remove the virtual environment
clean-git-ignored # remove all git ignored files
coverage # generate the test coverage report
docs # build and open the documentation
docs-live # serve the documentation, with auto-reload
fetch-refs LIB
fix # fix formatting, linting issues and import sorting
format # format the code and sort imports
install *OPTS # update and install development dependencies
install-docs # install documentation dependencies
install-precommit # install git pre-commit hooks
install_uv # install the uv package manager
lint # sort the imports and fix linting issues
manage *COMMAND # run the django admin
open-docs # open the html documentation
precommit # run the pre-commit checks
release VERSION # issue a release for the given semver string (e.g. 2.1.0)
run +ARGS # run the command in the virtual environment
setup python="python" # setup the venv and pre-commit hooks
sort-imports # sort the python imports
test *TESTS # run tests
validate_version VERSION # validate the given version string against the lib version
build # build docs and package
build-docs # build the docs
build-docs-html # build html documentation
build-docs-pdf # build pdf documentation
check # run all static checks
check-docs # lint the documentation
check-docs-links # check the documentation links for broken links
check-format # check if the code needs formatting
check-lint # lint the code
check-package # run package checks
check-readme # check that the readme renders
check-types # run static type checking
clean # remove all non repository artifacts
clean-docs # remove doc build artifacts
clean-env # remove the virtual environment
clean-git-ignored # remove all git ignored files
coverage # generate the test coverage report
docs # build and open the documentation
docs-live # serve the documentation, with auto-reload
fetch-refs LIB
fix # fix formatting, linting issues and import sorting
format # format the code and sort imports
install *OPTS # update and install development dependencies
install-docs # install documentation dependencies
install-precommit # install git pre-commit hooks
install_uv # install the uv package manager
lint # sort the imports and fix linting issues
manage *COMMAND # run the django admin
open-docs # open the html documentation
precommit # run the pre-commit checks
release VERSION # issue a release for the given semver string (e.g. 2.1.0)
run +ARGS # run the command in the virtual environment
setup python="python" # setup the venv and pre-commit hooks
sort-imports # sort the python imports
test *TESTS # run tests
validate_version VERSION # validate the given version string against the lib version
```

32
docs/_static/style.css vendored Normal file
View File

@ -0,0 +1,32 @@
section#api-documentation div.highlight pre {
color: #b30000;
display: block; /* ensures it's treated as a block */
margin-left: auto; /* auto margins center block elements */
margin-right: auto;
width: fit-content;
}
body[data-theme="light"] section#api-documentation div.highlight,
body[data-theme="light"] section#api-documentation div.highlight pre {
background-color: #f8f8f8;
}
body[data-theme="dark"] section#api-documentation div.highlight,
body[data-theme="dark"] section#api-documentation div.highlight pre {
background-color: #202020;
}
/* AUTO → system prefers DARK (acts like dark unless user forced light) */
@media (prefers-color-scheme: dark) {
body:not([data-theme="light"]) #api-documentation .highlight,
body:not([data-theme="light"]) #api-documentation .highlight pre {
background-color: #202020;
}
}
/* AUTO → system prefers LIGHT (acts like light unless user forced dark) */
@media (prefers-color-scheme: light) {
body:not([data-theme="dark"]) #api-documentation .highlight,
body:not([data-theme="dark"]) #api-documentation .highlight pre {
background-color: #f8f8f8;
}
}

View File

@ -1,26 +1,27 @@
Django admin integration
========================
Admin Integration
=================
Of course, it's possible to register individual polymorphic models in the Django admin interface.
However, to use these models in a single cohesive interface, some extra base classes are available.
Of course, it's possible to register individual polymorphic models in the
:doc:`Django admin interface <django:ref/contrib/admin/index>`. However, to use these models in a single
cohesive interface, some extra base classes are available.
Setup
-----
Both the parent model and child model need to have a ``ModelAdmin`` class.
Both the parent model and child model need to have a :class:`~django.contrib.admin.ModelAdmin` class.
The shared base model should use the :class:`~polymorphic.admin.PolymorphicParentModelAdmin` as base
class.
* :attr:`~polymorphic.admin.PolymorphicParentModelAdmin.base_model` should be set
* :attr:`~polymorphic.admin.PolymorphicParentModelAdmin.child_models` or
:meth:`~polymorphic.admin.PolymorphicParentModelAdmin.get_child_models` should return an iterable
of Model classes.
* :attr:`~polymorphic.admin.PolymorphicParentModelAdmin.base_model` should be set
* :attr:`~polymorphic.admin.PolymorphicParentModelAdmin.child_models` or
:meth:`~polymorphic.admin.PolymorphicParentModelAdmin.get_child_models` should return an iterable
of Model classes.
The admin class for every child model should inherit from
:class:`~polymorphic.admin.PolymorphicChildModelAdmin`
* :attr:`~polymorphic.admin.PolymorphicChildModelAdmin.base_model` should be set.
* :attr:`~polymorphic.admin.PolymorphicChildModelAdmin.base_model` should be set.
Although the child models are registered too, they won't be shown in the admin index page.
This only happens when :attr:`~polymorphic.admin.PolymorphicChildModelAdmin.show_in_index` is set to
@ -29,27 +30,31 @@ This only happens when :attr:`~polymorphic.admin.PolymorphicChildModelAdmin.show
Fieldset configuration
~~~~~~~~~~~~~~~~~~~~~~
The parent admin is only used for the list display of models,
and for the edit/delete view of non-subclassed models.
The parent admin is only used for the list display of models, and for the edit/delete view of
non-subclassed models.
All other model types are redirected to the edit/delete/history view of the child model admin.
Hence, the fieldset configuration should be placed on the child admin.
.. tip::
When the child admin is used as base class for various derived classes, avoid using
the standard ``ModelAdmin`` attributes ``form`` and ``fieldsets``.
Instead, use the ``base_form`` and ``base_fieldsets`` attributes.
This allows the :class:`~polymorphic.admin.PolymorphicChildModelAdmin` class
to detect any additional fields in case the child model is overwritten.
.. versionchanged:: 1.0
.. versionchanged:: 1.0
It's now needed to register the child model classes too.
In *django-polymorphic* 0.9 and below, the ``child_models`` was a tuple of a
``(Model, ChildModelAdmin)``. The admin classes were registered in an internal class, and kept
away from the main admin site. This caused various subtle problems with the ``ManyToManyField``
and related field wrappers, which are fixed by registering the child admin classes too. Note
that they are hidden from the main view, unless
In :pypi:`django-polymorphic` 0.9 and below, the
:meth:`~polymorphic.admin.PolymorphicParentModelAdmin.child_models` was a tuple of a
(:class:`~django.db.models.Model`, :class:`~polymorphic.admin.PolymorphicChildModelAdmin`). The
admin classes were registered in an internal class, and kept away from the main admin site. This
caused various subtle problems with the :class:`~django.db.models.ManyToManyField` and related
field wrappers, which are fixed by registering the child admin classes too. Note that they are
hidden from the main view, unless
:attr:`~polymorphic.admin.PolymorphicChildModelAdmin.show_in_index` is set.
.. _admin-example:
@ -104,8 +109,8 @@ Filtering child types
---------------------
Child model types can be filtered by adding a
:class:`~polymorphic.admin.PolymorphicChildModelFilter` to the ``list_filter`` attribute. See the
example above.
:class:`~polymorphic.admin.PolymorphicChildModelFilter` to the
:attr:`~django.contrib.admin.ModelAdmin.list_filter` attribute. See the example above.
Inline models
@ -118,15 +123,13 @@ Inline models are handled via a special :class:`~polymorphic.admin.StackedPolymo
For models with a generic foreign key, there is a
:class:`~polymorphic.admin.GenericStackedPolymorphicInline` class available.
When the inline is included to a normal :class:`~django.contrib.admin.ModelAdmin`,
make sure the :class:`~polymorphic.admin.PolymorphicInlineSupportMixin` is included.
This is not needed when the admin inherits from the
:class:`~polymorphic.admin.PolymorphicParentModelAdmin` /
When the inline is included to a normal :class:`~django.contrib.admin.ModelAdmin`, make sure the
:class:`~polymorphic.admin.PolymorphicInlineSupportMixin` is included. This is not needed when the
admin inherits from the :class:`~polymorphic.admin.PolymorphicParentModelAdmin` or
:class:`~polymorphic.admin.PolymorphicChildModelAdmin` classes.
In the following example, the ``PaymentInline`` supports several types.
These are defined as separate inline classes.
The child classes can be nested for clarity, but this is not a requirement.
In the following example, the ``PaymentInline`` supports several types. These are defined as
separate inline classes. The child classes can be nested for clarity, but this is not a requirement.
.. code-block:: python
@ -170,15 +173,13 @@ The child classes can be nested for clarity, but this is not a requirement.
inlines = (PaymentInline,)
Using polymorphic models in standard inlines
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
To add a polymorphic child model as an Inline for another model, add a field to the inline's
``readonly_fields`` list formed by the lowercased name of the polymorphic parent model with the
string ``_ptr`` appended to it. Otherwise, trying to save that model in the admin will raise an
AttributeError with the message "can't set attribute".
:attr:`~django.contrib.admin.ModelAdmin.readonly_fields` list formed by the lowercased name of the
polymorphic parent model with the string ``_ptr`` appended to it. Otherwise, trying to save that
model in the admin will raise an :exc:`AttributeError` with the message "can't set attribute".
.. code-block:: python
@ -207,9 +208,9 @@ The polymorphic admin interface works in a simple way:
* The edit screen displays the admin interface of the child model.
* The list screen still displays all objects of the base class.
The polymorphic admin is implemented via a parent admin that redirects the *edit* and *delete* views
to the ``ModelAdmin`` of the derived child model. The *list* page is still implemented by the parent
model admin.
The polymorphic admin is implemented via a parent admin that redirects the ``edit`` and ``delete``
views to the :class:`~django.contrib.admin.ModelAdmin` of the derived child model. The ``list`` page
is still implemented by the parent model admin.
The parent model
~~~~~~~~~~~~~~~~
@ -217,27 +218,32 @@ The parent model
The parent model needs to inherit :class:`~polymorphic.admin.PolymorphicParentModelAdmin`, and
implement the following:
* :attr:`~polymorphic.admin.PolymorphicParentModelAdmin.base_model` should be set
* :attr:`~polymorphic.admin.PolymorphicParentModelAdmin.child_models` or
:meth:`~polymorphic.admin.PolymorphicParentModelAdmin.get_child_models` should return an iterable
of Model classes.
* :attr:`~polymorphic.admin.PolymorphicParentModelAdmin.base_model` should be set
* :attr:`~polymorphic.admin.PolymorphicParentModelAdmin.child_models` or
:meth:`~polymorphic.admin.PolymorphicParentModelAdmin.get_child_models` should return an iterable
of Model classes.
The exact implementation can depend on the way your module is structured.
For simple inheritance situations, ``child_models`` is the best solution.
For large applications, ``get_child_models()`` can be used to query a plugin registration system.
The exact implementation can depend on the way your module is structured. For simple inheritance
situations, :meth:`~polymorphic.admin.PolymorphicParentModelAdmin.child_models` is the best
solution. For large applications,
:meth:`~polymorphic.admin.PolymorphicParentModelAdmin.get_child_models` can be used to query a
plugin registration system.
By default, the non_polymorphic() method will be called on the queryset, so
only the Parent model will be provided to the list template. This is to avoid
the performance hit of retrieving child models.
By default, the :meth:`~polymorphic.managers.PolymorphicQuerySet.non_polymorphic` method will be
called on the queryset, so only the Parent model will be provided to the list template. This is to
avoid the performance hit of retrieving child models.
This can be controlled by setting the ``polymorphic_list`` property on the
parent admin. Setting it to True will provide child models to the list template.
This can be controlled by setting the
:attr:`~polymorphic.admin.PolymorphicParentModelAdmin.polymorphic_list` property on the parent
admin. Setting it to True will provide child models to the list template.
If you use other applications such as django-reversion_ or django-mptt_, please check
+:ref:`third-party`.
:ref:`integrations`.
Note: If you are using non-integer primary keys in your model, you have to edit ``pk_regex``,
for example ``pk_regex = '([\w-]+)'`` if you use UUIDs. Otherwise you cannot change model entries.
Note: If you are using non-integer primary keys in your model, you have to edit
:attr:`~polymorphic.admin.PolymorphicParentModelAdmin.pk_regex`, for example
``pk_regex = '([\w-]+)'`` if you use :class:`~uuid.UUID` primary keys. Otherwise you cannot change
model entries.
The child models
~~~~~~~~~~~~~~~~

View File

@ -1,9 +1,11 @@
.. _advanced-features:
Advanced features
Advanced Features
=================
In the examples below, these models are being used::
In the examples below, these models are being used:
.. code-block:: python
from django.db import models
from polymorphic.models import PolymorphicModel
@ -17,35 +19,41 @@ In the examples below, these models are being used::
class ModelC(ModelB):
field3 = models.CharField(max_length=10)
Filtering for classes (equivalent to python's isinstance() ):
-------------------------------------------------------------
>>> ModelA.objects.instance_of(ModelB)
.
[ <ModelB: id 2, field1 (CharField), field2 (CharField)>,
<ModelC: id 3, field1 (CharField), field2 (CharField), field3 (CharField)> ]
Filtering for classes (equivalent to python's :func:`isinstance`):
------------------------------------------------------------------
In general, including or excluding parts of the inheritance tree::
.. code-block:: python
>>> ModelA.objects.instance_of(ModelB)
[ <ModelB: id 2, field1 (CharField), field2 (CharField)>,
<ModelC: id 3, field1 (CharField), field2 (CharField), field3 (CharField)> ]
In general, including or excluding parts of the inheritance tree:
.. code-block:: python
ModelA.objects.instance_of(ModelB [, ModelC ...])
ModelA.objects.not_instance_of(ModelB [, ModelC ...])
You can also use this feature in Q-objects (with the same result as above):
>>> ModelA.objects.filter( Q(instance_of=ModelB) )
.. code-block:: python
>>> ModelA.objects.filter( Q(instance_of=ModelB) )
Polymorphic filtering (for fields in inherited classes)
-------------------------------------------------------
For example, cherrypicking objects from multiple derived classes
anywhere in the inheritance tree, using Q objects (with the
syntax: ``exact model name + three _ + field name``):
For example, cherry-picking objects from multiple derived classes anywhere in the inheritance tree,
using Q objects (with the syntax: ``exact model name + three _ + field name``):
>>> ModelA.objects.filter( Q(ModelB___field2 = 'B2') | Q(ModelC___field3 = 'C3') )
.
[ <ModelB: id 2, field1 (CharField), field2 (CharField)>,
<ModelC: id 3, field1 (CharField), field2 (CharField), field3 (CharField)> ]
.. code-block:: python
>>> ModelA.objects.filter( Q(ModelB___field2 = 'B2') | Q(ModelC___field3 = 'C3') )
[ <ModelB: id 2, field1 (CharField), field2 (CharField)>,
<ModelC: id 3, field1 (CharField), field2 (CharField), field3 (CharField)> ]
Combining Querysets
@ -56,10 +64,12 @@ aggregation of different object types, very similar to python
lists - as long as the objects are accessed through the manager of
a common base class:
>>> Base.objects.instance_of(ModelX) | Base.objects.instance_of(ModelY)
.
[ <ModelX: id 1, field_x (CharField)>,
<ModelY: id 2, field_y (CharField)> ]
.. code-block:: python
>>> Base.objects.instance_of(ModelX) | Base.objects.instance_of(ModelY)
[ <ModelX: id 1, field_x (CharField)>,
<ModelY: id 2, field_y (CharField)> ]
ManyToManyField, ForeignKey, OneToOneField
@ -70,17 +80,23 @@ expected: like polymorphic querysets they now always return the
referred objects with the same type/class these were created and
saved as.
E.g., if in your model you define::
E.g., if in your model you define:
.. code-block:: python
field1 = OneToOneField(ModelA)
then field1 may now also refer to objects of type ``ModelB`` or ``ModelC``.
A ManyToManyField example::
A :class:`~django.db.models.ManyToManyField` example:
.. code-block:: python
# The model holding the relation may be any kind of model, polymorphic or not
class RelatingModel(models.Model):
many2many = models.ManyToManyField('ModelA') # ManyToMany relation to a polymorphic model
# ManyToMany relation to a polymorphic model
many2many = models.ManyToManyField('ModelA')
>>> o=RelatingModel.objects.create()
>>> o.many2many.add(ModelA.objects.get(id=1))
@ -96,9 +112,11 @@ Copying Polymorphic objects
---------------------------
When creating a copy of a polymorphic object, both the
``.id`` and the ``.pk`` of the object need to be set
to ``None`` before saving so that both the base table
and the derived table will be updated to the new object::
:attr:`~django.db.models.Model.id` and the :attr:`~django.db.models.Model.pk` of the object need to
be set to ``None`` before saving so that both the base table and the derived table will be updated
to the new object:
.. code-block:: python
>>> o = ModelB.objects.first()
>>> o.field1 = 'new val' # leave field2 unchanged
@ -112,7 +130,9 @@ Using Third Party Models (without modifying them)
Third party models can be used as polymorphic models without
restrictions by subclassing them. E.g. using a third party
model as the root of a polymorphic inheritance tree::
model as the root of a polymorphic inheritance tree:
.. code-block:: python
from thirdparty import ThirdPartyModel
@ -120,7 +140,9 @@ model as the root of a polymorphic inheritance tree::
pass # or add fields
Or instead integrating the third party model anywhere into an
existing polymorphic inheritance tree::
existing polymorphic inheritance tree:
.. code-block:: python
class MyBaseModel(SomePolymorphicModel):
my_field = models.CharField(max_length=10)
@ -132,90 +154,96 @@ existing polymorphic inheritance tree::
Non-Polymorphic Queries
-----------------------
If you insert ``.non_polymorphic()`` anywhere into the query chain, then
django_polymorphic will simply leave out the final step of retrieving the
real objects, and the manager/queryset will return objects of the type of
the base class you used for the query, like vanilla Django would
(``ModelA`` in this example).
If you insert :meth:`~polymorphic.managers.PolymorphicQuerySet.non_polymorphic` anywhere into the
query chain, then :pypi:`django-polymorphic` will simply leave out the final step of retrieving the
real objects, and the manager/queryset will return objects of the type of the base class you used
for the query, like vanilla Django would (``ModelA`` in this example).
>>> qs=ModelA.objects.non_polymorphic().all()
>>> qs
[ <ModelA: id 1, field1 (CharField)>,
<ModelA: id 2, field1 (CharField)>,
<ModelA: id 3, field1 (CharField)> ]
.. code-block:: python
>>> qs=ModelA.objects.non_polymorphic().all()
>>> qs
[ <ModelA: id 1, field1 (CharField)>,
<ModelA: id 2, field1 (CharField)>,
<ModelA: id 3, field1 (CharField)> ]
There are no other changes in the behaviour of the queryset. For example,
enhancements for ``filter()`` or ``instance_of()`` etc. still work as expected.
If you do the final step yourself, you get the usual polymorphic result:
>>> ModelA.objects.get_real_instances(qs)
[ <ModelA: id 1, field1 (CharField)>,
<ModelB: id 2, field1 (CharField), field2 (CharField)>,
<ModelC: id 3, field1 (CharField), field2 (CharField), field3 (CharField)> ]
.. code-block:: python
>>> ModelA.objects.get_real_instances(qs)
[ <ModelA: id 1, field1 (CharField)>,
<ModelB: id 2, field1 (CharField), field2 (CharField)>,
<ModelC: id 3, field1 (CharField), field2 (CharField), field3 (CharField)> ]
About Queryset Methods
----------------------
* ``annotate()`` and ``aggregate()`` work just as usual, with the
addition that the ``ModelX___field`` syntax can be used for the
keyword arguments (but not for the non-keyword arguments).
* :meth:`~django.db.models.query.QuerySet.annotate` and
:meth:`~django.db.models.query.QuerySet.aggregate` work just as usual, with the addition that
the ``ModelX___field`` syntax can be used for the keyword arguments (but not for the non-keyword
arguments).
* ``order_by()`` similarly supports the ``ModelX___field`` syntax
for specifying ordering through a field in a submodel.
* :meth:`~django.db.models.query.QuerySet.order_by` similarly supports the ``ModelX___field``
syntax for specifying ordering through a field in a submodel.
* ``distinct()`` works as expected. It only regards the fields of
the base class, but this should never make a difference.
* :meth:`~django.db.models.query.QuerySet.distinct` works as expected. It only regards the fields
of the base class, but this should never make a difference.
* ``select_related()`` works just as usual, but it can not (yet) be used
to select relations in inherited models
(like ``ModelA.objects.select_related('ModelC___fieldxy')`` )
* :meth:`~django.db.models.query.QuerySet.select_related` works just as usual, but it can not
(yet) be used to select relations in inherited models (like
``ModelA.objects.select_related('ModelC___fieldxy')`` )
* ``extra()`` works as expected (it returns polymorphic results) but
currently has one restriction: The resulting objects are required to have
a unique primary key within the result set - otherwise an error is thrown
(this case could be made to work, however it may be mostly unneeded)..
The keyword-argument "polymorphic" is no longer supported.
You can get back the old non-polymorphic behaviour
by using ``ModelA.objects.non_polymorphic().extra(...)``.
* :meth:`~django.db.models.query.QuerySet.extra` works as expected (it returns polymorphic
results) but currently has one restriction: The resulting objects are required to have a unique
primary key within the result set - otherwise an error is thrown (this case could be made to
work, however it may be mostly unneeded).. The keyword-argument "polymorphic" is no longer
supported. You can get back the old non-polymorphic behaviour by using
``ModelA.objects.non_polymorphic().extra(...)``.
* ``get_real_instances()`` allows you to turn a
* :meth:`~polymorphic.managers.PolymorphicQuerySet.get_real_instances` allows you to turn a
queryset or list of base model objects efficiently into the real objects.
For example, you could do ``base_objects_queryset=ModelA.extra(...).non_polymorphic()``
and then call ``real_objects=base_objects_queryset.get_real_instances()``. Or alternatively
``real_objects=ModelA.objects.get_real_instances(base_objects_queryset_or_object_list)``
* ``values()`` & ``values_list()`` currently do not return polymorphic
results. This may change in the future however. If you want to use these
methods now, it's best if you use ``Model.base_objects.values...`` as
this is guaranteed to not change.
* :meth:`~django.db.models.query.QuerySet.values` &
:meth:`~django.db.models.query.QuerySet.values_list` currently do not return polymorphic
results. This may change in the future however. If you want to use these methods now, it's best
if you use ``Model.base_objects.values...`` as this is guaranteed to not change.
* ``defer()`` and ``only()`` work as expected. On Django 1.5+ they support
the ``ModelX___field`` syntax, but on Django 1.4 it is only possible to
pass fields on the base model into these methods.
* :meth:`~django.db.models.query.QuerySet.defer` and :meth:`~django.db.models.query.QuerySet.only`
work as expected. On Django 1.5+ they support the ``ModelX___field`` syntax, but on Django 1.4
it is only possible to pass fields on the base model into these methods.
Using enhanced Q-objects in any Places
--------------------------------------
The queryset enhancements (e.g. ``instance_of``) only work as arguments
to the member functions of a polymorphic queryset. Occasionally it may
be useful to be able to use Q objects with these enhancements in other places.
As Django doesn't understand these enhanced Q objects, you need to
transform them manually into normal Q objects before you can feed them
to a Django queryset or function::
The queryset enhancements (e.g. :meth:`~polymorphic.managers.PolymorphicQuerySet.instance_of`)
only work as arguments to the member functions of a polymorphic queryset. Occasionally it may
be useful to be able to use Q objects with these enhancements in other places. As Django doesn't
understand these enhanced Q objects, you need to transform them manually into normal Q objects
before you can feed them to a Django queryset or function:
.. code-block:: python
normal_q_object = ModelA.translate_polymorphic_Q_object( Q(instance_of=Model2B) )
This function cannot be used at model creation time however (in models.py),
as it may need to access the ContentTypes database table.
This function cannot be used at model creation time however (in models.py), as it may need to access
the ContentTypes database table.
Nicely Displaying Polymorphic Querysets
---------------------------------------
In order to get the output as seen in all examples here, you need to use the
:class:`~polymorphic.showfields.ShowFieldType` class mixin::
:class:`~polymorphic.showfields.ShowFieldType` class mixin:
.. code-block:: python
from polymorphic.models import PolymorphicModel
from polymorphic.showfields import ShowFieldType
@ -223,12 +251,14 @@ In order to get the output as seen in all examples here, you need to use the
class ModelA(ShowFieldType, PolymorphicModel):
field1 = models.CharField(max_length=10)
You may also use :class:`~polymorphic.showfields.ShowFieldContent`
or :class:`~polymorphic.showfields.ShowFieldTypeAndContent` to display
additional information when printing querysets (or converting them to text).
You may also use :class:`~polymorphic.showfields.ShowFieldContent` or
:class:`~polymorphic.showfields.ShowFieldTypeAndContent` to display additional information when
printing querysets (or converting them to text).
When showing field contents, they will be truncated to 20 characters. You can
modify this behaviour by setting a class variable in your model like this::
When showing field contents, they will be truncated to 20 characters. You can modify this behavior
by setting a class variable in your model like this:
.. code-block:: python
class ModelA(ShowFieldType, PolymorphicModel):
polymorphic_showfield_max_field_width = 20
@ -238,36 +268,33 @@ Similarly, pre-V1.0 output formatting can be re-estated by using
``polymorphic_showfield_old_format = True``.
.. _restrictions:
Restrictions & Caveats
----------------------
* Database Performance regarding concrete Model inheritance in general.
Please see the :ref:`performance`.
* Database Performance regarding concrete Model inheritance in general. Please see
:ref:`performance`.
* Queryset methods ``values()``, ``values_list()``, and ``select_related()``
are not yet fully supported (see above). ``extra()`` has one restriction:
the resulting objects are required to have a unique primary key within
the result set.
* Queryset methods :meth:`~django.db.models.query.QuerySet.values`,
:meth:`~django.db.models.query.QuerySet.values_list`, and
:meth:`~django.db.models.query.QuerySet.select_related` are not yet fully supported (see above).
:meth:`~django.db.models.query.QuerySet.extra` has one restriction: the resulting objects are
required to have a unique primary key within the result set.
* Diamond shaped inheritance: There seems to be a general problem
with diamond shaped multiple model inheritance with Django models
(tested with V1.1 - V1.3).
An example is here: http://code.djangoproject.com/ticket/10808.
This problem is aggravated when trying to enhance models.Model
by subclassing it instead of modifying Django core (as we do here
with PolymorphicModel).
* Diamond shaped inheritance: There seems to be a general problem with diamond shaped multiple
model inheritance with Django models (tested with V1.1 - V1.3). An example
`is here <http://code.djangoproject.com/ticket/10808>`_. This problem is aggravated when trying
to enhance :class:`~django.db.models.Model` by subclassing it instead of modifying Django core
(as we do here with :class:`~polymorphic.models.PolymorphicModel`).
* The enhanced filter-definitions/Q-objects only work as arguments
for the methods of the polymorphic querysets. Please see above
for ``translate_polymorphic_Q_object``.
* The enhanced filter-definitions/Q-objects only work as arguments for the methods of the
polymorphic querysets. Please see above for ``translate_polymorphic_Q_object``.
* When using the ``dumpdata`` management command on polymorphic tables
(or any table that has a reference to
:class:`~django.contrib.contenttypes.models.ContentType`),
include the ``--natural-foreign`` and ``--natural-primary`` flags in the arguments.
* When using the :django-admin:`dumpdata` management command on polymorphic tables
(or any table that has a reference to :class:`~django.contrib.contenttypes.models.ContentType`),
include the :option:`--natural-primary <dumpdata.--natural-primary>` and
:option:`--natural-foreign <dumpdata.--natural-foreign>` flags in the arguments.

View File

@ -1,6 +1,13 @@
API Documentation
=================
.. _base:
.. automodule:: polymorphic
:members:
:show-inheritance:
:inherited-members:
.. toctree::
polymorphic.admin
@ -9,5 +16,6 @@ API Documentation
polymorphic.formsets
polymorphic.managers
polymorphic.models
polymorphic.showfields
polymorphic.templatetags
polymorphic.utils

View File

@ -0,0 +1,7 @@
polymorphic.showfields
======================
.. automodule:: polymorphic.showfields
:members:
:show-inheritance:
:inherited-members:

View File

@ -17,10 +17,10 @@ v4.0.0 (2025-05-20)
There were many updates modernizing the package and incorporating Jazzband standards:
* Updates to documentation
* Formatting and linting with ruff
* Moving to GHA from Travis CI
* Switch to pytest
* Updates to documentation
* Formatting and linting with ruff
* Moving to GHA from Travis CI
* Switch to pytest
Changes that touched the core package code were:

View File

@ -11,7 +11,9 @@
# serve to show the default.
import os
import shutil
import sys
from pathlib import Path
import django
@ -20,10 +22,12 @@ import django
# documentation root, use os.path.abspath to make it absolute, like shown here.
sys.path.insert(0, os.path.abspath("_ext"))
sys.path.insert(0, os.path.abspath(".."))
os.environ["DJANGO_SETTINGS_MODULE"] = "djangodummy.settings"
os.environ["DJANGO_SETTINGS_MODULE"] = "polymorphic.tests.settings"
django.setup()
import polymorphic # noqa: E402
# -- General configuration -----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
@ -35,6 +39,7 @@ extensions = [
"sphinx.ext.autodoc",
"sphinx.ext.graphviz",
"sphinx.ext.intersphinx",
"sphinx.ext.todo",
"sphinxcontrib_django",
]
@ -50,18 +55,13 @@ source_suffix = ".rst"
# The master toctree document.
master_doc = "index"
# General information about the project.
project = "django-polymorphic"
copyright = "2013, Bert Constantin, Chris Glass, Diederik van der Boor, Brian Kohan"
# -- Project information -----------------------------------------------------
project = polymorphic.__title__
copyright = polymorphic.__copyright__
author = polymorphic.__author__
release = polymorphic.__version__
version = ".".join(polymorphic.__version__.split(".")[:2])
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = "3.1"
# The full version, including alpha/beta/rc tags.
release = "3.1"
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
@ -92,7 +92,7 @@ exclude_patterns = ["_build"]
# show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = "sphinx"
# pygments_style = "sphinx"
# A list of ignored prefixes for module index sorting.
# modindex_common_prefix = []
@ -102,7 +102,12 @@ pygments_style = "sphinx"
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = "sphinx_rtd_theme"
html_theme = "furo"
html_theme_options = {
"source_repository": "https://github.com/jazzband/django-polymorphic/",
"source_branch": "master",
"source_directory": "docs",
}
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
@ -132,6 +137,9 @@ html_theme = "sphinx_rtd_theme"
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ["_static"]
html_css_files = [
"style.css",
]
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
@ -180,6 +188,7 @@ htmlhelp_basename = "django-polymorphicdoc"
# -- Options for LaTeX output --------------------------------------------------
latex_engine = "xelatex"
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
@ -273,7 +282,39 @@ intersphinx_mapping = {
"https://docs.djangoproject.com/en/stable/_objects/",
),
"python": ("https://docs.python.org/3", None),
"django-guardian": ("https://django-guardian.readthedocs.io/en/stable", None),
"django-reversion": ("https://django-reversion.readthedocs.io/en/stable", None),
"django-extra-views": ("https://django-extra-views.readthedocs.io/en/stable", None),
}
# autodoc settings
autodoc_member_order = "groupwise"
autodoc_default_options = {
"show-inheritance": True,
# Add other autodoc options here if desired, e.g.:
# 'members': True,
# 'inherited-members': True,
}
# In your Sphinx conf.py
autodoc_typehints = "description"
autodoc_typehints_format = "short"
autodoc_class_signature = "separated"
autodoc_member_order = "groupwise" # "bysource"
def pypi_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
from docutils import nodes
url = f"https://pypi.org/project/{text}/"
node = nodes.reference(rawtext, text, refuri=url, **options)
return [node], []
def setup(app):
from docutils.parsers.rst import roles
# https://sphinxcontrib-typer.readthedocs.io/en/latest/howto.html#build-to-multiple-formats
if Path(app.doctreedir).exists():
shutil.rmtree(app.doctreedir)
app.add_crossref_type(directivename="django-admin", rolename="django-admin")
roles.register_local_role("pypi", pypi_role)
return app

View File

@ -1,48 +0,0 @@
Contributing
============
You can contribute to *django-polymorphic* to forking the code on GitHub:
https://github.com/jazzband/django-polymorphic
Running tests
-------------
We require features to be backed by a unit test.
This way, we can test *django-polymorphic* against new Django versions.
To run the included test suite, execute::
pytest
To test support for multiple Python and Django versions, run tox from the repository root::
pip install tox
tox
The Python versions need to be installed at your system.
On Linux, download the versions at http://www.python.org/download/releases/.
On MacOS X, use Homebrew_ to install other Python versions.
Example project
----------------
The repository contains a complete Django project that may be used for tests or experiments,
without any installation needed.
The management command ``pcmd.py`` in the app ``pexp`` can be used for quick tests
or experiments - modify this file (pexp/management/commands/pcmd.py) to your liking.
Supported Django versions
-------------------------
The current release should be usable with the supported releases of Django;
the current stable release and the previous release. Supporting older Django
versions is a nice-to-have feature, but not mandatory.
In case you need to use *django-polymorphic* with older Django versions,
consider installing a previous version.
.. _Homebrew: http://mxcl.github.io/homebrew/

View File

@ -5,7 +5,7 @@ Formsets
Polymorphic models can be used in formsets.
The implementation is almost identical to the regular Django formsets.
The implementation is almost identical to the regular Django :doc:`django:topics/forms/formsets`.
As extra parameter, the factory needs to know how to display the child models.
Provide a list of :class:`~polymorphic.formsets.PolymorphicFormSetChild` objects for this.
@ -29,7 +29,7 @@ The formset can be used just like all other formsets:
else:
formset = ModelAFormSet(queryset=ModelA.objects.all())
Like standard Django formsets, there are 3 factory methods available:
Like standard Django :doc:`django:topics/forms/formsets`, there are 3 factory methods available:
* :func:`~polymorphic.formsets.polymorphic_modelformset_factory` - create a regular model formset.
* :func:`~polymorphic.formsets.polymorphic_inlineformset_factory` - create a inline model formset.

View File

@ -1,7 +1,56 @@
Welcome to django-polymorphic's documentation!
==============================================
django-polymorphic
==================
Django-polymorphic builds on top of the standard Django model inheritance.
.. image:: https://img.shields.io/badge/License-BSD-blue.svg
:target: https://opensource.org/license/bsd-3-clause
:alt: License: BSD
.. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json
:target: https://github.com/astral-sh/ruff
:alt: Ruff
.. image:: https://badge.fury.io/py/django-polymorphic.svg
:target: https://pypi.python.org/pypi/django-polymorphic/
:alt: PyPI version
.. image:: https://img.shields.io/pypi/pyversions/django-polymorphic.svg
:target: https://pypi.python.org/pypi/django-polymorphic/
:alt: PyPI pyversions
.. image:: https://img.shields.io/pypi/djversions/django-polymorphic.svg
:target: https://pypi.org/project/django-polymorphic/
:alt: PyPI Django versions
.. image:: https://img.shields.io/pypi/status/django-polymorphic.svg
:target: https://pypi.python.org/pypi/django-polymorphic
:alt: PyPI status
.. image:: https://readthedocs.org/projects/django-polymorphic/badge/?version=latest
:target: http://django-polymorphic.readthedocs.io/?badge=latest/
:alt: Documentation Status
.. image:: https://img.shields.io/codecov/c/github/jazzband/django-polymorphic/master.svg
:target: https://codecov.io/github/jazzband/django-polymorphic?branch=master
:alt: Code Coverage
.. image:: https://github.com/jazzband/django-polymorphic/actions/workflows/test.yml/badge.svg?branch=master
:target: https://github.com/jazzband/django-polymorphic/actions/workflows/test.yml?query=branch:master
:alt: Test Status
.. image:: https://github.com/jazzband/django-polymorphic/actions/workflows/lint.yml/badge.svg?branch=master
:target: https://github.com/jazzband/django-polymorphic/actions/workflows/lint.yml?query=branch:master
:alt: Lint Status
.. image:: https://img.shields.io/badge/Published%20on-Django%20Packages-0c3c26
:target: https://djangopackages.org/packages/p/django-polymorphic/
:alt: Published on Django Packages
.. image:: https://jazzband.co/static/img/badge.svg
:target: https://jazzband.co/
:alt: Jazzband
:pypi:`django-polymorphic` builds on top of the standard Django model inheritance.
It makes using inherited models easier. When a query is made at the base model,
the inherited model classes are returned.
@ -37,12 +86,13 @@ Features
* Full admin integration.
* ORM integration:
* Support for ForeignKey, ManyToManyField, OneToOneField descriptors.
* Support for proxy models.
* Filtering/ordering of inherited models (``ArtProject___artist``).
* Filtering model types: ``instance_of(...)`` and ``not_instance_of(...)``
* Combining querysets of different models (``qs3 = qs1 | qs2``)
* Support for custom user-defined managers.
- Support for ForeignKey, ManyToManyField, OneToOneField descriptors.
- Support for proxy models.
- Filtering/ordering of inherited models (``ArtProject___artist``).
- Filtering model types: :meth:`~polymorphic.managers.PolymorphicQuerySet.instance_of` and
:meth:`~polymorphic.managers.PolymorphicQuerySet.not_instance_of`
- Combining querysets of different models (``qs3 = qs1 | qs2``)
- Support for custom user-defined managers.
* Formset support.
* Uses the minimum amount of queries needed to fetch the inherited models.
@ -58,7 +108,7 @@ Getting started
quickstart
admin
performance
third-party
integrations
Advanced topics
---------------
@ -71,7 +121,6 @@ Advanced topics
managers
advanced
changelog
contributing
api/index

View File

@ -1,29 +1,35 @@
Third-party applications support
================================
.. _integrations:
Integrations
============
django-guardian support
-----------------------
.. _django-django-guardian-support:
django-guardian
---------------
.. versionadded:: 1.0.2
You can configure django-guardian_ to use the base model for object level permissions.
You can configure :pypi:`django-guardian` to use the base model for object level permissions.
Add this option to your settings:
.. code-block:: python
GUARDIAN_GET_CONTENT_TYPE = 'polymorphic.contrib.guardian.get_polymorphic_base_content_type'
GUARDIAN_GET_CONTENT_TYPE = \
'polymorphic.contrib.guardian.get_polymorphic_base_content_type'
This option requires django-guardian_ >= 1.4.6. Details about how this option works are available in
the `django-guardian documentation
<https://django-guardian.readthedocs.io/en/latest/configuration>`_.
This option requires :pypi:`django-guardian` >= 1.4.6. Details about how this option works are
available in the `django-guardian documentation
<https://django-guardian.readthedocs.io/en/stable/configuration>`_.
django-rest-framework support
-----------------------------
.. _django-rest-framework-support:
The django-rest-polymorphic_ package provides polymorphic serializers that help you integrate your
polymorphic models with `django-rest-framework`.
djangorestframework
-------------------
The :pypi:`django-rest-polymorphic` package provides polymorphic serializers that help you integrate
your polymorphic models with :pypi:`djangorestframework`.
Example
@ -77,42 +83,46 @@ Create viewset with serializer_class equals to your polymorphic serializer:
serializer_class = ProjectPolymorphicSerializer
.. _django-extra-views-support:
django-extra-views
------------------
.. versionadded:: 1.1
The :mod:`polymorphic.contrib.extra_views` package provides classes to display polymorphic formsets
using the classes from django-extra-views_. See the documentation of:
using the classes from :pypi:`django-extra-views`. See the documentation of:
* :class:`~polymorphic.contrib.extra_views.PolymorphicFormSetView`
* :class:`~polymorphic.contrib.extra_views.PolymorphicInlineFormSetView`
* :class:`~polymorphic.contrib.extra_views.PolymorphicInlineFormSet`
django-mptt support
-------------------
.. _django-mptt-support:
Combining polymorphic with django-mptt_ is certainly possible, but not straightforward.
django-mptt
-----------
Combining polymorphic with :pypi:`django-mptt` is certainly possible, but not straightforward.
It involves combining both managers, querysets, models, meta-classes and admin classes
using multiple inheritance.
The django-polymorphic-tree_ package provides this out of the box.
The :pypi:`django-polymorphic-tree` package provides this out of the box.
django-reversion support
------------------------
.. _django-reversion-support:
Support for django-reversion_ works as expected with polymorphic models.
django-reversion
----------------
Support for :pypi:`django-reversion` works as expected with polymorphic models.
However, they require more setup than standard models. That's become:
* Manually register the child models with django-reversion_, so their ``follow`` parameter can be
set.
* Polymorphic models use `multi-table inheritance
<https://docs.djangoproject.com/en/dev/topics/db/models/#multi-table-inheritance>`_.
See the `reversion documentation
<https://django-reversion.readthedocs.io/en/latest/api.html#registration-api>`_
how to deal with this by adding a ``follow`` field for the primary key.
* Manually register the child models with :pypi:`django-reversion`, so their ``follow`` parameter
can be set.
* Polymorphic models use :ref:`django:multi-table-inheritance`.
See the :doc:`django-reversion:api` for how to deal with this by adding a ``follow`` field for the
primary key.
* Both admin classes redefine ``object_history_template``.
@ -171,10 +181,10 @@ polymorphic model.
.. _django-reversion-compare-support:
django-reversion-compare support
--------------------------------
django-reversion-compare
------------------------
The django-reversion-compare_ views work as expected, the admin requires a little tweak.
The :pypi:`django-reversion-compare` views work as expected, the admin requires a little tweak.
In your parent admin, include the following method:
.. code-block:: python
@ -188,12 +198,3 @@ As the compare view resolves the the parent admin, it uses it's base model to fi
This doesn't work, since it needs to look for revisions of the child model. Using this tweak,
the view of the actual child model is used, similar to the way the regular change and delete views
are redirected.
.. _django-extra-views: https://github.com/AndrewIngram/django-extra-views
.. _django-guardian: https://github.com/django-guardian/django-guardian
.. _django-mptt: https://github.com/django-mptt/django-mptt
.. _django-polymorphic-tree: https://github.com/django-polymorphic/django-polymorphic-tree
.. _django-rest-polymorphic: https://github.com/apirobot/django-rest-polymorphic
.. _django-reversion-compare: https://github.com/jedie/django-reversion-compare
.. _django-reversion: https://github.com/etianen/django-reversion

View File

@ -1,14 +1,16 @@
Custom Managers, Querysets & Manager Inheritance
================================================
Managers & Querysets
====================
Using a Custom Manager
----------------------
A nice feature of Django is the possibility to define one's own custom object managers.
This is fully supported with django_polymorphic: For creating a custom polymorphic
manager class, just derive your manager from ``PolymorphicManager`` instead of
``models.Manager``. As with vanilla Django, in your model class, you should
explicitly add the default manager first, and then your custom manager::
This is fully supported with :pypi:`django-polymorphic`. For creating a custom polymorphic
manager class, just derive your manager from :class:`~polymorphic.managers.PolymorphicManager`
instead of :class:`~django.db.models.Manager`. As with vanilla Django, in your model class, you
should explicitly add the default manager first, and then your custom manager:
.. code-block:: python
from polymorphic.models import PolymorphicModel
from polymorphic.managers import PolymorphicManager
@ -16,29 +18,29 @@ explicitly add the default manager first, and then your custom manager::
class TimeOrderedManager(PolymorphicManager):
def get_queryset(self):
qs = super(TimeOrderedManager,self).get_queryset()
return qs.order_by('-start_date') # order the queryset
return qs.order_by('-start_date')
def most_recent(self):
qs = self.get_queryset() # get my ordered queryset
return qs[:10] # limit => get ten most recent entries
qs = self.get_queryset() # get my ordered queryset
return qs[:10] # limit => get ten most recent entries
class Project(PolymorphicModel):
objects = PolymorphicManager() # add the default polymorphic manager first
objects_ordered = TimeOrderedManager() # then add your own manager
start_date = DateTimeField() # project start is this date/time
objects = PolymorphicManager() # add the default polymorphic manager first
objects_ordered = TimeOrderedManager() # then add your own manager
start_date = DateTimeField() # project start is this date/time
The first manager defined ('objects' in the example) is used by
Django as automatic manager for several purposes, including accessing
related objects. It must not filter objects and it's safest to use
the plain ``PolymorphicManager`` here.
The first manager defined (:attr:`~django.db.models.Model.objects` in the example) is used by Django
as automatic manager for several purposes, including accessing related objects. It must not filter
objects and it's safest to use the plain :class:`~polymorphic.managers.PolymorphicManager` here.
Manager Inheritance
-------------------
Polymorphic models inherit/propagate all managers from their
base models, as long as these are polymorphic. This means that all
managers defined in polymorphic base models continue to work as
expected in models inheriting from this base model::
Polymorphic models inherit/propagate all managers from their base models, as long as these are
polymorphic. This means that all managers defined in polymorphic base models continue to work as
expected in models inheriting from this base model:
.. code-block:: python
from polymorphic.models import PolymorphicModel
from polymorphic.managers import PolymorphicManager
@ -46,33 +48,34 @@ expected in models inheriting from this base model::
class TimeOrderedManager(PolymorphicManager):
def get_queryset(self):
qs = super(TimeOrderedManager,self).get_queryset()
return qs.order_by('-start_date') # order the queryset
return qs.order_by('-start_date')
def most_recent(self):
qs = self.get_queryset() # get my ordered queryset
return qs[:10] # limit => get ten most recent entries
qs = self.get_queryset() # get my ordered queryset
return qs[:10] # limit => get ten most recent entries
class Project(PolymorphicModel):
objects = PolymorphicManager() # add the default polymorphic manager first
objects_ordered = TimeOrderedManager() # then add your own manager
start_date = DateTimeField() # project start is this date/time
objects = PolymorphicManager() # add the default polymorphic manager first
objects_ordered = TimeOrderedManager() # then add your own manager
start_date = DateTimeField() # project start is this date/time
class ArtProject(Project): # inherit from Project, inheriting its fields and managers
class ArtProject(Project): # inherit from Project, inheriting its fields and managers
artist = models.CharField(max_length=30)
ArtProject inherited the managers ``objects`` and ``objects_ordered`` from Project.
``ArtProject.objects_ordered.all()`` will return all art projects ordered
regarding their start time and ``ArtProject.objects_ordered.most_recent()``
will return the ten most recent art projects.
``ArtProject.objects_ordered.all()`` will return all art projects ordered regarding their start time
and ``ArtProject.objects_ordered.most_recent()`` will return the ten most recent art projects.
Using a Custom Queryset Class
-----------------------------
The ``PolymorphicManager`` class accepts one initialization argument,
which is the queryset class the manager should use. Just as with vanilla Django,
you may define your own custom queryset classes. Just use PolymorphicQuerySet
instead of Django's QuerySet as the base class::
The :class:`~polymorphic.managers.PolymorphicManager` class accepts one initialization argument,
which is the queryset class the manager should use. Just as with vanilla Django, you may define your
own custom queryset classes. Just use :class:`~polymorphic.managers.PolymorphicQuerySet` instead of
Django's :class:`~django.db.models.query.QuerySet` as the base class:
.. code-block:: python
from polymorphic.models import PolymorphicModel
from polymorphic.managers import PolymorphicManager

View File

@ -1,8 +1,8 @@
Migrating existing models to polymorphic
========================================
Migrating Existing Models
=========================
Existing models can be migrated to become polymorphic models.
During the migrating, the ``polymorphic_ctype`` field needs to be filled in.
Existing models can be migrated to become polymorphic models. During migration, the
:attr:`~polymorphic.models.PolymorphicModel.polymorphic_ctype` field needs to be populated.
This can be done in the following steps:
@ -13,7 +13,7 @@ This can be done in the following steps:
Filling the content type value
------------------------------
The following Python code can be used to fill the value of a model:
The following code can be used to fill the value of a model:
.. code-block:: python
@ -23,8 +23,8 @@ The following Python code can be used to fill the value of a model:
new_ct = ContentType.objects.get_for_model(MyModel)
MyModel.objects.filter(polymorphic_ctype__isnull=True).update(polymorphic_ctype=new_ct)
The creation and update of the ``polymorphic_ctype_id`` column
can be included in a single Django migration. For example:
The creation and update of the ``polymorphic_ctype_id`` column can be included in a single Django
migration. For example:
.. code-block:: python
@ -37,7 +37,9 @@ can be included in a single Django migration. For example:
ContentType = apps.get_model('contenttypes', 'ContentType')
new_ct = ContentType.objects.get_for_model(MyModel)
MyModel.objects.filter(polymorphic_ctype__isnull=True).update(polymorphic_ctype=new_ct)
MyModel.objects.filter(polymorphic_ctype__isnull=True).update(
polymorphic_ctype=new_ct
)
class Migration(migrations.Migration):
@ -51,18 +53,23 @@ can be included in a single Django migration. For example:
migrations.AddField(
model_name='mymodel',
name='polymorphic_ctype',
field=models.ForeignKey(related_name='polymorphic_myapp.mymodel_set+', editable=False, to='contenttypes.ContentType', null=True),
field=models.ForeignKey(
related_name='polymorphic_myapp.mymodel_set+',
editable=False,
to='contenttypes.ContentType',
null=True
),
),
migrations.RunPython(forwards_func, migrations.RunPython.noop),
]
It's recommended to let ``makemigrations`` create the migration file,
and include the ``RunPython`` manually before running the migration.
It's recommended to let :django-admin:`makemigrations` create the migration file, and include the
:class:`~django.db.migrations.operations.RunPython` manually before running the migration.
.. versionadded:: 1.1
When the model is created elsewhere, you can also use
the :func:`polymorphic.utils.reset_polymorphic_ctype` function:
When the model is created elsewhere, you can also use the
:func:`~polymorphic.utils.reset_polymorphic_ctype` function:
.. code-block:: python

View File

@ -3,37 +3,37 @@
Performance Considerations
==========================
Usually, when Django users create their own polymorphic ad-hoc solution
without a tool like *django-polymorphic*, this usually results in a variation of ::
Usually, when Django users create their own polymorphic ad-hoc solution without a tool like
:pypi:`django-polymorphic`, this usually results in a variation of:
.. code-block:: python
result_objects = [ o.get_real_instance() for o in BaseModel.objects.filter(...) ]
which has very bad performance, as it introduces one additional
SQL query for every object in the result which is not of class ``BaseModel``.
Compared to these solutions, *django-polymorphic* has the advantage
that it only needs 1 SQL query *per object type*, and not *per object*.
which has very bad performance, as it introduces one additional SQL query for every object in the
result which is not of class ``BaseModel``. Compared to these solutions, :pypi:`django-polymorphic`
has the advantage that it only needs 1 SQL query *per object type*, and not *per object*.
The current implementation does not use any custom SQL or Django DB layer
internals - it is purely based on the standard Django ORM. Specifically, the query::
The current implementation does not use any custom SQL or Django DB layer internals - it is purely
based on the standard Django ORM. Specifically, the query:
.. code-block:: python
result_objects = list( ModelA.objects.filter(...) )
performs one SQL query to retrieve ``ModelA`` objects and one additional
query for each unique derived class occurring in result_objects.
The best case for retrieving 100 objects is 1 SQL query if all are
class ``ModelA``. If 50 objects are ``ModelA`` and 50 are ``ModelB``, then
two queries are executed. The pathological worst case is 101 db queries if
result_objects contains 100 different object types (with all of them
subclasses of ``ModelA``).
performs one SQL query to retrieve ``ModelA`` objects and one additional query for each unique
derived class occurring in result_objects. The best case for retrieving 100 objects is 1 SQL query
if all are class ``ModelA``. If 50 objects are ``ModelA`` and 50 are ``ModelB``, then two queries
are executed. The pathological worst case is 101 db queries if result_objects contains 100 different
object types (with all of them subclasses of ``ModelA``).
ContentType retrieval
---------------------
:class:`~django.contrib.contenttypes.models.ContentType` retrieval
------------------------------------------------------------------
When fetching the :class:`~django.contrib.contenttypes.models.ContentType` class,
it's tempting to read the ``object.polymorphic_ctype`` field directly.
However, this performs an additional query via the :class:`~django.db.models.ForeignKey` object
to fetch the :class:`~django.contrib.contenttypes.models.ContentType`.
Instead, use:
When fetching the :class:`~django.contrib.contenttypes.models.ContentType` class, it's tempting to
read the :attr:`~polymorphic.models.PolymorphicModel.polymorphic_ctype` field directly. However,
this performs an additional query via the :class:`~django.db.models.ForeignKey` object to fetch the
:class:`~django.contrib.contenttypes.models.ContentType`. Instead, use:
.. code-block:: python
@ -43,16 +43,3 @@ Instead, use:
This uses the :meth:`~django.contrib.contenttypes.models.ContentTypeManager.get_for_id` function
which caches the results internally.
Database notes
--------------
Current relational DBM systems seem to have general problems with
the SQL queries produced by object relational mappers like the Django
ORM, if these use multi-table inheritance like Django's ORM does.
The "inner joins" in these queries can perform very badly.
This is independent of django_polymorphic and affects all uses of
multi table Model inheritance.
Please also see this `post (and comments) from Jacob Kaplan-Moss
<http://jacobian.org/writing/concrete-inheritance/>`_.

View File

@ -5,19 +5,37 @@ Install the project using::
pip install django-polymorphic
Update the settings file::
Update the settings file:
.. code-block:: python
INSTALLED_APPS += (
'polymorphic',
'django.contrib.contenttypes',
)
The current release of *django-polymorphic* supports Django 2.2 - 4.0 and Python 3.6+.
The current release of :pypi:`django-polymorphic` supports:
.. image:: https://badge.fury.io/py/django-polymorphic.svg
:target: https://pypi.python.org/pypi/django-polymorphic/
:alt: PyPI version
.. image:: https://img.shields.io/pypi/pyversions/django-polymorphic.svg
:target: https://pypi.python.org/pypi/django-polymorphic/
:alt: Supported Pythons
.. image:: https://img.shields.io/pypi/djversions/django-polymorphic.svg
:target: https://pypi.org/project/django-polymorphic/
:alt: Supported Django
Making Your Models Polymorphic
------------------------------
Use ``PolymorphicModel`` instead of Django's ``models.Model``, like so::
Use :class:`~polymorphic.models.PolymorphicModel` instead of Django's
:class:`~django.db.models.Model`, like so:
.. code-block:: python
from polymorphic.models import PolymorphicModel
@ -37,25 +55,35 @@ Using Polymorphic Models
Create some objects:
>>> Project.objects.create(topic="Department Party")
>>> ArtProject.objects.create(topic="Painting with Tim", artist="T. Turner")
>>> ResearchProject.objects.create(topic="Swallow Aerodynamics", supervisor="Dr. Winter")
.. code-block:: python
>>> Project.objects.create(topic="Department Party")
>>> ArtProject.objects.create(topic="Painting with Tim", artist="T. Turner")
>>> ResearchProject.objects.create(topic="Swallow Aerodynamics", supervisor="Dr. Winter")
Get polymorphic query results:
>>> Project.objects.all()
[ <Project: id 1, topic "Department Party">,
<ArtProject: id 2, topic "Painting with Tim", artist "T. Turner">,
<ResearchProject: id 3, topic "Swallow Aerodynamics", supervisor "Dr. Winter"> ]
.. code-block:: python
Use ``instance_of`` or ``not_instance_of`` for narrowing the result to specific subtypes:
>>> Project.objects.all()
[ <Project: id 1, topic "Department Party">,
<ArtProject: id 2, topic "Painting with Tim", artist "T. Turner">,
<ResearchProject: id 3, topic "Swallow Aerodynamics", supervisor "Dr. Winter"> ]
>>> Project.objects.instance_of(ArtProject)
[ <ArtProject: id 2, topic "Painting with Tim", artist "T. Turner"> ]
Use :meth:`~polymorphic.managers.PolymorphicQuerySet.instance_of` and
:meth:`~polymorphic.managers.PolymorphicQuerySet.not_instance_of` for narrowing the result to
specific subtypes:
>>> Project.objects.instance_of(ArtProject) | Project.objects.instance_of(ResearchProject)
[ <ArtProject: id 2, topic "Painting with Tim", artist "T. Turner">,
<ResearchProject: id 3, topic "Swallow Aerodynamics", supervisor "Dr. Winter"> ]
.. code-block:: python
>>> Project.objects.instance_of(ArtProject)
[ <ArtProject: id 2, topic "Painting with Tim", artist "T. Turner"> ]
.. code-block:: python
>>> Project.objects.instance_of(ArtProject) | Project.objects.instance_of(ResearchProject)
[ <ArtProject: id 2, topic "Painting with Tim", artist "T. Turner">,
<ResearchProject: id 3, topic "Swallow Aerodynamics", supervisor "Dr. Winter"> ]
Polymorphic filtering: Get all projects where Mr. Turner is involved as an artist
or supervisor (note the three underscores):
@ -64,20 +92,23 @@ or supervisor (note the three underscores):
>>> Project.objects.filter(Q(ArtProject___artist='T. Turner') | Q(ResearchProject___supervisor='T. Turner'))
[ <ArtProject: id 2, topic "Painting with Tim", artist "T. Turner">,
<ResearchProject: id 4, topic "Color Use in Late Cubism", supervisor "T. Turner"> ]
<ResearchProject: id 4, topic "Color Use in Late Cubism", supervisor "T. Turner"> ]
This is basically all you need to know, as *django-polymorphic* mostly
works fully automatic and just delivers the expected results.
Note: When using the ``dumpdata`` management command on polymorphic tables
(or any table that has a reference to :class:`~django.contrib.contenttypes.models.ContentType`),
include the ``--natural`` flag in the arguments. This makes sure the
:class:`~django.contrib.contenttypes.models.ContentType` models will be referenced by name
instead of their primary key as that changes between Django instances.
.. note::
When using the :django-admin:`dumpdata` management command on polymorphic tables
(or any table that has a reference to :class:`~django.contrib.contenttypes.models.ContentType`),
include the :option:`--natural-primary <dumpdata.--natural-primary>` and
:option:`--natural-foreign <dumpdata.--natural-foreign>` flag in the arguments. This makes sure
the :class:`~django.contrib.contenttypes.models.ContentType` models will be referenced by name
instead of their primary key as that changes between Django instances.
.. note::
While *django-polymorphic* makes subclassed models easy to use in Django,
While :pypi:`django-polymorphic` makes subclassed models easy to use in Django,
we still encourage to use them with caution. Each subclassed model will require
Django to perform an ``INNER JOIN`` to fetch the model fields from the database.
While taking this in mind, there are valid reasons for using subclassed models.

View File

@ -162,10 +162,10 @@ dev = [
docs = [
"django-extra-views>=0.15.0",
"doc8>=1.1.2",
"furo>=2025.7.19",
"readme-renderer[md]>=43.0",
"sphinx>=7.1.2",
"sphinx-autobuild>=2021.3.14",
"sphinx-rtd-theme>=3.0.2",
"sphinx-autobuild>=2024.10.3",
"sphinxcontrib-django>=2.5",
]
psycopg2 = [

View File

@ -1,12 +1,30 @@
"""
Seamless Polymorphic Inheritance for Django Models
r"""
::
Copyright:
This code and affiliated files are (C) by Bert Constantin and individual contributors.
Please see LICENSE and AUTHORS for more information.
Seamless Polymorphic Inheritance for Django Models
"""
VERSION = "4.2.0"
# version synonym for backwards compatibility
__version__ = VERSION
__title__ = "Django Polymorphic"
__version__ = VERSION # version synonym for backwards compatibility
__author__ = "Brian Kohan"
__license__ = "BSD-3-Clause"
__copyright__ = (
"Copyright 2010-2025, Bert Constantin, Chris Glass, Diederik van der Boor, Brian Kohan"
)

84
uv.lock
View File

@ -1,5 +1,5 @@
version = 1
revision = 3
revision = 2
requires-python = ">=3.9, <4.0"
resolution-markers = [
"python_full_version >= '3.11'",
@ -7,6 +7,18 @@ resolution-markers = [
"python_full_version < '3.10'",
]
[[package]]
name = "accessible-pygments"
version = "0.0.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/bc/c1/bbac6a50d02774f91572938964c582fff4270eee73ab822a4aeea4d8b11b/accessible_pygments-0.0.5.tar.gz", hash = "sha256:40918d3e6a2b619ad424cb91e556bd3bd8865443d9f22f1dcdf79e33c8046872", size = 1377899, upload-time = "2024-05-10T11:23:10.216Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8d/3f/95338030883d8c8b91223b4e21744b04d11b161a3ef117295d8241f50ab4/accessible_pygments-0.0.5-py3-none-any.whl", hash = "sha256:88ae3211e68a1d0b011504b2ffc1691feafce124b845bd072ab6f9f66f34d4b7", size = 1395903, upload-time = "2024-05-10T11:23:08.421Z" },
]
[[package]]
name = "alabaster"
version = "0.7.16"
@ -77,6 +89,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" },
]
[[package]]
name = "beautifulsoup4"
version = "4.13.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "soupsieve" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/85/2e/3e5079847e653b1f6dc647aa24549d68c6addb4c595cc0d902d1b19308ad/beautifulsoup4-4.13.5.tar.gz", hash = "sha256:5e70131382930e7c3de33450a2f54a63d5e4b19386eab43a5b34d594268f3695", size = 622954, upload-time = "2025-08-24T14:06:13.168Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/eb/f4151e0c7377a6e08a38108609ba5cede57986802757848688aeedd1b9e8/beautifulsoup4-4.13.5-py3-none-any.whl", hash = "sha256:642085eaa22233aceadff9c69651bc51e8bf3f874fb6d7104ece2beb24b47c4a", size = 105113, upload-time = "2025-08-24T14:06:14.884Z" },
]
[[package]]
name = "cachetools"
version = "6.1.0"
@ -571,12 +596,12 @@ docs = [
{ name = "django-extra-views" },
{ name = "doc8", version = "1.1.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
{ name = "doc8", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
{ name = "furo" },
{ name = "readme-renderer", extra = ["md"] },
{ name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
{ name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" },
{ name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "sphinx-autobuild" },
{ name = "sphinx-rtd-theme" },
{ name = "sphinxcontrib-django" },
]
mysql = [
@ -614,10 +639,10 @@ dev = [
docs = [
{ name = "django-extra-views", specifier = ">=0.15.0" },
{ name = "doc8", specifier = ">=1.1.2" },
{ name = "furo", specifier = ">=2025.7.19" },
{ name = "readme-renderer", extras = ["md"], specifier = ">=43.0" },
{ name = "sphinx", specifier = ">=7.1.2" },
{ name = "sphinx-autobuild", specifier = ">=2021.3.14" },
{ name = "sphinx-rtd-theme", specifier = ">=3.0.2" },
{ name = "sphinx-autobuild", specifier = ">=2024.10.3" },
{ name = "sphinxcontrib-django", specifier = ">=2.5" },
]
mysql = [{ name = "mysqlclient", specifier = ">=1.4.0" }]
@ -703,6 +728,24 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/42/14/42b2651a2f46b022ccd948bca9f2d5af0fd8929c4eec235b8d6d844fbe67/filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d", size = 15988, upload-time = "2025-08-14T16:56:01.633Z" },
]
[[package]]
name = "furo"
version = "2025.7.19"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "accessible-pygments" },
{ name = "beautifulsoup4" },
{ name = "pygments" },
{ name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
{ name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" },
{ name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "sphinx-basic-ng" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d0/69/312cd100fa45ddaea5a588334d2defa331ff427bcb61f5fe2ae61bdc3762/furo-2025.7.19.tar.gz", hash = "sha256:4164b2cafcf4023a59bb3c594e935e2516f6b9d35e9a5ea83d8f6b43808fe91f", size = 1662054, upload-time = "2025-07-19T10:52:09.754Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3a/34/2b07b72bee02a63241d654f5d8af87a2de977c59638eec41ca356ab915cd/furo-2025.7.19-py3-none-any.whl", hash = "sha256:bdea869822dfd2b494ea84c0973937e35d1575af088b6721a29c7f7878adc9e3", size = 342175, upload-time = "2025-07-19T10:52:02.399Z" },
]
[[package]]
name = "h11"
version = "0.16.0"
@ -1471,6 +1514,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" },
]
[[package]]
name = "soupsieve"
version = "2.7"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/3f/f4/4a80cd6ef364b2e8b65b15816a843c0980f7a5a2b4dc701fc574952aa19f/soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a", size = 103418, upload-time = "2025-04-20T18:50:08.518Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4", size = 36677, upload-time = "2025-04-20T18:50:07.196Z" },
]
[[package]]
name = "sphinx"
version = "7.4.7"
@ -1585,19 +1637,17 @@ wheels = [
]
[[package]]
name = "sphinx-rtd-theme"
version = "3.0.2"
name = "sphinx-basic-ng"
version = "1.0.0b2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "docutils" },
{ name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
{ name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" },
{ name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "sphinxcontrib-jquery" },
]
sdist = { url = "https://files.pythonhosted.org/packages/91/44/c97faec644d29a5ceddd3020ae2edffa69e7d00054a8c7a6021e82f20335/sphinx_rtd_theme-3.0.2.tar.gz", hash = "sha256:b7457bc25dda723b20b086a670b9953c859eab60a2a03ee8eb2bb23e176e5f85", size = 7620463, upload-time = "2024-11-13T11:06:04.545Z" }
sdist = { url = "https://files.pythonhosted.org/packages/98/0b/a866924ded68efec7a1759587a4e478aec7559d8165fac8b2ad1c0e774d6/sphinx_basic_ng-1.0.0b2.tar.gz", hash = "sha256:9ec55a47c90c8c002b5960c57492ec3021f5193cb26cebc2dc4ea226848651c9", size = 20736, upload-time = "2023-07-08T18:40:54.166Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/85/77/46e3bac77b82b4df5bb5b61f2de98637724f246b4966cfc34bc5895d852a/sphinx_rtd_theme-3.0.2-py2.py3-none-any.whl", hash = "sha256:422ccc750c3a3a311de4ae327e82affdaf59eb695ba4936538552f3b00f4ee13", size = 7655561, upload-time = "2024-11-13T11:06:02.094Z" },
{ url = "https://files.pythonhosted.org/packages/3c/dd/018ce05c532a22007ac58d4f45232514cd9d6dd0ee1dc374e309db830983/sphinx_basic_ng-1.0.0b2-py3-none-any.whl", hash = "sha256:eb09aedbabfb650607e9b4b68c9d240b90b1e1be221d6ad71d61c52e29f7932b", size = 22496, upload-time = "2023-07-08T18:40:52.659Z" },
]
[[package]]
@ -1644,20 +1694,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" },
]
[[package]]
name = "sphinxcontrib-jquery"
version = "4.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
{ name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" },
{ name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/de/f3/aa67467e051df70a6330fe7770894b3e4f09436dea6881ae0b4f3d87cad8/sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a", size = 122331, upload-time = "2023-03-14T15:01:01.944Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/76/85/749bd22d1a68db7291c89e2ebca53f4306c3f205853cf31e9de279034c3c/sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae", size = 121104, upload-time = "2023-03-14T15:01:00.356Z" },
]
[[package]]
name = "sphinxcontrib-jsmath"
version = "1.0.1"