-
Notifications
You must be signed in to change notification settings - Fork 14.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
chore(dao): Add generic type for better type checking #24465
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,8 +14,7 @@ | |
# KIND, either express or implied. See the License for the | ||
# specific language governing permissions and limitations | ||
# under the License. | ||
# pylint: disable=isinstance-second-argument-not-valid-type | ||
from typing import Any, Optional, Union | ||
from typing import Any, Generic, get_args, Optional, TypeVar, Union | ||
|
||
from flask_appbuilder.models.filters import BaseFilter | ||
from flask_appbuilder.models.sqla import Model | ||
|
@@ -31,8 +30,10 @@ | |
) | ||
from superset.extensions import db | ||
|
||
T = TypeVar("T", bound=Model) # pylint: disable=invalid-name | ||
|
||
class BaseDAO: | ||
|
||
class BaseDAO(Generic[T]): | ||
""" | ||
Base DAO, implement base CRUD sqlalchemy operations | ||
""" | ||
|
@@ -48,6 +49,11 @@ class BaseDAO: | |
""" | ||
id_column_name = "id" | ||
|
||
def __init_subclass__(cls) -> None: # pylint: disable=arguments-differ | ||
cls.model_cls = get_args( | ||
cls.__orig_bases__[0] # type: ignore # pylint: disable=no-member | ||
)[0] | ||
|
||
Comment on lines
+52
to
+56
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wow I didn't know Python has added core support for generic types! Adding a reference here for other reviewers: https://peps.python.org/pep-0560/ |
||
@classmethod | ||
def find_by_id( | ||
cls, | ||
|
@@ -78,7 +84,7 @@ def find_by_ids( | |
model_ids: Union[list[str], list[int]], | ||
session: Session = None, | ||
skip_base_filter: bool = False, | ||
) -> list[Model]: | ||
) -> list[T]: | ||
""" | ||
Find a List of models by a list of ids, if defined applies `base_filter` | ||
""" | ||
|
@@ -95,7 +101,7 @@ def find_by_ids( | |
return query.all() | ||
|
||
@classmethod | ||
def find_all(cls) -> list[Model]: | ||
def find_all(cls) -> list[T]: | ||
""" | ||
Get all that fit the `base_filter` | ||
""" | ||
|
@@ -108,7 +114,7 @@ def find_all(cls) -> list[Model]: | |
return query.all() | ||
|
||
@classmethod | ||
def find_one_or_none(cls, **filter_by: Any) -> Optional[Model]: | ||
def find_one_or_none(cls, **filter_by: Any) -> Optional[T]: | ||
""" | ||
Get the first that fit the `base_filter` | ||
""" | ||
|
@@ -121,7 +127,7 @@ def find_one_or_none(cls, **filter_by: Any) -> Optional[Model]: | |
return query.filter_by(**filter_by).one_or_none() | ||
|
||
@classmethod | ||
def create(cls, properties: dict[str, Any], commit: bool = True) -> Model: | ||
def create(cls, properties: dict[str, Any], commit: bool = True) -> T: | ||
""" | ||
Generic for creating models | ||
:raises: DAOCreateFailedError | ||
|
@@ -141,30 +147,23 @@ def create(cls, properties: dict[str, Any], commit: bool = True) -> Model: | |
return model | ||
|
||
@classmethod | ||
def save(cls, instance_model: Model, commit: bool = True) -> Model: | ||
def save(cls, instance_model: T, commit: bool = True) -> None: | ||
""" | ||
Generic for saving models | ||
:raises: DAOCreateFailedError | ||
""" | ||
if cls.model_cls is None: | ||
raise DAOConfigError() | ||
if not isinstance(instance_model, cls.model_cls): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is no longer needed. The Mypy checks ensure this can't happen. |
||
raise DAOCreateFailedError( | ||
"the instance model is not a type of the model class" | ||
) | ||
try: | ||
db.session.add(instance_model) | ||
if commit: | ||
db.session.commit() | ||
except SQLAlchemyError as ex: # pragma: no cover | ||
db.session.rollback() | ||
raise DAOCreateFailedError(exception=ex) from ex | ||
return instance_model | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Inconsistent return type detected with the |
||
|
||
@classmethod | ||
def update( | ||
cls, model: Model, properties: dict[str, Any], commit: bool = True | ||
) -> Model: | ||
def update(cls, model: T, properties: dict[str, Any], commit: bool = True) -> T: | ||
""" | ||
Generic update a model | ||
:raises: DAOCreateFailedError | ||
|
@@ -181,7 +180,7 @@ def update( | |
return model | ||
|
||
@classmethod | ||
def delete(cls, model: Model, commit: bool = True) -> Model: | ||
def delete(cls, model: T, commit: bool = True) -> T: | ||
""" | ||
Generic delete a model | ||
:raises: DAODeleteFailedError | ||
|
@@ -196,7 +195,7 @@ def delete(cls, model: Model, commit: bool = True) -> Model: | |
return model | ||
|
||
@classmethod | ||
def bulk_delete(cls, models: list[Model], commit: bool = True) -> None: | ||
def bulk_delete(cls, models: list[T], commit: bool = True) -> None: | ||
try: | ||
for model in models: | ||
cls.delete(model, False) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was needed because now Mypy correctly identified that on line #44
self._model
could have beenNone
which didn't adhere to the base class definition.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have a draft SIP recommending a slight refactor of the commands which address this problem.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@betodealmeida I’ve been thinking about the DAOs, commands and validation a bit recently and wonder whether we should adopt the “ask for forgiveness” try approach more often which relies less on Python validation and more on the database schema (foreign keys, uniqueness constraints, etc.) for validation, i.e., the DAO or command would fail if invalid.
The benefits are that we would reduce the Python code footprint and prevent possible race conditions.