# Строгие Dataclasses

Пакет `huggingface_hub` предоставляет утилиту для создания **строгих dataclass**. Это расширенные версии стандартного `dataclass` в Python с дополнительными возможностями валидации. Строгие dataclass обеспечивают проверку полей как при инициализации, так и при присваивании, что делает их идеальными для сценариев, где целостность данных имеет критическое значение.

## Обзор

Строгие dataclasses создаются с помощью декоратора `@strict`. Они расширяют функциональность обычных dataclasses, добавляя:

- Проверка типов полей на основе аннотаций типов
- Поддержка пользовательских валидаторов для дополнительных проверок
- Необязательное разрешение произвольных именованных аргументов в конструкторе
- Проверка полей как при инициализации, так и при присваивании

## Преимущества

- **Целостность данных**: гарантирует, что поля всегда содержат корректные данные
- **Простота использования**: бесшовная интеграция с модулем `dataclass` Python
- **Гибкость**: Поддержка пользовательских валидаторов для сложной логики проверки
- **Лёгкость**: не требует дополнительных зависимостей, таких как Pydantic, attrs или аналогичные библиотеки

## Использование

### Простой пример

```python
from dataclasses import dataclass
from huggingface_hub.dataclasses import strict, as_validated_field

# Custom validator to ensure a value is positive
@as_validated_field
def positive_int(value: int):
    if not value > 0:
        raise ValueError(f"Value must be positive, got {value}")

@strict
@dataclass
class Config:
    model_type: str
    hidden_size: int = positive_int(default=16)
    vocab_size: int = 32  # Default value

    # Methods named `validate_xxx` are treated as class-wise validators
    def validate_big_enough_vocab(self):
        if self.vocab_size < self.hidden_size:
            raise ValueError(f"vocab_size ({self.vocab_size}) must be greater than hidden_size ({self.hidden_size})")
```

Поля проверяются при инициализации:

```python
config = Config(model_type="bert", hidden_size=24)   # Valid
config = Config(model_type="bert", hidden_size=-1)   # Raises StrictDataclassFieldValidationError
```

Согласованность между полями также проверяется при инициализации (проверка на уровне класса):

```python
# `vocab_size` too small compared to `hidden_size`
config = Config(model_type="bert", hidden_size=32, vocab_size=16)   # Raises StrictDataclassClassValidationError
```

Поля также проверяются при присваивании:

```python
config.hidden_size = 512  # Valid
config.hidden_size = -1   # Raises StrictDataclassFieldValidationError
```

Чтобы повторно выполнить валидацию всего класса после назначения, необходимо явно вызвать `.validate`:

```python
config.validate()  # Runs all class validators
```

### Пользовательские валидаторы

Вы можете подключить несколько пользовательских валидаторов к полям с помощью `validated_field`. Валидатор — это вызываемый объект, который принимает один аргумент и вызывает исключение, если значение недействительно.

```python
from dataclasses import dataclass
from huggingface_hub.dataclasses import strict, validated_field

def multiple_of_64(value: int):
    if value % 64 != 0:
        raise ValueError(f"Value must be a multiple of 64, got {value}")

@strict
@dataclass
class Config:
    hidden_size: int = validated_field(validator=[positive_int, multiple_of_64])
```

В этом примере оба валидатора применяются к полю `hidden_size`.

### Дополнительные аргументы по ключевым словам

По умолчанию строгие dataclasses принимают только поля, определённые в классе. Вы можете разрешить дополнительные именованные аргументы, установив `accept_kwargs=True` в декораторе `@strict`.

```python
from dataclasses import dataclass
from huggingface_hub.dataclasses import strict

@strict(accept_kwargs=True)
@dataclass
class ConfigWithKwargs:
    model_type: str
    vocab_size: int = 16

config = ConfigWithKwargs(model_type="bert", vocab_size=30000, extra_field="extra_value")
print(config)  # ConfigWithKwargs(model_type='bert', vocab_size=30000, *extra_field='extra_value')
```

Дополнительные именованные аргументы отображаются в строковом представлении dataclass, но имеют префикс `*`, чтобы подчеркнуть, что они не проверяются.

### Интеграция с Type Hints

Строгие dataclasses соблюдают аннотации типов и автоматически их проверяют. Например:

```python
from typing import List
from dataclasses import dataclass
from huggingface_hub.dataclasses import strict

@strict
@dataclass
class Config:
    layers: List[int]

config = Config(layers=[64, 128])  # Valid
config = Config(layers="not_a_list")  # Raises StrictDataclassFieldValidationError
```

Поддерживаемые типы включают:
- Любой
- Union
- Необязательно
- Литерал
- Список
- Dict
- Кортеж
- Набор

И любые комбинации этих типов. Если вам требуется более сложная валидация типов, вы можете выполнить её с помощью пользовательского валидатора.

### Валидаторы классов

Методы с именем `validate_xxx` считаются классами-валидаторами. Такие методы должны принимать в качестве аргумента только `self`. Классы-валидаторы выполняются один раз при инициализации, сразу после `__post_init__`. Можно определить столько валидаторов, сколько потребуется: они будут выполняться последовательно в том порядке, в котором объявлены.

