From fc0d12ee512e025559450afc2cea70afcefd985a Mon Sep 17 00:00:00 2001 From: Santtu Pajukanta Date: Mon, 12 Jul 2021 09:04:14 +0300 Subject: [PATCH 1/2] tags/typeconv: add ToInteger, ToString, ToBoolean, ToFloat tags (closes #49) --- emrichen/tags/__init__.py | 5 ++++ emrichen/tags/typeconv.py | 56 +++++++++++++++++++++++++++++++++++++++ tests/test_typeconv.py | 30 +++++++++++++++++++++ 3 files changed, 91 insertions(+) create mode 100644 emrichen/tags/typeconv.py create mode 100644 tests/test_typeconv.py diff --git a/emrichen/tags/__init__.py b/emrichen/tags/__init__.py index 49b45d1..f84a077 100644 --- a/emrichen/tags/__init__.py +++ b/emrichen/tags/__init__.py @@ -22,6 +22,7 @@ from .not_ import Not from .op import Op from .typeop import IsBoolean, IsDict, IsInteger, IsList, IsNone, IsNumber, IsString +from .typeconv import ToBoolean, ToInteger, ToFloat, ToString from .urlencode import URLEncode from .var import Var from .void import Void @@ -64,6 +65,10 @@ 'Op', 'SHA1', 'SHA256', + 'ToBoolean', + 'ToFloat', + 'ToInteger', + 'ToString', 'URLEncode', 'Var', 'Void', diff --git a/emrichen/tags/typeconv.py b/emrichen/tags/typeconv.py new file mode 100644 index 0000000..a22a5b0 --- /dev/null +++ b/emrichen/tags/typeconv.py @@ -0,0 +1,56 @@ +from numbers import Number +from collections.abc import Mapping +from typing import Optional, Type, Union + +from ..context import Context +from ..void import Void, VoidType +from .base import BaseTag + + +class _BaseToType(BaseTag): + """ + arguments: Data to convert. + example: "`!{name} ...`" + description: Converts the input to the desired type. + """ + + value_types = (object,) + target_type: Type + + def enrich(self, context: Context): + return self.target_type(context.enrich(self.data)) + + +class ToBoolean(_BaseToType): + __doc__ = _BaseToType.__doc__ + target_type = bool + + +class ToInteger(_BaseToType): + """ + arguments: Either single argument containing the data to convert, or an object with `value:` and `radix:`. + example: `!ToInteger "50"`, `!ToInteger value: "C0FFEE", radix: 16` + description: Converts the input to Python `int`. Radix is never inferred from input: if not supplied, it is always 10. + """ + + target_type = int + + def enrich(self, context: Context): + data = context.enrich(self.data) + + if isinstance(data, Mapping): + value = data["value"] + radix = data.get("radix", 10) + return self.target_type(value, radix) + else: + return self.target_type(data) + + +class ToFloat(_BaseToType): + __doc__ = _BaseToType.__doc__ + target_type = float + + +class ToString(_BaseToType): + __doc__ = _BaseToType.__doc__ + target_type = str diff --git a/tests/test_typeconv.py b/tests/test_typeconv.py new file mode 100644 index 0000000..73d9ec8 --- /dev/null +++ b/tests/test_typeconv.py @@ -0,0 +1,30 @@ +import pytest + +from emrichen import Template, Context +from emrichen.void import Void + + +@pytest.mark.parametrize( + 'tag, val, result', + [ + ('ToBoolean', 0, False), + # ('ToBoolean', "false", False), + ('ToBoolean', "TRUE", True), + ('ToInteger', "8", 8), + ('ToInteger', {"value": "0644", "radix": 8}, 420), + ('ToFloat', "8.2", 8.2), + ('ToFloat', 8, 8.0), + ('ToFloat', True, 1.0), + ('ToString', True, "True"), # TODO too pythonic? should we return lowercase instead? + ('ToString', 8, "8"), + # ('ToString', {'a': 5, 'b': 6}, "{'a': 5, 'b': 6}"), # TODO OrderedDict([('a', 5), ('b', 6)]) + ], +) +def test_typeop(tag, val, result): + resolved = Template.parse(f"!{tag},Lookup 'a'").enrich(Context({'a': val}))[0] + assert resolved == result, f'{tag}({val!r}) returned {resolved}, expected {result}' + + # type equivalence instead of isinstance is intended: want strict conformance + assert type(resolved) == type( + result + ), f'{tag}({val!r}) returned type {type(resolved)}, expected {type(result)}' From 154e9e1d9953e8266e7cdd03e0e467030a544799 Mon Sep 17 00:00:00 2001 From: Santtu Pajukanta Date: Mon, 12 Jul 2021 09:04:23 +0300 Subject: [PATCH 2/2] make pyright happy again --- emrichen/tags/typeop.py | 7 ++++--- setup.py | 7 +++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/emrichen/tags/typeop.py b/emrichen/tags/typeop.py index 60f0fd0..4b80e82 100644 --- a/emrichen/tags/typeop.py +++ b/emrichen/tags/typeop.py @@ -1,5 +1,5 @@ from numbers import Number -from typing import Optional, Union +from typing import Optional, Type, Union from ..context import Context from ..void import Void, VoidType @@ -12,8 +12,9 @@ class _BaseIsType(BaseTag): example: "`!{name} ...`" description: Returns True if the value enriched is of the given type, False otherwise. """ - requisite_type = None + value_types = (object,) + requisite_type: Type def enrich(self, context: Context) -> bool: return self.check(context.enrich(self.data)) @@ -67,4 +68,4 @@ class IsNone(_BaseIsType): """ def check(self, value: Optional[Union[VoidType, str]]) -> bool: - return (value is None or value is Void) + return value is None or value is Void diff --git a/setup.py b/setup.py index 6def6de..a82660a 100755 --- a/setup.py +++ b/setup.py @@ -8,7 +8,10 @@ with open(os.path.join(source_dir, 'emrichen', '__init__.py')) as f: - version = re.search("__version__ = ['\"]([^'\"]+)['\"]", f.read()).group(1) + init_file = f.read() + match = re.search("__version__ = ['\"]([^'\"]+)['\"]", init_file) + assert match, "Failed to parse version from emrichen/__init__.py" + version = match.group(1) with open(os.path.join(source_dir, 'README.md'), encoding='utf-8') as f: @@ -29,7 +32,7 @@ author='Santtu Pajukanta', author_email='santtu@pajukanta.fi', url='http://github.com/con2/emrichen', - packages = find_packages(exclude=["tests"]), + packages=find_packages(exclude=["tests"]), zip_safe=True, entry_points={ 'console_scripts': [