From e45bbfc177c04d0c4ff6eb4ac95b68bf95e36162 Mon Sep 17 00:00:00 2001 From: AlessandroMiola Date: Sat, 15 Jun 2024 19:52:24 +0200 Subject: [PATCH 1/5] chore: update justfile to match [types: python] setting in pre-commit-config --- justfile | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/justfile b/justfile index dc4886d..541449f 100644 --- a/justfile +++ b/justfile @@ -17,23 +17,23 @@ check-project: ruff: @echo "🚀 Linting the project with Ruff" - @poetry run ruff check . + @poetry run ruff check tests ruff-show-violations: @echo "🚀 Linting the project with Ruff and show violations" - @poetry run ruff check --output-format="grouped" . + @poetry run ruff check --output-format="grouped" tests ruff-fix: @echo "🚀 Linting the project with Ruff and autofix violations (where possible)" - @poetry run ruff check --fix . + @poetry run ruff check --fix tests ruff-format: @echo "🚀 Formatting the code with Ruff" - @poetry run ruff format . + @poetry run ruff format tests ruff-format-check: @echo "🚀 Listing files Ruff would reformat" - @poetry run ruff format --check . + @poetry run ruff format --check tests lint-and-format: ruff-fix ruff-format From b7396e016a5a6b95463c2a3127576805be42699b Mon Sep 17 00:00:00 2001 From: AlessandroMiola Date: Sat, 15 Jun 2024 20:45:26 +0200 Subject: [PATCH 2/5] chore: temporarily update justfile and pre-commit-config to debug their interaction --- .pre-commit-config.yaml | 2 +- justfile | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8b84f4a..4bbe10d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,7 +13,7 @@ repos: hooks: - id: ruff-lint name: ruff (linter) - entry: just ruff + entry: just ruff . language: system types: [python] - id: ruff-format diff --git a/justfile b/justfile index 541449f..13b7ac6 100644 --- a/justfile +++ b/justfile @@ -15,9 +15,9 @@ check-project: @echo "🚀 Running the hooks against all files" @poetry run pre-commit run --all-files -ruff: +ruff file: @echo "🚀 Linting the project with Ruff" - @poetry run ruff check tests + @poetry run ruff check {{file}} ruff-show-violations: @echo "🚀 Linting the project with Ruff and show violations" From 493c0fde7fdcbaf84d6fbdbeed7479453453fcfe Mon Sep 17 00:00:00 2001 From: AlessandroMiola Date: Sat, 15 Jun 2024 21:17:01 +0200 Subject: [PATCH 3/5] chore: revert to the only (known) working config --- .pre-commit-config.yaml | 4 ++-- justfile | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4bbe10d..0f1fdbd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,11 +13,11 @@ repos: hooks: - id: ruff-lint name: ruff (linter) - entry: just ruff . + entry: poetry run ruff check . language: system types: [python] - id: ruff-format name: ruff (formatter) - entry: just ruff-format + entry: poetry run ruff format . language: system types: [python] diff --git a/justfile b/justfile index 13b7ac6..dc4886d 100644 --- a/justfile +++ b/justfile @@ -15,25 +15,25 @@ check-project: @echo "🚀 Running the hooks against all files" @poetry run pre-commit run --all-files -ruff file: +ruff: @echo "🚀 Linting the project with Ruff" - @poetry run ruff check {{file}} + @poetry run ruff check . ruff-show-violations: @echo "🚀 Linting the project with Ruff and show violations" - @poetry run ruff check --output-format="grouped" tests + @poetry run ruff check --output-format="grouped" . ruff-fix: @echo "🚀 Linting the project with Ruff and autofix violations (where possible)" - @poetry run ruff check --fix tests + @poetry run ruff check --fix . ruff-format: @echo "🚀 Formatting the code with Ruff" - @poetry run ruff format tests + @poetry run ruff format . ruff-format-check: @echo "🚀 Listing files Ruff would reformat" - @poetry run ruff format --check tests + @poetry run ruff format --check . lint-and-format: ruff-fix ruff-format From 3c2d72f8b3b4ce87273d56f7409098a88362ff74 Mon Sep 17 00:00:00 2001 From: AlessandroMiola Date: Sat, 15 Jun 2024 21:18:37 +0200 Subject: [PATCH 4/5] feat: add tests for AliasGenerator --- tests/aliasing/conftest.py | 54 +++- tests/aliasing/test_combination_aliases.py | 274 ++++++++++++++++++++- 2 files changed, 326 insertions(+), 2 deletions(-) diff --git a/tests/aliasing/conftest.py b/tests/aliasing/conftest.py index 0829168..3c5ee82 100644 --- a/tests/aliasing/conftest.py +++ b/tests/aliasing/conftest.py @@ -1,5 +1,6 @@ import pytest -from pydantic import AliasChoices, BaseModel, ConfigDict, Field +from pydantic import AliasChoices, AliasGenerator, BaseModel, ConfigDict, Field +from pydantic.alias_generators import to_camel, to_pascal @pytest.fixture @@ -89,3 +90,54 @@ class ModelWithValidationAliasPopByName(BaseModel): first_name: str = Field(validation_alias="firstName") return ModelWithValidationAliasPopByName + + +@pytest.fixture +def model_with_alias_generator_and_unset_priority(): + class ModelWithAliasGeneratorAndUnsetPriority(BaseModel): + model_config = ConfigDict( + alias_generator=AliasGenerator( + alias=to_camel, + validation_alias=lambda x: x.upper(), + serialization_alias=to_pascal, + ) + ) + first_name_pa: str = Field(alias="f_name_pa") + first_name_va: str = Field(validation_alias="f_name_va") + first_name_sa: str = Field(serialization_alias="f_name_sa") + + return ModelWithAliasGeneratorAndUnsetPriority + + +@pytest.fixture +def model_with_alias_generator_and_priority_1(): + class ModelWithAliasGeneratorAndAliasPriority1(BaseModel): + model_config = ConfigDict( + alias_generator=AliasGenerator( + alias=to_camel, + validation_alias=lambda x: x.upper(), + serialization_alias=to_pascal, + ) + ) + first_name_pa: str = Field(alias="f_name_pa", alias_priority=1) + first_name_va: str = Field(validation_alias="f_name_va", alias_priority=1) + first_name_sa: str = Field(serialization_alias="f_name_sa", alias_priority=1) + + return ModelWithAliasGeneratorAndAliasPriority1 + + +@pytest.fixture +def model_with_alias_generator_and_priority_2(): + class ModelWithAliasGeneratorAndAliasPriority2(BaseModel): + model_config = ConfigDict( + alias_generator=AliasGenerator( + alias=to_camel, + validation_alias=lambda x: x.upper(), + serialization_alias=to_pascal, + ) + ) + first_name_pa: str = Field(alias="f_name_pa", alias_priority=2) + first_name_va: str = Field(validation_alias="f_name_va", alias_priority=2) + first_name_sa: str = Field(serialization_alias="f_name_sa", alias_priority=2) + + return ModelWithAliasGeneratorAndAliasPriority2 diff --git a/tests/aliasing/test_combination_aliases.py b/tests/aliasing/test_combination_aliases.py index ea2fbed..214d647 100644 --- a/tests/aliasing/test_combination_aliases.py +++ b/tests/aliasing/test_combination_aliases.py @@ -2,7 +2,8 @@ from contextlib import nullcontext as does_not_raise import pytest -from pydantic import ValidationError +from pydantic import AliasGenerator, ValidationError +from pydantic.alias_generators import to_camel, to_pascal class TestPlainAndSerializationAlias: @@ -139,6 +140,10 @@ def test_should_class_attribute_have_field_name( assert hasattr(model, arg_name) is expected assert (arg_name in dict(model)) is expected + def test_alias_priority(self, model_with_plain_and_serialization_alias): + model = model_with_plain_and_serialization_alias(firstName="Mickey") + assert model.model_fields["first_name"].alias_priority == 2 + class TestPlainAndValidationAlias: @pytest.mark.parametrize( @@ -272,6 +277,10 @@ def test_should_class_attribute_have_field_name( assert hasattr(model, arg_name) is expected assert (arg_name in dict(model)) is expected + def test_alias_priority(self, model_with_plain_and_validation_alias): + model = model_with_plain_and_validation_alias(firstName="Mickey") + assert model.model_fields["first_name"].alias_priority == 2 + class TestPlainAndSerializationAndValidationAlias: @pytest.mark.parametrize( @@ -390,3 +399,266 @@ def test_should_class_attribute_have_field_name( model = model_with_plain_and_serialization_and_validation_alias(firstName="Mickey") assert hasattr(model, arg_name) is expected assert (arg_name in dict(model)) is expected + + def test_alias_priority(self, model_with_plain_and_serialization_and_validation_alias): + model = model_with_plain_and_serialization_and_validation_alias(firstName="Mickey") + assert model.model_fields["first_name"].alias_priority == 2 + + +class TestAliasGeneratorUnsetAliasPriority: + @pytest.mark.parametrize( + "field_name, plain_alias, gen_plain_alias, gen_val_alias, gen_ser_alias", + [ + ( + "first_name_pa", + "f_name_pa", + AliasGenerator(alias=to_camel("first_name_pa")).alias, + AliasGenerator(validation_alias="first_name_pa".upper()).validation_alias, + AliasGenerator(serialization_alias=to_pascal("first_name_pa")).serialization_alias, + ), + ], + ) + def test_should_plain_alias_with_unset_priority_override_generated_aliases( + self, + model_with_alias_generator_and_unset_priority, + field_name: str, + plain_alias: str, + gen_plain_alias: str, + gen_val_alias: str, + gen_ser_alias: str, + ): + assert model_with_alias_generator_and_unset_priority.model_fields[field_name].alias == plain_alias + assert model_with_alias_generator_and_unset_priority.model_fields[field_name].validation_alias == plain_alias + assert model_with_alias_generator_and_unset_priority.model_fields[field_name].serialization_alias == plain_alias + + assert model_with_alias_generator_and_unset_priority.model_fields[field_name].alias != gen_plain_alias + assert model_with_alias_generator_and_unset_priority.model_fields[field_name].validation_alias != gen_val_alias + assert ( + model_with_alias_generator_and_unset_priority.model_fields[field_name].serialization_alias != gen_ser_alias + ) + + @pytest.mark.parametrize( + "field_name, val_alias, gen_plain_alias, gen_val_alias, gen_ser_alias", + [ + ( + "first_name_va", + "f_name_va", + AliasGenerator(alias=to_camel("first_name_va")).alias, + AliasGenerator(validation_alias="first_name_va".upper()).validation_alias, + AliasGenerator(serialization_alias=to_pascal("first_name_va")).serialization_alias, + ), + ], + ) + def test_should_val_alias_with_unset_priority_override_generated_val_alias( + self, + model_with_alias_generator_and_unset_priority, + field_name: str, + val_alias: str, + gen_plain_alias: str, + gen_val_alias: str, + gen_ser_alias: str, + ): + assert model_with_alias_generator_and_unset_priority.model_fields[field_name].alias == gen_plain_alias + assert model_with_alias_generator_and_unset_priority.model_fields[field_name].validation_alias == val_alias + assert ( + model_with_alias_generator_and_unset_priority.model_fields[field_name].serialization_alias == gen_ser_alias + ) + + assert model_with_alias_generator_and_unset_priority.model_fields[field_name].validation_alias != gen_val_alias + + @pytest.mark.parametrize( + "field_name, ser_alias, gen_plain_alias, gen_val_alias, gen_ser_alias", + [ + ( + "first_name_sa", + "f_name_sa", + AliasGenerator(alias=to_camel("first_name_sa")).alias, + AliasGenerator(validation_alias="first_name_sa".upper()).validation_alias, + AliasGenerator(serialization_alias=to_pascal("first_name_sa")).serialization_alias, + ), + ], + ) + def test_should_ser_alias_with_unset_priority_override_generated_ser_alias( + self, + model_with_alias_generator_and_unset_priority, + field_name: str, + ser_alias: str, + gen_plain_alias: str, + gen_val_alias: str, + gen_ser_alias: str, + ): + assert model_with_alias_generator_and_unset_priority.model_fields[field_name].alias == gen_plain_alias + assert model_with_alias_generator_and_unset_priority.model_fields[field_name].validation_alias == gen_val_alias + assert model_with_alias_generator_and_unset_priority.model_fields[field_name].serialization_alias == ser_alias + + assert ( + model_with_alias_generator_and_unset_priority.model_fields[field_name].serialization_alias != gen_ser_alias + ) + + +class TestAliasGeneratorAliasPriority1: + @pytest.mark.parametrize( + "field_name, plain_alias, gen_plain_alias, gen_val_alias, gen_ser_alias", + [ + ( + "first_name_pa", + "f_name_pa", + AliasGenerator(alias=to_camel("first_name_pa")).alias, + AliasGenerator(validation_alias="first_name_pa".upper()).validation_alias, + AliasGenerator(serialization_alias=to_pascal("first_name_pa")).serialization_alias, + ), + ], + ) + def test_should_plain_alias_with_low_priority_be_overriden_by_generated_plain_alias( + self, + model_with_alias_generator_and_priority_1, + field_name: str, + plain_alias: str, + gen_plain_alias: str, + gen_val_alias: str, + gen_ser_alias: str, + ): + assert model_with_alias_generator_and_priority_1.model_fields[field_name].alias == gen_plain_alias + assert model_with_alias_generator_and_priority_1.model_fields[field_name].validation_alias == gen_val_alias + assert model_with_alias_generator_and_priority_1.model_fields[field_name].serialization_alias == gen_ser_alias + + assert model_with_alias_generator_and_priority_1.model_fields[field_name].alias != plain_alias + + @pytest.mark.parametrize( + "field_name, val_alias, gen_plain_alias, gen_val_alias, gen_ser_alias", + [ + ( + "first_name_va", + "f_name_va", + AliasGenerator(alias=to_camel("first_name_va")).alias, + AliasGenerator(validation_alias="first_name_va".upper()).validation_alias, + AliasGenerator(serialization_alias=to_pascal("first_name_va")).serialization_alias, + ), + ], + ) + def test_should_val_alias_with_low_priority_be_overriden_by_generated_val_alias( + self, + model_with_alias_generator_and_priority_1, + field_name: str, + val_alias: str, + gen_plain_alias: str, + gen_val_alias: str, + gen_ser_alias: str, + ): + assert model_with_alias_generator_and_priority_1.model_fields[field_name].alias == gen_plain_alias + assert model_with_alias_generator_and_priority_1.model_fields[field_name].validation_alias == gen_val_alias + assert model_with_alias_generator_and_priority_1.model_fields[field_name].serialization_alias == gen_ser_alias + + assert model_with_alias_generator_and_priority_1.model_fields[field_name].validation_alias != val_alias + + @pytest.mark.parametrize( + "field_name, ser_alias, gen_plain_alias, gen_val_alias, gen_ser_alias", + [ + ( + "first_name_sa", + "f_name_sa", + AliasGenerator(alias=to_camel("first_name_sa")).alias, + AliasGenerator(validation_alias="first_name_sa".upper()).validation_alias, + AliasGenerator(serialization_alias=to_pascal("first_name_sa")).serialization_alias, + ), + ], + ) + def test_should_ser_alias_with_low_priority_be_overridden_by_generated_ser_alias( + self, + model_with_alias_generator_and_priority_1, + field_name: str, + ser_alias: str, + gen_plain_alias: str, + gen_val_alias: str, + gen_ser_alias: str, + ): + assert model_with_alias_generator_and_priority_1.model_fields[field_name].alias == gen_plain_alias + assert model_with_alias_generator_and_priority_1.model_fields[field_name].validation_alias == gen_val_alias + assert model_with_alias_generator_and_priority_1.model_fields[field_name].serialization_alias == gen_ser_alias + + assert model_with_alias_generator_and_priority_1.model_fields[field_name].serialization_alias != ser_alias + + +class TestAliasGeneratorAliasPriority2: + @pytest.mark.parametrize( + "field_name, plain_alias, gen_plain_alias, gen_val_alias, gen_ser_alias", + [ + ( + "first_name_pa", + "f_name_pa", + AliasGenerator(alias=to_camel("first_name_pa")).alias, + AliasGenerator(validation_alias="first_name_pa".upper()).validation_alias, + AliasGenerator(serialization_alias=to_pascal("first_name_pa")).serialization_alias, + ), + ], + ) + def test_should_plain_alias_with_high_priority_override_generated_aliases( + self, + model_with_alias_generator_and_priority_2, + field_name: str, + plain_alias: str, + gen_plain_alias: str, + gen_val_alias: str, + gen_ser_alias: str, + ): + assert model_with_alias_generator_and_priority_2.model_fields[field_name].alias == plain_alias + assert model_with_alias_generator_and_priority_2.model_fields[field_name].validation_alias == plain_alias + assert model_with_alias_generator_and_priority_2.model_fields[field_name].serialization_alias == plain_alias + + assert model_with_alias_generator_and_priority_2.model_fields[field_name].alias != gen_plain_alias + assert model_with_alias_generator_and_priority_2.model_fields[field_name].validation_alias != gen_val_alias + assert model_with_alias_generator_and_priority_2.model_fields[field_name].serialization_alias != gen_ser_alias + + @pytest.mark.parametrize( + "field_name, val_alias, gen_plain_alias, gen_val_alias, gen_ser_alias", + [ + ( + "first_name_va", + "f_name_va", + AliasGenerator(alias=to_camel("first_name_va")).alias, + AliasGenerator(validation_alias="first_name_va".upper()).validation_alias, + AliasGenerator(serialization_alias=to_pascal("first_name_va")).serialization_alias, + ), + ], + ) + def test_should_val_alias_with_high_priority_override_generated_val_alias( + self, + model_with_alias_generator_and_priority_2, + field_name: str, + val_alias: str, + gen_plain_alias: str, + gen_val_alias: str, + gen_ser_alias: str, + ): + assert model_with_alias_generator_and_priority_2.model_fields[field_name].alias == gen_plain_alias + assert model_with_alias_generator_and_priority_2.model_fields[field_name].validation_alias == val_alias + assert model_with_alias_generator_and_priority_2.model_fields[field_name].serialization_alias == gen_ser_alias + + assert model_with_alias_generator_and_priority_2.model_fields[field_name].validation_alias != gen_val_alias + + @pytest.mark.parametrize( + "field_name, ser_alias, gen_plain_alias, gen_val_alias, gen_ser_alias", + [ + ( + "first_name_sa", + "f_name_sa", + AliasGenerator(alias=to_camel("first_name_sa")).alias, + AliasGenerator(validation_alias="first_name_sa".upper()).validation_alias, + AliasGenerator(serialization_alias=to_pascal("first_name_sa")).serialization_alias, + ), + ], + ) + def test_should_ser_alias_with_high_priority_override_generated_ser_alias( + self, + model_with_alias_generator_and_priority_2, + field_name: str, + ser_alias: str, + gen_plain_alias: str, + gen_val_alias: str, + gen_ser_alias: str, + ): + assert model_with_alias_generator_and_priority_2.model_fields[field_name].alias == gen_plain_alias + assert model_with_alias_generator_and_priority_2.model_fields[field_name].validation_alias == gen_val_alias + assert model_with_alias_generator_and_priority_2.model_fields[field_name].serialization_alias == ser_alias + + assert model_with_alias_generator_and_priority_2.model_fields[field_name].serialization_alias != gen_ser_alias From 9ba22fbd44b3b5826690c75ff1abd4ce34c4065f Mon Sep 17 00:00:00 2001 From: AlessandroMiola Date: Sun, 16 Jun 2024 12:25:41 +0200 Subject: [PATCH 5/5] feat: add tests for AliasChoices and AliasPath --- tests/aliasing/conftest.py | 7 +- tests/aliasing/test_validation_alias.py | 99 ++++++++++++++++++++++++- 2 files changed, 102 insertions(+), 4 deletions(-) diff --git a/tests/aliasing/conftest.py b/tests/aliasing/conftest.py index 3c5ee82..63344e6 100644 --- a/tests/aliasing/conftest.py +++ b/tests/aliasing/conftest.py @@ -1,5 +1,5 @@ import pytest -from pydantic import AliasChoices, AliasGenerator, BaseModel, ConfigDict, Field +from pydantic import AliasChoices, AliasGenerator, AliasPath, BaseModel, ConfigDict, Field from pydantic.alias_generators import to_camel, to_pascal @@ -38,7 +38,10 @@ class ModelWithValidationAliasChoices(BaseModel): @pytest.fixture def model_with_validation_alias_path(): class ModelWithValidationAliasPath(BaseModel): - pass + first_name: str = Field(validation_alias=AliasPath("names", 0)) + last_name: str = Field(validation_alias=AliasPath("names", 1)) + + return ModelWithValidationAliasPath @pytest.fixture diff --git a/tests/aliasing/test_validation_alias.py b/tests/aliasing/test_validation_alias.py index da2fb97..b430ee3 100644 --- a/tests/aliasing/test_validation_alias.py +++ b/tests/aliasing/test_validation_alias.py @@ -135,8 +135,103 @@ def test_should_class_attribute_have_field_name(self, model_with_validation_alia assert hasattr(model, arg_name) is expected assert (arg_name in dict(model)) is expected - def test_alias_choices(self, model_with_validation_alias_choices): - pass + @pytest.mark.parametrize( + "data, expectation, expected_error", + [ + ({"firstName": "Mickey"}, does_not_raise(), None), + ('{"firstName": "Mickey"}', does_not_raise(), None), + ({"givenName": "Mickey"}, does_not_raise(), None), + ('{"givenName": "Mickey"}', does_not_raise(), None), + ({"preferredName": "Mickey"}, does_not_raise(), None), + ('{"preferredName": "Mickey"}', does_not_raise(), None), + ( + {"first_name": "Mickey"}, + pytest.raises(ValidationError), + [ + { + "type": "missing", + "loc": ("firstName",), + "msg": "Field required", + "input": {"first_name": "Mickey"}, + } + ], + ), + ( + '{"first_name": "Mickey"}', + pytest.raises(ValidationError), + [ + { + "type": "missing", + "loc": ("firstName",), + "msg": "Field required", + "input": {"first_name": "Mickey"}, + } + ], + ), + ( + {"anyOtherName": "Mickey"}, + pytest.raises(ValidationError), + [ + { + "type": "missing", + "loc": ("firstName",), + "msg": "Field required", + "input": {"anyOtherName": "Mickey"}, + } + ], + ), + ( + '{"anyOtherName": "Mickey"}', + pytest.raises(ValidationError), + [ + { + "type": "missing", + "loc": ("firstName",), + "msg": "Field required", + "input": {"anyOtherName": "Mickey"}, + } + ], + ), + ], + ) + def test_deserialize_by_any_of_alias_choices( + self, + model_with_validation_alias_choices, + data: dict | str, + expectation: ContextManager, + expected_error: list[dict] | None, + ): + with expectation as exc_info: + if isinstance(data, dict): + _ = model_with_validation_alias_choices.model_validate(data) + else: + _ = model_with_validation_alias_choices.model_validate_json(data) + if expected_error is None: + assert exc_info is None + else: + assert exc_info is not None + assert exc_info.value.errors(include_url=False) == expected_error + + @pytest.mark.parametrize( + "data, field_name, valid_alias", + [ + ({"names": ["Mickey", "Mouse", "Mortimer"]}, "first_name", "Mickey"), + ({"names": ["Mickey", "Mouse", "Mortimer"]}, "last_name", "Mouse"), + ({"names": ["Mortimer", "Mickey", "Mouse"]}, "first_name", "Mortimer"), + ], + ) + def test_deserialize_by_alias_in_alias_path( + self, + model_with_validation_alias_path, + data: dict, + field_name: str, + valid_alias: str, + ): + assert model_with_validation_alias_path.model_validate(data).__getattribute__(field_name) == valid_alias + assert model_with_validation_alias_path.model_validate(data).first_name != data["names"][1] + assert model_with_validation_alias_path.model_validate(data).first_name != data["names"][2] + assert model_with_validation_alias_path.model_validate(data).last_name != data["names"][0] + assert model_with_validation_alias_path.model_validate(data).last_name != data["names"][2] class TestValidationAliasPopByName: