# Dataclasses strictes

Le package `huggingface_hub` fournit un utilitaire pour créer des **dataclasses strictes**. Il s’agit de versions améliorées des `dataclass` standard de Python, dotées de fonctionnalités de validation supplémentaires. Les dataclasses strictes garantissent que les champs sont validés à la fois lors de l’initialisation et de l’assignation, ce qui les rend idéales pour les scénarios où l’intégrité des données est cruciale.

## Aperçu

Les dataclasses stricts sont créés à l'aide du décorateur `@strict`. Ils étendent la fonctionnalité des dataclasses classiques en :

- Validation des types de champs en fonction des annotations de type
- Prise en charge de validateurs personnalisés pour des vérifications supplémentaires
- Autoriser facultativement des arguments de mot-clé arbitraires dans le constructeur
- Validation des champs à l'initialisation et lors de l'assignation

## Avantages

- **Intégrité des données** : Garantit que les champs contiennent toujours des données valides
- **Facilité d'utilisation** : Intégration transparente avec le module `dataclass` de Python
- **Flexibilité** : prend en charge des validateurs personnalisés pour une logique de validation complexe
- **Léger** : N'exige aucune dépendance supplémentaire telle que Pydantic, attrs ou des bibliothèques similaires

## Utilisation

### Exemple de base

```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})")
```

Les champs sont validés lors de l'initialisation :

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

La cohérence entre les champs est également validée lors de l’initialisation (validation par classe) :

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

Les champs sont également validés lors de l'assignation :

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

Pour relancer la validation de la classe après l’attribution, vous devez appeler explicitement `.validate` :

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

### Validateurs personnalisés

Vous pouvez attacher plusieurs validateurs personnalisés à des champs à l’aide de `validated_field`. Un validateur est un objet appelable qui prend un seul argument et lève une exception si la valeur est invalide.

```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])
```

Dans cet exemple, les deux validateurs sont appliqués au champ `hidden_size`.

### Arguments de mot-clé supplémentaires

Par défaut, les dataclasses strictes n'acceptent que les champs définis dans la classe. Vous pouvez autoriser des arguments de mot-clé supplémentaires en définissant `accept_kwargs=True` dans le décorateur `@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')
```

Les arguments de mot-clé supplémentaires apparaissent dans la représentation en chaîne de la dataclass, mais sont préfixés par `*` pour souligner qu’ils ne sont pas validés.

### Intégration avec les Type Hints

Les dataclasses strictes respectent les annotations de type et les valident automatiquement. Par exemple :

```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
```

Les types pris en charge incluent :
- Tous
- Union
- Facultatif
- Littéral
- Liste
- Dict
- Tuple
- Ensemble

Et toute combinaison de ces types. Si vous avez besoin d'une validation de type plus complexe, vous pouvez le faire à l'aide d'un validateur personnalisé.

### Validateurs de classes

Les méthodes nommées `validate_xxx` sont traitées comme des validateurs de classe. Ces méthodes ne doivent prendre que `self` en argument. Les validateurs de classe sont exécutés une seule fois lors de l'initialisation, juste après `__post_init__`. Vous pouvez en définir autant que nécessaire ; ils seront exécutés séquentiellement dans l'ordre où ils apparaissent.

Notez que les validateurs de classe ne sont pas relancés automatiquement lorsqu'un champ est mis à jour après l'initialisation. Pour revalider manuellement l'objet, vous devez appeler `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]
> La méthode `.validate()` est un nom réservé pour les dataclasses strictes. Afin d'éviter tout comportement inattendu, une erreur `StrictDataclassDefinitionError` sera levée si votre classe en définit déjà une.

## Référence de l'API

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

Le décorateur `@strict` renforce une dataclass avec une validation stricte.

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

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

