diff --git a/.gitignore b/.gitignore index 36b13f1..b620425 100644 --- a/.gitignore +++ b/.gitignore @@ -86,7 +86,7 @@ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: -# .python-version +.python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. @@ -99,7 +99,7 @@ ipython_config.py # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. -#uv.lock +uv.lock # poetry # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..754b0aa --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,19 @@ +[project] +name = "primitive-type" +version = "0.0.1" +description = "Check a value or object if the type of it is primitive, or convert it to other primitive type in Python." +readme = "README.md" +license = { file = "LICENSE" } +authors = [{ name = "CleMooling", email = "clemooling@staringplanet.top" }] +keywords = ["type-checking", "primitive", "conversion"] +classifiers = [ + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + # "Typing :: Typed", +] +requires-python = ">=3.12" +dependencies = ["beartype>=0.22.9"] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" diff --git a/src/primitive_type/__init__.py b/src/primitive_type/__init__.py new file mode 100644 index 0000000..6bc3e53 --- /dev/null +++ b/src/primitive_type/__init__.py @@ -0,0 +1,21 @@ +"""Entrypoint of this package.""" + +from primitive_type.types import Primitive, PrimitiveMap +from primitive_type.checker import is_primitive, is_nested_dict +from primitive_type.converter import get_primitive_object, ConvertError +from importlib.metadata import version, PackageNotFoundError + +try: + __version__ = version("primitive-type") +except PackageNotFoundError: + __version__ = "unknown" + + +__all__ = [ + "Primitive", + "PrimitiveMap", + "is_primitive", + "is_nested_dict", + "get_primitive_object", + "ConvertError", +] diff --git a/src/primitive_type/checker.py b/src/primitive_type/checker.py new file mode 100644 index 0000000..dc232d9 --- /dev/null +++ b/src/primitive_type/checker.py @@ -0,0 +1,31 @@ +"""Utils for checking objects.""" + +from beartype import beartype + + +def is_primitive(val: object) -> bool: + """Check an object if it's a primitive object. + + :param val: Any instance for checking. + + :return: + * :obj:`True` -- If the object is primitive + * :obj:`False` -- Otherwise. + """ + return isinstance(val, (str, int, float, bool)) or val is None + + +@beartype +def is_nested_dict(obj: dict) -> bool: + """Check a dict if it has nested structures. + + :param obj: Any dict object for checking. + + :return: + * :obj:`True` -- If the dict has nested structures. + * :obj:`False` -- Otherwise. + """ + for k, v in obj.items(): + if not is_primitive(v): + return True + return False diff --git a/src/primitive_type/converter.py b/src/primitive_type/converter.py new file mode 100644 index 0000000..0e447d3 --- /dev/null +++ b/src/primitive_type/converter.py @@ -0,0 +1,74 @@ +"""A quick and simple converter to convert the type of an object.""" + +import re + +from beartype import beartype +from primitive_type.types import Primitive + + +class ConvertError(TypeError): + """An exception should happens when conversion failed.""" + + pass + + +@beartype +def get_primitive_object(val: Primitive, obj_type: type[Primitive] = None) -> Primitive: + """Get a primitive object from a given value. + + :param val: The given value wants to be converted. + :param obj_type: The target type of object that finally converted out. Default to :obj:`None` as disabled. + + :return: + * :class:`str` -- If given value is a normal string, or convert to if type specified. + * :class:`int` -- The origin object or convert to. + * :class:`float` -- The origin object or convert to. + * :class:`bool` -- The origin object or convert to. + * :obj:`None` -- Only when given value is :obj:`None`. + """ + obj_type_required: bool = obj_type is not None + + if val is None: + if obj_type_required: + raise TypeError("Given value is None, could not convert to other type!") + return None + + if not obj_type_required: + if not isinstance(val, str): + return val + + val_lower = val.lower() + if val_lower == "true": + return True + if val_lower == "false": + return False + + if re.fullmatch(r"[+-]?\d+", val): + return int(val) + + if re.fullmatch(r"[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?", val): + return float(val) + + return val + + try: + if obj_type is str: + return str(val) + + if obj_type is bool: + if isinstance(val, str): + return val.lower() == "true" + if isinstance(val, (int, float)): + return val > 0 + return bool(val) + + if obj_type is int: + return int(float(val)) if isinstance(val, str) else int(val) + + if obj_type is float: + return float(val) + + except (ValueError, TypeError, SyntaxError): + raise ConvertError(f"Could not convert '{val}' to type '{obj_type}'.") + + return val diff --git a/src/primitive_type/py.typed b/src/primitive_type/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/src/primitive_type/types.py b/src/primitive_type/types.py new file mode 100644 index 0000000..9ef9178 --- /dev/null +++ b/src/primitive_type/types.py @@ -0,0 +1,16 @@ +"""Type aliases for primitive types.""" + +type Primitive = str | int | float | bool | None +""" +Type alias for primitive values. + +Including: :class:`str`, :class:`int`, :class:`float`, :class:`bool`, and :obj:`None`. +""" + +type PrimitiveMap = dict[str, Primitive] +"""Type alias for a mapping of string keys to primitive values. + +This dictionary represents a collection of metadata or attributes where each +value is guaranteed to be a :data:`Primitive` type, ensuring type safety +and ease of serialization. +""" diff --git a/tests/test_primitive_type_converter.py b/tests/test_primitive_type_converter.py new file mode 100644 index 0000000..3483c6e --- /dev/null +++ b/tests/test_primitive_type_converter.py @@ -0,0 +1,66 @@ +import unittest + +from primitive_type.converter import get_primitive_object, ConvertError + + +class TestPrimitiveTypeConverter(unittest.TestCase): + # Assertion of conversion tests. + + def test_auto_infer_bool(self): + """Test conversion of bool strings.""" + self.assertEqual(get_primitive_object("true"), True) + self.assertEqual(get_primitive_object("FALSE"), False) + self.assertEqual(get_primitive_object("True"), True) + + def test_auto_infer_int(self): + """Test conversion of integer number strings.""" + self.assertEqual(get_primitive_object("123"), 123) + self.assertEqual(get_primitive_object("-456"), -456) + + def test_auto_infer_float(self): + """Test conversion of float number strings.""" + self.assertEqual(get_primitive_object("3.14"), 3.14) + self.assertEqual(get_primitive_object(".5"), 0.5) + self.assertEqual(get_primitive_object("1e-10"), 1e-10) + + def test_auto_infer_none_and_others(self): + """Test assertion of conversion for `None` object or other.""" + self.assertIsNone(get_primitive_object(None)) + self.assertEqual(get_primitive_object("hello"), "hello") + self.assertEqual(get_primitive_object(100), 100) + self.assertEqual(get_primitive_object(3.14), 3.14) + self.assertEqual(get_primitive_object(True), True) + + # Explicit conversion tests + + def test_explicit_to_str(self): + """Test explicit convert a value to a `str` value.""" + self.assertEqual(get_primitive_object(123, obj_type=str), "123") + self.assertEqual(get_primitive_object(True, obj_type=str), "True") + + def test_explicit_to_int(self): + """Test explicit convert a value to a `int` value.""" + self.assertEqual(get_primitive_object("123", obj_type=int), 123) + self.assertEqual(get_primitive_object(3.9, obj_type=int), 3) + # 你的实现中包含 int(float(val)) 逻辑,测试字符串带小数点的转换 + self.assertEqual(get_primitive_object("12.3", obj_type=int), 12) + + def test_explicit_to_bool(self): + """Test explicit convert a value to a `bool` value.""" + self.assertEqual(get_primitive_object("true", obj_type=bool), True) + self.assertEqual(get_primitive_object(1, obj_type=bool), True) + self.assertEqual(get_primitive_object(0, obj_type=bool), False) + + def test_convert_none_error(self): + """Test raise `TypeError` when wants to convert a `None` object into a value that is not None.""" + with self.assertRaises(TypeError): + get_primitive_object(None, obj_type=int) + + def test_convert_failure(self): + """Test raise `ConvertError` when conversion failed.""" + with self.assertRaises(ConvertError): + get_primitive_object("abc", obj_type=float) + + +if __name__ == "__main__": + unittest.main()