diff --git a/docs/fields/field-types.md b/docs/fields/field-types.md index b76e288..99c5409 100644 --- a/docs/fields/field-types.md +++ b/docs/fields/field-types.md @@ -14,23 +14,19 @@ Each of the `Fields` has assigned both `sqlalchemy` column class and python type ### String -`String(max_length, - allow_blank: bool = True, - strip_whitespace: bool = False, +`String(max_length: int, min_length: int = None, - max_length: int = None, - curtail_length: int = None, regex: str = None,)` has a required `max_length` parameter. * Sqlalchemy column: `sqlalchemy.String` * Type (used for pydantic): `str` !!!tip - For explanation of other parameters check [pydantic][pydantic] documentation. + For explanation of other parameters check [pydantic](https://pydantic-docs.helpmanual.io/usage/schema/#field-customisation) documentation. ### Text -`Text(allow_blank: bool = True, strip_whitespace: bool = False)` has no required parameters. +`Text()` has no required parameters. * Sqlalchemy column: `sqlalchemy.Text` * Type (used for pydantic): `str` @@ -247,5 +243,5 @@ response = client.post( [relations]: ../relations/index.md [queries]: ../queries.md -[pydantic]: https://pydantic-docs.helpmanual.io/usage/types/#constrained-types +[pydantic]: https://pydantic-docs.helpmanual.io/usage/schema/#field-customisation [server default]: https://docs.sqlalchemy.org/en/13/core/defaults.html#server-invoked-ddl-explicit-default-expressions \ No newline at end of file diff --git a/docs/releases.md b/docs/releases.md index 48b0a0e..a891e9e 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1,3 +1,18 @@ +# 0.10.16 + +## ✨ Features + +* Allow passing your own pydantic `Config` to `ormar.Model` that will be merged with the default one by @naturalethic (thanks!) [#285](https://github.com/collerek/ormar/issues/285) +* Add `SmallInteger` field type by @ProgrammerPlus1998 (thanks!) [#297](https://github.com/collerek/ormar/pull/297) + + +## 🐛 Fixes + +* Fix generating openapi schema by removing obsolete pydantic field parameters that were directly exposed in schema [#291](https://github.com/collerek/ormar/issues/291) +* Fix unnecessary warning for auto generated through models [#295](https://github.com/collerek/ormar/issues/295) + + + # 0.10.15 ## 🐛 Fixes diff --git a/ormar/__init__.py b/ormar/__init__.py index 1c71fa9..1542801 100644 --- a/ormar/__init__.py +++ b/ormar/__init__.py @@ -41,7 +41,6 @@ from ormar.exceptions import ( # noqa: I100 from ormar.fields import ( BaseField, BigInteger, - SmallInteger, Boolean, DECODERS_MAP, Date, @@ -57,6 +56,7 @@ from ormar.fields import ( LargeBinary, ManyToMany, ManyToManyField, + SmallInteger, String, Text, Time, @@ -77,7 +77,7 @@ class UndefinedType: # pragma no cover Undefined = UndefinedType() -__version__ = "0.10.15" +__version__ = "0.10.16" __all__ = [ "Integer", "BigInteger", diff --git a/ormar/fields/__init__.py b/ormar/fields/__init__.py index 9745b1b..a879735 100644 --- a/ormar/fields/__init__.py +++ b/ormar/fields/__init__.py @@ -9,7 +9,6 @@ from ormar.fields.foreign_key import ForeignKey, ForeignKeyField, UniqueColumns from ormar.fields.many_to_many import ManyToMany, ManyToManyField from ormar.fields.model_fields import ( BigInteger, - SmallInteger, Boolean, Date, DateTime, @@ -18,6 +17,7 @@ from ormar.fields.model_fields import ( Integer, JSON, LargeBinary, + SmallInteger, String, Text, Time, diff --git a/ormar/fields/model_fields.py b/ormar/fields/model_fields.py index a53ac0e..f1230b5 100644 --- a/ormar/fields/model_fields.py +++ b/ormar/fields/model_fields.py @@ -142,10 +142,7 @@ class String(ModelFieldFactory, str): cls, *, max_length: int, - allow_blank: bool = True, - strip_whitespace: bool = False, min_length: int = None, - curtail_length: int = None, regex: str = None, **kwargs: Any ) -> BaseField: # type: ignore @@ -157,7 +154,6 @@ class String(ModelFieldFactory, str): if k not in ["cls", "__class__", "kwargs"] }, } - kwargs["allow_blank"] = kwargs.get("nullable", True) return super().__new__(cls, **kwargs) @classmethod @@ -244,7 +240,7 @@ class Text(ModelFieldFactory, str): _sample = "text" def __new__( # type: ignore - cls, *, allow_blank: bool = True, strip_whitespace: bool = False, **kwargs: Any + cls, **kwargs: Any ) -> BaseField: kwargs = { **kwargs, @@ -254,7 +250,6 @@ class Text(ModelFieldFactory, str): if k not in ["cls", "__class__", "kwargs"] }, } - kwargs["allow_blank"] = kwargs.get("nullable", True) return super().__new__(cls, **kwargs) @classmethod diff --git a/ormar/models/helpers/__init__.py b/ormar/models/helpers/__init__.py index 146aab4..0d9e6ce 100644 --- a/ormar/models/helpers/__init__.py +++ b/ormar/models/helpers/__init__.py @@ -8,6 +8,7 @@ from ormar.models.helpers.pydantic import ( get_potential_fields, get_pydantic_base_orm_config, get_pydantic_field, + merge_or_generate_pydantic_config, remove_excluded_parent_fields, ) from ormar.models.helpers.relations import ( @@ -33,6 +34,7 @@ __all__ = [ "get_pydantic_field", "get_potential_fields", "get_pydantic_base_orm_config", + "merge_or_generate_pydantic_config", "check_required_meta_parameters", "sqlalchemy_columns_from_model_fields", "populate_choices_validators", diff --git a/ormar/models/helpers/pydantic.py b/ormar/models/helpers/pydantic.py index 0830dab..87ed5d1 100644 --- a/ormar/models/helpers/pydantic.py +++ b/ormar/models/helpers/pydantic.py @@ -1,3 +1,4 @@ +import inspect from typing import Dict, Optional, TYPE_CHECKING, Tuple, Type import pydantic @@ -5,6 +6,7 @@ from pydantic.fields import ModelField from pydantic.utils import lenient_issubclass from ormar.fields import BaseField # noqa: I100, I202 +from ormar.exceptions import ModelDefinitionError if TYPE_CHECKING: # pragma no cover from ormar import Model @@ -88,6 +90,31 @@ def populate_pydantic_default_values(attrs: Dict) -> Tuple[Dict, Dict]: return attrs, model_fields +def merge_or_generate_pydantic_config(attrs: Dict, name: str) -> None: + """ + Checks if the user provided pydantic Config, + and if he did merges it with the default one. + + Updates the attrs in place with a new config. + + :rtype: None + """ + 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 + + def get_pydantic_base_orm_config() -> Type[pydantic.BaseConfig]: """ Returns empty pydantic Config with orm_mode set to True. diff --git a/ormar/models/metaclass.py b/ormar/models/metaclass.py index df15cac..5a40c0c 100644 --- a/ormar/models/metaclass.py +++ b/ormar/models/metaclass.py @@ -1,4 +1,3 @@ -import inspect from typing import ( Any, Dict, @@ -37,8 +36,8 @@ from ormar.models.helpers import ( expand_reverse_relationships, extract_annotations_and_default_vals, get_potential_fields, - get_pydantic_base_orm_config, get_pydantic_field, + merge_or_generate_pydantic_config, meta_field_not_set, populate_choices_validators, populate_default_options_values, @@ -553,21 +552,7 @@ class ModelMetaclass(pydantic.main.ModelMetaclass): :param attrs: class namespace :type attrs: Dict """ - 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 - + merge_or_generate_pydantic_config(attrs=attrs, name=name) attrs["__name__"] = name attrs, model_fields = extract_annotations_and_default_vals(attrs) for base in reversed(bases): diff --git a/tests/test_fastapi/test_schema_not_allowed_params.py b/tests/test_fastapi/test_schema_not_allowed_params.py new file mode 100644 index 0000000..bc24a08 --- /dev/null +++ b/tests/test_fastapi/test_schema_not_allowed_params.py @@ -0,0 +1,29 @@ +import databases +import sqlalchemy + +import ormar + +DATABASE_URL = "sqlite:///db.sqlite" +database = databases.Database(DATABASE_URL) +metadata = sqlalchemy.MetaData() + + +class BaseMeta(ormar.ModelMeta): + metadata = metadata + database = database + + +class Author(ormar.Model): + class Meta(BaseMeta): + tablename = "authors" + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100) + contents: str = ormar.Text() + + +def test_schema_not_allowed(): + schema = Author.schema() + for field_schema in schema.get("properties").values(): + for key in field_schema.keys(): + assert "_" not in key, f"Found illegal field in openapi schema: {key}" diff --git a/tests/test_model_definition/test_models.py b/tests/test_model_definition/test_models.py index aaea566..71d12aa 100644 --- a/tests/test_model_definition/test_models.py +++ b/tests/test_model_definition/test_models.py @@ -595,3 +595,9 @@ async def test_get_and_first(): user = await User2.objects.first() assert user.name == "Jane" + + +def test_constraints(): + with pytest.raises(pydantic.ValidationError) as e: + Product(name="T-Shirt", rating=50, in_stock=True) + assert "ensure this value is less than or equal to 5" in str(e.value)