diff --git a/pyairtable/orm/model.py b/pyairtable/orm/model.py index 86c8fcb4..65bbb905 100644 --- a/pyairtable/orm/model.py +++ b/pyairtable/orm/model.py @@ -12,6 +12,7 @@ Optional, Type, Union, + cast, ) from typing_extensions import Self as SelfType @@ -446,13 +447,16 @@ class _Meta: @property def _config(self) -> Mapping[str, Any]: - if not (model_meta := getattr(self.model, "Meta", None)): + if not (meta := getattr(self.model, "Meta", None)): raise AttributeError(f"{self.model.__name__}.Meta must be defined") - if isinstance(model_meta, dict): - return model_meta - if isinstance(model_meta, type): - return model_meta.__dict__ - raise TypeError(type(model_meta)) + if isinstance(meta, dict): + return meta + try: + return cast(Mapping[str, Any], meta.__dict__) + except AttributeError: + raise TypeError( + f"{self.model.__name__}.Meta must be a dict or class; got {type(meta)}" + ) def get( self, diff --git a/pyairtable/testing.py b/pyairtable/testing.py index a594bf24..359ad33b 100644 --- a/pyairtable/testing.py +++ b/pyairtable/testing.py @@ -5,8 +5,10 @@ import datetime import random import string -from typing import Any, Optional +from typing import Any, Optional, Union +from pyairtable.api import retrying +from pyairtable.api.api import TimeoutTuple from pyairtable.api.types import AttachmentDict, CollaboratorDict, Fields, RecordDict @@ -34,6 +36,8 @@ def fake_meta( base_id: str = "appFakeTestingApp", table_name: str = "tblFakeTestingTbl", api_key: str = "patFakePersonalAccessToken", + timeout: Optional[TimeoutTuple] = None, + retry: Optional[Union[bool, retrying.Retry]] = None, use_field_ids: bool = False, ) -> type: """ @@ -43,6 +47,8 @@ def fake_meta( "base_id": base_id, "table_name": table_name, "api_key": api_key, + "timeout": timeout, + "retry": retry, "use_field_ids": use_field_ids, } return type("Meta", (), attrs) diff --git a/tests/test_orm_model.py b/tests/test_orm_model.py index 4d1c287e..df8a7e8c 100644 --- a/tests/test_orm_model.py +++ b/tests/test_orm_model.py @@ -68,6 +68,47 @@ class Address(Model): m.assert_not_called() +def test_model_meta_dict(): + """ + Test that we can define Meta as a dict rather than a class. + """ + + class Address(Model): + Meta = { + "api_key": "fake_api_key", + "base_id": "fake_base_id", + "table_name": "fake_table_name", + "timeout": (1, 1), + "retry": False, + } + + assert Address.meta.api.api_key == "fake_api_key" + + +@pytest.mark.parametrize("invalid_meta", ([1, 2, 3], "invalid", True)) +def test_model_invalid_meta(invalid_meta): + """ + Test that model creation raises a TypeError if Meta is an invalid type. + """ + with pytest.raises(TypeError): + + class Address(Model): + Meta = invalid_meta + + +@pytest.mark.parametrize("meta_kwargs", [{"timeout": 1}, {"retry": "sure"}]) +def test_model_meta_checks_types(meta_kwargs): + """ + Test that accessing meta raises a TypeError if a value is an invalid type. + """ + + class Address(Model): + Meta = fake_meta(**meta_kwargs) + + with pytest.raises(TypeError): + Address.meta.api + + @pytest.mark.parametrize("name", ("exists", "id")) def test_model_overlapping(name): """