diff --git a/docs/models/index.md b/docs/models/index.md index 9f4da22..3fdf2da 100644 --- a/docs/models/index.md +++ b/docs/models/index.md @@ -373,6 +373,31 @@ You can set this parameter by providing `Meta` class `constraints` argument. To set one column as unique use [`unique`](../fields/common-parameters.md#unique) common parameter. Of course you can set many columns as unique with this param but each of them will be checked separately. +### Pydantic configuration + +As each `ormar.Model` is also a `pydantic` model, you might want to tweak the settings of the pydantic configuration. + +The way to do this in pydantic is to adjust the settings on the `Config` class provided to your model, and it works exactly the same for ormer.Models. + +So in order to set your own preferences you need to provide not only the `Meta` class but also the `Config` class to your model. + +!!!note + To read more about available settings visit the [pydantic](https://pydantic-docs.helpmanual.io/usage/model_config/) config page. + +Note that if you do not provide your own configuration, ormar will do it for you. +The default config provided is as follows: + +```python +class Config(pydantic.BaseConfig): + orm_mode = True + validate_assignment = True +``` + +So to overwrite setting or provide your own a sample model can look like following: +```Python hl_lines="15-16" +--8<-- "../docs_src/models/docs016.py" +``` + ## Model sort order When querying the database with given model by default the Model is ordered by the `primary_key` diff --git a/docs_src/models/docs016.py b/docs_src/models/docs016.py new file mode 100644 index 0000000..5f9d71c --- /dev/null +++ b/docs_src/models/docs016.py @@ -0,0 +1,20 @@ +import databases +import sqlalchemy + +import ormar + +database = databases.Database("sqlite:///db.sqlite") +metadata = sqlalchemy.MetaData() + + +class Course(ormar.Model): + class Meta: + database = database + metadata = metadata + + class Config: + allow_mutation = False + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100) + completed: bool = ormar.Boolean(default=False) diff --git a/ormar/models/metaclass.py b/ormar/models/metaclass.py index 88f5681..df15cac 100644 --- a/ormar/models/metaclass.py +++ b/ormar/models/metaclass.py @@ -1,3 +1,4 @@ +import inspect from typing import ( Any, Dict, @@ -552,7 +553,21 @@ class ModelMetaclass(pydantic.main.ModelMetaclass): :param attrs: class namespace :type attrs: Dict """ - attrs["Config"] = get_pydantic_base_orm_config() + DefaultConfig = get_pydantic_base_orm_config() + if "Config" in attrs: + ProvidedConfig = attrs["Config"] + if not inspect.isclass(ProvidedConfig): + raise ModelDefinitionError( + f"Config provided for class {name} has to be a class." + ) + + class Config(ProvidedConfig, DefaultConfig): # type: ignore + pass + + attrs["Config"] = Config + else: + attrs["Config"] = DefaultConfig + attrs["__name__"] = name attrs, model_fields = extract_annotations_and_default_vals(attrs) for base in reversed(bases): @@ -580,9 +595,11 @@ class ModelMetaclass(pydantic.main.ModelMetaclass): populate_meta_sqlalchemy_table_if_required(new_model.Meta) expand_reverse_relationships(new_model) # TODO: iterate only related fields - for name, field in new_model.Meta.model_fields.items(): + for field_name, field in new_model.Meta.model_fields.items(): register_relation_in_alias_manager(field=field) - add_field_descriptor(name=name, field=field, new_model=new_model) + add_field_descriptor( + name=field_name, field=field, new_model=new_model + ) if ( new_model.Meta.pkname @@ -640,10 +657,7 @@ class ModelMetaclass(pydantic.main.ModelMetaclass): model=field.to, access_chain=item, ) - else: - return FieldAccessor( - source_model=cast(Type["Model"], self), - field=field, - access_chain=item, - ) + return FieldAccessor( + source_model=cast(Type["Model"], self), field=field, access_chain=item, + ) return object.__getattribute__(self, item) diff --git a/tests/test_inheritance_and_pydantic_generation/test_inheritance_concrete.py b/tests/test_inheritance_and_pydantic_generation/test_inheritance_concrete.py index 41eac11..fda8e7c 100644 --- a/tests/test_inheritance_and_pydantic_generation/test_inheritance_concrete.py +++ b/tests/test_inheritance_and_pydantic_generation/test_inheritance_concrete.py @@ -150,6 +150,12 @@ class Bus2(Car2): max_persons: int = ormar.Integer() +class ImmutablePerson(Person): + class Config: + allow_mutation = False + validate_assignment = False + + @pytest.fixture(autouse=True, scope="module") def create_test_database(): metadata.create_all(engine) @@ -173,6 +179,13 @@ def test_duplicated_related_name_on_different_model(): max_persons: int = ormar.Integer() +def test_config_is_not_a_class_raises_error(): + with pytest.raises(ModelDefinitionError): + + class ImmutablePerson2(Person): + Config = dict(allow_mutation=False, validate_assignment=False) + + def test_field_redefining_in_concrete_models(): class RedefinedField(DateFieldsModel): class Meta(ormar.ModelMeta): @@ -495,3 +508,13 @@ async def test_inheritance_with_multi_relation(): assert len(unicorns) == 2 assert unicorns[1].name == "Unicorn 2" assert len(unicorns[1].co_owners) == 1 + + +def test_custom_config(): + # Custom config inherits defaults + assert getattr(ImmutablePerson.__config__, "orm_mode") is True + # Custom config can override defaults + assert getattr(ImmutablePerson.__config__, "validate_assignment") is False + sam = ImmutablePerson(name="Sam") + with pytest.raises(TypeError): + sam.name = "Not Sam"