From 88240f7a76e9634d42a68e4d9eade9c9d600e1a9 Mon Sep 17 00:00:00 2001 From: Fabio Caccamo Date: Wed, 23 Apr 2025 20:02:53 +0200 Subject: [PATCH 01/12] Add data validation support using `Pydantic` models. --- README.md | 40 +++++++++++ benedict/dicts/io/io_dict.py | 29 ++++++-- benedict/extras.py | 5 ++ benedict/utils/pydantic_util.py | 50 ++++++++++++++ pyproject.toml | 5 +- tests/dicts/io/test_io_dict_schema.py | 86 +++++++++++++++++++++++ tests/dicts/io/test_io_dict_validate.py | 91 +++++++++++++++++++++++++ 7 files changed, 301 insertions(+), 5 deletions(-) create mode 100644 benedict/utils/pydantic_util.py create mode 100644 tests/dicts/io/test_io_dict_schema.py create mode 100644 tests/dicts/io/test_io_dict_validate.py diff --git a/README.md b/README.md index 6ab15bc9..14817ab9 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,7 @@ Here the hierarchy of possible installation targets available when running `pip - `[yaml]` - `[parse]` - `[s3]` + - `[validate]` ## Usage @@ -279,6 +280,36 @@ Here are the details of the supported formats, operations and extra options docs | `xml` | :white_check_mark: | :white_check_mark: | [xmltodict](https://github.com/martinblech/xmltodict) | | `yaml` | :white_check_mark: | :white_check_mark: | [PyYAML](https://pyyaml.org/wiki/PyYAMLDocumentation) | +#### Data validation + +`benedict` supports data validation using `Pydantic` models. + +This feature **requires** the `validate` extra to be installed: + +```bash +pip install "python-benedict[validate]" +``` + +You can validate data in different ways: + +##### Using the `validate` method directly +```python +d = benedict(my_data) +d.validate(schema=MySchema) +``` + +##### Using the `schema` parameter during initialization +```python +d = benedict(my_data, schema=MySchema) +``` + +##### Using the `schema` parameter with any `from_{format}` method +```python +d = benedict.from_json(my_data, schema=MySchema) +``` + +If validation fails, a `ValidationError` will be raised with details about what went wrong. + ### API - **Utility methods** @@ -333,6 +364,7 @@ Here are the details of the supported formats, operations and extra options docs - [`to_toml`](#to_toml) - [`to_xml`](#to_xml) - [`to_yaml`](#to_yaml) + - [`validate`](#validate) - **Parse methods** @@ -815,6 +847,14 @@ s = d.to_xml(**kwargs) s = d.to_yaml(**kwargs) ``` +#### `validate` + +```python +# Validate the dict and update it using a Pydantic schema. +# A ValidationError is raised in case of failure. +d.validate(schema=MySchema) +``` + ### Parse methods These methods are wrappers of the `get` method, they parse data trying to return it in the expected type. diff --git a/benedict/dicts/io/io_dict.py b/benedict/dicts/io/io_dict.py index 584f37ec..77727a17 100644 --- a/benedict/dicts/io/io_dict.py +++ b/benedict/dicts/io/io_dict.py @@ -3,7 +3,8 @@ from benedict.dicts.base import BaseDict from benedict.dicts.io import io_util from benedict.exceptions import ExtrasRequireModuleNotFoundError -from benedict.utils import type_util +from benedict.utils import pydantic_util, type_util +from benedict.utils.pydantic_util import PydanticModel class IODict(BaseDict): @@ -20,7 +21,11 @@ def __init__(self, *args, **kwargs): d = IODict._decode_init(arg, **kwargs) super().__init__(d) return + + schema = kwargs.pop("schema", None) super().__init__(*args, **kwargs) + if schema: + self.validate(schema=schema) @staticmethod def _decode_init(s, **kwargs): @@ -28,10 +33,12 @@ def _decode_init(s, **kwargs): default_format = autodetected_format or "json" format = kwargs.pop("format", default_format).lower() # decode data-string and initialize with dict data. - return IODict._decode(s, format, **kwargs) + data = IODict._decode(s, format, **kwargs) + return data @staticmethod def _decode(s, format, **kwargs): + schema = kwargs.pop("schema", None) data = None try: data = io_util.decode(s, format, **kwargs) @@ -45,12 +52,15 @@ def _decode(s, format, **kwargs): ) from None # if possible return data as dict, otherwise raise exception if type_util.is_dict(data): - return data + pass elif type_util.is_list(data): # force list to dict - return {"values": data} + data = {"values": data} else: raise ValueError(f"Invalid data type: {type(data)}, expected dict or list.") + if schema: + data = pydantic_util.validate_data(data, schema=schema) + return data @staticmethod def _encode(d, format, **kwargs): @@ -325,3 +335,14 @@ def to_yaml(self, **kwargs): A ValueError is raised in case of failure. """ return self._encode(self.dict(), "yaml", **kwargs) + + def validate(self, *, schema: PydanticModel): + """ + Validate the dict and update it using a Pydantic schema. + + Args: + schema: Pydantic model class for validation + """ + data = pydantic_util.validate_data(self, schema=schema) + self.clear() + self.update(data) diff --git a/benedict/extras.py b/benedict/extras.py index 0a1122f5..483f9912 100644 --- a/benedict/extras.py +++ b/benedict/extras.py @@ -5,6 +5,7 @@ "require_parse", "require_s3", "require_toml", + "require_validate", "require_xls", "require_xml", "require_yaml", @@ -32,6 +33,10 @@ def require_toml(*, installed): _require_optional_dependencies(target="toml", installed=installed) +def require_validate(*, installed): + _require_optional_dependencies(target="validate", installed=installed) + + def require_xls(*, installed): _require_optional_dependencies(target="xls", installed=installed) diff --git a/benedict/utils/pydantic_util.py b/benedict/utils/pydantic_util.py new file mode 100644 index 00000000..ef7de92a --- /dev/null +++ b/benedict/utils/pydantic_util.py @@ -0,0 +1,50 @@ +from typing import Any + +from benedict.extras import require_validate + +try: + from pydantic.v2 import BaseModel + from pydantic.v2.json import pydantic_encoder + + pydantic_installed = True +except ImportError: + pydantic_installed = False + BaseModel = None + pydantic_encoder = None + +PydanticModel = type["BaseModel"] + + +def is_pydantic_model(obj: Any) -> bool: + """ + Check if an object is a Pydantic model. + """ + return pydantic_installed and isinstance(obj, BaseModel) + + +def is_pydantic_model_class(obj: Any) -> bool: + """ + Check if an object is a Pydantic model class. + """ + return ( + pydantic_installed + and BaseModel is not None + and isinstance(obj, type) + and issubclass(obj, BaseModel) + ) + + +def validate_data(data: Any, *, schema: PydanticModel | None = None) -> Any: + """ + Validate data against a Pydantic schema if provided. + """ + if schema is None: + return data + + require_validate(installed=pydantic_installed) + + if not is_pydantic_model_class(schema): + raise ValueError("Invalid schema. Schema must be a Pydantic model class.") + + validated = schema.model_validate(data) + return validated.model_dump() diff --git a/pyproject.toml b/pyproject.toml index d4b51c53..6e755339 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -115,7 +115,7 @@ Twitter = "https://twitter.com/fabiocaccamo" [project.optional-dependencies] all = [ - "python-benedict[io,parse,s3]", + "python-benedict[io,parse,s3,validate]", ] html = [ "beautifulsoup4 >= 4.12.0, < 5.0.0", @@ -136,6 +136,9 @@ s3 = [ toml = [ "toml >= 0.10.2, < 1.0.0", ] +validate = [ + "pydantic >= 1.10.0", +] xls = [ "openpyxl >= 3.0.0, < 4.0.0", "xlrd >= 2.0.0, < 3.0.0", diff --git a/tests/dicts/io/test_io_dict_schema.py b/tests/dicts/io/test_io_dict_schema.py new file mode 100644 index 00000000..34fe42c3 --- /dev/null +++ b/tests/dicts/io/test_io_dict_schema.py @@ -0,0 +1,86 @@ +import json +import unittest + +from benedict import benedict + +try: + from pydantic.v2 import BaseModel + from pydantic.v2.errors import ValidationError + + pydantic_installed = True +except ImportError: + pydantic_installed = False + BaseModel = None + ValidationError = None + + +@unittest.skipIf(not pydantic_installed, "pydantic not installed") +class TestIODictSchema(unittest.TestCase): + def setUp(self): + class User(BaseModel): + name: str + age: int + email: str + + class UserList(BaseModel): + users: list[User] + + class UserOptional(BaseModel): + name: str + age: int | None = None + email: str | None = None + + self.User = User + self.UserList = UserList + self.UserOptional = UserOptional + self.valid_data = { + "name": "John", + "age": 30, + "email": "john@example.com", + } + self.invalid_data = { + "name": "John", + "age": "not_an_int", + "email": "john@example.com", + } + self.minimal_data = {"name": "John"} + + def test_constructor_with_schema(self): + d = benedict(self.valid_data, schema=self.User) + self.assertEqual(d["name"], "John") + self.assertEqual(d["age"], 30) + self.assertEqual(d["email"], "john@example.com") + + with self.assertRaises(ValidationError): + benedict(self.invalid_data, schema=self.User) + + def test_constructor_with_schema_and_optional_fields(self): + d = benedict(self.minimal_data, schema=self.UserOptional) + self.assertEqual(d["name"], "John") + self.assertIsNone(d.get("age")) + self.assertIsNone(d.get("email")) + + def test_constructor_with_invalid_schema(self): + class InvalidSchema: + pass + + with self.assertRaises(ValueError): + benedict(self.valid_data, schema=InvalidSchema) + + with self.assertRaises(ValueError): + benedict(self.valid_data, schema="not_a_schema") + + def test_from_json_with_schema_and_valid_data(self): + json_data = json.dumps(self.valid_data) + d = benedict.from_json(json_data, schema=self.User) + self.assertEqual(d["name"], "John") + self.assertEqual(d["age"], 30) + + def test_from_json_with_schema_and_invalid_data(self): + json_data = json.dumps(self.invalid_data) + with self.assertRaises(ValidationError): + benedict.from_json(json_data, schema=self.User) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/dicts/io/test_io_dict_validate.py b/tests/dicts/io/test_io_dict_validate.py new file mode 100644 index 00000000..fc2219fc --- /dev/null +++ b/tests/dicts/io/test_io_dict_validate.py @@ -0,0 +1,91 @@ +import unittest + +from benedict.dicts.io import IODict + +try: + from pydantic.v2 import BaseModel + from pydantic.v2.errors import ValidationError + + pydantic_installed = True +except ImportError: + pydantic_installed = False + BaseModel = None + ValidationError = None + + +@unittest.skipIf(not pydantic_installed, "pydantic not installed") +class TestIODictValidate(unittest.TestCase): + def setUp(self): + class User(BaseModel): + name: str + age: int + email: str + + class UserOptional(BaseModel): + name: str + age: int | None = None + email: str | None = None + + self.User = User + self.UserOptional = UserOptional + self.valid_data = { + "name": "John", + "age": 30, + "email": "john@example.com", + } + self.invalid_data = { + "name": "John", + "age": "not_an_int", + "email": "john@example.com", + } + self.minimal_data = {"name": "John"} + self.data_with_extra_fields = { + "name": "John", + "age": 30, + "email": "john@example.com", + "role": "admin", + "active": True, + } + + def test_validate_valid_data(self): + d = IODict(self.valid_data) + d.validate(schema=self.User) + self.assertEqual(d["name"], "John") + self.assertEqual(d["age"], 30) + self.assertEqual(d["email"], "john@example.com") + + def test_validate_invalid_data(self): + d = IODict(self.invalid_data) + with self.assertRaises(ValidationError): + d.validate(schema=self.User) + + def test_validate_optional_fields(self): + d = IODict(self.minimal_data) + d.validate(schema=self.UserOptional) + self.assertEqual(d["name"], "John") + self.assertIsNone(d.get("age")) + self.assertIsNone(d.get("email")) + + def test_validate_removes_extra_fields(self): + d = IODict(self.data_with_extra_fields) + d.validate(schema=self.User) + # required fields are preserved + self.assertEqual(d["name"], "John") + self.assertEqual(d["age"], 30) + self.assertEqual(d["email"], "john@example.com") + # extra fields are removed + self.assertNotIn("role", d) + self.assertNotIn("active", d) + # only the schema fields exist + self.assertEqual(set(d.keys()), {"name", "age", "email"}) + + def test_validate_invalid_schema(self): + class InvalidSchema: + pass + + d = IODict(self.valid_data) + with self.assertRaises(ValueError): + d.validate(schema=InvalidSchema) + + with self.assertRaises(ValueError): + d.validate(schema="not_a_schema") From 42ee6010edb45ac8af66a59897e83c7cce3b6e4f Mon Sep 17 00:00:00 2001 From: Fabio Caccamo Date: Wed, 23 Apr 2025 20:10:15 +0200 Subject: [PATCH 02/12] Update README.md --- README.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 14817ab9..0af793af 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ python-benedict is a dict subclass with **keylist/keypath/keyattr** support, **I - [Disable keypath functionality](#disable-keypath-functionality) - [List index support](#list-index-support) - [I/O](#io) + - [Data validation using Pydantic models](#data-validation-using-pydantic-models) - [API](#api) - [Utility methods](#utility-methods) - [I/O methods](#io-methods) @@ -280,9 +281,7 @@ Here are the details of the supported formats, operations and extra options docs | `xml` | :white_check_mark: | :white_check_mark: | [xmltodict](https://github.com/martinblech/xmltodict) | | `yaml` | :white_check_mark: | :white_check_mark: | [PyYAML](https://pyyaml.org/wiki/PyYAMLDocumentation) | -#### Data validation - -`benedict` supports data validation using `Pydantic` models. +#### Data validation using Pydantic models This feature **requires** the `validate` extra to be installed: @@ -292,18 +291,18 @@ pip install "python-benedict[validate]" You can validate data in different ways: -##### Using the `validate` method directly +1. Using the `validate` method directly ```python d = benedict(my_data) d.validate(schema=MySchema) ``` -##### Using the `schema` parameter during initialization +2. Using the `schema` parameter during initialization ```python d = benedict(my_data, schema=MySchema) ``` -##### Using the `schema` parameter with any `from_{format}` method +3. Using the `schema` parameter with any `from_{format}` method ```python d = benedict.from_json(my_data, schema=MySchema) ``` From 14468166328f086570a1fc0b115d290758e0a0a8 Mon Sep 17 00:00:00 2001 From: Fabio Caccamo Date: Wed, 23 Apr 2025 20:22:08 +0200 Subject: [PATCH 03/12] Update README.md --- README.md | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 0af793af..0ad74251 100644 --- a/README.md +++ b/README.md @@ -129,7 +129,8 @@ or using the `getter/setter` property. d.keyattr_dynamic = True ``` -> **Warning** - even if this feature is very useful, it has some obvious limitations: it works only for string keys that are *unprotected* (not starting with an `_`) and that don't clash with the currently supported methods names. +> [!WARNING] +> Even if this feature is very useful, it has some obvious limitations: it works only for string keys that are *unprotected* (not starting with an `_`) and that don't clash with the currently supported methods names. ### Keylist Wherever a **key** is used, it is possible to use also a **list of keys**. @@ -281,13 +282,10 @@ Here are the details of the supported formats, operations and extra options docs | `xml` | :white_check_mark: | :white_check_mark: | [xmltodict](https://github.com/martinblech/xmltodict) | | `yaml` | :white_check_mark: | :white_check_mark: | [PyYAML](https://pyyaml.org/wiki/PyYAMLDocumentation) | -#### Data validation using Pydantic models +### Data validation using Pydantic models -This feature **requires** the `validate` extra to be installed: - -```bash -pip install "python-benedict[validate]" -``` +> [!IMPORTANT] +> This feature **requires** the `validate` extra to be installed: `pip install "python-benedict[validate]` You can validate data in different ways: From 9741cf9c3f9be30aeb38fb5ad165f063415a1504 Mon Sep 17 00:00:00 2001 From: Fabio Caccamo Date: Wed, 23 Apr 2025 20:35:16 +0200 Subject: [PATCH 04/12] Fix imports and code cleanup. --- benedict/utils/pydantic_util.py | 11 +---------- tests/dicts/io/test_io_dict_schema.py | 13 ++----------- tests/dicts/io/test_io_dict_validate.py | 13 ++----------- 3 files changed, 5 insertions(+), 32 deletions(-) diff --git a/benedict/utils/pydantic_util.py b/benedict/utils/pydantic_util.py index ef7de92a..73194e4c 100644 --- a/benedict/utils/pydantic_util.py +++ b/benedict/utils/pydantic_util.py @@ -3,25 +3,16 @@ from benedict.extras import require_validate try: - from pydantic.v2 import BaseModel - from pydantic.v2.json import pydantic_encoder + from pydantic import BaseModel pydantic_installed = True except ImportError: pydantic_installed = False BaseModel = None - pydantic_encoder = None PydanticModel = type["BaseModel"] -def is_pydantic_model(obj: Any) -> bool: - """ - Check if an object is a Pydantic model. - """ - return pydantic_installed and isinstance(obj, BaseModel) - - def is_pydantic_model_class(obj: Any) -> bool: """ Check if an object is a Pydantic model class. diff --git a/tests/dicts/io/test_io_dict_schema.py b/tests/dicts/io/test_io_dict_schema.py index 34fe42c3..524b0de6 100644 --- a/tests/dicts/io/test_io_dict_schema.py +++ b/tests/dicts/io/test_io_dict_schema.py @@ -1,20 +1,11 @@ import json import unittest -from benedict import benedict - -try: - from pydantic.v2 import BaseModel - from pydantic.v2.errors import ValidationError +from pydantic import BaseModel, ValidationError - pydantic_installed = True -except ImportError: - pydantic_installed = False - BaseModel = None - ValidationError = None +from benedict import benedict -@unittest.skipIf(not pydantic_installed, "pydantic not installed") class TestIODictSchema(unittest.TestCase): def setUp(self): class User(BaseModel): diff --git a/tests/dicts/io/test_io_dict_validate.py b/tests/dicts/io/test_io_dict_validate.py index fc2219fc..75f5bfa1 100644 --- a/tests/dicts/io/test_io_dict_validate.py +++ b/tests/dicts/io/test_io_dict_validate.py @@ -1,19 +1,10 @@ import unittest -from benedict.dicts.io import IODict - -try: - from pydantic.v2 import BaseModel - from pydantic.v2.errors import ValidationError +from pydantic import BaseModel, ValidationError - pydantic_installed = True -except ImportError: - pydantic_installed = False - BaseModel = None - ValidationError = None +from benedict.dicts.io import IODict -@unittest.skipIf(not pydantic_installed, "pydantic not installed") class TestIODictValidate(unittest.TestCase): def setUp(self): class User(BaseModel): From 299a6e8254c42124afd04e03119ed72bd2ee41e6 Mon Sep 17 00:00:00 2001 From: Fabio Caccamo Date: Wed, 23 Apr 2025 21:08:45 +0200 Subject: [PATCH 05/12] Update requirements.txt --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 8679d01f..b33aea78 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,7 @@ ftfy == 6.3.1 mailchecker == 6.0.14 openpyxl == 3.1.5 phonenumbers == 8.13.52 +pydantic == 2.11.3 python-dateutil == 2.9.0.post0 python-fsutil == 0.14.1 python-slugify == 8.0.4 From 8e19a195f24e5ecd62e1ab7fc67e0df6da4a5d73 Mon Sep 17 00:00:00 2001 From: Fabio Caccamo Date: Wed, 23 Apr 2025 21:23:51 +0200 Subject: [PATCH 06/12] Improve tests. --- benedict/utils/pydantic_util.py | 4 +- tests/utils/test_pydantic_util.py | 65 +++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 tests/utils/test_pydantic_util.py diff --git a/benedict/utils/pydantic_util.py b/benedict/utils/pydantic_util.py index 73194e4c..e98e1528 100644 --- a/benedict/utils/pydantic_util.py +++ b/benedict/utils/pydantic_util.py @@ -13,7 +13,7 @@ PydanticModel = type["BaseModel"] -def is_pydantic_model_class(obj: Any) -> bool: +def _is_pydantic_model(obj: Any) -> bool: """ Check if an object is a Pydantic model class. """ @@ -34,7 +34,7 @@ def validate_data(data: Any, *, schema: PydanticModel | None = None) -> Any: require_validate(installed=pydantic_installed) - if not is_pydantic_model_class(schema): + if not _is_pydantic_model(schema): raise ValueError("Invalid schema. Schema must be a Pydantic model class.") validated = schema.model_validate(data) diff --git a/tests/utils/test_pydantic_util.py b/tests/utils/test_pydantic_util.py new file mode 100644 index 00000000..634632c1 --- /dev/null +++ b/tests/utils/test_pydantic_util.py @@ -0,0 +1,65 @@ +import unittest +from unittest.mock import patch + +from pydantic import BaseModel, ValidationError + +from benedict.exceptions import ExtrasRequireModuleNotFoundError +from benedict.utils import pydantic_util + + +class User(BaseModel): + name: str + age: int + + +class TestPydanticUtil(unittest.TestCase): + def setUp(self): + self.valid_data = {"name": "John", "age": 30} + self.invalid_data = {"name": "John", "age": "not_an_int"} + + def test_validate_data_with_valid_data(self): + # validation with valid data succeeds + validated = pydantic_util.validate_data(self.valid_data, schema=User) + self.assertEqual(validated["name"], "John") + self.assertEqual(validated["age"], 30) + + def test_validate_data_with_invalid_data(self): + # validation with invalid data raises ValidationError + with self.assertRaises(ValidationError): + pydantic_util.validate_data(self.invalid_data, schema=User) + + def test_validate_data_with_no_schema(self): + # validation without schema returns original data + data = {"any": "data"} + validated = pydantic_util.validate_data(data, schema=None) + self.assertEqual(validated, data) + + def test_validate_data_with_invalid_schema(self): + # validation with non-pydantic schema raises ValueError + class NotAModel: + pass + + with self.assertRaises(ValueError) as cm: + pydantic_util.validate_data(self.valid_data, schema=NotAModel) + self.assertEqual( + str(cm.exception), "Invalid schema. Schema must be a Pydantic model class." + ) + + # validation with invalid schema type raises ValueError + with self.assertRaises(ValueError) as cm: + pydantic_util.validate_data(self.valid_data, schema="not_a_schema") + self.assertEqual( + str(cm.exception), "Invalid schema. Schema must be a Pydantic model class." + ) + + @patch("benedict.utils.pydantic_util.pydantic_installed", False) + @patch("benedict.utils.pydantic_util.BaseModel", None) + def test_validate_data_when_pydantic_not_installed(self): + # validation without schema still works when pydantic is not installed + data = {"any": "data"} + validated = pydantic_util.validate_data(data, schema=None) + self.assertEqual(validated, data) + + # validation with schema raises ExtrasRequireModuleNotFoundError + with self.assertRaises(ExtrasRequireModuleNotFoundError): + pydantic_util.validate_data(self.valid_data, schema=User) From 2b2e40feeafe79946dfa724e93b622aac7ed14b4 Mon Sep 17 00:00:00 2001 From: Fabio Caccamo Date: Thu, 24 Apr 2025 15:56:06 +0200 Subject: [PATCH 07/12] Change exception type. --- benedict/utils/pydantic_util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benedict/utils/pydantic_util.py b/benedict/utils/pydantic_util.py index e98e1528..388d367b 100644 --- a/benedict/utils/pydantic_util.py +++ b/benedict/utils/pydantic_util.py @@ -6,7 +6,7 @@ from pydantic import BaseModel pydantic_installed = True -except ImportError: +except ModuleNotFoundError: pydantic_installed = False BaseModel = None From a47059f4dfabb34489966066a9d5ad1cc03db8e1 Mon Sep 17 00:00:00 2001 From: Fabio Caccamo Date: Thu, 24 Apr 2025 15:56:15 +0200 Subject: [PATCH 08/12] Simplify check. --- benedict/utils/pydantic_util.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/benedict/utils/pydantic_util.py b/benedict/utils/pydantic_util.py index 388d367b..19c50861 100644 --- a/benedict/utils/pydantic_util.py +++ b/benedict/utils/pydantic_util.py @@ -17,12 +17,7 @@ def _is_pydantic_model(obj: Any) -> bool: """ Check if an object is a Pydantic model class. """ - return ( - pydantic_installed - and BaseModel is not None - and isinstance(obj, type) - and issubclass(obj, BaseModel) - ) + return pydantic_installed and isinstance(obj, type) and issubclass(obj, BaseModel) def validate_data(data: Any, *, schema: PydanticModel | None = None) -> Any: From 1bc8cfb40cb3befc203d2ed079b7dec3b64d3d11 Mon Sep 17 00:00:00 2001 From: Fabio Caccamo Date: Wed, 11 Jun 2025 21:57:15 +0200 Subject: [PATCH 09/12] Update `README.md`. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0ad74251..a5916876 100644 --- a/README.md +++ b/README.md @@ -1077,7 +1077,7 @@ Released under [MIT License](LICENSE.txt). - :star: Star this project on [GitHub](https://github.com/fabiocaccamo/python-benedict) - :octocat: Follow me on [GitHub](https://github.com/fabiocaccamo) -- :blue_heart: Follow me on [Twitter](https://twitter.com/fabiocaccamo) +- :blue_heart: Follow me on [Bluesky](https://bsky.app/profile/fabiocaccamo.bsky.social) - :moneybag: Sponsor me on [Github](https://github.com/sponsors/fabiocaccamo) ## See also From ffabdb18ebac36a0f76270cffda8d7e0c2357cac Mon Sep 17 00:00:00 2001 From: Fabio Caccamo Date: Wed, 11 Jun 2025 21:59:55 +0200 Subject: [PATCH 10/12] Reduce `codeql-analysis` workflow frequency. --- .github/workflows/codeql-analysis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index ec8d1756..5978520b 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -15,10 +15,9 @@ on: push: branches: [ main ] pull_request: - # The branches below must be a subset of the branches above branches: [ main ] schedule: - - cron: '16 20 * * 0' + - cron: "0 0 1 * *" jobs: analyze: From b047a0799c5faf2a3f8e8b0af34ef03460f970ad Mon Sep 17 00:00:00 2001 From: Fabio Caccamo Date: Wed, 11 Jun 2025 22:02:02 +0200 Subject: [PATCH 11/12] Avoid `test` workflow double runs. --- .github/workflows/test-package.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test-package.yml b/.github/workflows/test-package.yml index 091a4565..a3112360 100644 --- a/.github/workflows/test-package.yml +++ b/.github/workflows/test-package.yml @@ -2,6 +2,7 @@ name: Test package on: push: + branches: [ main ] pull_request: workflow_dispatch: From c815f0676631dc7f30807fc913b69b19f43739f9 Mon Sep 17 00:00:00 2001 From: Fabio Caccamo Date: Wed, 11 Jun 2025 22:16:36 +0200 Subject: [PATCH 12/12] Bump `pre-commit` hooks. --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2932a1b9..73922952 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,13 +3,13 @@ repos: - repo: https://github.com/asottile/pyupgrade - rev: v3.19.1 + rev: v3.20.0 hooks: - id: pyupgrade args: ["--py310-plus"] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.2 + rev: v0.11.13 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix]