Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for strings and byte arrays #97

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion pydsdl/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import sys as _sys
from pathlib import Path as _Path

__version__ = "1.20.1"
__version__ = "1.21.0"
__version_info__ = tuple(map(int, __version__.split(".")[:3]))
__license__ = "MIT"
__author__ = "OpenCyphal"
Expand Down Expand Up @@ -41,6 +41,8 @@
from ._serializable import IntegerType as IntegerType
from ._serializable import SignedIntegerType as SignedIntegerType
from ._serializable import UnsignedIntegerType as UnsignedIntegerType
from ._serializable import ByteType as ByteType
from ._serializable import UTF8Type as UTF8Type
from ._serializable import FloatType as FloatType
from ._serializable import VoidType as VoidType
from ._serializable import ArrayType as ArrayType
Expand Down
16 changes: 11 additions & 5 deletions pydsdl/_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ def generic_visit(self, node: _Node, visited_children: typing.Sequence[typing.An
"""If the node has children, replace the node with them."""
return tuple(visited_children) or node

def visit_line(self, node: _Node, children: _Children) -> None:
def visit_line(self, node: _Node, _c: _Children) -> None:
if len(node.text) == 0:
# Line is empty, flush comment
self._flush_comment()
Expand All @@ -173,7 +173,7 @@ def visit_end_of_line(self, _n: _Node, _c: _Children) -> None:
visit_statement_attribute = _make_typesafe_child_lifter(type(None)) # because processing terminates here; these
visit_statement_directive = _make_typesafe_child_lifter(type(None)) # nodes are above the top level.

def visit_comment(self, node: _Node, children: _Children) -> None:
def visit_comment(self, node: _Node, _c: _Children) -> None:
assert isinstance(node.text, str)
self._comment += "\n" if self._comment != "" else ""
self._comment += node.text[2:] if node.text.startswith("# ") else node.text[1:]
Expand Down Expand Up @@ -262,6 +262,15 @@ def visit_type_version_specifier(self, _n: _Node, children: _Children) -> _seria
assert isinstance(major, _expression.Rational) and isinstance(minor, _expression.Rational)
return _serializable.Version(major=major.as_native_integer(), minor=minor.as_native_integer())

def visit_type_primitive_boolean(self, _n: _Node, _c: _Children) -> _serializable.PrimitiveType:
return _serializable.BooleanType()

def visit_type_primitive_byte(self, _n: _Node, _c: _Children) -> _serializable.PrimitiveType:
return _serializable.ByteType()

def visit_type_primitive_utf8(self, _n: _Node, _c: _Children) -> _serializable.PrimitiveType:
return _serializable.UTF8Type()

def visit_type_primitive_truncated(self, _n: _Node, children: _Children) -> _serializable.PrimitiveType:
_kw, _sp, cons = children # type: _Node, _Node, _PrimitiveTypeConstructor
return cons(_serializable.PrimitiveType.CastMode.TRUNCATED)
Expand All @@ -270,9 +279,6 @@ def visit_type_primitive_saturated(self, _n: _Node, children: _Children) -> _ser
_, cons = children # type: _Node, _PrimitiveTypeConstructor
return cons(_serializable.PrimitiveType.CastMode.SATURATED)

def visit_type_primitive_name_boolean(self, _n: _Node, _c: _Children) -> _PrimitiveTypeConstructor:
return typing.cast(_PrimitiveTypeConstructor, _serializable.BooleanType)

def visit_type_primitive_name_unsigned_integer(self, _n: _Node, children: _Children) -> _PrimitiveTypeConstructor:
return lambda cm: _serializable.UnsignedIntegerType(children[-1], cm)

Expand Down
2 changes: 2 additions & 0 deletions pydsdl/_serializable/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
from ._primitive import IntegerType as IntegerType
from ._primitive import SignedIntegerType as SignedIntegerType
from ._primitive import UnsignedIntegerType as UnsignedIntegerType
from ._primitive import ByteType as ByteType
from ._primitive import UTF8Type as UTF8Type

from ._void import VoidType as VoidType

Expand Down
27 changes: 21 additions & 6 deletions pydsdl/_serializable/_array.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
import abc
import math
import typing
import warnings
from .._bit_length_set import BitLengthSet
from ._serializable import SerializableType, TypeParameterError
from ._serializable import SerializableType, TypeParameterError, AggregationFailure
from ._primitive import UnsignedIntegerType, PrimitiveType


Expand All @@ -22,6 +23,16 @@ def __init__(self, element_type: SerializableType, capacity: int):
if self._capacity < 1:
raise InvalidNumberOfElementsError("Array capacity cannot be less than 1")

@property
def deprecated(self) -> bool:
return self.element_type.deprecated

def _check_aggregation(self, aggregate: "SerializableType") -> typing.Optional[AggregationFailure]:
af = self.element_type._check_aggregation(self) # pylint: disable=protected-access
if af is not None:
return AggregationFailure(self, aggregate, "Element type of %r is not valid: %s" % (str(self), af.message))
return super()._check_aggregation(aggregate)

@property
def element_type(self) -> SerializableType:
return self._element_type
Expand All @@ -36,10 +47,10 @@ def capacity(self) -> int:
@property
def string_like(self) -> bool:
"""
True if the array might contain a text string, in which case it is termed to be "string-like".
A string-like array is a variable-length array of ``uint8``.
See https://github.com/OpenCyphal/specification/issues/51.
**This property is deprecated** and will be removed in a future release.
Replace with an explicit check for ``isinstance(array.element_type, UTF8Type)``.
"""
warnings.warn("use isinstance(array.element_type, UTF8Type) instead of string_like", DeprecationWarning)
return False

@property
Expand Down Expand Up @@ -149,9 +160,13 @@ def bit_length_set(self) -> BitLengthSet:

@property
def string_like(self) -> bool:
"""See the base class."""
from ._primitive import UTF8Type

warnings.warn("use isinstance(array.element_type, UTF8Type) instead of string_like", DeprecationWarning)
et = self.element_type # Without this temporary MyPy yields a false positive type error
return isinstance(et, UnsignedIntegerType) and (et.bit_length == self.BITS_PER_BYTE)
return isinstance(et, UTF8Type) or (
isinstance(et, UnsignedIntegerType) and (et.bit_length == self.BITS_PER_BYTE)
)

@property
def length_field_type(self) -> UnsignedIntegerType:
Expand Down
4 changes: 2 additions & 2 deletions pydsdl/_serializable/_attribute.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,9 +155,9 @@ def _unittest_attribute() -> None:
from pytest import raises
from ._primitive import SignedIntegerType

assert str(Field(BooleanType(PrimitiveType.CastMode.SATURATED), "flag")) == "saturated bool flag"
assert str(Field(BooleanType(), "flag")) == "bool flag"
assert (
repr(Field(BooleanType(PrimitiveType.CastMode.SATURATED), "flag"))
repr(Field(BooleanType(), "flag"))
== "Field(data_type=BooleanType(bit_length=1, cast_mode=<CastMode.SATURATED: 0>), name='flag')"
)

Expand Down
49 changes: 24 additions & 25 deletions pydsdl/_serializable/_composite.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@
from .. import _expression
from .. import _port_id_ranges
from .._bit_length_set import BitLengthSet
from ._serializable import SerializableType, TypeParameterError
from .._error import InvalidDefinitionError
from ._serializable import SerializableType, TypeParameterError, AggregationFailure
from ._attribute import Attribute, Field, PaddingField, Constant
from ._name import check_name, InvalidNameError
from ._void import VoidType
from ._primitive import PrimitiveType, UnsignedIntegerType


Expand All @@ -40,7 +40,7 @@ class MalformedUnionError(TypeParameterError):
pass


class DeprecatedDependencyError(TypeParameterError):
class AggregationError(InvalidDefinitionError):
pass


Expand Down Expand Up @@ -82,10 +82,8 @@ def __init__( # pylint: disable=too-many-arguments
# Name check
if not self._name:
raise InvalidNameError("Composite type name cannot be empty")

if self.NAME_COMPONENT_SEPARATOR not in self._name:
raise InvalidNameError("Root namespace is not specified")

if len(self._name) > self.MAX_NAME_LENGTH:
# TODO
# Notice that per the Specification, service request/response types are unnamed,
Expand All @@ -96,7 +94,6 @@ def __init__( # pylint: disable=too-many-arguments
raise InvalidNameError(
"Name is too long: %r is longer than %d characters" % (self._name, self.MAX_NAME_LENGTH)
)

for component in self._name.split(self.NAME_COMPONENT_SEPARATOR):
check_name(component)

Expand All @@ -106,7 +103,6 @@ def __init__( # pylint: disable=too-many-arguments
and (0 <= self._version.minor <= self.MAX_VERSION_NUMBER)
and ((self._version.major + self._version.minor) > 0)
)

if not version_valid:
raise InvalidVersionError("Invalid version numbers: %s.%s" % (self._version.major, self._version.minor))

Expand All @@ -128,17 +124,13 @@ def __init__( # pylint: disable=too-many-arguments
if not (0 <= port_id <= _port_id_ranges.MAX_SUBJECT_ID):
raise InvalidFixedPortIDError("Fixed subject ID %r is not valid" % port_id)

# Consistent deprecation check.
# A non-deprecated type cannot be dependent on deprecated types.
# A deprecated type can be dependent on anything.
if not self.deprecated:
for a in self._attributes:
t = a.data_type
if isinstance(t, CompositeType):
if t.deprecated:
raise DeprecatedDependencyError(
"A type cannot depend on deprecated types " "unless it is also deprecated."
)
# Aggregation check. For example:
# - Types like utf8 and byte cannot be used outside of arrays.
# - A non-deprecated type cannot depend on a deprecated type.
for a in self._attributes:
af = a.data_type._check_aggregation(self)
if af is not None:
raise AggregationError("Type of %r is not a valid field type for %s: %s" % (str(a), self, af.message))

@property
def full_name(self) -> str:
Expand Down Expand Up @@ -196,9 +188,12 @@ def bit_length_set(self) -> BitLengthSet:
"""
raise NotImplementedError

def _check_aggregation(self, aggregate: "SerializableType") -> typing.Optional[AggregationFailure]:
return super()._check_aggregation(aggregate)

@property
def deprecated(self) -> bool:
"""Whether the definition is marked ``@deprecated``."""
"""True if the definition is marked ``@deprecated``."""
return self._deprecated

@property
Expand Down Expand Up @@ -371,10 +366,6 @@ def __init__( # pylint: disable=too-many-arguments
"A tagged union cannot contain fewer than %d variants" % self.MIN_NUMBER_OF_VARIANTS
)

for a in attributes:
if isinstance(a, PaddingField) or not a.name or isinstance(a.data_type, VoidType):
raise MalformedUnionError("Padding fields not allowed in unions")

self._tag_field_type = UnsignedIntegerType(
self._compute_tag_bit_length([x.data_type for x in self.fields]), PrimitiveType.CastMode.TRUNCATED
)
Expand Down Expand Up @@ -612,6 +603,12 @@ def iterate_fields_with_offsets(
base_offset = base_offset + self.delimiter_header_type.bit_length_set
return self.inner_type.iterate_fields_with_offsets(base_offset)

def _check_aggregation(self, aggregate: "SerializableType") -> typing.Optional[AggregationFailure]:
af = self.inner_type._check_aggregation(aggregate) # pylint: disable=protected-access
if af is not None:
return af
return super()._check_aggregation(aggregate)

def __repr__(self) -> str:
return "%s(inner=%r, extent=%r)" % (self.__class__.__name__, self.inner_type, self.extent)

Expand Down Expand Up @@ -685,6 +682,7 @@ def _unittest_composite_types() -> None: # pylint: disable=too-many-statements
from pytest import raises
from ._primitive import SignedIntegerType, FloatType
from ._array import FixedLengthArrayType, VariableLengthArrayType
from ._void import VoidType

def try_name(name: str) -> CompositeType:
return StructureType(
Expand Down Expand Up @@ -737,7 +735,7 @@ def try_name(name: str) -> CompositeType:
has_parent_service=False,
)

with raises(MalformedUnionError, match="(?i).*padding.*"):
with raises(AggregationError, match="(?i).*not a valid field type.*"):
UnionType(
name="a.A",
version=Version(0, 1),
Expand Down Expand Up @@ -947,6 +945,7 @@ def _unittest_field_iterators() -> None: # pylint: disable=too-many-locals
from pytest import raises
from ._primitive import BooleanType, FloatType
from ._array import FixedLengthArrayType, VariableLengthArrayType
from ._void import VoidType

saturated = PrimitiveType.CastMode.SATURATED
_seq_no = 0
Expand Down Expand Up @@ -979,7 +978,7 @@ def validate_iterator(
StructureType,
[
Field(UnsignedIntegerType(10, saturated), "a"),
Field(BooleanType(saturated), "b"),
Field(BooleanType(), "b"),
Field(VariableLengthArrayType(FloatType(32, saturated), 2), "c"),
Field(FixedLengthArrayType(FloatType(32, saturated), 7), "d"),
PaddingField(VoidType(3)),
Expand Down
Loading