Skip to content

Commit

Permalink
Fixed support for built in types and iterables of types as handlers, …
Browse files Browse the repository at this point in the history
…updated documentation
  • Loading branch information
marcusfrdk committed Feb 4, 2025
1 parent a1497cf commit e91726d
Show file tree
Hide file tree
Showing 6 changed files with 191 additions and 3 deletions.
146 changes: 144 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,145 @@
# tomlval
# TOML Validator

A simple and easy to use TOML parser and validator for Python.
![top language](https://img.shields.io/github/languages/top/marcusfrdk/tomlval)
![code size](https://img.shields.io/github/languages/code-size/marcusfrdk/tomlval)
![last commit](https://img.shields.io/github/last-commit/marcusfrdk/tomlval)
![issues](https://img.shields.io/github/issues/marcusfrdk/tomlval)
![contributors](https://img.shields.io/github/contributors/marcusfrdk/tomlval)
![PyPI](https://img.shields.io/pypi/v/tomlval)
![License](https://img.shields.io/github/license/marcusfrdk/tomlval)

A simple and easy to use TOML validator for Python.

## Installation

You can install the package from [PyPI](https://pypi.org/project/tomlval/):

```bash
pip install tomlval
```

The package is available for Python 3.11 and newer.

## Concepts

Before using the package, there are some concepts you may need to understand for the most optimal use of the package.

### Key

A key is the name of a field in a TOML file, such as `name`, `person.name`, etc. Keys must conform to the TOML specification, which means keys are either **snake_case** or **SCREAMING_SNAKE_CASE**. For the validator, keys may also include wildcards, such as `*name`, `person.*`, etc.

### Handler

A _handler_ is a function that is called for a certain key. Handlers can be of the following types:

- Types, such as `int`, `str`, `float`.
- Tuples/Lists of types, such as `(int, str)`, `[int, str]`.
- Anonymous functions (`lambda`)
- Named functions (Callable objects, such as `def my_handler(key, value): ...`)

The following argument configurations are supported:

- `fn()`
- `fn(key)`
- `fn(value)`
- `fn(key, value)`

If the handler has any other parameters than `key` or `value`, the validator will raise a `TOMLHandlerError`.

Handlers may return any type, but is is recommended to use the return type as an error message if the value is invalid. The validator considers a `None` return value a successful validation.

### Schema

The schema is used to bring give the validator default values. The schema is defined in the `TOMLSchema` class, and is passed to the `TOMLValidator` class. To create a schema, you pass a dictionary with the keys and their respective allowed types.

Here is an example of a schema:

```python
{
"single_type": str,
"list_of_strings": [str],
"mixed_list:" [str, int],
"multiple_types": (int, float),
"optional?": str,
"nested": {
"key": str
}
}
```

When a schema is defined, the validator will also check if values are missing and if their types are correct. If a handler is defined for a key, the validator will use the handler instead of the type defined in the schema.

### Validator

The validator is the core of the package. It is used to validate a TOML file. A schema is optionally passed to the validator, and handlers are added using the `add_handler` method. Once you feel ready, you can call the `validate` method to get a dictionary of errors.

Currently, there are two type of error structures, for type errors and all other errors.

Type errors are structured as follows:

```python
"key": (message, (value, expected_type, actual_type))
```

_`expected_type` and `actual_type` can be either `type` or `tuple[type]`_

All other errors have a slightly simpler structure:

```python
"key": (message, value)
```

The point of the validator is to parse the data and get the errors in a clean and easy way. **What you do with the errors is up to you.**

## Example

Here is a full example of how to use the validator.

```python
import pathlib
import tomllib
import datetime
from tomlval import TOMLValidator, TOMLSchema

# Load data
path = pathlib.Path("data.toml")

with path.open("rb") as file:
data = tomllib.load(file)

# Define schema (optional)
structure = {
"first_name": str,
"last_name": str,
"age": int,
"email": str,
"phone": str,
"birthday": datetime.datetime,
"address": {
"street": str,
"city": str,
"zip": int
}
}

schema = TOMLSchema(structure) # If the struture is invalid, a TOMLSchemaError is raised

# Define validator
validator = TOMLValidator(data, schema)

# Add handlers
validator.add_handler("*_name", lambda key: None if key in ["first_name", "last_name"] else "invalid-key")
validator.add_handler("age", lambda value: None if 18 < value < 100 else "invalid-age")
validator.add_handler("*", lambda: "invalid-key")

# Validate the data
errors = validator.validate()
```

## Future Plans

Future plans are found in the [TODO](TODO.md) file.

## License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
4 changes: 4 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# TODO

- [ ] Add schema validation for lists of dictionaries
- [ ] Add support for multiple handlers for a single key
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[project]
name = "tomlval"
version = "1.0.0"
description = "A simple and easy to use TOML parser and validator for Python."
description = "A simple and easy to use TOML validator for Python."
authors = [
{ name = "Marcus Fredriksson", email = "[email protected]" },
]
Expand Down
1 change: 1 addition & 0 deletions tomlval/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
""" toml_parser package """

from .errors import *
from .toml_schema import TOMLSchema
from .toml_validator import TOMLValidator
39 changes: 39 additions & 0 deletions tomlval/toml_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ def _inspect_function(self, fn: Callable) -> List[str]:
def _get_missing_keys(self) -> list[str]:
"""Get a list of keys missing in the data."""
# return [k for k in self._schema if k not in self._data]
if not isinstance(self._schema, TOMLSchema):
return []

return [
k
for k in self._schema
Expand All @@ -108,6 +111,9 @@ def _get_invalid_types(
"""Get a list of keys with invalid types."""
invalid_types = []

if not isinstance(self._schema, TOMLSchema):
return invalid_types

for key, value in self._data.items():
if key in self._schema:
# List of types
Expand Down Expand Up @@ -152,6 +158,24 @@ def _get_handler_results(self) -> dict[str, Any]:
if h is None:
continue

# Built in type
if isinstance(h, type):
value = self._data[k]
if not isinstance(value, h):
results[k] = ("invalid-type", (value, h, type(value)))
continue

# List of build in types
if isinstance(h, (list, tuple)):
_value = self._data[k]
_type = type(_value)

if not any(isinstance(_value, t) for t in h):
results[k] = ("invalid-type", (_value, h, _type))

continue

# Custom handler
fn_args = self._inspect_function(h)

# No arguments
Expand Down Expand Up @@ -196,6 +220,13 @@ def add_handler(self, key: str, handler: Handler):
self._handlers[key] = handler
return

# Iterable of types
if isinstance(handler, (list, tuple)) and all(
isinstance(h, type) for h in handler
):
self._handlers[key] = handler
return

# Not a function
if not isinstance(handler, Callable):
raise TOMLHandlerError("Handler must be a callable.")
Expand Down Expand Up @@ -257,3 +288,11 @@ def validate(self) -> ValidatedSchema:
}

return errors


if __name__ == "__main__":
val = TOMLValidator({"a_1_b": "1"})
# val.add_handler("a_*_b", int)
val.add_handler("a_*_b", (int, float))

print(val.validate())
2 changes: 2 additions & 0 deletions tomlval/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
""" 'tomlval.utils' module containing utilities used throughout the project. """

from .flatten import flatten, flatten_all
from .is_toml import is_toml
from .regex import key_pattern
from .to_path import to_path
from .unflatten import unflatten

0 comments on commit e91726d

Please sign in to comment.