Module pyracmon.testing.model
Expand source code
from datetime import date, datetime, time, timedelta
from decimal import Decimal
from enum import Enum
from uuid import UUID, uuid1, uuid3
from typing import Optional, Union, TypeVar, Any, overload, TYPE_CHECKING
from typing_extensions import Self
from pyracmon.config import PyracmonConfiguration
from pyracmon.connection import Connection
from pyracmon.mixin import CRUDMixin
from pyracmon.model import Model
from pyracmon.graph.typing import issubgeneric
from pyracmon.dialect.shared import MultiInsertMixin, TruncateMixin
from .util import default_test_config, Matcher
if TYPE_CHECKING:
class TestingModel(MultiInsertMixin, CRUDMixin):
pass
else:
class TestingModel():
pass
M = TypeVar('M', bound=TestingModel)
class TestingState:
indexes = {}
@classmethod
def reset(cls, model=None):
cls.indexes.clear()
@classmethod
def inc(cls, model, count):
v = cls.indexes.setdefault(model, 1)
cls.indexes[model] = v + count
return v
@classmethod
def set_index(cls, model, index):
cls.indexes[model] = index
class TestingMixin(TestingModel):
"""
Mixin class for model types providing methods designed for testing.
"""
@classmethod
def by(cls, index: int) -> Self:
"""
Set current fixture index.
Args:
index: Fixture index.
Returns:
This type.
"""
TestingState.set_index(cls, index)
return cls # type: ignore
@overload
@classmethod
def fixture(
cls,
db: Optional[Connection],
variable: None = None,
index: Optional[int] = None,
cfg: Optional[PyracmonConfiguration] = None,
) -> list[Self]: ...
@overload
@classmethod
def fixture(
cls,
db: Optional[Connection],
variable: int,
index: Optional[int] = None,
cfg: Optional[PyracmonConfiguration] = None,
) -> list[Self]: ...
@overload
@classmethod
def fixture(
cls,
db: Optional[Connection],
variable: Self,
index: Optional[int] = None,
cfg: Optional[PyracmonConfiguration] = None,
) -> Self: ...
@overload
@classmethod
def fixture(
cls,
db: Optional[Connection],
variable: dict[str, Any],
index: Optional[int] = None,
cfg: Optional[PyracmonConfiguration] = None,
) -> Self: ...
@overload
@classmethod
def fixture(
cls,
db: Optional[Connection],
variable: list[Self],
index: Optional[int] = None,
cfg: Optional[PyracmonConfiguration] = None,
) -> list[Self]: ...
@overload
@classmethod
def fixture(
cls,
db: Optional[Connection],
variable: list[dict[str, Any]],
index: Optional[int] = None,
cfg: Optional[PyracmonConfiguration] = None,
) -> list[Self]: ...
@classmethod
def fixture(
cls,
db: Optional[Connection],
variable: Optional[Union[int, dict[str, Any], Self, list[dict[str, Any]], list[Self]]] = None,
index: Optional[int] = None,
cfg: Optional[PyracmonConfiguration] = None,
) -> Union[Self, list[Self]]:
"""
Inserts record(s) with auto-generated column values.
Args:
db: DB connection. If a value evaluated to be `False` in boolean context, generated model is not inserted and just returned.
variable: When `int`, inserts records as many as the number. All of their column values are generated.
When `dict`, model object or a list of them, inserts record(s) represented by them. Unspecified column values are generated.
index: Use this to specify index used to generate column values explicitly. If set, indexing state is not updated.
cfg: Configuration used to control the generation of fixuture values.
This argument is prepared only for internal use and can be changed or removed in future version.
Returns:
Inserted model(s).
"""
if variable is None or isinstance(variable, int):
num = variable or 1
index = TestingState.inc(cls, num) if index is None else index
models = [_generate_model(cls, index+i, None, cfg) for i in range(num)]
if db:
cls.inserts(db, models)
return models
elif isinstance(variable, (cls, dict)):
num = 1
index = TestingState.inc(cls, num) if index is None else index
model = _generate_model(cls, index, variable, cfg)
if db:
cls.insert(db, model)
return model
elif isinstance(variable, list):
num = len(variable)
index = TestingState.inc(cls, num) if index is None else index
models = [_generate_model(cls, index+i, v, cfg) for i, v in enumerate(variable)]
if db:
cls.inserts(db, models)
return models
else:
raise ValueError(f"Second argument of fixture() must be an int, dict, model or list of dict or model but {type(variable)} is passed.")
def match(self, **expected: Union[Matcher, Any]) -> bool:
"""
Tests columns values matches to expected values.
.. warning::
This method will be replaced in different implementation.
Args:
expected: Expected values.
Returns
Matches or not.
"""
for k, v in expected.items():
actual = getattr(self, k)
if isinstance(v, Matcher):
if not v.match(actual):
return False
else:
if v != actual:
return False
return True
def truncate(db: Connection, *models: type[TruncateMixin]):
"""
Truncate tables in order.
Args:
db: DB connection.
tables: Models of tables to truncate.
"""
if len(models) == 0:
raise ValueError(f"No tables are specified. Did you forget to pass DB connection at the first argument?")
for m in models:
m.truncate(db)
def _generate_model(model: type[M], index, model_or_dict, cfg) -> M:
values = {}
if isinstance(model_or_dict, TestingMixin):
values = {c.name:v for c, v in model_or_dict}
elif isinstance(model_or_dict, dict):
values = dict(**model_or_dict)
values.update(**{c.name:_generate_value(model.table, c, index, cfg) for c in model.columns if c.name not in values and not c.pk})
return model(**values)
fixed_uuid=uuid1(0, 0)
def _generate_value(table, column, index, cfg):
"""
Generates a value for the column on an index.
Args:
table: Table.
column: Column.
index: Index
Returns:
Generated value.
"""
cfg = cfg or default_test_config()
mapping = cfg.fixture_mapping
if mapping:
value = mapping(table, column, index)
if value is not None:
return value
tz_aware = cfg.fixture_tz_aware
if column.nullable and cfg.fixture_ignore_nullable:
return None
elif column.fk and cfg.fixture_ignore_fk:
return None
elif column.ptype is bool:
return True
elif column.ptype is float:
return float(f"{index}.{index+1}")
elif column.ptype is int:
return index
elif column.ptype is Decimal:
return Decimal(index)
elif column.ptype is str:
return f"{column.name}-{index}"
elif column.ptype is bytes:
return f"{column.name}-{index}".encode()
elif column.ptype is date:
return date.today()
elif column.ptype is datetime:
return datetime.now().astimezone() if tz_aware else datetime.now()
elif column.ptype is time:
return datetime.now().astimezone().time() if tz_aware else datetime.now().time()
elif column.ptype is timedelta:
return timedelta(days=index+1)
elif column.ptype is UUID:
return str(uuid3(fixed_uuid, f"{table.name}-{column.name}-{index}"))
elif issubgeneric(column.ptype, list):
return []
elif isinstance(column.ptype, type) and issubclass(column.ptype, Enum):
return next(iter(column.ptype), None)
else:
return None
Functions
def truncate(db: Connection, *models: type[TruncateMixin])
-
Truncate tables in order.
Args
db
- DB connection.
tables
- Models of tables to truncate.
Expand source code
def truncate(db: Connection, *models: type[TruncateMixin]): """ Truncate tables in order. Args: db: DB connection. tables: Models of tables to truncate. """ if len(models) == 0: raise ValueError(f"No tables are specified. Did you forget to pass DB connection at the first argument?") for m in models: m.truncate(db)
Classes
class TestingMixin
-
Mixin class for model types providing methods designed for testing.
Expand source code
class TestingMixin(TestingModel): """ Mixin class for model types providing methods designed for testing. """ @classmethod def by(cls, index: int) -> Self: """ Set current fixture index. Args: index: Fixture index. Returns: This type. """ TestingState.set_index(cls, index) return cls # type: ignore @overload @classmethod def fixture( cls, db: Optional[Connection], variable: None = None, index: Optional[int] = None, cfg: Optional[PyracmonConfiguration] = None, ) -> list[Self]: ... @overload @classmethod def fixture( cls, db: Optional[Connection], variable: int, index: Optional[int] = None, cfg: Optional[PyracmonConfiguration] = None, ) -> list[Self]: ... @overload @classmethod def fixture( cls, db: Optional[Connection], variable: Self, index: Optional[int] = None, cfg: Optional[PyracmonConfiguration] = None, ) -> Self: ... @overload @classmethod def fixture( cls, db: Optional[Connection], variable: dict[str, Any], index: Optional[int] = None, cfg: Optional[PyracmonConfiguration] = None, ) -> Self: ... @overload @classmethod def fixture( cls, db: Optional[Connection], variable: list[Self], index: Optional[int] = None, cfg: Optional[PyracmonConfiguration] = None, ) -> list[Self]: ... @overload @classmethod def fixture( cls, db: Optional[Connection], variable: list[dict[str, Any]], index: Optional[int] = None, cfg: Optional[PyracmonConfiguration] = None, ) -> list[Self]: ... @classmethod def fixture( cls, db: Optional[Connection], variable: Optional[Union[int, dict[str, Any], Self, list[dict[str, Any]], list[Self]]] = None, index: Optional[int] = None, cfg: Optional[PyracmonConfiguration] = None, ) -> Union[Self, list[Self]]: """ Inserts record(s) with auto-generated column values. Args: db: DB connection. If a value evaluated to be `False` in boolean context, generated model is not inserted and just returned. variable: When `int`, inserts records as many as the number. All of their column values are generated. When `dict`, model object or a list of them, inserts record(s) represented by them. Unspecified column values are generated. index: Use this to specify index used to generate column values explicitly. If set, indexing state is not updated. cfg: Configuration used to control the generation of fixuture values. This argument is prepared only for internal use and can be changed or removed in future version. Returns: Inserted model(s). """ if variable is None or isinstance(variable, int): num = variable or 1 index = TestingState.inc(cls, num) if index is None else index models = [_generate_model(cls, index+i, None, cfg) for i in range(num)] if db: cls.inserts(db, models) return models elif isinstance(variable, (cls, dict)): num = 1 index = TestingState.inc(cls, num) if index is None else index model = _generate_model(cls, index, variable, cfg) if db: cls.insert(db, model) return model elif isinstance(variable, list): num = len(variable) index = TestingState.inc(cls, num) if index is None else index models = [_generate_model(cls, index+i, v, cfg) for i, v in enumerate(variable)] if db: cls.inserts(db, models) return models else: raise ValueError(f"Second argument of fixture() must be an int, dict, model or list of dict or model but {type(variable)} is passed.") def match(self, **expected: Union[Matcher, Any]) -> bool: """ Tests columns values matches to expected values. .. warning:: This method will be replaced in different implementation. Args: expected: Expected values. Returns Matches or not. """ for k, v in expected.items(): actual = getattr(self, k) if isinstance(v, Matcher): if not v.match(actual): return False else: if v != actual: return False return True
Ancestors
Static methods
def by(index: int) ‑> typing_extensions.Self
-
Set current fixture index.
Args
index
- Fixture index.
Returns
This type.
Expand source code
@classmethod def by(cls, index: int) -> Self: """ Set current fixture index. Args: index: Fixture index. Returns: This type. """ TestingState.set_index(cls, index) return cls # type: ignore
def fixture(db: Optional[Connection], variable: Union[int, dict[str, Any], typing_extensions.Self, list[dict[str, Any]], list[typing_extensions.Self], ForwardRef(None)] = None, index: Optional[int] = None, cfg: Optional[PyracmonConfiguration] = None) ‑> Union[typing_extensions.Self, list[typing_extensions.Self]]
-
Inserts record(s) with auto-generated column values.
Args
db
- DB connection. If a value evaluated to be
False
in boolean context, generated model is not inserted and just returned. variable
- When
int
, inserts records as many as the number. All of their column values are generated. Whendict
, model object or a list of them, inserts record(s) represented by them. Unspecified column values are generated. index
- Use this to specify index used to generate column values explicitly. If set, indexing state is not updated.
cfg
- Configuration used to control the generation of fixuture values. This argument is prepared only for internal use and can be changed or removed in future version.
Returns
Inserted model(s).
Expand source code
@classmethod def fixture( cls, db: Optional[Connection], variable: Optional[Union[int, dict[str, Any], Self, list[dict[str, Any]], list[Self]]] = None, index: Optional[int] = None, cfg: Optional[PyracmonConfiguration] = None, ) -> Union[Self, list[Self]]: """ Inserts record(s) with auto-generated column values. Args: db: DB connection. If a value evaluated to be `False` in boolean context, generated model is not inserted and just returned. variable: When `int`, inserts records as many as the number. All of their column values are generated. When `dict`, model object or a list of them, inserts record(s) represented by them. Unspecified column values are generated. index: Use this to specify index used to generate column values explicitly. If set, indexing state is not updated. cfg: Configuration used to control the generation of fixuture values. This argument is prepared only for internal use and can be changed or removed in future version. Returns: Inserted model(s). """ if variable is None or isinstance(variable, int): num = variable or 1 index = TestingState.inc(cls, num) if index is None else index models = [_generate_model(cls, index+i, None, cfg) for i in range(num)] if db: cls.inserts(db, models) return models elif isinstance(variable, (cls, dict)): num = 1 index = TestingState.inc(cls, num) if index is None else index model = _generate_model(cls, index, variable, cfg) if db: cls.insert(db, model) return model elif isinstance(variable, list): num = len(variable) index = TestingState.inc(cls, num) if index is None else index models = [_generate_model(cls, index+i, v, cfg) for i, v in enumerate(variable)] if db: cls.inserts(db, models) return models else: raise ValueError(f"Second argument of fixture() must be an int, dict, model or list of dict or model but {type(variable)} is passed.")
Methods
def match(self, **expected: Union[Matcher, Any]) ‑> bool
-
Tests columns values matches to expected values.
Warning
This method will be replaced in different implementation.
Args
expected
- Expected values.
Returns Matches or not.
Expand source code
def match(self, **expected: Union[Matcher, Any]) -> bool: """ Tests columns values matches to expected values. .. warning:: This method will be replaced in different implementation. Args: expected: Expected values. Returns Matches or not. """ for k, v in expected.items(): actual = getattr(self, k) if isinstance(v, Matcher): if not v.match(actual): return False else: if v != actual: return False return True
class TestingModel
-
Expand source code
class TestingModel(MultiInsertMixin, CRUDMixin): pass
Subclasses
class TestingState
-
Expand source code
class TestingState: indexes = {} @classmethod def reset(cls, model=None): cls.indexes.clear() @classmethod def inc(cls, model, count): v = cls.indexes.setdefault(model, 1) cls.indexes[model] = v + count return v @classmethod def set_index(cls, model, index): cls.indexes[model] = index
Class variables
var indexes
Static methods
def inc(model, count)
-
Expand source code
@classmethod def inc(cls, model, count): v = cls.indexes.setdefault(model, 1) cls.indexes[model] = v + count return v
def reset(model=None)
-
Expand source code
@classmethod def reset(cls, model=None): cls.indexes.clear()
def set_index(model, index)
-
Expand source code
@classmethod def set_index(cls, model, index): cls.indexes[model] = index