pyracmon.testing.model API documentation

# 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

  - TestingModel

  ### 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. 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).

    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

  - TestingMixin
`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