From 0c432047c0ecb6d894a9a2bd708ca3c88c10c8a4 Mon Sep 17 00:00:00 2001 From: Vaughn Kottler Date: Mon, 18 Sep 2023 02:58:26 -0500 Subject: [PATCH] 2.10.3 - Some fixes and improvements --- .github/workflows/python-package.yml | 2 +- README.md | 4 +-- local/variables/package.yaml | 2 +- pyproject.toml | 2 +- runtimepy/__init__.py | 4 +-- runtimepy/channel/environment/base.py | 7 ++++ .../channel/environment/command/__init__.py | 35 ++++++++++++++----- runtimepy/channel/environment/create.py | 13 +++++++ runtimepy/primitives/__init__.py | 3 +- runtimepy/primitives/bool.py | 19 ++++++++++ runtimepy/primitives/field/__init__.py | 22 ++++++++++-- runtimepy/primitives/field/manager/base.py | 7 +++- tests/channel/environment/test_command.py | 8 +++++ tests/channel/environment/test_create.py | 32 +++++++++++++++++ 14 files changed, 139 insertions(+), 21 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 0475fd81..cb2b9a94 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -67,7 +67,7 @@ jobs: - run: | mk python-release owner=vkottler \ - repo=runtimepy version=2.10.2 + repo=runtimepy version=2.10.3 if: | matrix.python-version == '3.11' && matrix.system == 'ubuntu-latest' diff --git a/README.md b/README.md index c10a717c..aa34f735 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,11 @@ ===================================== generator=datazen version=3.1.3 - hash=6c843a31c1edfdff5d1d97f2dce9f1ab + hash=b75ca71568bc2a3269b08871ad1d1cd3 ===================================== --> -# runtimepy ([2.10.2](https://pypi.org/project/runtimepy/)) +# runtimepy ([2.10.3](https://pypi.org/project/runtimepy/)) [![python](https://img.shields.io/pypi/pyversions/runtimepy.svg)](https://pypi.org/project/runtimepy/) ![Build Status](https://github.com/vkottler/runtimepy/workflows/Python%20Package/badge.svg) diff --git a/local/variables/package.yaml b/local/variables/package.yaml index 4a182511..9de29dc6 100644 --- a/local/variables/package.yaml +++ b/local/variables/package.yaml @@ -1,5 +1,5 @@ --- major: 2 minor: 10 -patch: 2 +patch: 3 entry: runtimepy diff --git a/pyproject.toml b/pyproject.toml index f32e046b..25e86c19 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta:__legacy__" [project] name = "runtimepy" -version = "2.10.2" +version = "2.10.3" description = "A framework for implementing Python services." readme = "README.md" requires-python = ">=3.11" diff --git a/runtimepy/__init__.py b/runtimepy/__init__.py index ff6b6279..74038d4d 100644 --- a/runtimepy/__init__.py +++ b/runtimepy/__init__.py @@ -1,7 +1,7 @@ # ===================================== # generator=datazen # version=3.1.3 -# hash=3f7d5094777980a2982efb1a76c05c19 +# hash=08fd0eee0ba8aebac4d0651f3d3fbfb8 # ===================================== """ @@ -10,7 +10,7 @@ DESCRIPTION = "A framework for implementing Python services." PKG_NAME = "runtimepy" -VERSION = "2.10.2" +VERSION = "2.10.3" # runtimepy-specific content. METRICS_NAME = "metrics" diff --git a/runtimepy/channel/environment/base.py b/runtimepy/channel/environment/base.py index 1192dcf4..ec18ab4a 100644 --- a/runtimepy/channel/environment/base.py +++ b/runtimepy/channel/environment/base.py @@ -24,6 +24,7 @@ from runtimepy.enum import RuntimeEnum as _RuntimeEnum from runtimepy.enum.registry import EnumRegistry as _EnumRegistry from runtimepy.mixins.finalize import FinalizeMixin +from runtimepy.primitives import StrToBool from runtimepy.primitives.field import BitField as _BitField from runtimepy.primitives.field.fields import BitFields as _BitFields from runtimepy.primitives.field.manager import ( @@ -122,6 +123,12 @@ def set(self, key: _RegistryKey, value: ChannelValue) -> None: except ValueError: pass + # Handle booleans. + else: + parsed = StrToBool.parse(value) + value = parsed.result + resolved = parsed.valid + if not resolved: raise ValueError( ( diff --git a/runtimepy/channel/environment/command/__init__.py b/runtimepy/channel/environment/command/__init__.py index 95246083..5b6d0549 100644 --- a/runtimepy/channel/environment/command/__init__.py +++ b/runtimepy/channel/environment/command/__init__.py @@ -4,7 +4,7 @@ # built-in from argparse import Namespace -from typing import Any, Optional, cast +from typing import Any, Optional, Union, cast # third-party from vcorelib.logging import LoggerType @@ -19,6 +19,7 @@ from runtimepy.channel.environment.command.result import SUCCESS, CommandResult from runtimepy.mixins.environment import ChannelEnvironmentMixin from runtimepy.primitives.bool import Bool +from runtimepy.primitives.field import BitField class ChannelCommandProcessor(ChannelEnvironmentMixin): @@ -62,14 +63,23 @@ def do_set(self, args: Namespace) -> CommandResult: try: self.env.set(args.channel, args.extra[0]) except (ValueError, KeyError) as exc: - result = CommandResult(False, str(exc)) + self.logger.exception( + "Exception setting '%s':", args.channel, exc_info=exc + ) + result = CommandResult( + False, f"Exception while setting '{args.channel}'." + ) return result - def do_toggle(self, args: Namespace, channel: AnyChannel) -> CommandResult: + def do_toggle( + self, args: Namespace, channel: Union[BitField, AnyChannel] + ) -> CommandResult: """Attempt to toggle a channel.""" - if args.command == ChannelCommand.TOGGLE: + if isinstance(channel, BitField): + channel.invert() + else: if not channel.type.is_boolean: return CommandResult( False, @@ -89,13 +99,20 @@ def handle_command(self, args: Namespace) -> CommandResult: result = SUCCESS chan = self.env.get(args.channel) - if chan is None: - return CommandResult(False, f"No channel '{args.channel}'.") - channel, enum = chan - del enum + channel: Union[BitField, AnyChannel] - # Check if channel is commandable (or if a -f/--force flag is set?). + if chan is None: + # Check if the name is a field. + field = self.env.fields.get_field(args.channel) + if field is None: + return CommandResult(False, f"No channel '{args.channel}'.") + channel = field + else: + channel, _ = chan + + # Check if channel is commandable (or if a -f/--force flag is + # set?). if not channel.commandable and not args.force: return CommandResult( False, diff --git a/runtimepy/channel/environment/create.py b/runtimepy/channel/environment/create.py index b20e9016..6eff3516 100644 --- a/runtimepy/channel/environment/create.py +++ b/runtimepy/channel/environment/create.py @@ -28,6 +28,8 @@ from runtimepy.mapping import EnumMappingData as _EnumMappingData from runtimepy.primitives import ChannelScaling, Primitive from runtimepy.primitives import Primitivelike as _Primitivelike +from runtimepy.primitives.field import BitField +from runtimepy.primitives.field.fields import BitFields from runtimepy.registry.name import RegistryKey as _RegistryKey @@ -142,3 +144,14 @@ def enum( ) assert result is not None, f"Can't create enum '{name}'!" return result + + def add_field( + self, name: str, field: BitField, namespace: _Namespace = None + ) -> str: + """Add a bit field to the environment.""" + + fields = BitFields.new() + name = self.namespace(name=name, namespace=namespace) + fields.fields[name] = field + self.fields.add(fields) + return name diff --git a/runtimepy/primitives/__init__.py b/runtimepy/primitives/__init__.py index 869a0498..8a5f0d05 100644 --- a/runtimepy/primitives/__init__.py +++ b/runtimepy/primitives/__init__.py @@ -10,7 +10,7 @@ # internal from runtimepy.primitives.base import Primitive -from runtimepy.primitives.bool import Bool +from runtimepy.primitives.bool import Bool, StrToBool from runtimepy.primitives.float import Double, Float, Half from runtimepy.primitives.int import ( Int8, @@ -49,6 +49,7 @@ "SignedInt", "UnsignedInt", "Primitive", + "StrToBool", ] AnyPrimitive = _Union[ diff --git a/runtimepy/primitives/bool.py b/runtimepy/primitives/bool.py index 8f5ac21f..24ebd645 100644 --- a/runtimepy/primitives/bool.py +++ b/runtimepy/primitives/bool.py @@ -2,6 +2,9 @@ A module implementing a boolean-primitive interface. """ +# built-in +from typing import NamedTuple + # internal from runtimepy.primitives.base import Primitive as _Primitive from runtimepy.primitives.type.bool import Bool as _Bool @@ -30,3 +33,19 @@ def clear(self) -> None: Bool = BooleanPrimitive + + +class StrToBool(NamedTuple): + """A container for results when converting strings to boolean.""" + + result: bool + valid: bool + + @staticmethod + def parse(data: str) -> "StrToBool": + """Parse a string to boolean.""" + + data = data.lower() + is_true = data == "true" + resolved = is_true or data == "false" + return StrToBool(is_true, resolved) diff --git a/runtimepy/primitives/field/__init__.py b/runtimepy/primitives/field/__init__.py index 38049dfe..05062433 100644 --- a/runtimepy/primitives/field/__init__.py +++ b/runtimepy/primitives/field/__init__.py @@ -19,11 +19,18 @@ class BitFieldBase: """A simple bit-field implementation.""" - def __init__(self, raw: _UnsignedInt, index: int, width: int) -> None: + def __init__( + self, + raw: _UnsignedInt, + index: int, + width: int, + commandable: bool = False, + ) -> None: """Initialize this bit-field.""" self.raw = raw self.index = index + self.commandable = commandable # Compute a bit-mask for this field. self.width = width @@ -52,6 +59,10 @@ def __call__(self, val: int = None) -> int: return result + def invert(self) -> int: + """Invert the value of this field and return the result.""" + return self(~self()) + class BitField(BitFieldBase, _RegexMixin, _EnumMixin): """A class managing a portion of an unsigned-integer primitive.""" @@ -65,10 +76,11 @@ def __init__( index: int, width: int, enum: _RegistryKey = None, + commandable: bool = False, ) -> None: """Initialize this bit-field.""" - super().__init__(raw, index, width) + super().__init__(raw, index, width, commandable=commandable) # Verify bit-field parameters. assert ( @@ -108,9 +120,13 @@ def __init__( raw: _UnsignedInt, index: int, enum: _RegistryKey = None, + commandable: bool = False, ) -> None: """Initialize this bit flag.""" - super().__init__(name, raw, index, 1, enum=enum) + + super().__init__( + name, raw, index, 1, enum=enum, commandable=commandable + ) def clear(self) -> None: """Clear this field.""" diff --git a/runtimepy/primitives/field/manager/base.py b/runtimepy/primitives/field/manager/base.py index c007a55e..6bbf9ffb 100644 --- a/runtimepy/primitives/field/manager/base.py +++ b/runtimepy/primitives/field/manager/base.py @@ -21,6 +21,7 @@ # internal from runtimepy.enum import RuntimeEnum as _RuntimeEnum from runtimepy.enum.registry import EnumRegistry as _EnumRegistry +from runtimepy.primitives import StrToBool from runtimepy.primitives.field import BitField as _BitField from runtimepy.primitives.field import BitFlag as _BitFlag from runtimepy.primitives.field.fields import BitFields as _BitFields @@ -114,7 +115,11 @@ def set(self, key: _RegistryKey, value: _Union[int, bool, str]) -> None: field = self[key] if isinstance(value, str): - value = self.enum_lookup[field.name].get_int(value) + parsed = StrToBool.parse(value) + if not parsed.valid: + value = self.enum_lookup[field.name].get_int(value) + else: + value = parsed.result # Update the value. field(int(value)) diff --git a/tests/channel/environment/test_command.py b/tests/channel/environment/test_command.py index bca8353a..5a55facc 100644 --- a/tests/channel/environment/test_command.py +++ b/tests/channel/environment/test_command.py @@ -51,3 +51,11 @@ def test_channel_command_processor_basic(): assert isclose(env.value("float1"), 42) # type: ignore assert processor.command("set float1 -101.5 -f") assert isclose(env.value("float1"), -101.5) # type: ignore + + assert processor.command("set bool1 true -f") + assert env.value("bool1") + + assert not processor.command("set bool1 ttrue -f") + + assert processor.command("set bool1 false -f") + assert not env.value("bool1") diff --git a/tests/channel/environment/test_create.py b/tests/channel/environment/test_create.py index 263d7a47..058a077f 100644 --- a/tests/channel/environment/test_create.py +++ b/tests/channel/environment/test_create.py @@ -2,8 +2,14 @@ Test the 'channel.environment.create' module. """ +# built-in +import logging + # module under test from runtimepy.channel.environment import ChannelEnvironment +from runtimepy.channel.environment.command import ChannelCommandProcessor +from runtimepy.primitives import Uint8 +from runtimepy.primitives.field import BitField def test_channel_environment_create_basic(): @@ -15,3 +21,29 @@ def test_channel_environment_create_basic(): result = env.channel("sample_channel", "bool", enum=enum) assert result + + name = "test_field" + underlying = Uint8() + + assert env.add_field( + name, BitField(name, underlying, 0, 1, commandable=True) + ) + assert not env.value("test_field") + + env.set("test_field", True) + assert env.value("test_field") + assert underlying.value == 1 + + proc = ChannelCommandProcessor(env, logging.getLogger(__name__)) + + assert proc.command("set test_field true") + assert underlying.value == 1 + + assert proc.command("set test_field false") + assert underlying.value == 0 + + assert proc.command("toggle test_field") + assert underlying.value == 1 + + assert proc.command("toggle test_field") + assert underlying.value == 0