跳转到主要内容

从字典中简单创建数据类(dacite的分支)。

项目描述

dcd

Build Status Coverage Status License Version Python versions Code style: black

这是dacite的分支:https://github.com/konradhalas/dacite

此模块简化了从字典创建数据类(PEP 557)。

安装

要安装dcd,只需使用pip

$ pip install dcd

需求

dcd支持的最低Python版本是3.7。

快速入门

from dataclasses import dataclass
from dcd import from_dict


@dataclass
class User:
    name: str
    age: int
    is_active: bool


data = {
    'name': 'John',
    'age': 30,
    'is_active': True,
}

user = from_dict(data_class=User, data=data)

assert user == User(name='John', age=30, is_active=True)

功能

Dcd支持以下功能

  • 嵌套结构
  • (基本)类型检查
  • 可选字段(例如,typing.Optional
  • 联合
  • 前向引用
  • 集合
  • 自定义类型钩子

动机

在您的函数或方法之间传递普通字典作为数据容器并不是一个好的做法。当然,您始终可以创建自己的自定义类,但如果您只想在单个对象中合并几个字段,则此解决方案过于复杂。

幸运的是,Python有一个很好的解决方案来解决这个问题 - 数据类。多亏了@dataclass装饰器,您可以通过声明的方式轻松创建一个新的自定义类型,其中包含给定字段列表。数据类默认支持类型提示。

然而,即使您在使用数据类,您也必须以某种方式创建它们的实例。在许多这样的情况下,您的输入是一个字典 - 它可以来自HTTP请求的有效负载或数据库的原始数据。如果您想将这些字典转换为数据类,dcd就是您的最佳选择。

该库最初是为了简化创建类型提示的数据传输对象(DTO)而创建的,这些对象可以在应用程序架构的边界之间传输。

需要指出的是,dcd不是一个数据验证库。有数十个出色的数据验证项目,在dcd中重复此功能是没有意义的。如果您想首先验证您的数据,您应该将dcd与一个数据验证库结合使用。

请查看用例部分以获取实际示例。

用法

Dacite基于一个单一的功能 - dcd.from_dict。此函数接受3个参数

  • data_class - 数据类类型
  • data - 输入数据的字典
  • config(可选)- 创建过程的配置,dcd.Config类的实例

配置是一个(数据)类,具有以下字段

  • type_hooks
  • cast
  • forward_references
  • check_types
  • strict
  • strict_unions_match

以下示例展示了from_dict函数的所有功能以及所有Config参数的用法。

嵌套结构

您可以通过传递嵌套字典的数据,它将创建适当的结果。

@dataclass
class A:
    x: str
    y: int


@dataclass
class B:
    a: A


data = {
    'a': {
        'x': 'test',
        'y': 1,
    }
}

result = from_dict(data_class=B, data=data)

assert result == B(a=A(x='test', y=1))

可选字段

每当您的数据类有一个Optional字段,并且您不会为此字段提供输入数据时,它将采用None值。

from typing import Optional

@dataclass
class A:
    x: str
    y: Optional[int]


data = {
    'x': 'test',
}

result = from_dict(data_class=A, data=data)

assert result == A(x='test', y=None)

联合

如果您的字段可以接受多种类型,您应该使用Union。Dacite将尝试按顺序匹配提供的数据类型。如果没有匹配项,它将引发UnionMatchError异常。

from typing import Union

@dataclass
class A:
    x: str

@dataclass
class B:
    y: int

@dataclass
class C:
    u: Union[A, B]


data = {
    'u': {
        'y': 1,
    },
}

result = from_dict(data_class=C, data=data)

assert result == C(u=B(y=1))

集合

Dacite支持定义为集合的字段。它适用于基本类型和数据类。

@dataclass
class A:
    x: str
    y: int


@dataclass
class B:
    a_list: List[A]


data = {
    'a_list': [
        {
            'x': 'test1',
            'y': 1,
        },
        {
            'x': 'test2',
            'y': 2,
        }
    ],
}

result = from_dict(data_class=B, data=data)

assert result == B(a_list=[A(x='test1', y=1), A(x='test2', y=2)])

类型钩子

如果您想将具有给定类型的数据类字段输入数据转换为新的值,请使用Config.type_hooks参数。您必须传递一个类型为Mapping[Type, Callable[[Any], Any]的映射。

@dataclass
class A:
    x: str


data = {
    'x': 'TEST',
}

result = from_dict(data_class=A, data=data, config=Config(type_hooks={str: str.lower}))

assert result == A(x='test')

如果数据类字段类型是Optional[T],您可以传递Optional[T]或仅T作为type_hooks中的键。对于泛型集合也是如此,例如,当一个字段具有类型List[T]时,您可以使用List[T]转换整个集合或使用T转换每个项目。

要针对所有类型,请使用Any。针对没有其子类型的集合将针对该类型的所有集合,例如ListDict

@dataclass
class ShoppingCart:
    store: str
    item_ids: List[int]

data = {
    'store': '7-Eleven',
    'item_ids': [1, 2, 3],
}

def print_value(value):
    print(value)
    return value

def print_collection(collection):
    for item in collection:
        print(item)
    return collection

result = from_dict(
    data_class=ShoppingCart, 
    data=data, 
    config=Config(
        type_hooks={
            Any: print_value, 
            List: print_collection
        }
    )
)

打印

7-Eleven
[1, 2, 3]
1
2
3

如果数据类字段类型是Optional[T],您可以传递Optional[T]或仅T作为type_hooks中的键。对于泛型集合也是如此,例如,当一个字段具有类型List[T]时,您可以使用List[T]转换整个集合或使用T转换每个项目。

转换

这是一个非常常见的情况,您只想通过调用您的类型并传入输入值来从输入数据创建字段类型的实例。当然,您可以使用type_hooks={T: T}来实现这个目标,但cast=[T]是一种更简单、更直观的方式。它也与基类一起工作 - 如果T是类型S的基类,则所有类型为S的字段也将被“转换”。

from enum import Enum

class E(Enum):
    X = 'x'
    Y = 'y'
    Z = 'z'

@dataclass
class A:
    e: E


data = {
    'e': 'x',
}

result = from_dict(data_class=A, data=data, config=Config(cast=[E]))

# or

result = from_dict(data_class=A, data=data, config=Config(cast=[Enum]))

assert result == A(e=E.X)

前进引用

可以以{'name': Type}映射的形式将前向引用的定义传递给Config.forward_references。当评估每个字段的类型时,此映射作为globalns参数传递给typing.get_type_hints()

@dataclass
class X:
    y: "Y"

@dataclass
class Y:
    s: str

data = from_dict(X, {"y": {"s": "text"}}, Config(forward_references={"Y": Y}))
assert data == X(Y("text"))

类型检查

在罕见的情况下,dcd内置的类型检查器无法验证您的类型(例如自定义泛型类)或您已由其他库覆盖此类功能并且您不想重复验证类型。在这种情况下,您可以使用Config(check_types=False)禁用类型检查。默认情况下启用类型检查。

T = TypeVar('T')


class X(Generic[T]):
    pass


@dataclass
class A:
    x: X[str]


x = X[str]()

assert from_dict(A, {'x': x}, config=Config(check_types=False)) == A(x=x)

严格模式

默认情况下,from_dict 会忽略输入数据中的额外键(与数据类字段不匹配)。如果您想更改此行为,请将 Config.strict 设置为 True。在遇到意外键时,from_dict 将抛出 UnexpectedDataError 异常。

严格联合匹配

Union 允许为给定字段定义多个可能的类型。默认情况下,dcd 尝试找到与提供的数据匹配的第一个类型,并返回该类型的实例。这意味着还有其他匹配的类型可能在 Union 类型列表中。在 strict_unions_match 模式下,只允许单个匹配,否则 dcd 将抛出 StrictUnionMatchError

异常

每当出现错误时,from_dict 将抛出适当的异常。其中有一些:

  • WrongTypeError - 当输入值的类型与数据类字段的类型不匹配时抛出
  • MissingValueError - 当您未为必填字段提供值时抛出
  • UnionMatchError - 当提供的数据与 Union 的任何类型不匹配时抛出
  • ForwardReferenceError - 当在数据类中遇到未定义的前向引用时抛出
  • UnexpectedDataError - 当启用 strict 模式且输入数据没有匹配的键时抛出
  • StrictUnionMatchError - 当启用 strict_unions_match 模式且输入数据具有模糊的 Union 匹配时抛出

开发

首先,如果您想提交您的拉取请求,非常感谢!我非常感激您的支持。

请记住,每个新功能、错误修复或改进都应该经过测试。100% 的代码覆盖率是必须的。

我们使用一些静态代码分析工具来提高代码质量(blackmypypylint)。请在提交 PR 之前确保您没有生成任何错误/警告。您可以在 .travis.yml 文件中找到当前配置。

最后但同样重要的是,如果您想引入新功能,请首先在问题中进行讨论。

如何开始

克隆 dcd 仓库

$ git clone git@github.com:idanmiara/dcd.git

创建并激活您喜欢的虚拟环境

$ python3 -m venv dcd-env
$ source dcd-env/bin/activate

安装所有 dcd 依赖项

$ pip install -e .[dev]

要运行测试,您只需执行以下操作

$ pytest

用例

在我们接收到许多“原始”数据(Python 字典)作为系统输入的情况下有很多情况。HTTP 请求有效负载是一个非常常见的用例。在大多数 Web 框架中,我们以简单字典的形式接收请求数据。与其将此字典传递到您的“业务”代码中,不如创建一些更“健壮”的东西。

以下是一个简单的 flask 应用程序 - 它有一个单一的 /products 端点。您可以使用此端点在系统中“创建”产品。我们的核心 create_product 函数期望数据类作为参数。多亏了 dcd,我们可以轻松地从 POST 请求有效负载构建此类数据类。

from dataclasses import dataclass
from typing import List

from flask import Flask, request, Response

import dcd

app = Flask(__name__)


@dataclass
class ProductVariantData:
    code: str
    description: str = ''
    stock: int = 0


@dataclass
class ProductData:
    name: str
    price: float
    variants: List[ProductVariantData]


def create_product(product_data: ProductData) -> None:
    pass  # your business logic here


@app.route("/products", methods=['POST'])
def products():
    product_data = dcd.from_dict(
        data_class=ProductData,
        data=request.get_json(),
    )
    create_product(product_data=product_data)
    return Response(status=201)

如果我们想验证我们的数据(例如,检查 code 是否有 6 个字符)怎么办?这样的功能超出了 dcd 的范围,但我们可以轻松地将其与数据验证库之一结合使用。让我们尝试使用 marshmallow

首先,我们必须定义我们的数据验证模式

from marshmallow import Schema, fields, ValidationError


def validate_code(code):
    if len(code) != 6:
        raise ValidationError('Code must have 6 characters.')


class ProductVariantDataSchema(Schema):
    code = fields.Str(required=True, validate=validate_code)
    description = fields.Str(required=False)
    stock = fields.Int(required=False)


class ProductDataSchema(Schema):
    name = fields.Str(required=True)
    price = fields.Decimal(required=True)
    variants = fields.Nested(ProductVariantDataSchema(many=True))

并在我们的端点中使用它们

@app.route("/products", methods=['POST'])
def products():
    schema = ProductDataSchema()
    result, errors = schema.load(request.get_json())
    if errors:
        return Response(
            response=json.dumps(errors), 
            status=400, 
            mimetype='application/json',
        )
    product_data = dcd.from_dict(
        data_class=ProductData,
        data=result,
    )
    create_product(product_data=product_data)
    return Response(status=201)

仍然,dcd 帮助我们使用经过验证的数据从“原始”字典创建数据类。

变更日志

CHANGELOG 中跟踪 dcd 的更新。

作者

Konrad Hałas 创建。由 Idan Miara 维护。

项目详情


下载文件

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

源代码分布

本发行版没有提供源代码分布文件。请参阅有关生成分布存档的教程。

编译后的分布

dcd-2.0b1-py3-none-any.whl (13.8 kB 查看哈希值)

上传时间 Python 3

支持

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