diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index ddc538e..0870d7f 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -7,8 +7,8 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2.5.0 - - uses: actions/setup-python@v2.3.3 + - uses: actions/checkout@v4.1.0 + - uses: actions/setup-python@v4.7.0 with: python-version: "3.10" architecture: "x64" diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 0447a75..a220a1c 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -4,11 +4,8 @@ jobs: run: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2.5.0 - - uses: actions/setup-python@v2.3.3 - with: - python-version: "3.10" - architecture: "x64" + - uses: actions/checkout@v4.1.0 + - uses: actions/setup-python@v4.7.0 - name: Install dependencies for pre-commit run: | python -m pip install --upgrade pip diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml index 236863d..4da96b0 100644 --- a/.github/workflows/pypi.yml +++ b/.github/workflows/pypi.yml @@ -7,8 +7,8 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2.5.0 - - uses: actions/setup-python@v2.3.3 + - uses: actions/checkout@v4.1.0 + - uses: actions/setup-python@v4.7.0 with: python-version: "3.10" architecture: "x64" @@ -28,8 +28,8 @@ jobs: publish: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2.5.0 - - uses: actions/setup-python@v2.3.3 + - uses: actions/checkout@v4.1.0 + - uses: actions/setup-python@v4.7.0 with: python-version: "3.10" architecture: "x64" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 349cc05..e3720c3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,10 +8,10 @@ jobs: os: [ubuntu-latest, macos-latest] python-version: ["3.8", "3.9", "3.10"] steps: - - uses: actions/checkout@v2.5.0 + - uses: actions/checkout@v4.1.0 - name: Set up Python${{ matrix.python-version }} - uses: actions/setup-python@v2.3.3 + uses: actions/setup-python@v4.7.0 with: python-version: ${{ matrix.python-version }} architecture: x64 @@ -26,10 +26,10 @@ jobs: coverage: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2.5.0 + - uses: actions/checkout@v4.1.0 - name: Set up Python3.10 - uses: actions/setup-python@v2.3.3 + uses: actions/setup-python@v4.7.0 with: python-version: "3.10" architecture: x64 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b2adcb1..f06c671 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,9 @@ # Install the git hook scripts, run `$ pre-commit install` to set up the git hook scripts # Run against all the files, `$ pre-commit run --all-files` # Updating hooks automatically: `$ pre-commit autoupdate` - +default_language_version: + python: python3.8 +fail_fast: true repos: - repo: https://github.com/PyCQA/isort rev: 5.12.0 @@ -9,7 +11,7 @@ repos: - id: isort - repo: https://github.com/hakancelikdev/unimport - rev: 0.16.0 + rev: 1.0.0 hooks: - id: unimport args: @@ -26,13 +28,13 @@ repos: - --refactor - repo: https://github.com/myint/docformatter - rev: v1.6.5 + rev: v1.7.5 hooks: - id: docformatter args: [--in-place] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.3.0 + rev: v1.5.1 hooks: - id: mypy args: [--no-strict-optional, --ignore-missing-imports, --show-error-codes] @@ -40,7 +42,7 @@ repos: exclude: "tests/" - repo: https://github.com/pycqa/flake8 - rev: 6.0.0 + rev: 6.1.0 hooks: - id: flake8 args: diff --git a/src/pydbm/__init__.py b/src/pydbm/__init__.py index f3b171a..8a16bba 100644 --- a/src/pydbm/__init__.py +++ b/src/pydbm/__init__.py @@ -3,6 +3,9 @@ DbmModel, Field, Undefined, + validate_array_float, + validate_array_int, + validate_array_str, validate_bool, validate_bytes, validate_date, @@ -14,7 +17,7 @@ validate_none, validate_str, ) -from pydbm.typing_extra import NormalizationT, ValidatorT +from pydbm.typing_extra import NormalizationT, ValidatorT, array __all__ = ( "PydbmBaseException", @@ -33,6 +36,10 @@ "validate_min_value", "validate_none", "validate_str", + "validate_array_str", + "validate_array_int", + "validate_array_float", "NormalizationT", "ValidatorT", + "array", ) diff --git a/src/pydbm/database/data_types/base.py b/src/pydbm/database/data_types/base.py index bbbaa13..29e6d75 100644 --- a/src/pydbm/database/data_types/base.py +++ b/src/pydbm/database/data_types/base.py @@ -12,7 +12,7 @@ class BaseDataType(abc.ABC): - data_types: dict[SupportedClassT, typing.Type[BaseDataType]] = {} + data_types: typing.ClassVar[dict[SupportedClassT, typing.Type[BaseDataType]]] = {} @staticmethod @abc.abstractmethod @@ -33,4 +33,4 @@ def get_data_type(cls, item: SupportedClassT) -> typing.Type[BaseDataType]: try: return cls.data_types[item] except KeyError: - raise TypeError(f"Type {item} is not supported yet!") + raise TypeError(f"Type {item!r} is not supported yet!") diff --git a/src/pydbm/database/data_types/types.py b/src/pydbm/database/data_types/types.py index 88533d2..98a44ac 100644 --- a/src/pydbm/database/data_types/types.py +++ b/src/pydbm/database/data_types/types.py @@ -1,10 +1,15 @@ from __future__ import annotations +import ast import datetime from pydbm.database.data_types.base import BaseDataType +from pydbm.typing_extra import array __all__ = ( + "ArrayFloatDataType", + "ArrayIntDataType", + "ArrayTextDataType", "BoolDataType", "BytesDataType", "DateDataType", @@ -98,4 +103,26 @@ def get(cls, value: str) -> str: @staticmethod def set(value: str) -> str: - return str(value) + return value + + +class _ArrayDataType(BaseDataType, data_type=array): + @classmethod + def get(cls, value: str) -> array: + return array(*ast.literal_eval(value)) + + @staticmethod + def set(value: array) -> str: + return str(value.tolist()) + + +class ArrayIntDataType(_ArrayDataType, data_type=array[int]): + pass + + +class ArrayFloatDataType(_ArrayDataType, data_type=array[float]): + pass + + +class ArrayTextDataType(_ArrayDataType, data_type=array[str]): + pass diff --git a/src/pydbm/database/manager.py b/src/pydbm/database/manager.py index 92ca7e5..645dc62 100644 --- a/src/pydbm/database/manager.py +++ b/src/pydbm/database/manager.py @@ -10,6 +10,7 @@ from pydbm.database.data_types import BaseDataType from pydbm.inspect_extra import get_obj_annotations from pydbm.models.fields import AutoField +from pydbm.typing_extra import array if typing.TYPE_CHECKING: from pydbm import DbmModel @@ -36,6 +37,9 @@ int: "int", None: "null", str: "str", + array[int]: "array.int", + array[float]: "array.float", + array[str]: "array.str", } DATABASE_EXTENSION: str = "pydbm" DATABASE_PATH: Path = Path("pydbm") # TODO: take from env diff --git a/src/pydbm/models/__init__.py b/src/pydbm/models/__init__.py index 69bb2dd..dcb648d 100644 --- a/src/pydbm/models/__init__.py +++ b/src/pydbm/models/__init__.py @@ -1,6 +1,9 @@ from pydbm.models.base import DbmModel from pydbm.models.fields import Field, Undefined from pydbm.models.validators import ( + validate_array_float, + validate_array_int, + validate_array_str, validate_bool, validate_bytes, validate_date, @@ -27,4 +30,7 @@ "validate_min_value", "validate_none", "validate_str", + "validate_array_float", + "validate_array_int", + "validate_array_str", ) diff --git a/src/pydbm/models/fields/base.py b/src/pydbm/models/fields/base.py index ca1042b..37e1354 100644 --- a/src/pydbm/models/fields/base.py +++ b/src/pydbm/models/fields/base.py @@ -6,7 +6,7 @@ from pydbm import contstant as C from pydbm.exceptions import ValidationError from pydbm.logging import logger -from pydbm.models.validators import validate_max_value, validate_min_value, validator_mapping +from pydbm.models.validators import VALIDATOR_MAPPING, validate_max_value, validate_min_value if typing.TYPE_CHECKING: from pydbm.models.base import DbmModel @@ -64,6 +64,27 @@ def __init__( self._is_call_run = False + def get_default_value(self) -> typing.Any: + if self.default is not Undefined: + return self.default + + if self.default_factory is not Undefined: + return self.default_factory() + + def through_normalizers(self, value: typing.Any) -> typing.Any: + for normalizer in self.normalizers: + value = normalizer(value) + + return value + + def through_validators(self, value: typing.Any) -> None: + for validator in self.validators: + try: + if validator(value) is False: + raise ValueError("Value is not valid") + except ValueError as exc: + raise ValidationError(self.field_name, value, exc) + def __set_name__(self, instance: Meta, name: str) -> None: self.public_name = name self.private_name = "_" + name @@ -87,11 +108,12 @@ def __set__(self, instance: DbmModel, value: typing.Any) -> None: f"They are ignored for {value.__class__.__name__} type." ) - eligible_value = self.before_set(value) + value = self.through_normalizers(value) + self.through_validators(value) - setattr(instance, self.private_name, eligible_value) + setattr(instance, self.private_name, value) if self.field_name != C.PRIMARY_KEY: - instance.fields[self.field_name] = eligible_value + instance.fields[self.field_name] = value def __call__(self: Self, field_name: str, field_type: SupportedClassT, *args, **kwargs) -> Self: # type: ignore[valid-type] # noqa: E501 self._is_call_run = True @@ -102,7 +124,7 @@ def __call__(self: Self, field_name: str, field_type: SupportedClassT, *args, ** self.public_name = field_name self.private_name = "_" + field_name - self.validators.append(validator_mapping[field_type]) + self.validators.append(VALIDATOR_MAPPING[field_type]) return self def __repr__(self) -> str: @@ -117,29 +139,9 @@ def __repr__(self) -> str: f")" ) - def before_set(self, value: typing.Any) -> typing.Any: - for normalizer in self.normalizers: - value = normalizer(value) - - for validator in self.validators: - try: - if validator(value) is False: - raise ValueError("Value is not valid") - except ValueError as exc: - raise ValidationError(self.field_name, value, exc) - - return value - def is_required(self) -> bool: return self.default is Undefined and self.default_factory is Undefined - def get_default_value(self) -> typing.Any: - if self.default is not Undefined: - return self.default - - if self.default_factory is not Undefined: - return self.default_factory() - class Field(BaseField): pass diff --git a/src/pydbm/models/meta.py b/src/pydbm/models/meta.py index c038a28..b5d74cc 100644 --- a/src/pydbm/models/meta.py +++ b/src/pydbm/models/meta.py @@ -30,19 +30,19 @@ class Meta(type): @staticmethod def __new__(mcs, cls_name: str, bases: tuple[Meta, ...], namespace: dict[str, typing.Any], **kwargs: typing.Any) -> type: # noqa: E501 - annotations = namespace.pop("__annotations__", {}) - annotations[C.PRIMARY_KEY] = str - slots = mcs.generate_slots(annotations) + _annotations = namespace.pop("__annotations__", {}) + _annotations[C.PRIMARY_KEY] = str + slots = mcs.generate_slots(_annotations) if not [b for b in bases if isinstance(b, mcs)]: slots.remove("_id") slots.append("fields") else: - if not annotations or list(annotations.keys()) == [C.PRIMARY_KEY]: + if not _annotations or list(_annotations.keys()) == [C.PRIMARY_KEY]: raise EmptyModelError("Empty model is not allowed.") slots.append("database") namespace["__slots__"] = tuple(slots) - namespace["__annotations__"] = annotations + namespace["__annotations__"] = _annotations return super().__new__(mcs, cls_name, bases, namespace) def __init__(cls, cls_name: str, bases: tuple[Meta, ...], namespace: dict[str, typing.Any], **kwargs: typing.Any) -> None: # noqa: E501 @@ -62,7 +62,7 @@ def __init__(cls, cls_name: str, bases: tuple[Meta, ...], namespace: dict[str, t setattr(cls, key, value) def __call__(cls, **kwargs): - for extra_field_name in (set(kwargs.keys()) - set(cls.__annotations__.keys())): + for extra_field_name in set(kwargs.keys()) - set(cls.__annotations__.keys()): raise UnnecessaryParamsError(f"{extra_field_name} is not defined in {cls.__name__}") for field in cls.not_required_fields: diff --git a/src/pydbm/models/validators/__init__.py b/src/pydbm/models/validators/__init__.py index a23ed7b..075c1b8 100644 --- a/src/pydbm/models/validators/__init__.py +++ b/src/pydbm/models/validators/__init__.py @@ -1,5 +1,6 @@ import datetime +from pydbm.models.validators.array import validate_array_float, validate_array_int, validate_array_str from pydbm.models.validators.builtin_types import ( validate_bool, validate_bytes, @@ -11,6 +12,7 @@ validate_str, ) from pydbm.models.validators.compare import validate_max_value, validate_min_value +from pydbm.typing_extra import array __all__ = ( "validate_bool", @@ -22,12 +24,15 @@ "validate_max_value", "validate_min_value", "validate_none", + "validate_array_int", + "validate_array_float", + "validate_array_str", "validate_str", - "validator_mapping", + "VALIDATOR_MAPPING", ) -validator_mapping = { +VALIDATOR_MAPPING = { bool: validate_bool, bytes: validate_bytes, datetime.date: validate_date, @@ -36,4 +41,7 @@ int: validate_int, None: validate_none, str: validate_str, + array[int]: validate_array_int, + array[float]: validate_array_float, + array[str]: validate_array_str, } diff --git a/src/pydbm/models/validators/array.py b/src/pydbm/models/validators/array.py new file mode 100644 index 0000000..e4dc83e --- /dev/null +++ b/src/pydbm/models/validators/array.py @@ -0,0 +1,30 @@ +import typing + +from pydbm.typing_extra import array + +__all__ = ( + "validate_array_float", + "validate_array_int", + "validate_array_str", +) + + +def validate_array_int(value: typing.Any) -> None: + if value.__class__ is not array: + raise ValueError("It must be array") + if value.array_type is not int: + raise ValueError("It must be array[int]") + + +def validate_array_str(value: typing.Any) -> None: + if value.__class__ is not array: + raise ValueError("It must be array") + if value.array_type is not str: + raise ValueError("It must be array[str]") + + +def validate_array_float(value: typing.Any) -> None: + if value.__class__ is not array: + raise ValueError("It must be array") + if value.array_type is not float: + raise ValueError("It must be array[float]") diff --git a/src/pydbm/models/validators/builtin_types.py b/src/pydbm/models/validators/builtin_types.py index 031bb15..675dafb 100644 --- a/src/pydbm/models/validators/builtin_types.py +++ b/src/pydbm/models/validators/builtin_types.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import datetime import typing diff --git a/src/pydbm/typing_extra.py b/src/pydbm/typing_extra.py index 0c373dc..1d6f23d 100644 --- a/src/pydbm/typing_extra.py +++ b/src/pydbm/typing_extra.py @@ -1,18 +1,28 @@ from __future__ import annotations +import array as build_in_array +import datetime import typing -__all__ = ( - "dataclass_transform", +__all__ = [ + "ArrayT", "NormalizationT", - "ValidatorT", "SupportedClassT", -) + "TYPE_TO_TYPECODE", + "ValidatorT", + "array", + "dataclass_transform", +] +ArrayT = typing.TypeVar("ArrayT") NormalizationT = typing.Callable[[typing.Any], typing.Any] ValidatorT = typing.Callable[[typing.Any], typing.Optional[bool]] -SupportedClassT = typing.Type[typing.Any] # TODO: Improve this annotations, It must be more correctly +TYPE_TO_TYPECODE: typing.Final[dict[typing.Type[int | float | str], typing.Literal["q", "d", "u"]]] = { + int: "q", + float: "d", + str: "u", +} def dataclass_transform( @@ -37,3 +47,38 @@ def decorator(cls_or_fn): return cls_or_fn return decorator + + +class array(build_in_array.array, typing.Generic[ArrayT]): + if typing.TYPE_CHECKING: + array_type: typing.Type[int | float | str] + + def __new__(cls, *initializer: int | float | str) -> array[ArrayT]: + first_element_class = initializer[0].__class__ + typecode = TYPE_TO_TYPECODE.get(first_element_class) + if typecode is None: + raise TypeError(f"Only int, float, str are supported, but got {first_element_class.__name__}") + + obj = super().__new__(cls, typecode, list(initializer)) # type: ignore[call-arg] + obj.array_type = first_element_class + return obj + + def __repr__(self): + initializer = ", ".join(map(repr, self)) + return f"array({initializer})" + + +SupportedClassT = typing.Union[ + typing.Type[bool], + typing.Type[bytes], + typing.Type[datetime.date], + typing.Type[datetime.datetime], + typing.Type[float], + typing.Type[int], + typing.Type[None], + typing.Type[str], + typing.Type[array], + typing.Type[array[int]], + typing.Type[array[float]], + typing.Type[array[str]], +] diff --git a/tests/models/fields/test_auto.py b/tests/models/fields/test_auto.py index 6e14153..7cd9a40 100644 --- a/tests/models/fields/test_auto.py +++ b/tests/models/fields/test_auto.py @@ -1,4 +1,3 @@ - from pydbm.models.fields import AutoField diff --git a/tests/models/fields/test_base.py b/tests/models/fields/test_base.py index 80b2260..49d59be 100644 --- a/tests/models/fields/test_base.py +++ b/tests/models/fields/test_base.py @@ -1,6 +1,7 @@ import pytest -from pydbm import Field, Undefined, ValidationError, validate_str +from pydbm import Field, Undefined, ValidationError +from pydbm.models.validators.builtin_types import validate_str def test_base_attributes_with_call(): diff --git a/tests/models/test_base.py b/tests/models/test_base.py index fe9df23..89c634c 100644 --- a/tests/models/test_base.py +++ b/tests/models/test_base.py @@ -1,9 +1,12 @@ +from __future__ import annotations + import datetime as datetime import pytest from pydbm import DbmModel from pydbm.exceptions import EmptyModelError +from pydbm.typing_extra import array class Model(DbmModel): @@ -15,6 +18,9 @@ class Model(DbmModel): int: int none: None str: str + array_int: array[int] + array_float: array[float] + array_str: array[str] def test_base_slots(): @@ -31,6 +37,9 @@ def test_base_init(): int=1, none=None, str="str", + array_int=array(1, 2), + array_float=array(1.1, 2.2), + array_str=array("a", "b"), ) assert model.bool is True @@ -41,8 +50,11 @@ def test_base_init(): assert model.int == 1 assert model.none is None assert model.str == "str" + assert model.array_int == array(1, 2) + assert model.array_float == array(1.1, 2.2) + assert model.array_str == array("a", "b") - assert model.id == "552eb2e66df095304137be35af85aaed" + assert model.id == "881e613637953a2a3b9b2f00cf7b7555" assert model.fields == { "bool": True, "bytes": b"123", @@ -52,6 +64,9 @@ def test_base_init(): "int": 1, "none": None, "str": "str", + "array_int": array(1, 2), + "array_float": array(1.1, 2.2), + "array_str": array("a", "b"), } @@ -65,9 +80,12 @@ def test_base_repr(): int=1, none=None, str="str", + array_int=array(1, 2), + array_float=array(1.1, 2.2), + array_str=array("a", "b"), ) - assert repr(model) == "Model(bool=True, bytes=b'123', date=datetime.date(2020, 1, 1), datetime=datetime.datetime(2020, 1, 1, 2, 10, 40), float=1.0, int=1, none=None, str='str')" # noqa: E501 + assert repr(model) == "Model(bool=True, bytes=b'123', date=datetime.date(2020, 1, 1), datetime=datetime.datetime(2020, 1, 1, 2, 10, 40), float=1.0, int=1, none=None, str='str', array_int=array(1, 2), array_float=array(1.1, 2.2), array_str=array('a', 'b'))" # noqa: E501 def test_base_eq(): @@ -80,6 +98,9 @@ def test_base_eq(): int=1, none=None, str="str", + array_int=array(1, 2), + array_float=array(1.1, 2.2), + array_str=array("a", "b"), ) model_2 = Model( bool=True, @@ -90,6 +111,9 @@ def test_base_eq(): int=1, none=None, str="str", + array_int=array(1, 2), + array_float=array(1.1, 2.2), + array_str=array("a", "b"), ) assert model_1 == model_2 @@ -103,6 +127,9 @@ def test_base_eq(): int=1, none=None, str="str", + array_int=array(1, 2), + array_float=array(1.1, 2.2), + array_str=array("a", "b"), ) assert model_1 != model_3 assert model_2 != model_3 diff --git a/tests/models/test_meta.py b/tests/models/test_meta.py index 28d6d49..0902212 100644 --- a/tests/models/test_meta.py +++ b/tests/models/test_meta.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import datetime import pytest @@ -7,6 +9,7 @@ from pydbm import exceptions from pydbm.models import meta from pydbm.models.fields import AutoField +from pydbm.typing_extra import array def test_generate_table_name(): @@ -28,9 +31,7 @@ def test_get_config(): assert config.unique_together == () config = meta.Meta.get_config( - meta.Meta, - "User", - {C.CLASS_CONFIG_NAME: meta.Config(table_name="users", unique_together=("email", "username"))} + meta.Meta, "User", {C.CLASS_CONFIG_NAME: meta.Config(table_name="users", unique_together=("email", "username"))} ) assert config.table_name == "users" @@ -94,6 +95,7 @@ class Example(DbmModel): ({"int": 1.1}, "Invalid value for int=1.1; It must be int."), ({"none": b"test"}, "Invalid value for none=b'test'; It must be None."), ({"str": 1}, "Invalid value for str=1; It must be str."), + ({"array_int": "a"}, "Invalid value for array_int='a'; It must be array."), ], ) def test_base_type_validator(updated_fields, expected_error_ms): @@ -106,6 +108,7 @@ class Model(DbmModel): int: int none: None str: str + array_int: array[int] model_body = { "bool": True, @@ -116,6 +119,7 @@ class Model(DbmModel): "int": 1, "none": None, "str": "str", + "array_int": array(1, 2, 3), } model_body.update(updated_fields) diff --git a/tests/models/validators/test_array.py b/tests/models/validators/test_array.py new file mode 100644 index 0000000..6461e07 --- /dev/null +++ b/tests/models/validators/test_array.py @@ -0,0 +1,36 @@ + +import pytest + +from pydbm.models import validators +from pydbm.typing_extra import array + + +@pytest.mark.parametrize( + "validator_name, value", + [ + ("validate_array_float", array(1.1, 2.2)), + ("validate_array_int", array(1, 2)), + ("validate_array_str", array("a", "b")), + ], +) +def test_builtin_types_valid(validator_name, value): + validator = getattr(validators, validator_name) + assert validator(value) is None + + +@pytest.mark.parametrize( + "validator_name, value, exception_msg", + [ + ("validate_array_float", [1, 2], "It must be array"), + ("validate_array_int", [1.2, 2.1], "It must be array"), + ("validate_array_str", [1, 2], "It must be array"), + ("validate_array_float", array(1, 2), "It must be array[float]"), + ("validate_array_int", array(1.2, 2.1), "It must be array[int]"), + ("validate_array_str", array(1, 2), "It must be array[str]"), + ], +) +def test_builtin_types_invalid(validator_name, value, exception_msg): + validator = getattr(validators, validator_name) + with pytest.raises(ValueError) as cm: + validator(value) + assert str(cm.value) == exception_msg diff --git a/tests/test_inspect_extra.py b/tests/test_inspect_extra.py index c7797f0..f9a574f 100644 --- a/tests/test_inspect_extra.py +++ b/tests/test_inspect_extra.py @@ -3,6 +3,7 @@ import pytest from pydbm.inspect_extra import get_obj_annotations +from pydbm.typing_extra import array def test_get_obj_annotations_not_class(): @@ -22,6 +23,9 @@ def foo(p: int): (int, int), (str, str), (None, None), + (array[int], array[int]), + (array[float], array[float]), + (array[str], array[str]), ("bool", bool), ("bytes", bytes), ("datetime.date", datetime.date), @@ -30,6 +34,9 @@ def foo(p: int): ("int", int), ("str", str), ("None", None), + ("array[int]", array[int]), + ("array[float]", array[float]), + ("array[str]", array[str]), ]) def test_get_obj_annotations(type_, expected_type): # Normal class diff --git a/tests/test_typing_extra.py b/tests/test_typing_extra.py new file mode 100644 index 0000000..5e22182 --- /dev/null +++ b/tests/test_typing_extra.py @@ -0,0 +1,38 @@ +import array as build_in_array + +import pytest + +from pydbm.typing_extra import array + + +@pytest.mark.parametrize("actual_array, expected_array", [ + (array(1, 2, 3), build_in_array.array("q", [1, 2, 3])), + (array("a", "b", "c"), build_in_array.array("u", ["a", "b", "c"])), + (array(1.1, 2.2, 3.3), build_in_array.array("d", [1.1, 2.2, 3.3])), +]) +def test_array(actual_array, expected_array): + assert actual_array == expected_array + + +@pytest.mark.parametrize( + "value_type, value", + [ + (float, array(1.1, 2.2)), + (int, array(1, 2)), + (str, array("a", "b")), + ], +) +def test_array_type(value_type, value): + assert value.array_type is value_type + + +def test_array_not_valid(): + with pytest.raises(TypeError) as cm: + assert array(b"1", b"2", b"3") + assert str(cm.value) == "Only int, float, str are supported, but got bytes" + + +def test_second_argument_not_valid(): + with pytest.raises(TypeError) as cm: + assert array(1, "a") + assert str(cm.value) == "'str' object cannot be interpreted as an integer"