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

New pyln modules. #3733

Merged
merged 18 commits into from
Jun 12, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
ca71845
pyln: add Makefile
rustyrussell May 18, 2020
31fa55e
pyln: add pyln.proto.message.
rustyrussell May 28, 2020
16297af
patch message-export-types.patch
rustyrussell Jun 3, 2020
1bff330
pyln.proto.message: use BufferedIOBase instead of bytes for binary ops.
rustyrussell Jun 4, 2020
46151ed
pyln.proto.message: expose fundamental MessageTypes as variables.
rustyrussell Jun 4, 2020
e274226
pyln: add (undocumented) u8 fundamental type.
rustyrussell Jun 4, 2020
59e2064
message: support option fields.
rustyrussell Jun 4, 2020
784d138
pyln.proto.message: separate fundamental types from other subtypes.
rustyrussell Jun 4, 2020
a4eb933
pyln: new module pyln.proto.message.bolts
rustyrussell Jun 4, 2020
42fc48f
new modules: pyln.proto.message.{bolt1,bolt2,bolt4,bolt7}
rustyrussell Jun 4, 2020
3ed6831
pyln.proto.message: support adding two namespaces.
rustyrussell Jun 4, 2020
360bcaf
pyln.proto.message: python-fluency feedback from @darosior
rustyrussell Jun 4, 2020
4a336db
pyln.proto.message: export more.
rustyrussell Jun 4, 2020
efd38a4
pyln.proto.message: allow fields with options to be missing.
rustyrussell Jun 4, 2020
bef68d3
pyln.proto.message.*: Add Makefile to do mypy checks.
rustyrussell Jun 4, 2020
851122d
pyln.proto.message.*: add type annotations.
rustyrussell Jun 12, 2020
e0d3174
pyln.proto.message: expose array types, add set_field for Message class.
rustyrussell Jun 12, 2020
5dbec8f
pyln.proto.message: fix handling of missing optional fields.
rustyrussell Jun 12, 2020
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
*.po
*.pyc
.cppcheck-suppress
.mypy_cache
TAGS
tags
ccan/tools/configurator/configurator
Expand Down
13 changes: 13 additions & 0 deletions contrib/pyln-proto/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#! /usr/bin/make

check:
pytest

check-source: check-flake8 check-mypy

check-flake8:
flake8 --ignore=E501,E731,W503

# mypy . does not recurse. I have no idea why...
check-mypy:
mypy --ignore-missing-imports `find * -name '*.py'`
4 changes: 4 additions & 0 deletions contrib/pyln-proto/pyln/proto/message/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#! /usr/bin/make

refresh:
for d in bolt*; do $(MAKE) -C $$d; done
33 changes: 33 additions & 0 deletions contrib/pyln-proto/pyln/proto/message/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from .array_types import SizedArrayType, DynamicArrayType, EllipsisArrayType
from .message import MessageNamespace, MessageType, Message, SubtypeType
from .fundamental_types import split_field, FieldType

__version__ = '0.0.1'

__all__ = [
"MessageNamespace",
"MessageType",
"Message",
"SubtypeType",
"FieldType",
"split_field",
"SizedArrayType",
"DynamicArrayType",
"EllipsisArrayType",
Comment on lines +14 to +16
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these only required for unit tests? In that case we could also not expose them here, and have the unit tests reach directly into .array_types

from pyln.proto.message.array_types import SizedArrayType, DynamicArrayType, EllipsisArrayType

If that helps keep the interface clean for the actual protocol tests.


# fundamental_types
'byte',
'u16',
'u32',
'u64',
'tu16',
'tu32',
'tu64',
'chain_hash',
'channel_id',
'sha256',
'point',
'short_channel_id',
'signature',
'bigsize',
]
184 changes: 184 additions & 0 deletions contrib/pyln-proto/pyln/proto/message/array_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
from .fundamental_types import FieldType, IntegerType, split_field
from typing import List, Optional, Dict, Tuple, TYPE_CHECKING, Any
from io import BufferedIOBase
if TYPE_CHECKING:
from .message import SubtypeType, TlvStreamType


class ArrayType(FieldType):
"""Abstract class for the different kinds of arrays.

These are not in the namespace, but generated when a message says it
wants an array of some type.

"""
def __init__(self, outer: 'SubtypeType', name: str, elemtype: FieldType):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems you were far quicker than me finding out about recursive type definitions being possible via string annotations 😉

I got quite lost when trying to figure out how to annotate something with a type that'd be defined later...

super().__init__("{}.{}".format(outer.name, name))
self.elemtype = elemtype

def val_from_str(self, s: str) -> Tuple[List[Any], str]:
# Simple arrays of bytes don't need commas
if self.elemtype.name == 'byte':
a, b = split_field(s)
return [b for b in bytes.fromhex(a)], b