Обратите внимание, что валидаторы класса не запускаются автоматически повторно при обновлении поля после инициализации. Чтобы вручную выполнить повторную валидацию объекта, необходимо вызвать `obj.validate()`.

```py
from dataclasses import dataclass
from huggingface_hub.dataclasses import strict

@strict
@dataclass
class Config:
    foo: str
    foo_length: int
    upper_case: bool = False

    def validate_foo_length(self):
        if len(self.foo) != self.foo_length:
            raise ValueError(f"foo must be {self.foo_length} characters long, got {len(self.foo)}")

    def validate_foo_casing(self):
        if self.upper_case and self.foo.upper() != self.foo:
            raise ValueError(f"foo must be uppercase, got {self.foo}")

config = Config(foo="bar", foo_length=3) # ok

config.upper_case = True
config.validate() # Raises StrictDataclassClassValidationError

Config(foo="abcd", foo_length=3) # Raises StrictDataclassFieldValidationError
Config(foo="Bar", foo_length=3, upper_case=True) # Raises StrictDataclassFieldValidationError
```

> [!WARNING]
> Метод `.validate()` является зарезервированным именем для строгих dataclasses. Чтобы предотвратить непредвиденное поведение, будет выброшена ошибка `StrictDataclassDefinitionError`, если ваш класс уже определяет такой метод.

## Справочник API

### `@strict`[[huggingface_hub.dataclasses.strict]]

Декоратор `@strict` добавляет к dataclass строгую валидацию.

#### huggingface_hub.dataclasses.strict[[huggingface_hub.dataclasses.strict]]

```python
huggingface_hub.dataclasses.strict(accept_kwargs: bool = False)
```

