A Django plugin for mimicking the power of SQLAlchemy hybrid_property and hybrid_method
Project description
Django Hybrid Attributes
This is a (pretty basic) implementation of the SQLAlchemy Hybrid Attributes for Django - more precisely hybrid_property
and hybrid_method
.
Example of basic usage:
from django.db import models
from django_hybrid_attributes import hybrid_method, hybrid_property, HybridQuerySet
class User(models.Model):
first_name = models.CharField(max_length=63)
last_name = models.CharField(max_length=63)
some_value = models.PositiveSmallIntegerField()
objects = HybridQuerySet.as_manager()
@hybrid_property
def full_name(self):
return f'{self.first_name} {self.last_name}'
@full_name.expression
def full_name(cls, through=''):
return models.functions.Concat(f'{through}first_name', models.Value(' '), f'{through}last_name')
@hybrid_method
def some_value_plus_n(self, n):
return self.some_value + n
@some_value_plus_n.expression
def some_value_plus_n(cls, n, through=''):
return models.F(f'{through}some_value') + models.Value(n)
user1 = User.objects.create(first_name='Filipe', last_name='Waitman', some_value=10)
user2 = User.objects.create(first_name='Agent', last_name='Smith', some_value=5)
# Compatible with regular django .filter() - so this won't break your existing code
assert User.objects.filter(first_name='Filipe').count() == 1
assert User.objects.filter(models.Q(last_name='Waitman')).count() == 1
# hybrid_property/hybrid_method functions are common properties/methods
assert user1.full_name == 'Filipe Waitman'
assert user2.some_value_plus_n(10) == 15
# hybrid_property/hybrid_method expressions are translated to Q() objects, annotated, and filtered accordingly
assert User.objects.filter(User.full_name == 'Filipe Waitman').count() == 1
assert User.objects.filter(User.full_name == 'FILIPE WAITMAN').count() == 0
assert User.objects.filter(User.full_name != 'FILIPE WAITMAN').count() == 2
assert User.objects.filter(User.full_name.i() == 'FILIPE WAITMAN').count() == 1 # .i() ignores case, so iexact is applied
assert User.objects.filter(User.full_name.i().l('contains') == 'WAIT').count() == 1 # icontains is applied
assert User.objects.filter(User.some_value_plus_n(20) < 25).count() == 0
assert User.objects.filter(User.some_value_plus_n(20) > 25).count() == 1
assert User.objects.filter(User.some_value_plus_n(20) >= 25).count() == 2
# `.e()` returns the equivalent Django expression so you can use it as you wish
qs = User.objects.annotate(value_plus_3=User.some_value_plus_n(3).e())
assert [x.value_plus_3 for x in qs.order_by('value_plus_3')] == [8, 13]
For another examples, please refer to the tests folder.
Features:
- Filter support using Python magic methods. Examples:
Klass.objects.filter(Klass.my_hybrid_property == 'value') # lookup=exact
Klass.objects.filter(Klass.my_hybrid_property.i() == 'value') # lookup=iexact
Klass.objects.filter(Klass.my_hybrid_property != 'value') # lookup=exact, queryset_method=exclude
Klass.objects.filter(~Klass.my_hybrid_property == 'value') # lookup=exact, queryset_method=exclude
Klass.objects.filter(Klass.my_hybrid_property > 'value') # lookup=gt
Klass.objects.filter(Klass.my_hybrid_property < 'value') # lookup=lt
Klass.objects.filter(Klass.my_hybrid_property >= 'value') # lookup=gte
Klass.objects.filter(Klass.my_hybrid_property <= 'value') # lookup=lte
- Support of all django lookups via
l()
attribute. Examples:
Klass.objects.filter(Klass.my_hybrid_property.l('istartswith') == 'value')
Klass.objects.filter(Klass.my_hybrid_property.i().l('startswith') == 'value') # lookup=istartswith
Klass.objects.filter(Klass.my_hybrid_property.l('contains') == 'value')
Klass.objects.filter(Klass.my_hybrid_property.l('date__year') == 'value')
- Relations support via
t()
attribute. Examples:
Klass.objects.filter(Parent.my_hybrid_property.t('parent') == 'value')
Klass.objects.filter(GrandParent.my_hybrid_property.t('parent__grandparent') > 'value')
Klass.objects.filter(Child.my_hybrid_property.t('children') < 'value')
- Raw expressions (for you to use it whatever you want) via
.e()
attribute. Examples:
Klass.objects.annotate(my_method_result=Klass.my_hybrid_method().e())
- Custom alias via
.a()
attribute (so you can reference the annotated expression later on). Examples:
Klass.objects.filter(Klass.my_hybrid_property.a('_expr_alias') > 'value').order_by('_expr_alias')
- Test/script helper to ensure hybrid expressions are sane compared to its properties/methods. Examples:
from django_hybrid_attributes.test_utils import assert_hybrid_attributes_are_consistent, HybridTestCaseMixin
class MyTestCase(HybridTestCaseMixin, YourBaseTestcase):
def test_expressions_are_sane(self):
self.assertHybridAttributesAreConsistent(Klass.my_hybrid_property)
self.assertHybridAttributesAreConsistent(Klass.my_hybrid_method_without_args)
# In order to pass arguments to your function, pass them as args/kwargs in the assert call:
self.assertHybridAttributesAreConsistent(Klass.my_hybrid_method_with_args, 1)
self.assertHybridAttributesAreConsistent(Klass.my_hybrid_method_with_args, n=1)
# By default this will compare return of expression/function for all instances (Klass.objects.all()).
# In order to run for a subset of results use the `queryset` param:
self.assertHybridAttributesAreConsistent(Klass.my_hybrid_property, queryset=Klass.objects.filter(id=1))
# You can also use it as a helper (outside of tests scope) of some sort (HybridTestCaseMixin is not required):
assert_hybrid_attributes_are_consistent(Klass.my_hybrid_property)
- No dark magic: under the hood, all it does is to
annotate()
an expression to a queryset andfilter/exclude()
using this annotation.
FAQ
Q: Why do I need this project? Couldn't I use Klass.objects.annotate(whatever=<expression>).filter(whatever=<value>)
?
A: You don't need this project. And you could use this approach. That being said, I still see some reasons to use this project, such as:
- Cleaner and more concise code;
- Support for relations via
.t()/.through()
; - Better code placement (method and its expression lives alongside each other, instead of spread over 2 different files (models.py and managers.py))
Q: Why is this .t()
needed? Couldn't I use through
parameter directly?
A: You could do that for hybrid_methods (and you can, nothing stops you from doing this). However, this wouldn't work for hybrid_properties for obvious reasons. =P
Q: SQLAlchemy creates automatically the .expression
function for the simplest cases. Does this project do it as well?
A: No, I didn't find a decent (meaning: non-smelly) way of doing this using Django structure (yet). Suggestions are welcome.
Q: Why is there that amount of abbreviations in the code?
A: I don't like code abbreviations either. However, Django querysets are rather way too long which makes them hard to read anyway. This is an attempt to make them a bit shorter. Still, if you don't buy it, you can use the non-abbreviated aliases:
.a()
-->.alias()
.e()
-->.expression()
.i()
-->.ignore_case_in_lookup()
.l()
-->.lookup()
.t()
-->.through()
Limitations and known issues
-
.expression()
must return a plain Django expression (at least for now). It means that if, for instance, an expression depends on a prior annotation, at least the prior annotation must be done out of the.expression()
attribute (which might be a bad design as the logic would be kind of segmented). -
There's no interface to call
.distinct()
for the expressions. SoKlass.my_property.t('this__duplicates__rows')
might return duplicated rows (specially on reverse relationships via.t()
)
Development:
Run linter:
pip install -r requirements_dev.txt
isort -rc .
tox -e lint
Run tests via tox
:
pip install -r requirements_dev.txt
tox
Release a new major/minor/patch version:
pip install -r requirements_dev.txt
bump2version <PART> # <PART> can be either 'patch' or 'minor' or 'major'
Upload to PyPI:
pip install -r requirements_dev.txt
python setup.py sdist bdist_wheel
python -m twine upload dist/*
Contributing:
Please open issues if you see one, or create a pull request when possible. In case of a pull request, please consider the following:
- Respect the line length (132 characters)
- Write automated tests
- Run
tox
locally so you can see if everything is green (including linter and other python versions)
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
File details
Details for the file django-hybrid-attributes-0.1.0.tar.gz
.
File metadata
- Download URL: django-hybrid-attributes-0.1.0.tar.gz
- Upload date:
- Size: 9.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/3.1.1 pkginfo/1.5.0.1 requests/2.23.0 setuptools/45.3.0 requests-toolbelt/0.9.1 tqdm/4.46.1 CPython/3.6.9
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 85297052835a728ccf0df1090811b41ff905ebceb6380dcf673c7c99b2ab2ec0 |
|
MD5 | bc99ca61d6d643ec61e499fe0e7a0188 |
|
BLAKE2b-256 | eb8ff9667372113fd68cbd714a6dfe81b6005dfa7d29f5b0078535cf0fe5dc30 |
File details
Details for the file django_hybrid_attributes-0.1.0-py3-none-any.whl
.
File metadata
- Download URL: django_hybrid_attributes-0.1.0-py3-none-any.whl
- Upload date:
- Size: 10.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/3.1.1 pkginfo/1.5.0.1 requests/2.23.0 setuptools/45.3.0 requests-toolbelt/0.9.1 tqdm/4.46.1 CPython/3.6.9
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 4e5c2cb6ee8dbc192f41171370a7c698b9458c828314b1aeeabdbfee02204037 |
|
MD5 | 3382b060f3ada5a99fbb2f8afb43502f |
|
BLAKE2b-256 | 9562e224e2b016070390ce0b8cdf8bbb8378301d75231328c1a49fd248fc6215 |