diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 6af27290..9d042b7f 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -70,7 +70,7 @@ jobs: - run: | mk python-release owner=vkottler \ - repo=runtimepy version=2.0.0 + repo=runtimepy version=2.1.0 if: | matrix.python-version == '3.11' && matrix.system == 'ubuntu-latest' diff --git a/README.md b/README.md index 48392945..f349e2d2 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,11 @@ ===================================== generator=datazen version=3.1.2 - hash=31bf81f7cf74ce38be9e121deda89fbe + hash=81f6301e885d2982710c34e69c537077 ===================================== --> -# runtimepy ([2.0.0](https://pypi.org/project/runtimepy/)) +# runtimepy ([2.1.0](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/configs/package.yaml b/local/configs/package.yaml index decb3591..765d31a5 100644 --- a/local/configs/package.yaml +++ b/local/configs/package.yaml @@ -5,7 +5,7 @@ description: A framework for implementing Python services. entry: {{entry}} requirements: - - vcorelib>=2.0.2 + - vcorelib>=2.5.4 - websockets - "windows-curses; sys_platform == 'win32'" diff --git a/local/variables/package.yaml b/local/variables/package.yaml index 7dca0a01..f288d170 100644 --- a/local/variables/package.yaml +++ b/local/variables/package.yaml @@ -1,5 +1,5 @@ --- major: 2 -minor: 0 +minor: 1 patch: 0 entry: runtimepy diff --git a/pyproject.toml b/pyproject.toml index b7e5dd78..3eeb1fad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta:__legacy__" [project] name = "runtimepy" -version = "2.0.0" +version = "2.1.0" description = "A framework for implementing Python services." readme = "README.md" requires-python = ">=3.8" diff --git a/runtimepy/__init__.py b/runtimepy/__init__.py index 70ed08ea..ce4191e4 100644 --- a/runtimepy/__init__.py +++ b/runtimepy/__init__.py @@ -1,7 +1,7 @@ # ===================================== # generator=datazen # version=3.1.2 -# hash=25c862b538abb92065ece95c39caae77 +# hash=e2256d2409dc864fe3a4f05643274ad3 # ===================================== """ @@ -10,4 +10,4 @@ DESCRIPTION = "A framework for implementing Python services." PKG_NAME = "runtimepy" -VERSION = "2.0.0" +VERSION = "2.1.0" diff --git a/runtimepy/codec/protocol/base.py b/runtimepy/codec/protocol/base.py index e7252c86..bea2fdfe 100644 --- a/runtimepy/codec/protocol/base.py +++ b/runtimepy/codec/protocol/base.py @@ -182,10 +182,15 @@ def value(self, name: str, resolve_enum: bool = True) -> ProtocolPrimitive: return self._fields.get(name, resolve_enum=resolve_enum) + @property + def size(self) -> int: + """Get this protocol's size in bytes.""" + return self.array.length() + def __str__(self) -> str: """Get this instance as a string.""" - return f"({self.array.size})\t" + "\t".join( + return f"({self.size})\t" + "\t".join( f"{name}={self[name]}" for name in self._names.registered_order ) diff --git a/runtimepy/codec/system/__init__.py b/runtimepy/codec/system/__init__.py index 1101085a..32f75c89 100644 --- a/runtimepy/codec/system/__init__.py +++ b/runtimepy/codec/system/__init__.py @@ -3,15 +3,19 @@ """ # built-in -from typing import Dict +from typing import Dict, Optional, Type # third-party from vcorelib.namespace import CPP_DELIM, Namespace # internal +from runtimepy import PKG_NAME from runtimepy.codec.protocol import Protocol -from runtimepy.enum import RuntimeEnum -from runtimepy.enum.registry import EnumRegistry +from runtimepy.enum.registry import ( + DEFAULT_ENUM_PRIMITIVE, + EnumRegistry, + RuntimeIntEnum, +) from runtimepy.primitives.byte_order import ByteOrder from runtimepy.primitives.type import AnyPrimitiveType, PrimitiveTypes @@ -24,6 +28,7 @@ def __init__(self, *namespace: str) -> None: self.primitives: Dict[str, AnyPrimitiveType] = {} self.custom: Dict[str, Protocol] = {} + self._enums = EnumRegistry() global_namespace = Namespace(delim=CPP_DELIM) @@ -31,49 +36,120 @@ def __init__(self, *namespace: str) -> None: for name, kind in PrimitiveTypes.items(): self.primitives[global_namespace.namespace(name)] = kind - self.root_namespace = global_namespace.child(*namespace) + self.root_namespace = global_namespace # Register enums. - self._enums = EnumRegistry() - self.runtime_enum( - "ByteOrder", ByteOrder.register_enum(self._enums, name="ByteOrder") - ) + with self.root_namespace.pushed(PKG_NAME): + for enum in [ByteOrder]: + self.runtime_int_enum(enum) - def register(self, name: str) -> Protocol: + self.root_namespace = global_namespace.child(*namespace) + + def runtime_int_enum(self, enum: Type[RuntimeIntEnum]) -> None: + """Register an enumeration class.""" + + name = self._name(enum.enum_name(), check_available=True) + runtime = enum.register_enum(self._enums, name=name) + self._register_primitive(name, runtime.primitive) + + def enum( + self, + name: str, + items: Dict[str, int], + *namespace: str, + primitive: str = DEFAULT_ENUM_PRIMITIVE, + ) -> None: + """Register an enumeration.""" + + name = self._name(name, *namespace, check_available=True) + + enum = self._enums.enum(name, "int", items=items, primitive=primitive) + assert enum is not None + self._register_primitive(name, enum.primitive) + + def register(self, name: str, *namespace: str) -> Protocol: """Register a custom type.""" new_type = Protocol(self._enums) - name = self.root_namespace.namespace(name) - self.custom[name] = new_type + self.custom[ + self._name(name, *namespace, check_available=True) + ] = new_type return new_type - def runtime_enum(self, name: str, enum: RuntimeEnum) -> bool: - """Register an enumeration.""" + def add(self, custom_type: str, field_name: str, field_type: str) -> None: + """Add a field to a custom type.""" - name = self.root_namespace.namespace(name) + type_name = self._find_name(custom_type, strict=True) + assert type_name is not None + field_type_name = self._find_name(field_type, strict=True) + assert field_type_name is not None - result = self._enums.register(name, enum) + assert type_name in self.custom, type_name + custom = self.custom[type_name] - assert name not in self.primitives, name - self.primitives[name] = PrimitiveTypes[enum.primitive] + # Handle enumerations. + enum = self._enums.get(field_type_name) + if enum is not None: + custom.add_field(field_name, enum=field_type_name) + return + + # Lookup field type. + if field_type_name in self.custom: + custom.array.add_to_end(self.custom[field_type_name].array) + return + + custom.add_field( + field_name, kind=self.primitives[field_type_name].name + ) + + def _find_name( + self, name: str, *namespace: str, strict: bool = False + ) -> Optional[str]: + """Attempt to find a registered name.""" + + if name in self.primitives: + return name + + with self.root_namespace.pushed(*namespace): + matches = list(self.root_namespace.search(pattern=name)) + + assert ( + 0 <= len(matches) <= 1 + ), f"Duplicate type names! {name}: {matches}" + + assert not strict or matches, f"Name '{name}' not found." + + return matches[0] if matches else None + + def _name( + self, name: str, *namespace: str, check_available: bool = False + ) -> str: + """Resolve a given name against the current namespace.""" + + with self.root_namespace.pushed(*namespace): + if check_available: + resolved = self._find_name(name) + assert ( + resolved is None + ), f"Name '{name}' not available! found '{resolved}'" + + result = self.root_namespace.namespace(name) return result - def enum( - self, name: str, items: Dict[str, int], primitive: str = "uint8" - ) -> None: - """Register an enumeration.""" + def _register_primitive(self, name: str, kind: str) -> None: + """Register a type alias for a primitive value.""" - self._enums.enum(name, "int", items=items, primitive=primitive) + assert name not in self.primitives, name + self.primitives[name] = PrimitiveTypes[kind] - def size(self, name: str) -> int: + def size(self, name: str, *namespace: str) -> int: """Get the size of a named type.""" - matches = list(self.root_namespace.search(pattern=name)) - assert len(matches) == 1, f"Duplicate type names {name}: {matches}" - name = matches[0] + found = self._find_name(name, *namespace, strict=True) + assert found is not None - if name in self.primitives: - return self.primitives[name].size + if found in self.primitives: + return self.primitives[found].size - return self.custom[name].array.size + return self.custom[found].size diff --git a/runtimepy/data/schemas/EnumRegistry.yaml b/runtimepy/data/schemas/EnumRegistry.yaml index b158268b..f681d15a 100644 --- a/runtimepy/data/schemas/EnumRegistry.yaml +++ b/runtimepy/data/schemas/EnumRegistry.yaml @@ -3,5 +3,5 @@ type: object additionalProperties: false patternProperties: - "^\\w+$": + "^[\\w\\:]+$": $ref: package://runtimepy/schemas/RuntimeEnum.yaml diff --git a/runtimepy/enum/registry.py b/runtimepy/enum/registry.py index dba3cdbd..2109b7a3 100644 --- a/runtimepy/enum/registry.py +++ b/runtimepy/enum/registry.py @@ -17,6 +17,8 @@ from runtimepy.mapping import EnumMappingData as _EnumMappingData from runtimepy.registry import Registry as _Registry +DEFAULT_ENUM_PRIMITIVE = "uint8" + class EnumRegistry(_Registry[_RuntimeEnum]): """A runtime enumeration registry.""" @@ -31,7 +33,7 @@ def enum( name: str, kind: _EnumTypelike, items: _EnumMappingData = None, - primitive: str = "uint8", + primitive: str = DEFAULT_ENUM_PRIMITIVE, ) -> _Optional[_RuntimeEnum]: """Create a new runtime enumeration.""" @@ -44,6 +46,16 @@ def enum( class RuntimeIntEnum(_IntEnum): """An integer enumeration extension.""" + @classmethod + def primitive(cls) -> str: + """The underlying primitive type for this runtime enumeration.""" + return DEFAULT_ENUM_PRIMITIVE + + @classmethod + def enum_name(cls) -> str: + """Get a name for this enumeration.""" + return cls.__name__ + @classmethod def runtime_enum(cls, identifier: int) -> _RuntimeEnum: """Obtain a runtime enumeration from this class.""" @@ -56,8 +68,10 @@ def register_enum( """Register an enumeration to a registry.""" if name is None: - name = cls.__name__ + name = cls.enum_name() - result = registry.register_dict(name, _RuntimeEnum.data_from_enum(cls)) + data = _RuntimeEnum.data_from_enum(cls) + data["primitive"] = cls.primitive() + result = registry.register_dict(name, data) assert result is not None return result diff --git a/runtimepy/mixins/regex.py b/runtimepy/mixins/regex.py index d558f585..f9969f9b 100644 --- a/runtimepy/mixins/regex.py +++ b/runtimepy/mixins/regex.py @@ -7,7 +7,7 @@ from re import Pattern as _Pattern from re import compile as _compile -DEFAULT_PATTERN = _compile("^\\w+$") +DEFAULT_PATTERN = _compile("^[\\w\\:]+$") CHANNEL_PATTERN = _compile("^[a-z0-9-_.]+$") diff --git a/runtimepy/requirements.txt b/runtimepy/requirements.txt index ed44c000..04c0c9c9 100644 --- a/runtimepy/requirements.txt +++ b/runtimepy/requirements.txt @@ -1,3 +1,3 @@ -vcorelib>=2.0.2 +vcorelib>=2.5.4 websockets windows-curses; sys_platform == 'win32' diff --git a/tests/codec/system/test_system.py b/tests/codec/system/test_system.py index c3019fc7..9f1ea6d9 100644 --- a/tests/codec/system/test_system.py +++ b/tests/codec/system/test_system.py @@ -6,24 +6,71 @@ from runtimepy.codec.system import TypeSystem -def test_type_system_basic(): - """Test basic interactions with a custom type system.""" +def get_test_system() -> TypeSystem: + """Get a simple test system.""" system = TypeSystem("test") - assert system.size("ByteOrder") == 1 + system.enum("TestEnum1", {"a": 1, "b": 2, "c": 3}) + system.enum("TestEnum2", {"a": 1, "b": 2, "c": 3}, primitive="uint16") - new_type = system.register("SomeStruct") - assert new_type + system.register("SampleStruct") + system.add("SampleStruct", "enum1", "TestEnum1") + system.add("SampleStruct", "enum2", "TestEnum2") + system.add("SampleStruct", "field", "uint32") + + assert system.size("SampleStruct") == 7 + + return system + +def test_type_system_compound_types(): + """Test a custom type system with multiple complex types.""" + + system = get_test_system() + + system.register("SomeStruct") assert system.size("SomeStruct") == 0 - system.enum("TestEnum1", {"a": 1, "b": 2, "c": 3}) + system.add("SomeStruct", "enum1", "TestEnum1") + system.add("SomeStruct", "enum2", "TestEnum2") + system.add("SomeStruct", "field", "uint32") + + system.add("SomeStruct", "sample", "SampleStruct") + + system.add("SomeStruct", "field2", "int16") - new_type.add_field("a", enum="TestEnum1") + assert system.size("SomeStruct") == 16 + + +def test_type_system_basic(): + """Test basic interactions with a custom type system.""" + + system = get_test_system() + + assert system.size("runtimepy::ByteOrder") == 1 + + new_type = system.register("SomeStruct") + assert system.size("SomeStruct") == 0 + + system.add("SomeStruct", "a", "TestEnum1") assert new_type.array.size == 1 - system.enum("TestEnum2", {"a": 1, "b": 2, "c": 3}, primitive="uint16") - new_type.add_field("b", enum="TestEnum2") + system.add("SomeStruct", "b", "TestEnum2") assert new_type.array.size == 3 + assert system.size("SomeStruct") == 3 + + for field in [ + "uint8", + "uint16", + "uint32", + "uint64", + "int8", + "int16", + "int32", + "int64", + "float", + "double", + ]: + system.add("SomeStruct", f"{field}_field", field)