跳转到主要内容

既是ORM表达式又是Python代码的属性。

项目描述

https://img.shields.io/pypi/v/django_shared_property.svg https://img.shields.io/travis/schinckel/django-shared-property.svg Build status on Appveyor Documentation Status

既是ORM表达式又是Python代码的属性。

安装

$ pip install django_shared_property

哲学

我经常发现我在Django查询集中有一些基于一个或多个其他字段的注释,并且经常使用。在某些情况下,我甚至知道我使用自定义查询集/管理器确保这些注释总是可在模型上。

但是,与Python属性不同,这些注释不是“活”的。例如,如果您有如下内容

class FullNameQueryset(models.query.QuerySet):
    def with_full_name(self):
        return self.annotate(
          full_name=Concat(models.F('first_name'), models.Value(' '), models.F('last_name')),
        )


class Person(models.Model):
    first_name = models.TextField()
    last_name = models.TextField()

    objects = FullNameQueryset.as_manager()

那么,您可以对 person = Person.objects.with_full_name().get(pk=1) 进行操作,然后引用 person.full_name

但是,如果您修改 person.first_name,则需要将其写回数据库,然后重新加载它。这可能不是理想的,并且最多需要两次数据库操作。

django-shared-property 允许您拥有自动作为注释的属性,允许您定义表达式,并让Django在任何数据库查询中使用该操作。然后它可以在过滤器(或进一步注释)中使用,甚至跨越关系。最后,如果对对象的任何本地更改会影响存储在数据库中的值,则属性值也将更新为Python。

让我展示给您看!

类似于Python属性,django-shared-property需要一个不接受任何参数的方法。然而,它应该返回一个Django表达式。例如,根据我们上面的注释

from django.db import models
from django.db.models.functions import Concat
from django_shared_property.decorator import shared_property


class Person(models.Model):
    first_name = models.TextField()
    last_name = models.TextField()

    @shared_property
    def full_name(self):
        return Concat(models.F('first_name'), models.Value(' '), models.F('last_name'))

然后就像引用任何其他字段一样引用它。

Person.objects.filter(full_name__contains='Bob')

它还能做什么?

共享属性可以引用模型上的任意多个字段,甚至其他共享属性,就像在注解中一样。它们甚至可以使用熟悉的 models.F('relation__field') 查找语法来引用相关模型中的字段。您还可以使用一些Django表达式(如Concat、Lower、Upper),其中存在与Python概念之间的明确关系。

  • 案例

  • F

  • Q(特别是在When中,但可能在其他地方也能工作)

  • Concat

  • Lower和Upper(但仅限于具有这些属性的Python对象)

  • ExpressionWrapper

  • CombinedExpresson

  • Coalesce(但请参阅以下注释)

在Q表达式的上下文中,您可以使用 __isnull__exact 查找。

您甚至可以引用Python文件中的常量,例如枚举的不同值。然后您的Python对象将正确返回枚举的实例。

如果您的选择表达式/函数/值不起作用,则可能可以实施它(请参阅以下内容)。

共享属性应该是纯函数 - 它们不能引用 self(实际上,这将导致错误),也不应引用变量,因为它们将在装饰之前执行。

它是如何工作的?

由于装饰的函数必须是纯函数的限制,我们能够执行可调用对象,并将其结果用作Django表达式。

Django部分相对简单。作为共享属性装饰的方法返回的表达式在类似于注解的上下文中使用。但是,需要做一些事情以确保表达式具有正确的数据,以确保它指向正确的表。我们向Django指示不应将其写入数据库,通过将其标记为 private 字段。

创建Python属性有些复杂。我们仍然需要表达式,但我们基于表达式构建一个抽象语法树。然后我们将其编译成一个可调用的对象,用作属性。

更详细地说

  • 调用装饰的函数,返回表达式

  • 使用Python的自省工具检查表达式(及其“源表达式”)和递归下降解析器来构建与表达式等价的AST。具体来说,AST包含一个函数定义。

  • 将此AST编译成代码对象

  • 使用正确的上下文 eval 此代码对象以拉入任何来自外部命名空间的自定义常量。

  • 提取新定义的函数,并将其用于属性中的可调用对象。

高级使用

有时您想自己定义可调用对象:为此存在一种替代语法。这可能是表达式尚未定义的地方,或者可能通过手动创建更有效的可调用对象。

class MyModel(models.Model):
    # other fields

    @shared_property(Case(
        When(models.Q(x__gte=2, x__lt=5), then=models.Value('B')),
        When(models.Q(x__lt=2), then=models.Value('A')),
        default=models.Value('C'),
        output_field=models.TextField(),
    )
    def state(self):
        if 2 <= self.x < 5:
            return 'B'
        elif x < 2:
            return 'A'
        return 'C'

    @shared_property(Coalesce(
      CombinedExpression(F('expiry_date'), '<', Func(function='current_timestamp')),
      models.Value(True),
    ))
    def active(self):
        return self.expiry_date is None or self.expiry_date < timezone.now()

在这种情况下,生成的代码将相当类似(尽管它不会使用 a < b < c 习语),但它显示了如何显式提供Python代码。请注意,确保在这种情况下表达式和函数等效的责任在于开发者。

第二个示例显示了Python比较与SQL代码不完全对应的地方:`COALESCE(expiry_date < now(), true)`依赖于涉及NULL的SQL比较也返回NULL,但在Python中无法这样做。

请注意,在这种情况下,只能将单个表达式用作装饰器的参数。

注册表达式

您可以注册自己的表达式。结构相当严格,您需要引用解析器实例以及传入的表达式。有时将表达式转换为(a)正确的Python,然后是(b)所需AST需要做很多工作。

from django_shared_property.parser import register


@register
def handle_foo(parser, expression):
    # This assumes a foo() function in python that matches a foo()
    # function in SQL, neither of which takes arguments.
    return Call(
        func=Name(id='foo', **parser.file),
        args=[],
        keywords=[],
        kwonlyargs=[],
        **parser.file,
    )


class Foo(Func):
    function = 'foo'


class MyModel(models.Model):
    # ...

    @shared_property
    def the_foo(self):
        return Foo()

这是一个玩具示例 - 尝试查看parser模块中的其他示例。

限制

使用django queryset方法defer/only将防止加载任何共享属性。然而,由于该功能的工作方式,您仍然可以使用此属性 - 至少在引用的字段是本地的情况下。

当您使用引用相关模型的共享属性,然后尝试过滤该属性时,您无法执行计数或存在查询。请参阅https://github.com/schinckel/django-shared-property/issues/2

致谢

Matthew Schinckel开发。

此包是用Cookiecutterwboxx1/cookiecutter-pypackage-poetry项目模板创建的。

项目详情


下载文件

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

源分布

django_shared_property-0.8.0.tar.gz (15.1 kB 查看哈希值)

上传时间

构建分布

django_shared_property-0.8.0-py3-none-any.whl (12.3 kB 查看哈希值)

上传时间 Python 3

由以下支持

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