if not s.startswith('['):
raise ValueError("array of {} must be wrapped in '[]': bad {}"
.format(self.elemtype.name, s))
s = s[1:]
ret = []
while not s.startswith(']'):
val, s = self.elemtype.val_from_str(s)
ret.append(val)
if s[0] == ',':
s = s[1:]
return ret, s[1:]

def val_to_str(self, v: List[Any], otherfields: Dict[str, Any]) -> str:
if self.elemtype.name == 'byte':
return bytes(v).hex()

s = ','.join(self.elemtype.val_to_str(i, otherfields) for i in v)
return '[' + s + ']'

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)

def read_arr(self, io_in: BufferedIOBase, otherfields: Dict[str, Any], arraysize: Optional[int]) -> List[Any]:
"""arraysize None means take rest of io entirely and exactly"""
vals: List[Any] = []
while arraysize is None or len(vals) < arraysize:
# Throws an exception on partial read, so None means completely empty.
val = self.elemtype.read(io_in, otherfields)
if val is None:
if arraysize is not None:
raise ValueError('{}: not enough remaining to read'
.format(self))
break

vals.append(val)

return vals


class SizedArrayType(ArrayType):
"""A fixed-size array"""
def __init__(self, outer: 'SubtypeType', name: str, elemtype: FieldType, arraysize: int):
super().__init__(outer, name, elemtype)
self.arraysize = arraysize

def val_to_str(self, v: List[Any], otherfields: Dict[str, Any]) -> str:
if len(v) != self.arraysize:
raise ValueError("Length of {} != {}", v, self.arraysize)
return super().val_to_str(v, otherfields)

def val_from_str(self, s: str) -> Tuple[List[Any], str]:
a, b = super().val_from_str(s)
if len(a) != self.arraysize:
raise ValueError("Length of {} != {}", s, self.arraysize)
return a, b

def write(self, io_out: BufferedIOBase, v: List[Any], otherfields: Dict[str, Any]) -> None:
if len(v) != self.arraysize:
raise ValueError("Length of {} != {}", v, self.arraysize)
return super().write(io_out, v, otherfields)

def read(self, io_in: BufferedIOBase, otherfields: Dict[str, Any]) -> List[Any]:
return super().read_arr(io_in, otherfields, self.arraysize)


class EllipsisArrayType(ArrayType):
"""This is used for ... fields at the end of a tlv: the array ends
when the tlv ends"""
def __init__(self, tlv: 'TlvStreamType', name: str, elemtype: FieldType):
super().__init__(tlv, name, elemtype)

def read(self, io_in: BufferedIOBase, otherfields: Dict[str, Any]) -> List[Any]:
"""Takes rest of bytestream"""
return super().read_arr(io_in, otherfields, None)

def only_at_tlv_end(self) -> bool:
"""These only make sense at the end of a TLV"""
return True


class LengthFieldType(FieldType):
"""Special type to indicate this serves as a length field for others"""
def __init__(self, inttype: IntegerType):
if type(inttype) is not IntegerType:
raise ValueError("{} cannot be a length; not an integer!"
.format(self.name))
super().__init__(inttype.name)
self.underlying_type = inttype
# You can be length for more than one field!
self.len_for: List[DynamicArrayType] = []

def is_optional(self) -> bool:
"""This field value is always implies, never specified directly"""
return True

def add_length_for(self, field: 'DynamicArrayType') -> None:
assert isinstance(field.fieldtype, DynamicArrayType)
self.len_for.append(field)

def calc_value(self, otherfields: Dict[str, Any]) -> int:
"""Calculate length value from field(s) themselves"""
if self.len_fields_bad('', otherfields):
raise ValueError("Lengths of fields {} not equal!"
.format(self.len_for))

return len(otherfields[self.len_for[0].name])

def _maybe_calc_value(self, fieldname: str, otherfields: Dict[str, Any]) -> int:
# Perhaps we're just demarshalling from binary now, so we actually
# stored it. Remove, and we'll calc from now on.
if fieldname in otherfields:
v = otherfields[fieldname]
del otherfields[fieldname]
return v
return self.calc_value(otherfields)

