Skip to content

Commit

Permalink
Merge pull request #126 from vkottler/dev/2.10.3
Browse files Browse the repository at this point in the history
2.10.3 - Some fixes and improvements
  • Loading branch information
vkottler authored Sep 18, 2023
2 parents 5a93700 + 06ec3ef commit 6ec8466
Show file tree
Hide file tree
Showing 15 changed files with 175 additions and 21 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion local/variables/package.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
major: 2
minor: 10
patch: 2
patch: 3
entry: runtimepy
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 2 additions & 2 deletions runtimepy/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# =====================================
# generator=datazen
# version=3.1.3
# hash=3f7d5094777980a2982efb1a76c05c19
# hash=08fd0eee0ba8aebac4d0651f3d3fbfb8
# =====================================

"""
Expand All @@ -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"
18 changes: 18 additions & 0 deletions runtimepy/channel/environment/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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(
(
Expand Down Expand Up @@ -229,3 +236,14 @@ def __eq__(self, other) -> bool:
return bool(
self.channels == other.channels and self.enums == other.enums
)

def age_ns(self, key: _RegistryKey) -> int:
"""Get the age of an entity based on registry key."""

chan = self.get(key)
if chan is not None:
prim = chan[0].raw
else:
prim = self.fields[key].raw

return prim.age_ns()
35 changes: 26 additions & 9 deletions runtimepy/channel/environment/command/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
11 changes: 11 additions & 0 deletions runtimepy/channel/environment/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -142,3 +144,12 @@ def enum(
)
assert result is not None, f"Can't create enum '{name}'!"
return result

def add_field(self, field: BitField, namespace: _Namespace = None) -> str:
"""Add a bit field to the environment."""

fields = BitFields.new()
name = self.namespace(name=field.name, namespace=namespace)
fields.fields[name] = field
self.fields.add(fields)
return name
3 changes: 2 additions & 1 deletion runtimepy/primitives/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -49,6 +49,7 @@
"SignedInt",
"UnsignedInt",
"Primitive",
"StrToBool",
]

AnyPrimitive = _Union[
Expand Down
19 changes: 19 additions & 0 deletions runtimepy/primitives/bool.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
38 changes: 35 additions & 3 deletions runtimepy/primitives/field/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,40 @@
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
self.mask = (2**self.width) - 1
self.shifted_mask = self.mask << self.index

def where_str(self) -> str:
"""
Get a simple string representing the bit locations this field occupies.
"""

result = ""

for idx in range(self.raw.size * 8):
val = 1 << idx
if val & self.shifted_mask:
result += "^"
else:
result += "-"

return result

def __call__(self, val: int = None) -> int:
"""
Set (or get) the underlying value of this field. Return the actual
Expand All @@ -52,6 +75,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."""
Expand All @@ -65,10 +92,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 (
Expand Down Expand Up @@ -108,9 +136,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."""
Expand Down
8 changes: 7 additions & 1 deletion runtimepy/primitives/field/manager/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -114,7 +115,12 @@ 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)
if field.name in self.enum_lookup:
value = self.enum_lookup[field.name].get_int(value)
else:
parsed = StrToBool.parse(value)
if parsed.valid:
value = parsed.result

# Update the value.
field(int(value))
Expand Down
8 changes: 8 additions & 0 deletions tests/channel/environment/test_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
39 changes: 39 additions & 0 deletions tests/channel/environment/test_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand All @@ -15,3 +21,36 @@ def test_channel_environment_create_basic():

result = env.channel("sample_channel", "bool", enum=enum)
assert result

env.age_ns("sample_channel")

name = "test_field"
underlying = Uint8()

assert env.add_field(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
env.age_ns("test_field")

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

assert proc.command("set test_field 1")
assert underlying.value == 1

assert proc.command("set test_field 0")
assert underlying.value == 0
Loading

0 comments on commit 6ec8466

Please sign in to comment.