From 6363d0ca06e05caa88a3595fdd5aafa6df4f8e67 Mon Sep 17 00:00:00 2001 From: mic1on Date: Sat, 14 Mar 2026 13:32:22 +0800 Subject: [PATCH] feat(converter): add 5 new conversion functions - to_int: safely convert to integer - to_float: safely convert to float - to_json: convert object to JSON string - from_json: parse JSON string to object - to_uuid: generate UUID (v1, v3, v4, v5) With comprehensive unit tests (26 test cases) --- src/usepy/converter/__init__.py | 11 ++- src/usepy/converter/to_int.py | 59 ++++++++++++ src/usepy/converter/to_json.py | 50 ++++++++++ src/usepy/converter/to_uuid.py | 38 ++++++++ tests/test_converter_new.py | 159 ++++++++++++++++++++++++++++++++ 5 files changed, 316 insertions(+), 1 deletion(-) create mode 100644 src/usepy/converter/to_int.py create mode 100644 src/usepy/converter/to_json.py create mode 100644 src/usepy/converter/to_uuid.py create mode 100644 tests/test_converter_new.py diff --git a/src/usepy/converter/__init__.py b/src/usepy/converter/__init__.py index 41ead6e..1562fb8 100644 --- a/src/usepy/converter/__init__.py +++ b/src/usepy/converter/__init__.py @@ -3,6 +3,9 @@ from .to_string import to_string from .to_set import to_set from .to_bool import to_bool +from .to_int import to_int, to_float +from .to_json import to_json, from_json +from .to_uuid import to_uuid __all__ = [ @@ -11,4 +14,10 @@ "to_string", "to_set", "to_bool", -] + # New functions + "to_int", + "to_float", + "to_json", + "from_json", + "to_uuid", +] \ No newline at end of file diff --git a/src/usepy/converter/to_int.py b/src/usepy/converter/to_int.py new file mode 100644 index 0000000..ab031eb --- /dev/null +++ b/src/usepy/converter/to_int.py @@ -0,0 +1,59 @@ +from typing import Any, Optional + + +def to_int(value: Any, default: Optional[int] = None) -> Optional[int]: + """ + Safely converts a value to an integer. + + Args: + value (Any): The value to convert. + default (Optional[int]): The default value to return if conversion fails. Defaults to None. + + Returns: + Optional[int]: The integer value, or default if conversion fails. + + Examples: + >>> to_int('123') + 123 + >>> to_int('abc', default=0) + 0 + >>> to_int(45.67) + 45 + >>> to_int(None) + None + """ + if value is None: + return default + try: + return int(value) + except (ValueError, TypeError): + return default + + +def to_float(value: Any, default: Optional[float] = None) -> Optional[float]: + """ + Safely converts a value to a float. + + Args: + value (Any): The value to convert. + default (Optional[float]): The default value to return if conversion fails. Defaults to None. + + Returns: + Optional[float]: The float value, or default if conversion fails. + + Examples: + >>> to_float('123.45') + 123.45 + >>> to_float('abc', default=0.0) + 0.0 + >>> to_float(123) + 123.0 + >>> to_float(None) + None + """ + if value is None: + return default + try: + return float(value) + except (ValueError, TypeError): + return default \ No newline at end of file diff --git a/src/usepy/converter/to_json.py b/src/usepy/converter/to_json.py new file mode 100644 index 0000000..a7e51b7 --- /dev/null +++ b/src/usepy/converter/to_json.py @@ -0,0 +1,50 @@ +import json +from typing import Any, Optional + + +def to_json(obj: Any, indent: Optional[int] = None, ensure_ascii: bool = False) -> str: + """ + Converts an object to a JSON string. + + Args: + obj (Any): The object to convert. + indent (Optional[int]): The number of spaces for indentation. Defaults to None (compact). + ensure_ascii (bool): Whether to escape non-ASCII characters. Defaults to False. + + Returns: + str: The JSON string. + + Examples: + >>> to_json({'a': 1, 'b': 2}) + '{"a": 1, "b": 2}' + >>> to_json({'name': '张三'}, ensure_ascii=False) + '{"name": "张三"}' + >>> to_json([1, 2, 3], indent=2) + '[\\n 1,\\n 2,\\n 3\\n]' + """ + return json.dumps(obj, indent=indent, ensure_ascii=ensure_ascii) + + +def from_json(json_string: str, default: Any = None) -> Any: + """ + Parses a JSON string into a Python object. + + Args: + json_string (str): The JSON string to parse. + default (Any): The default value to return if parsing fails. Defaults to None. + + Returns: + Any: The parsed Python object, or default if parsing fails. + + Examples: + >>> from_json('{"a": 1, "b": 2}') + {'a': 1, 'b': 2} + >>> from_json('[1, 2, 3]') + [1, 2, 3] + >>> from_json('invalid json', default={}) + {} + """ + try: + return json.loads(json_string) + except (json.JSONDecodeError, TypeError): + return default \ No newline at end of file diff --git a/src/usepy/converter/to_uuid.py b/src/usepy/converter/to_uuid.py new file mode 100644 index 0000000..9f7ba23 --- /dev/null +++ b/src/usepy/converter/to_uuid.py @@ -0,0 +1,38 @@ +import uuid +from typing import Optional + + +def to_uuid(version: int = 4, namespace: Optional[uuid.UUID] = None, name: Optional[str] = None) -> str: + """ + Generates a UUID string. + + Args: + version (int): The UUID version (1, 3, 4, or 5). Defaults to 4. + namespace (Optional[uuid.UUID]): The namespace for UUID v3 and v5. + name (Optional[str]): The name for UUID v3 and v5. + + Returns: + str: The UUID string. + + Examples: + >>> len(to_uuid()) + 36 + >>> to_uuid(version=1) # UUID v1 based on timestamp + '...' + >>> to_uuid(version=3, namespace=uuid.NAMESPACE_DNS, name='example.com') + '...' + """ + if version == 1: + return str(uuid.uuid1()) + elif version == 3: + if namespace is None or name is None: + raise ValueError("UUID v3 requires namespace and name") + return str(uuid.uuid3(namespace, name)) + elif version == 4: + return str(uuid.uuid4()) + elif version == 5: + if namespace is None or name is None: + raise ValueError("UUID v5 requires namespace and name") + return str(uuid.uuid5(namespace, name)) + else: + raise ValueError(f"Unsupported UUID version: {version}") \ No newline at end of file diff --git a/tests/test_converter_new.py b/tests/test_converter_new.py new file mode 100644 index 0000000..2340393 --- /dev/null +++ b/tests/test_converter_new.py @@ -0,0 +1,159 @@ +import pytest +import uuid +from usepy.converter import to_int, to_float, to_json, from_json, to_uuid + + +class TestToInt: + """Tests for to_int() function""" + + def test_to_int_string(self): + """Test converting string to int""" + assert to_int('123') == 123 + assert to_int('-456') == -456 + + def test_to_int_float(self): + """Test converting float to int""" + assert to_int(45.67) == 45 + assert to_int(-78.9) == -78 + + def test_to_int_invalid_string(self): + """Test converting invalid string returns default""" + assert to_int('abc', default=0) == 0 + assert to_int('not a number') is None + + def test_to_int_none(self): + """Test converting None returns default""" + assert to_int(None) is None + assert to_int(None, default=0) == 0 + + def test_to_int_already_int(self): + """Test converting int returns same value""" + assert to_int(123) == 123 + + +class TestToFloat: + """Tests for to_float() function""" + + def test_to_float_string(self): + """Test converting string to float""" + assert to_float('123.45') == 123.45 + assert to_float('-67.89') == -67.89 + + def test_to_float_int(self): + """Test converting int to float""" + assert to_float(123) == 123.0 + + def test_to_float_invalid_string(self): + """Test converting invalid string returns default""" + assert to_float('abc', default=0.0) == 0.0 + assert to_float('not a number') is None + + def test_to_float_none(self): + """Test converting None returns default""" + assert to_float(None) is None + assert to_float(None, default=0.0) == 0.0 + + def test_to_float_already_float(self): + """Test converting float returns same value""" + assert to_float(123.45) == 123.45 + + +class TestToJson: + """Tests for to_json() function""" + + def test_to_json_dict(self): + """Test converting dict to JSON""" + result = to_json({'a': 1, 'b': 2}) + assert result == '{"a": 1, "b": 2}' + + def test_to_json_list(self): + """Test converting list to JSON""" + result = to_json([1, 2, 3]) + assert result == '[1, 2, 3]' + + def test_to_json_unicode(self): + """Test converting dict with unicode""" + result = to_json({'name': '张三'}, ensure_ascii=False) + assert result == '{"name": "张三"}' + + def test_to_json_indent(self): + """Test converting with indentation""" + result = to_json({'a': 1}, indent=2) + assert '{\n "a": 1\n}' == result + + def test_to_json_string(self): + """Test converting string to JSON""" + result = to_json('hello') + assert result == '"hello"' + + +class TestFromJson: + """Tests for from_json() function""" + + def test_from_json_dict(self): + """Test parsing JSON dict""" + result = from_json('{"a": 1, "b": 2}') + assert result == {'a': 1, 'b': 2} + + def test_from_json_list(self): + """Test parsing JSON list""" + result = from_json('[1, 2, 3]') + assert result == [1, 2, 3] + + def test_from_json_invalid(self): + """Test parsing invalid JSON returns default""" + result = from_json('invalid json', default={}) + assert result == {} + + def test_from_json_none(self): + """Test parsing None returns default""" + result = from_json(None, default=[]) + assert result == [] + + def test_from_json_unicode(self): + """Test parsing JSON with unicode""" + result = from_json('{"name": "张三"}') + assert result == {'name': '张三'} + + +class TestToUuid: + """Tests for to_uuid() function""" + + def test_to_uuid_v4(self): + """Test generating UUID v4""" + result = to_uuid() + assert len(result) == 36 + # Validate it's a valid UUID + uuid.UUID(result) + + def test_to_uuid_v1(self): + """Test generating UUID v1""" + result = to_uuid(version=1) + assert len(result) == 36 + uuid.UUID(result) + + def test_to_uuid_v3(self): + """Test generating UUID v3""" + result = to_uuid(version=3, namespace=uuid.NAMESPACE_DNS, name='example.com') + assert len(result) == 36 + # UUID v3 is deterministic + result2 = to_uuid(version=3, namespace=uuid.NAMESPACE_DNS, name='example.com') + assert result == result2 + + def test_to_uuid_v5(self): + """Test generating UUID v5""" + result = to_uuid(version=5, namespace=uuid.NAMESPACE_DNS, name='example.com') + assert len(result) == 36 + # UUID v5 is deterministic + result2 = to_uuid(version=5, namespace=uuid.NAMESPACE_DNS, name='example.com') + assert result == result2 + + def test_to_uuid_v3_missing_params(self): + """Test UUID v3 without namespace/name raises error""" + with pytest.raises(ValueError): + to_uuid(version=3) + + def test_to_uuid_invalid_version(self): + """Test invalid version raises error""" + with pytest.raises(ValueError): + to_uuid(version=6) \ No newline at end of file