[Источник](https://github.com/huggingface/huggingface_hub/blob/main/src/huggingface_hub/dataclasses.py#L56)

**Параметры:**

cls : класс, который нужно преобразовать в строгий dataclass.

accept_kwargs (`bool`, *необязательный*) : Если True, разрешает произвольные именованные аргументы в `__init__`. По умолчанию False.

**Возвращает:**

Расширенный dataclass с строгой валидацией при присваивании значений полям.

Декоратор для добавления строгой валидации к dataclass.

Этот декоратор должен использоваться поверх `@dataclass`, чтобы IDE и инструменты статической типизации распознавали класс как dataclass.

Может использоваться с аргументами или без них:
- `@strict`
- `@strict(accept_kwargs=True)`

Пример:
```py
>>> from dataclasses import dataclass
>>> from huggingface_hub.dataclasses import as_validated_field, strict, validated_field

>>> @as_validated_field
>>> def positive_int(value: int):
...     if not value >= 0:
...         raise ValueError(f"Value must be positive, got {value}")

>>> @strict(accept_kwargs=True)
... @dataclass
... class User:
...     name: str
...     age: int = positive_int(default=10)

# Initialize
>>> User(name="John")
User(name='John', age=10)

# Extra kwargs are accepted
>>> User(name="John", age=30, lastname="Doe")
User(name='John', age=30, *lastname='Doe')

# Invalid type => raises
>>> User(name="John", age="30")
huggingface_hub.errors.StrictDataclassFieldValidationError: Validation error for field 'age':
    TypeError: Field 'age' expected int, got str (value: '30')

# Invalid value => raises
>>> User(name="John", age=-1)
huggingface_hub.errors.StrictDataclassFieldValidationError: Validation error for field 'age':
    ValueError: Value must be positive, got -1
```

### `validate_typed_dict`[[huggingface_hub.dataclasses.validate_typed_dict]]

Метод для проверки того, что словарь соответствует типам, определённым в классе `TypedDict`.

Это аналог валидации dataclass, но для `TypedDict`. Поскольку типизированные словари никогда не инстанцируются (они используются только статическими проверщиками типов), шаг валидации необходимо вызывать вручную.

#### huggingface_hub.dataclasses.validate_typed_dict[[huggingface_hub.dataclasses.validate_typed_dict]]

```python
huggingface_hub.dataclasses.validate_typed_dict(schema: type, data: dict)
```

[Источник](https://github.com/huggingface/huggingface_hub/blob/main/src/huggingface_hub/dataclasses.py#L286)

**Параметры:**

schema (`type[TypedDictType]`) : Класс TypedDict, определяющий ожидаемую структуру и типы.

data (`dict`) : Словарь для проверки.

**Исключения:** ``StrictDataclassFieldValidationError``

- ``StrictDataclassFieldValidationError`` -- если хотя бы одно поле в словаре не соответствует ожидаемому типу.

Проверьте, что словарь соответствует типам, определённым в классе TypedDict.

Под капотом типизированный словарь преобразуется в строгий dataclass и валидируется с помощью декоратора `@strict`.

Пример:
```py
>>> from typing import Annotated, TypedDict
>>> from huggingface_hub.dataclasses import validate_typed_dict

>>> def positive_int(value: int):
...     if not value >= 0:
...         raise ValueError(f"Value must be positive, got {value}")

>>> class User(TypedDict):
...     name: str
...     age: Annotated[int, positive_int]

>>> # Valid data
>>> validate_typed_dict(User, {"name": "John", "age": 30})

>>> # Invalid type for age
>>> validate_typed_dict(User, {"name": "John", "age": "30"})
huggingface_hub.errors.StrictDataclassFieldValidationError: Validation error for field 'age':
    TypeError: Field 'age' expected int, got str (value: '30')

>>> # Invalid value for age
>>> validate_typed_dict(User, {"name": "John", "age": -1})
huggingface_hub.errors.StrictDataclassFieldValidationError: Validation error for field 'age':
    ValueError: Value must be positive, got -1
```

### `as_validated_field`[[huggingface_hub.dataclasses.as_validated_field]]

Декоратор для создания `validated_field`. Рекомендуется для полей с одним валидатором, чтобы избежать шаблонного кода.

#### huggingface_hub.dataclasses.as_validated_field[[huggingface_hub.dataclasses.as_validated_field]]

```python
huggingface_hub.dataclasses.as_validated_field(validator: Callable)
```

[Источник](https://github.com/huggingface/huggingface_hub/blob/main/src/huggingface_hub/dataclasses.py#L426)

**Параметры:**

validator (`Callable`) : метод, который принимает значение на вход и вызывает ValueError/TypeError, если значение недействительно.

Декорирует функцию валидатора как `validated_field` (то есть поле dataclass с кастомным валидатором).

### `validated_field`[[huggingface_hub.dataclasses.validated_field]]

Создаёт поле dataclass с пользовательской валидацией.

#### huggingface_hub.dataclasses.validated_field[[huggingface_hub.dataclasses.validated_field]]

```python
huggingface_hub.dataclasses.validated_field(validator: list[collections.abc.Callable[[typing.Any], None]] | collections.abc.Callable[[typing.Any], None], default: typing.Any = <dataclasses._MISSING_TYPE object at 0x7fabedd4e6b0>, default_factory: typing.Any = <dataclasses._MISSING_TYPE object at 0x7fabedd4e6b0>, init: bool = True, repr: bool = True, hash: bool | None = None, compare: bool = True, metadata: dict | None = None, **kwargs: typing.Any)
```

[Источник](https://github.com/huggingface/huggingface_hub/blob/main/src/huggingface_hub/dataclasses.py#L383)

**Параметры:**

validator (`Callable` или `list[Callable]`) : Метод, который принимает значение на вход и вызывает ValueError/TypeError, если значение недействительно. Может быть списком валидаторов для применения нескольких проверок.

- ****kwargs** : Дополнительные аргументы для передачи в `dataclasses.field()`.

**Возвращает:**

Поле с валидатором, привязанным в метаданных

Создайте поле dataclass с кастомным валидатором.

Полезно применять несколько проверок к полю. Если применяется только одно правило, обратите внимание на декоратор `as_validated_field`.

### Ошибки[[huggingface_hub.errors.StrictDataclassError]]

#### huggingface_hub.errors.StrictDataclassError[[huggingface_hub.errors.StrictDataclassError]]

[Источник](https://github.com/huggingface/huggingface_hub/blob/main/src/huggingface_hub/errors.py#L540)

Базовое исключение для строгих dataclasses.

#### huggingface_hub.errors.StrictDataclassDefinitionError[[huggingface_hub.errors.StrictDataclassDefinitionError]]

[Источник](https://github.com/huggingface/huggingface_hub/blob/main/src/huggingface_hub/errors.py#L544)

Исключение, возникающее при неправильном определении строгого dataclass.

#### huggingface_hub.errors.StrictDataclassFieldValidationError[[huggingface_hub.errors.StrictDataclassFieldValidationError]]

```python
huggingface_hub.errors.StrictDataclassFieldValidationError(field: str, cause: Exception)
```

[Источник](https://github.com/huggingface/huggingface_hub/blob/main/src/huggingface_hub/errors.py#L548)

Исключение, возникающее при неудачной валидации поля в строгом dataclass.

## Почему не использовать `pydantic`? (или `attrs`? или `marshmallow_dataclass`?)

- См. обсуждение в https://github.com/huggingface/transformers/issues/36329 о добавлении Pydantic в зависимости. Это было бы тяжелым дополнением и потребовало бы тщательной логики для поддержки обеих версий, v1 и v2.
- Нам не нужны большинство возможностей Pydantic, особенно те, что связаны с автоматическим приведением типов, jsonschema, сериализацией, псевдонимами и т. д.
- Нам не нужна возможность создавать экземпляр класса из словаря.
- Мы не хотим изменять данные. В `@strict` «validation» означает «проверку того, является ли значение валидным». В Pydantic «validation» означает «приведение значения, возможно, с его изменением, а затем проверку его валидности».
- Нам не нужна сверхбыстрая валидация. `@strict` не предназначен для высоких нагрузок, где критична производительность. Типичные сценарии использования включают валидацию конфигурации модели (выполняется один раз и пренебрежимо мала по сравнению с запуском модели). Это позволяет нам держать код минимальным.