def val_to_str(self, _, otherfields: Dict[str, Any]) -> str:
return self.underlying_type.val_to_str(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"""
return ''

def read(self, io_in: BufferedIOBase, otherfields: Dict[str, Any]) -> None:
"""We store this, but it'll be removed from the fields as soon as it's used (i.e. by DynamicArrayType's val_from_bin)"""
return self.underlying_type.read(io_in, otherfields)

def write(self, io_out: BufferedIOBase, _, otherfields: Dict[str, Any]) -> None:
self.underlying_type.write(io_out, self.calc_value(otherfields),
otherfields)

def val_from_str(self, s: str):
raise ValueError('{} is implied, cannot be specified'.format(self))

def len_fields_bad(self, fieldname: str, otherfields: Dict[str, Any]) -> List[str]:
"""fieldname is the name to return if this length is bad"""
mylen = None
for lens in self.len_for:
if mylen is not None:
if mylen != len(otherfields[lens.name]):
return [fieldname]
# Field might be missing!
if lens.name in otherfields:
mylen = len(otherfields[lens.name])
return []


class DynamicArrayType(ArrayType):
"""This is used for arrays where another field controls the size"""
def __init__(self, outer: 'SubtypeType', name: str, elemtype: FieldType, lenfield: LengthFieldType):
super().__init__(outer, name, elemtype)
assert type(lenfield.fieldtype) is LengthFieldType
self.lenfield = lenfield

def read(self, io_in: BufferedIOBase, otherfields: Dict[str, Any]) -> List[Any]:
return super().read_arr(io_in, otherfields,
self.lenfield.fieldtype._maybe_calc_value(self.lenfield.name, otherfields))
7 changes: 7 additions & 0 deletions contrib/pyln-proto/pyln/proto/message/bolt1/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#! /usr/bin/make

SPECDIR := ../../../../../../../lightning-rfc

csv.py: $(SPECDIR)/01-messaging.md Makefile
SPECNUM=`basename $< | sed 's/-.*//'`; (echo csv = '['; python3 $(SPECDIR)/tools/extract-formats.py $< | sed 's/\(.*\)/ "\1",/'; echo ']') > $@
chmod a+x $@
16 changes: 16 additions & 0 deletions contrib/pyln-proto/pyln/proto/message/bolt1/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from .csv import csv
from .bolt import namespace
import sys

__version__ = '0.0.1'

__all__ = [
'csv',
'namespace',
]

mod = sys.modules[__name__]
for d in namespace.subtypes, namespace.tlvtypes, namespace.messagetypes:
for name in d:
setattr(mod, name, d[name])
__all__.append(name)
5 changes: 5 additions & 0 deletions contrib/pyln-proto/pyln/proto/message/bolt1/bolt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from pyln.proto.message import MessageNamespace
from .csv import csv


namespace = MessageNamespace(csv_lines=csv)
35 changes: 35 additions & 0 deletions contrib/pyln-proto/pyln/proto/message/bolt1/csv.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
csv = [
"msgtype,init,16",
"msgdata,init,gflen,u16,",
"msgdata,init,globalfeatures,byte,gflen",
"msgdata,init,flen,u16,",
"msgdata,init,features,byte,flen",
"msgdata,init,tlvs,init_tlvs,",
"tlvtype,init_tlvs,networks,1",
"tlvdata,init_tlvs,networks,chains,chain_hash,...",
"msgtype,error,17",
"msgdata,error,channel_id,channel_id,",
"msgdata,error,len,u16,",
"msgdata,error,data,byte,len",
"msgtype,ping,18",
"msgdata,ping,num_pong_bytes,u16,",
"msgdata,ping,byteslen,u16,",
"msgdata,ping,ignored,byte,byteslen",
"msgtype,pong,19",
"msgdata,pong,byteslen,u16,",
"msgdata,pong,ignored,byte,byteslen",
"tlvtype,n1,tlv1,1",
"tlvdata,n1,tlv1,amount_msat,tu64,",
"tlvtype,n1,tlv2,2",
"tlvdata,n1,tlv2,scid,short_channel_id,",
"tlvtype,n1,tlv3,3",
"tlvdata,n1,tlv3,node_id,point,",
"tlvdata,n1,tlv3,amount_msat_1,u64,",
"tlvdata,n1,tlv3,amount_msat_2,u64,",
"tlvtype,n1,tlv4,254",
"tlvdata,n1,tlv4,cltv_delta,u16,",
"tlvtype,n2,tlv1,0",
"tlvdata,n2,tlv1,amount_msat,tu64,",
"tlvtype,n2,tlv2,11",
"tlvdata,n2,tlv2,cltv_expiry,tu32,",
]
7 changes: 7 additions & 0 deletions contrib/pyln-proto/pyln/proto/message/bolt2/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#! /usr/bin/make

SPECDIR := ../../../../../../../lightning-rfc

csv.py: $(SPECDIR)/02-peer-protocol.md Makefile
SPECNUM=`basename $< | sed 's/-.*//'`; (echo csv = '['; python3 $(SPECDIR)/tools/extract-formats.py $< | sed 's/\(.*\)/ "\1",/'; echo ']') > $@
chmod a+x $@
16 changes: 16 additions & 0 deletions contrib/pyln-proto/pyln/proto/message/bolt2/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from .csv import csv
from .bolt import namespace
import sys

__version__ = '0.0.1'

__all__ = [
'csv',
'namespace',
]

mod = sys.modules[__name__]
for d in namespace.subtypes, namespace.tlvtypes, namespace.messagetypes:
for name in d:
setattr(mod, name, d[name])
__all__.append(name)
5 changes: 5 additions & 0 deletions contrib/pyln-proto/pyln/proto/message/bolt2/bolt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from pyln.proto.message import MessageNamespace
from .csv import csv


namespace = MessageNamespace(csv_lines=csv)
Loading