belikethis 0.1.2
Simple template class.
Requires Python: >=3.12

# belikethis

## 安装

```bash
pip install belikethis
```

## 简介

`belikethis`是一个简单易用的数据验证库。

有时我们会需要验证一些数据，以及将它们转换到合适的格式。这时，本库就能派上用场。

### 起步：通过`Template`子类创建模板

`belikethis`围绕着核心`Template`类展开。

定义继承自`Template`的模板类，即可利用它们进行简单的匹配和自动转换。

```python
from belikethis import Template
from belikethis.typing.types import uint


class VideoAuthor(Template):
	uid: uint
	exp: float
	name: str
	is_vip = False

class VideoInformation(Template):
	bid: uint
	author: VideoAuthor
	title: str
	description: str | None = None
	timestamps: list[int] = []
	extra_contents = '...'
```

从上面的代码可以看到创建一个模板有多简单。

- 你可以使用简单的类型注解`exp: float`。
  - 这会限定`exp`字段只接受可以转化为`float`的类型。
  - 这意味着你可以传入`1000.0`, `2000`, `'inf'`等，它们都会被自动转换为对应的`float`值并保存。
  - 与此同时，模板也会拒绝诸如`'hello'`, `[]`等不能被转换为`float`的值，并抛出一个`TemplateError`。

- 部分受支持的泛型，如`timestamps: list[int]`，也同样可以使用。
  - 此时，字段`timestamp`本身的类型为`list`，而其中的每个元素都必须是`int`类型。
  - 同样，只要传入的内容可转化为对应类型，模型就可以接受它。
  - 使用泛型时，可以嵌套。分析逻辑并无差异。

- 声明字段时，你可以按自己的需求为其赋一个默认值。
  - 没有赋默认值的字段将成为**必需字段**，实例化时必须提供；否则为**可选字段**，实例化时采用提供的值（如有）或默认值。
  - 指定了默认值的字段可以不添加类型注解，此时模板类会根据默认值的类型进行自动推断。

我们可以通过`TemplateClass.field_name`来获取字段本身的相关信息。

```python
# 这将展示 VideoAuthor 模板中 is_vip 字段的相关信息
print(VideoAuthor.is_vip)
print()

# 同理，这会展示 VideoInformation 模板中 description 字段的相关信息。
print(VideoInformation.description)
```

输出如下：
```text
Information of field 'is_vip'
    Annotation Type      bool
    Default Value        False

Information of field 'description'
    Annotation Type      str | NoneType
    Default Value        None
```

### 怎么用：实例化，自动类型转换

我们可以使用模板类的`load`方法来加载数据并生成一个实例。
- 最常见的输入数据是字典，因此模板类默认支持字典输入。
- 当然，你也可以通过关键字参数的形式输入数据。
- 至于其它格式的输入，则取决于模板子类的实现。

```python
video_data = {
	'bid': 10000,
	'author': {
		'uid': 3727,
		'exp': 1000.0,
		'name': 'Bob',
	},
	'title': 'Bob\'s Farm',
	'description': 'We have best beers here. Welcome to enjoy!'
}

info = VideoInformation.load(video_data)
print(info)
```

输出如下：

```text
VideoInformation (
    bid = 10000, 
    author = VideoAuthor(uid=3727, exp=1000.0, name='Bob'), 
    title = "Bob's Farm", 
    description = 'We have best beers here. Welcome to enjoy!', 
)
```

我们前面提到过，`Template`并不要求数据类型严格对应，只要能够转换为目标类型即可。以下是一个使用自动转换的例子。

```python
video_data = {
	'bid': 1e5, # float -> uint
	'author': {
		'uid': '3727', # str -> int
		'exp': 1000,
		'name': 'Bob',
		'is_vip': [{(1,)}], # truthy value -> bool
	},
	'title': 'Bob\'s Farm',
	'description': 123472953, # int -> str
}

info = VideoInformation.load(video_data)
print(info)
```

输出如下：

```text
VideoInformation (
    bid = 100000, 
    author = VideoAuthor(uid=3727, exp=1000.0, name='Bob', is_vip=True), 
    title = "Bob's Farm", 
    description = '123472953', 
)
```

### 数据有问题：`TemplateError`

如果传入的数据有问题，则模板会抛出一个`TemplateError`来告知你。数据有问题的情况包括以下几种：
- 数据类型出错：
- 创建模板类时，可选字段的默认值无法转换到注明的类型注解；
- 创建或修改模板实例时，传入的数据无法转换到目标类型；
- 字段出错：
- 创建模板实例时，传入了多余的字段；
- 创建模板实例时，没有提供全部必需字段。

#### 数据类型出错
1. **创建模板类时，可选字段的默认值无法转换到注明的类型注解**

```python
try:
	class WrongTemplate(Template):
		wrong_field: int = 'what'
		another_field: list[int] = {'name': 'data'}

except Exception as e:
	print(f'Exception type: {type(e).__name__}')
	print()
	print(e)
```

输出如下：

```text
Exception type: TemplateError

Failed to create template WrongTemplate. 
2 error(s) in total; detailed information: 

	wrong_field
		CastError | Failed to cast value 'what' (of type str) to int. 
		ValueError | invalid literal for int() with base 10: 'what'

	another_field
		CastError | Failed to cast value {'name': 'data'} (of type dict) to list[int]. 
		CastError | Failed to cast value 'name' (of type str) to int. 
		ValueError | invalid literal for int() with base 10: 'name'
```

2. **创建模板实例时，传入的数据无法转换到目标类型**
```python
try:
	class SimpleTemplate(Template):
		idx: int
	
	simple = SimpleTemplate(idx='no_way')

except Exception as e:
	print(f'Exception type: {type(e).__name__}')
	print()
	print(e)
```

输出如下：
```text
Exception type: TemplateError

Failed to create an instance of template SimpleTemplate. 
1 error(s) in total; detailed information: 

	idx
		CastError | Failed to cast value 'no_way' (of type str) to int. 
		ValueError | invalid literal for int() with base 10: 'no_way'
```

3. **修改模板实例时，传入的数据无法转换到目标类型**
```python
simple = SimpleTemplate(idx=0) # 可以成功创建

try:
	simple.idx = 'again'

except Exception as e:
	print(f'Exception type: {type(e).__name__}')
	print()
	print(e)
```

输出如下：

```text
Exception type: TemplateError

Failed to modify an instance of template SimpleTemplate. 
1 error(s) in total; detailed information: 

    idx
        CastError | Failed to cast value 'again' (of type str) to int. 
        ValueError | invalid literal for int() with base 10: 'again'
```

#### 字段出错
1. **创建模板实例时，传入了多余的字段；**
2. **创建模板实例时，没有提供全部必需字段。**
```python
try:
	class AnotherWrongTemplate(Template):
		necessary_field: int
		optional_field: float = 0.0
	
	instance = AnotherWrongTemplate.load({
		'field_not_exists': 1234
	})

except Exception as e:
	print(f'Exception type: {type(e).__name__}')
	print()
	print(e)
```

输出如下：

```text
Exception type: TemplateError

Failed to create an instance of template AnotherWrongTemplate. 
1 error(s) in total; detailed information: 

    from_dict
        TemplateError | Failed to create an instance of template AnotherWrongTemplate. 
        2 error(s) in total; detailed information: 
        
            necessary_field
                MissingFieldError | Missing field 'necessary_field'
        
            field_not_exists
                UnexpectedFieldError | Unexpected field 'field_not_exists'
```

## API

待补充。

## 许可证
GPL v3.0. 详见`LICENSE`.