Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != 'collerek/ormar'
strategy:
matrix:
python-version: [3.8, 3.9, "3.10", 3.11, 3.12]
python-version: [3.9, "3.10", 3.11, 3.12]
fail-fast: false
services:
mysql:
Expand Down
1 change: 0 additions & 1 deletion docs/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ engine: Optional[sqlalchemy.engine.Engine]
tablename: Optional[str]
order_by: Optional[List[str]]
abstract: bool
exclude_parent_fields: Optional[List[str]]
queryset_class: Type[QuerySet]
extra: Extra
constraints: Optional[List[ColumnCollectionConstraint]]
Expand Down
102 changes: 0 additions & 102 deletions docs/models/inheritance.md
Original file line number Diff line number Diff line change
Expand Up @@ -443,105 +443,3 @@ abstract parent model you may lose your data on through table if not careful.

That means that each time you define a Child model you need to either manually create
the table in the database, or run a migration (with alembic).

## exclude_parent_fields

Ormar allows you to skip certain fields in inherited model that are coming from a parent model.

!!!Note
Note that the same behaviour can be achieved by splitting the model into more abstract models and mixins - which is a preferred way in normal circumstances.

To skip certain fields from a child model, list all fields that you want to skip in `model.ormar_config.exclude_parent_fields` parameter like follows:

```python
base_ormar_config = OrmarConfig(
metadata=sa.MetaData(),
database=databases.Database(DATABASE_URL),
)


class AuditModel(ormar.Model):
ormar_config = base_ormar_config.copy(abstract=True)

created_by: str = ormar.String(max_length=100)
updated_by: str = ormar.String(max_length=100, default="Sam")


class DateFieldsModel(ormar.Model):
ormar_config = base_ormar_config.copy(abstract=True)

created_date: datetime.datetime = ormar.DateTime(
default=datetime.datetime.now, name="creation_date"
)
updated_date: datetime.datetime = ormar.DateTime(
default=datetime.datetime.now, name="modification_date"
)


class Category(DateFieldsModel, AuditModel):
ormar_config = base_ormar_config.copy(
tablename="categories",
# set fields that should be skipped
exclude_parent_fields=["updated_by", "updated_date"],
)

id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=50, unique=True, index=True)
code: int = ormar.Integer()

# Note that now the update fields in Category are gone in all places -> ormar fields, pydantic fields and sqlachemy table columns
# so full list of available fields in Category is: ["created_by", "created_date", "id", "name", "code"]
```

Note how you simply need to provide field names and it will exclude the parent field regardless of from which parent model the field is coming from.

!!!Note
Note that if you want to overwrite a field in child model you do not have to exclude it, simply overwrite the field declaration in child model with same field name.

!!!Warning
Note that this kind of behavior can confuse mypy and static type checkers, yet accessing the non existing fields will fail at runtime. That's why splitting the base classes is preferred.

The same effect can be achieved by splitting base classes like:

```python
base_ormar_config = OrmarConfig(
metadata=sa.MetaData(),
database=databases.Database(DATABASE_URL),
)


class AuditCreateModel(ormar.Model):
ormar_config = base_ormar_config.copy(abstract=True)

created_by: str = ormar.String(max_length=100)


class AuditUpdateModel(ormar.Model):
ormar_config = base_ormar_config.copy(abstract=True)

updated_by: str = ormar.String(max_length=100, default="Sam")

class CreateDateFieldsModel(ormar.Model):
ormar_config = base_ormar_config.copy(abstract=True)

created_date: datetime.datetime = ormar.DateTime(
default=datetime.datetime.now, name="creation_date"
)

class UpdateDateFieldsModel(ormar.Model):
ormar_config = base_ormar_config.copy(abstract=True)

updated_date: datetime.datetime = ormar.DateTime(
default=datetime.datetime.now, name="modification_date"
)


class Category(CreateDateFieldsModel, AuditCreateModel):
ormar_config = base_ormar_config.copy(tablename="categories")

id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=50, unique=True, index=True)
code: int = ormar.Integer()
```

That way you can inherit from both create and update classes if needed, and only one of them otherwise.
1 change: 0 additions & 1 deletion docs/releases.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,6 @@
tablename: Optional[str]
order_by: Optional[List[str]]
abstract: bool
exclude_parent_fields: Optional[List[str]]
queryset_class: Type[QuerySet]
extra: Extra
constraints: Optional[List[ColumnCollectionConstraint]]
Expand Down
35 changes: 30 additions & 5 deletions ormar/fields/base.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Type, Union
from typing import (
TYPE_CHECKING,
Any,
Dict,
List,
Literal,
Optional,
Type,
Union,
overload,
)

import sqlalchemy
from pydantic.fields import FieldInfo, _Unset
Expand Down Expand Up @@ -167,8 +177,23 @@ def default_value(self, use_server: bool = False) -> Optional[Dict]:
return dict(default=default)
return None

