From 02338a6b25d509417d4dc3581e11c7a1bcbb5caa Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Tue, 16 Jun 2020 17:03:13 +0930 Subject: [PATCH] pyln.proto.message: add to_py() operation. This delivers the message contents in a much friendlier form for manipulation: in particular, it makes it easy to compare two messages without having to know all the message type internals. Signed-off-by: Rusty Russell --- .../pyln/proto/message/array_types.py | 14 ++++++++++- .../pyln/proto/message/fundamental_types.py | 20 ++++++++++++++++ .../pyln-proto/pyln/proto/message/message.py | 23 ++++++++++++++++++- 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/contrib/pyln-proto/pyln/proto/message/array_types.py b/contrib/pyln-proto/pyln/proto/message/array_types.py index 077609dd4..ea8616d2d 100644 --- a/contrib/pyln-proto/pyln/proto/message/array_types.py +++ b/contrib/pyln-proto/pyln/proto/message/array_types.py @@ -1,5 +1,5 @@ from .fundamental_types import FieldType, IntegerType, split_field -from typing import List, Optional, Dict, Tuple, TYPE_CHECKING, Any +from typing import List, Optional, Dict, Tuple, TYPE_CHECKING, Any, Union from io import BufferedIOBase if TYPE_CHECKING: from .message import SubtypeType, TlvStreamType @@ -41,6 +41,13 @@ wants an array of some type. s = ','.join(self.elemtype.val_to_str(i, otherfields) for i in v) return '[' + s + ']' + def val_to_py(self, v: Any, otherfields: Dict[str, Any]) -> Union[str, List[Any]]: + """Convert to a python object: for arrays, this means a list (or hex, if bytes)""" + if self.elemtype.name == 'byte': + return bytes(v).hex() + + return [self.elemtype.val_to_py(i, otherfields) for i in v] + def write(self, io_out: BufferedIOBase, v: List[Any], otherfields: Dict[str, Any]) -> None: for i in v: self.elemtype.write(io_out, i, otherfields) @@ -143,6 +150,11 @@ class LengthFieldType(FieldType): return self.underlying_type.val_to_str(self.calc_value(otherfields), otherfields) + def val_to_py(self, v: Any, otherfields: Dict[str, Any]) -> int: + """Convert to a python object: for integer fields, this means an int""" + return self.underlying_type.val_to_py(self.calc_value(otherfields), + otherfields) + def name_and_val(self, name: str, v: int) -> str: """We don't print out length fields when printing out messages: they're implied by the length of other fields""" diff --git a/contrib/pyln-proto/pyln/proto/message/fundamental_types.py b/contrib/pyln-proto/pyln/proto/message/fundamental_types.py index 80c21570e..8341a5c90 100644 --- a/contrib/pyln-proto/pyln/proto/message/fundamental_types.py +++ b/contrib/pyln-proto/pyln/proto/message/fundamental_types.py @@ -59,6 +59,10 @@ These are further specialized. def val_to_str(self, v: Any, otherfields: Dict[str, Any]) -> str: raise NotImplementedError() + def val_to_py(self, v: Any, otherfields: Dict[str, Any]) -> Any: + """Convert to a python object: for simple fields, this means a string""" + return self.val_to_str(v, otherfields) + def __str__(self): return self.name @@ -79,6 +83,10 @@ class IntegerType(FieldType): a, b = split_field(s) return int(a), b + def val_to_py(self, v: Any, otherfields: Dict[str, Any]) -> int: + """Convert to a python object: for integer fields, this means an int""" + return int(v) + def write(self, io_out: BufferedIOBase, v: int, otherfields: Dict[str, Any]) -> None: io_out.write(struct.pack(self.structfmt, v)) @@ -107,6 +115,10 @@ basically a u64. | (int(parts[1]) << 16) | (int(parts[2]))), b + def val_to_py(self, v: Any, otherfields: Dict[str, Any]) -> str: + # Unlike a normal int, this returns a str. + return self.val_to_str(v, otherfields) + class TruncatedIntType(FieldType): """Truncated integer types""" @@ -128,6 +140,10 @@ class TruncatedIntType(FieldType): .format(a, self.name)) return int(a), b + def val_to_py(self, v: Any, otherfields: Dict[str, Any]) -> int: + """Convert to a python object: for integer fields, this means an int""" + return int(v) + def write(self, io_out: BufferedIOBase, v: int, otherfields: Dict[str, Any]) -> None: binval = struct.pack('>Q', v) while len(binval) != 0 and binval[0] == 0: @@ -219,6 +235,10 @@ class BigSizeType(FieldType): def val_to_str(self, v: int, otherfields: Dict[str, Any]) -> str: return "{}".format(int(v)) + def val_to_py(self, v: Any, otherfields: Dict[str, Any]) -> int: + """Convert to a python object: for integer fields, this means an int""" + return int(v) + def fundamental_types(): # From 01-messaging.md#fundamental-types: diff --git a/contrib/pyln-proto/pyln/proto/message/message.py b/contrib/pyln-proto/pyln/proto/message/message.py index 01d4fb534..d8b5d848d 100644 --- a/contrib/pyln-proto/pyln/proto/message/message.py +++ b/contrib/pyln-proto/pyln/proto/message/message.py @@ -4,7 +4,7 @@ from .fundamental_types import fundamental_types, BigSizeType, split_field, try_ from .array_types import ( SizedArrayType, DynamicArrayType, LengthFieldType, EllipsisArrayType ) -from typing import Dict, List, Optional, Tuple, Any, cast +from typing import Dict, List, Optional, Tuple, Any, Union, cast class MessageNamespace(object): @@ -278,6 +278,12 @@ inherit from this too. return '{' + s + '}' + def val_to_py(self, val: Dict[str, Any], otherfields: Dict[str, Any]) -> Dict[str, Any]: + ret: Dict[str, Any] = {} + for k, v in val.items(): + ret[k] = self.find_field(k).fieldtype.val_to_py(v, val) + return ret + def write(self, io_out: BufferedIOBase, v: Dict[str, Any], otherfields: Dict[str, Any]) -> None: self._raise_if_badvals(v) for fname, val in v.items(): @@ -471,6 +477,12 @@ tlvdata,reply_channel_range_tlvs,timestamps_tlv,encoding_type,u8, return '{' + s + '}' + def val_to_py(self, val: Dict[str, Any], otherfields: Dict[str, Any]) -> Dict[str, Any]: + ret: Dict[str, Any] = {} + for k, v in val.items(): + ret[k] = self.find_field(k).val_to_py(v, val) + return ret + def write(self, io_out: BufferedIOBase, v: Optional[Dict[str, Any]], otherfields: Dict[str, Any]) -> None: # If they didn't specify this tlvstream, it's empty. if v is None: @@ -662,3 +674,12 @@ Must not have missing fields. if f.name in self.fields: ret += f.fieldtype.name_and_val(f.name, self.fields[f.name]) return ret + + def to_py(self) -> Dict[str, Any]: + """Convert to a Python native object: dicts, lists, strings, ints""" + ret: Dict[str, Union[Dict[str, Any], List[Any], str, int]] = {} + for f, v in self.fields.items(): + fieldtype = self.messagetype.find_field(f).fieldtype + ret[f] = fieldtype.val_to_py(v, self.fields) + + return ret