[Source](https://github.com/huggingface/huggingface_hub/blob/v2.4.0/src/huggingface_hub/dataclasses.py#L56)

**Paramètres :**

cls : La classe à convertir en dataclass stricte.

accept_kwargs (`bool`, *facultatif*) : Si True, autorise des arguments de mot-clé arbitraires dans `__init__`. La valeur par défaut est False.

**Retourne :**

La classe de données améliorée avec une validation stricte lors de l'attribution des champs.

Décorateur pour ajouter une validation stricte à une dataclass.

Ce décorateur doit être utilisé au-dessus de `@dataclass` pour s’assurer que les IDE et les outils de typage statique reconnaissent la classe comme un dataclass.

Peut être utilisé avec ou sans arguments :
- `@strict`
- `@strict(accept_kwargs=True)`

Exemple :
```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]]

Méthode permettant de vérifier qu'un dictionnaire est conforme aux types définis dans une classe `TypedDict`.

C'est l'équivalent de la validation de dataclass, mais pour les `TypedDict`. Comme les typed dicts ne sont jamais instanciés (ils ne sont utilisés que par les vérificateurs de types statiques), l'étape de validation doit être appelée manuellement.

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

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

[Source](https://github.com/huggingface/huggingface_hub/blob/v2.4.0/src/huggingface_hub/dataclasses.py#L286)

**Paramètres :**

schema (`type[TypedDictType]`) : La classe TypedDict définissant la structure et les types attendus.

data (`dict`) : Le dictionnaire à valider.

**Exceptions levées :** ``StrictDataclassFieldValidationError``

- ``StrictDataclassFieldValidationError`` -- Si un champ du dictionnaire ne respecte pas le type attendu.

Valider qu'un dictionnaire est conforme aux types définis dans une classe TypedDict.

Sous le capot, le dictionnaire typé est converti en dataclass stricte et validé à l'aide du décorateur `@strict`.

Exemple :
```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]]

Décorateur pour créer un `validated_field`. Recommandé pour les champs avec un seul validateur afin d'éviter le code répétitif.

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

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

[Source](https://github.com/huggingface/huggingface_hub/blob/v2.4.0/src/huggingface_hub/dataclasses.py#L426)

**Paramètres :**

validator (`Callable`) : une méthode qui prend une valeur en entrée et lève ValueError/TypeError si la valeur est invalide.

Décore une fonction de validation en tant que `validated_field` (c’est-à-dire un champ de dataclass avec un validateur personnalisé).

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

Crée un champ de dataclass avec une validation personnalisée.

#### 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 0x7f9675fda620>, default_factory: typing.Any = <dataclasses._MISSING_TYPE object at 0x7f9675fda620>, init: bool = True, repr: bool = True, hash: bool | None = None, compare: bool = True, metadata: dict | None = None, **kwargs: typing.Any)
```

[Source](https://github.com/huggingface/huggingface_hub/blob/v2.4.0/src/huggingface_hub/dataclasses.py#L383)

**Paramètres :**

validator (`Callable` ou `list[Callable]`) : Une méthode qui prend une valeur en entrée et lève ValueError/TypeError si la valeur est invalide. Peut être une liste de validateurs pour appliquer plusieurs vérifications.

- ****kwargs** : Arguments supplémentaires à passer à `dataclasses.field()`.

**Retourne :**

Un champ avec le validateur attaché dans les métadonnées

Créer un champ de dataclass avec un validateur personnalisé.

Utile pour appliquer plusieurs vérifications à un champ. Si vous n'appliquez qu'une seule règle, consultez le décorateur `as_validated_field`.

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

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

[Source](https://github.com/huggingface/huggingface_hub/blob/v2.4.0/src/huggingface_hub/errors.py#L540)

Exception de base pour les dataclasses strictes.

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

[Source](https://github.com/huggingface/huggingface_hub/blob/v2.4.0/src/huggingface_hub/errors.py#L544)

Exception levée lorsqu'une dataclass stricte est définie de manière incorrecte.

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

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

[Source](https://github.com/huggingface/huggingface_hub/blob/v2.4.0/src/huggingface_hub/errors.py#L548)

Exception levée lorsqu'une dataclass stricte échoue à la validation pour un champ donné.

## Pourquoi ne pas utiliser `pydantic` ? (ou `attrs` ? ou `marshmallow_dataclass` ?)

- Voir la discussion dans https://github.com/huggingface/transformers/issues/36329 concernant l'ajout de Pydantic en tant que dépendance. Il s'agirait d'une dépendance lourde et nécessiterait une logique soignée pour prendre en charge à la fois les versions v1 et v2.
- Nous n'avons pas besoin de la plupart des fonctionnalités de Pydantic, en particulier celles liées au cast automatique, à jsonschema, à la sérialisation, aux alias, etc.
- Nous n'avons pas besoin de la capacité d'instantier une classe à partir d'un dictionnaire.
- Nous ne souhaitons pas muter les données. Dans `@strict`, « validation » signifie « vérifier si une valeur est valide ». Dans Pydantic, « validation » signifie « convertir une valeur, éventuellement en la modifiant, puis vérifier si elle est valide ».
- Nous n'avons pas besoin d'une validation ultra-rapide. `@strict` n'est pas conçu pour des charges lourdes où les performances sont critiques. Les cas d'utilisation courants impliquent la validation d'une configuration de modèle (effectuée une seule fois et négligeable par rapport à l'exécution d'un modèle). Cela nous permet de garder le code minimal.