@overload
def get_default(
self,
*,
call_default_factory: Literal[True],
validated_data: Union[dict[str, Any], None] = None,
) -> Any: ...

@overload
def get_default(self, *, call_default_factory: Literal[False] = ...) -> Any: ...

def get_default(
self, use_server: bool = False, call_default_factory: bool = True
self,
*,
call_default_factory: bool = True,
validated_data: Union[dict[str, Any], None] = None,
use_server: bool = False,
) -> Any: # noqa CCR001
"""
Return default value for a field.
Expand All @@ -192,7 +217,7 @@ def get_default(
call_default_factory=call_default_factory,
)

def _get_default_server_value(self, use_server: bool) -> Any:
def _get_default_server_value(self, use_server: bool) -> Any: # pragma: no cover
"""
Return default value for a server side if use_server is True
"""
Expand Down Expand Up @@ -266,7 +291,7 @@ def get_column(self, name: str) -> sqlalchemy.Column:
:rtype: sqlalchemy.Column
"""
if self.encrypt_backend == EncryptBackends.NONE:
column = sqlalchemy.Column(
column: sqlalchemy.Column = sqlalchemy.Column(
self.db_alias or name,
self.column_type,
*self.construct_constraints(),
Expand Down Expand Up @@ -295,7 +320,7 @@ def _get_encrypted_column(self, name: str) -> sqlalchemy.Column:
raise ModelDefinitionError(
"Primary key field and relations fields" "cannot be encrypted!"
)
column = sqlalchemy.Column(
column: sqlalchemy.Column = sqlalchemy.Column(
self.db_alias or name,
EncryptedString(
_field_type=self,
Expand Down
2 changes: 1 addition & 1 deletion ormar/fields/model_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from typing import Literal # type: ignore
except ImportError: # pragma: no cover
from typing_extensions import Literal # type: ignore

try:
from typing import Self # type: ignore
except ImportError: # pragma: no cover
Expand Down
2 changes: 1 addition & 1 deletion ormar/fields/sqlalchemy_encrypted.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ def process_bind_param(self, value: Any, dialect: Dialect) -> Optional[str]:
return encrypted_value

def process_result_value(self, value: Any, dialect: Dialect) -> Any:
if value is None:
if value is None: # pragma: no cover
return value
self._refresh()
decrypted_value = self.backend.decrypt(value)
Expand Down
4 changes: 2 additions & 2 deletions ormar/fields/sqlalchemy_uuid.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,15 @@ def load_dialect_impl(self, dialect: Dialect) -> Any:
else dialect.type_descriptor(CHAR(32))
)

def process_bind_param(self, value: uuid.UUID, dialect: Dialect) -> Optional[str]:
def process_bind_param(self, value: Any, dialect: Dialect) -> Optional[str]:
if value is None:
return value
return str(value) if self.uuid_format == "string" else "%.32x" % value.int

def process_result_value(
self, value: Optional[str], dialect: Dialect
) -> Optional[uuid.UUID]:
if value is None:
if value is None: # pragma: no cover
return value
if not isinstance(value, uuid.UUID):
return uuid.UUID(value)
Expand Down
1 change: 0 additions & 1 deletion ormar/models/helpers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
get_potential_fields,
get_pydantic_base_orm_config,
merge_or_generate_pydantic_config,
remove_excluded_parent_fields,
)
from ormar.models.helpers.relations import (
alias_manager,
Expand Down
21 changes: 3 additions & 18 deletions ormar/models/helpers/pydantic.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ def create_pydantic_field(
model_field.through.model_fields[field_name] = FieldInfo.from_annotated_attribute(
annotation=Optional[model], default=None # type: ignore
)
model_field.through.model_rebuild(force=True)
model_field.through.model_rebuild(
force=True, _types_namespace={model_field.owner.__name__: model_field.owner}
)


def populate_pydantic_default_values(attrs: Dict) -> Tuple[Dict, Dict]:
Expand Down Expand Up @@ -123,20 +125,3 @@ def get_potential_fields(attrs: Union[Dict, MappingProxyType]) -> Dict:
or isinstance(v, BaseField)
)
}


def remove_excluded_parent_fields(model: Type["Model"]) -> None:
"""
Removes pydantic fields that should be excluded from parent models

:param model:
:type model: Type["Model"]
"""
excludes = {*model.ormar_config.exclude_parent_fields} - {
*model.ormar_config.model_fields.keys()
}
if excludes:
model.model_fields = {
k: v for k, v in model.model_fields.items() if k not in excludes
}
model.model_rebuild(force=True)
14 changes: 12 additions & 2 deletions ormar/models/helpers/relations.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import inspect
import warnings
from typing import TYPE_CHECKING, Any, List, Optional, Type, Union, cast
from typing import TYPE_CHECKING, Any, ForwardRef, List, Optional, Type, Union, cast

from pydantic import BaseModel, create_model, field_serializer
from pydantic._internal._decorators import DecoratorInfos
Expand Down Expand Up @@ -156,7 +156,17 @@ def register_reverse_model_fields(model_field: "ForeignKeyField") -> None:
add_field_serializer_for_reverse_relations(
to_model=model_field.to, related_name=related_name
)
model_field.to.model_rebuild(force=True)
model_field.to.model_rebuild(
force=True,
_types_namespace={
**{model_field.owner.__name__: model_field.owner},
**{
field.to.__name__: field.to
for field in related_model_fields.values()
if field.is_relation and field.to.__class__ != ForwardRef
},
},
)
setattr(model_field.to, related_name, RelationDescriptor(name=related_name))


Expand Down
2 changes: 1 addition & 1 deletion ormar/models/helpers/sqlalchemy.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ def create_and_append_m2m_fk(
),
)
model_field.through.ormar_config.columns.append(column)
model_field.through.ormar_config.table.append_column(column)
model_field.through.ormar_config.table.append_column(column, replace_existing=True)


def check_pk_column_validity(
Expand Down
16 changes: 5 additions & 11 deletions ormar/models/metaclass.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@
populate_config_tablename_columns_and_pk,
populate_default_options_values,
register_relation_in_alias_manager,
remove_excluded_parent_fields,
sqlalchemy_columns_from_model_fields,
)
from ormar.models.ormar_config import OrmarConfig
Expand Down Expand Up @@ -195,7 +194,9 @@ def get_constraint_copy(
}
checks = (key if isinstance(constraint, key) else None for key in constraints)
target_class = next((target for target in checks if target is not None), None)
constructor: Optional[Callable] = constraints.get(target_class)
constructor: Optional[Callable] = (
constraints.get(target_class) if target_class else None
)
if not constructor:
raise ValueError(f"{constraint} must be a ColumnCollectionMixin!")

Expand Down Expand Up @@ -293,13 +294,12 @@ def copy_and_replace_m2m_through_model( # noqa: CFQ002
metadata=through_class.ormar_config.metadata,
database=through_class.ormar_config.database,
abstract=through_class.ormar_config.abstract,
exclude_parent_fields=through_class.ormar_config.exclude_parent_fields,
queryset_class=through_class.ormar_config.queryset_class,
extra=through_class.ormar_config.extra,
constraints=through_class.ormar_config.constraints,
order_by=through_class.ormar_config.orders_by,
)
new_config.table = through_class.ormar_config.pkname
new_config.table = through_class.ormar_config.pkname # type: ignore
new_config.pkname = through_class.ormar_config.pkname
new_config.alias_manager = through_class.ormar_config.alias_manager
new_config.signals = through_class.ormar_config.signals
Expand All @@ -316,7 +316,7 @@ def copy_and_replace_m2m_through_model( # noqa: CFQ002
# they will be populated later in expanding reverse relation
# if hasattr(new_config, "table"):
new_config.tablename += "_" + ormar_config.tablename
new_config.table = None
new_config.table = None # type: ignore
new_config.model_fields = {
name: field
for name, field in new_config.model_fields.items()
Expand Down Expand Up @@ -385,11 +385,6 @@ def copy_data_from_parent_model( # noqa: CCR001
else attrs.get("__name__", "").lower() + "s"
)
for field_name, field in base_class.ormar_config.model_fields.items():
if (
hasattr(ormar_config, "exclude_parent_fields")
and field_name in ormar_config.exclude_parent_fields
):
continue
if field.is_multi:
field = cast(ManyToManyField, field)
copy_and_replace_m2m_through_model(
Expand Down Expand Up @@ -682,7 +677,6 @@ def __new__( # type: ignore # noqa: CCR001
new_model.model_rebuild(force=True)

new_model.pk = PkDescriptor(name=new_model.ormar_config.pkname)
remove_excluded_parent_fields(new_model)

return new_model

Expand Down
4 changes: 1 addition & 3 deletions ormar/models/mixins/pydantic_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
import pydantic
from pydantic import BaseModel
from pydantic._internal._decorators import DecoratorInfos
from pydantic.fields import FieldInfo

from ormar.fields import BaseField, ForeignKeyField, ManyToManyField
from ormar.models.mixins.relation_mixin import RelationMixin # noqa: I100, I202
Expand All @@ -30,7 +29,6 @@ class PydanticMixin(RelationMixin):

if TYPE_CHECKING: # pragma: no cover
__pydantic_decorators__: DecoratorInfos
model_fields: Dict[str, FieldInfo]
_skip_ellipsis: Callable
_get_not_excluded_fields: Callable

Expand Down Expand Up @@ -126,7 +124,7 @@ def _determine_pydantic_field_type(
relation_map=relation_map,
)
elif not field.is_relation:
defaults[name] = cls.model_fields[name].default
defaults[name] = cls.model_fields[name].default # type: ignore
target = field.__type__
if target is not None and field.nullable:
target = Optional[target]
Expand Down
Loading
Loading