跳转到主要内容

一个模仿SQLAlchemy hybrid_property和hybrid_method功能的Django插件

项目描述

Travis Codecov PyPI License Python versions PyPI downloads per month

Django Hybrid Attributes

这是Django中SQLAlchemy混合属性的(相当基本)实现 - 更确切地说,是hybrid_propertyhybrid_method

基本用法示例

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]

有关其他示例,请参阅测试文件夹。

功能

  • 使用Python魔术方法支持过滤。示例
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
  • 通过l()属性支持所有Django查找。示例
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')
  • 通过t()属性支持关系。示例
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')
  • 通过.e()属性支持原始表达式(供您随意使用)。示例
Klass.objects.annotate(my_method_result=Klass.my_hybrid_method().e())
  • 通过.a()属性支持自定义别名(以便您可以在以后引用注解的表达式)。示例
Klass.objects.filter(Klass.my_hybrid_property.a('_expr_alias') > 'value').order_by('_expr_alias')
  • 测试/脚本助手以确保混合表达式与其属性/方法相比是合理的。示例
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)
  • 没有暗箱操作:在底层,它所做的只是将表达式annotate()到一个查询集中,并使用这个注解来filter/exclude()

常见问题解答

问:我为什么需要这个项目?我为什么不可以用Klass.objects.annotate(whatever=<expression>).filter(whatever=<value>)呢?

答:您不需要这个项目。您也可以使用这种方法。话虽如此,我仍然认为使用这个项目有一些原因,例如

  • 更干净、更简洁的代码;
  • 通过.t()/.through()支持关系;
  • 更好的代码位置(方法和其表达式彼此相邻,而不是分布在两个不同的文件(models.py和managers.py)中)

问:为什么需要这个 .t()?我直接使用 through 参数不行吗?

答:对于 hybrid_methods,你可以这样做(实际上你完全可以这样做)。然而,由于显而易见的原因,这对 hybrid_properties 不适用。=P

问:SQLAlchemy 会自动为最简单的情况创建 .expression 函数。这个项目也这样做吗?

答:不,我还没有找到一种合理的方法(意思是:非糟糕的方法)使用 Django 结构来做这件事(目前还没有)。欢迎提出建议。

问:代码中为什么有那么多缩写?

答:我也不喜欢代码缩写。然而,Django 查询集相当长,这本身就让它们难以阅读。这是尝试使它们更短的一种方法。不过,如果你不同意,你仍然可以使用非缩写的别名

  • .a() --> .alias()
  • .e() --> .expression()
  • .i() --> .ignore_case_in_lookup()
  • .l() --> .lookup()
  • .t() --> .through()

限制和已知问题

  • .expression() 必须返回一个纯 Django 表达式(至少目前是这样的)。这意味着,如果某个表达式依赖于先前的注释,至少先前的注释必须在 .expression() 属性之外完成(这可能是一个糟糕的设计,因为逻辑会被分割)。

  • 没有接口可以调用 .distinct()。因此,Klass.my_property.t('this__duplicates__rows') 可能会返回重复的行(特别是在通过 .t() 的反向关系时)。

开发

运行代码检查器

pip install -r requirements_dev.txt
isort -rc .
tox -e lint

通过 tox 运行测试

pip install -r requirements_dev.txt
tox

发布新的主版本/次版本/补丁版本

pip install -r requirements_dev.txt
bump2version <PART>  # <PART> can be either 'patch' or 'minor' or 'major'

上传到 PyPI

pip install -r requirements_dev.txt
python setup.py sdist bdist_wheel
python -m twine upload dist/*

贡献

如果你发现任何问题,请提交问题,或在可能的情况下创建拉取请求。如果是拉取请求,请考虑以下事项

  • 尊重行长度(132个字符)
  • 编写自动化测试
  • 在本地运行 tox,这样你可以看到是否一切正常(包括代码检查器和其他 Python 版本)

项目详情


下载文件

为您的平台下载文件。如果您不确定选择哪个,请了解有关 安装包 的更多信息。

源代码分发

django-hybrid-attributes-0.1.0.tar.gz (9.3 kB 查看哈希值)

上传时间: 源代码

构建分发

django_hybrid_attributes-0.1.0-py3-none-any.whl (10.6 kB 查看哈希值)

上传时间: Python 3

由以下支持

AWS AWS 云计算和安全赞助商 Datadog Datadog 监控 Fastly Fastly CDN Google Google 下载分析 Microsoft Microsoft PSF赞助商 Pingdom Pingdom 监控 Sentry Sentry 错误日志 StatusPage StatusPage 状态页面