From 9988ec44462b1bb14f85fe7fdee147b4856b5677 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Tue, 21 Nov 2023 14:23:35 -0500 Subject: [PATCH 1/8] Add serialize method and bencode Serialize should transform objects into a simple Python structure and bencode should take that structure and turn it into binary. There is no deserialization yet. --- scrapscript.py | 118 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 116 insertions(+), 2 deletions(-) diff --git a/scrapscript.py b/scrapscript.py index ff470474..db6298f5 100755 --- a/scrapscript.py +++ b/scrapscript.py @@ -345,37 +345,54 @@ def parse(tokens: typing.List[str], p: float = 0) -> "Object": @dataclass(eq=True, frozen=True, unsafe_hash=True) class Object: - pass + def serialize(self) -> dict[bytes, object]: + raise NotImplementedError("{type(self).__name__}.serialize()") @dataclass(eq=True, frozen=True, unsafe_hash=True) class Int(Object): value: int + def serialize(self) -> dict[bytes, object]: + return {b"type": b"Int", b"value": self.value} + @dataclass(eq=True, frozen=True, unsafe_hash=True) class String(Object): value: str + def serialize(self) -> dict[bytes, object]: + return {b"type": b"String", b"value": self.value.encode("utf-8")} + @dataclass(eq=True, frozen=True, unsafe_hash=True) class Bytes(Object): value: bytes + def serialize(self) -> dict[bytes, object]: + return {b"type": b"Bytes", b"value": self.value} + @dataclass(eq=True, frozen=True, unsafe_hash=True) class Var(Object): name: str + def serialize(self) -> dict[bytes, object]: + return {b"type": b"Var", b"name": self.name.encode("utf-8")} + @dataclass(eq=True, frozen=True, unsafe_hash=True) class Bool(Object): value: bool + def serialize(self) -> dict[bytes, object]: + return {b"type": b"Bool", b"value": self.value} + @dataclass(eq=True, frozen=True, unsafe_hash=True) class Hole(Object): - pass + def serialize(self) -> dict[bytes, object]: + return {b"type": b"Hole"} Env = Mapping[str, Object] @@ -664,6 +681,23 @@ def eval_exp(env: Env, exp: Object) -> Object: raise NotImplementedError(f"eval_exp not implemented for {exp}") +def bencode(obj: object) -> bytes: + if isinstance(obj, int): + return b"i" + str(int(obj)).encode("ascii") + b"e" + if isinstance(obj, bytes): + return str(len(obj)).encode("ascii") + b":" + obj + if isinstance(obj, list): + return b"l" + b"".join(bencode(x) for x in obj) + b"e" + if isinstance(obj, dict): + sorted_items = sorted(obj.items(), key=lambda x: x[0]) + return b"d" + b"".join(bencode(k) + bencode(v) for k, v in sorted_items) + b"e" + raise NotImplementedError(f"bencode not implemented for {type(obj)}") + + +def serialize(obj: Object) -> bytes: + return bencode(obj.serialize()) + + class TokenizerTests(unittest.TestCase): def test_tokenize_digit(self) -> None: self.assertEqual(tokenize("1"), ["1"]) @@ -1750,6 +1784,86 @@ def test_stdlib_quote_reverse_pipe(self) -> None: self.assertEqual(self._run("$$quote <| 3 + 4"), Binop(BinopKind.ADD, Int(3), Int(4))) +class BencodeTests(unittest.TestCase): + def test_bencode_int(self) -> None: + self.assertEqual(bencode(123), b"i123e") + + def test_bencode_bool(self) -> None: + self.assertEqual(bencode(True), b"i1e") + + def test_bencode_negative_int(self) -> None: + self.assertEqual(bencode(-123), b"i-123e") + + def test_serialize_bytes(self) -> None: + self.assertEqual(bencode(b"abc"), b"3:abc") + + def test_bencode_empty_list(self) -> None: + self.assertEqual(bencode([]), b"le") + + def test_bencode_list_of_ints(self) -> None: + self.assertEqual(bencode([1, 2, 3]), b"li1ei2ei3ee") + + def test_bencode_list_of_lists(self) -> None: + self.assertEqual(bencode([[1, 2], [3, 4]]), b"lli1ei2eeli3ei4eee") + + def test_bencode_dict_sorts_keys(self) -> None: + d = {} + d[b"b"] = 1 + d[b"a"] = 2 + # It's sorted by insertion order (guaranteed Python 3.6+) + self.assertEqual([*d], [b"b", b"a"]) + # It's sorted lexicographically + self.assertEqual(bencode(d), b"d1:ai2e1:bi1ee") + + +class ObjectSerializeTests(unittest.TestCase): + def test_serialize_int(self) -> None: + obj = Int(123) + self.assertEqual(obj.serialize(), {b"type": b"Int", b"value": 123}) + + def test_serialize_negative_int(self) -> None: + obj = Int(-123) + self.assertEqual(obj.serialize(), {b"type": b"Int", b"value": -123}) + + def test_serialize_str(self) -> None: + obj = String("abc") + self.assertEqual(obj.serialize(), {b"type": b"String", b"value": b"abc"}) + + def test_serialize_bytes(self) -> None: + obj = Bytes(b"abc") + self.assertEqual(obj.serialize(), {b"type": b"Bytes", b"value": b"abc"}) + + def test_serialize_var(self) -> None: + obj = Var("abc") + self.assertEqual(obj.serialize(), {b"type": b"Var", b"name": b"abc"}) + + def test_serialize_bool(self) -> None: + obj = Bool(True) + self.assertEqual(obj.serialize(), {b"type": b"Bool", b"value": True}) + + +class SerializeTests(unittest.TestCase): + def test_serialize_int(self) -> None: + obj = Int(3) + self.assertEqual(serialize(obj), b"d4:type3:Int5:valuei3ee") + + def test_serialize_str(self) -> None: + obj = String("abc") + self.assertEqual(serialize(obj), b"d4:type6:String5:value3:abce") + + def test_serialize_bytes(self) -> None: + obj = Bytes(b"abc") + self.assertEqual(serialize(obj), b"d4:type5:Bytes5:value3:abce") + + def test_serialize_var(self) -> None: + obj = Var("abc") + self.assertEqual(serialize(obj), b"d4:name3:abc4:type3:Vare") + + def test_serialize_bool(self) -> None: + obj = Bool(True) + self.assertEqual(serialize(obj), b"d4:type4:Bool5:valuei1ee") + + def eval_command(args: argparse.Namespace) -> None: if args.debug: logging.basicConfig(level=logging.DEBUG) From f069a2619c6e121e5c4f6f83750f105d1e6ce82f Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Tue, 21 Nov 2023 16:06:12 -0500 Subject: [PATCH 2/8] Add more serialization functions and helper --- scrapscript.py | 68 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/scrapscript.py b/scrapscript.py index db6298f5..81c53473 100755 --- a/scrapscript.py +++ b/scrapscript.py @@ -348,13 +348,16 @@ class Object: def serialize(self) -> dict[bytes, object]: raise NotImplementedError("{type(self).__name__}.serialize()") + def _serialize(self, **kwargs: object) -> dict[bytes, object]: + return {key.encode("utf-8"): value for key, value in kwargs.items()} + @dataclass(eq=True, frozen=True, unsafe_hash=True) class Int(Object): value: int def serialize(self) -> dict[bytes, object]: - return {b"type": b"Int", b"value": self.value} + return self._serialize(type=b"Int", value=self.value) @dataclass(eq=True, frozen=True, unsafe_hash=True) @@ -446,63 +449,103 @@ class Binop(Object): left: Object right: Object + def serialize(self) -> dict[bytes, object]: + return { + b"type": b"Binop", + b"op": self.op.name.encode("utf-8"), + b"left": self.left.serialize(), + b"right": self.right.serialize(), + } + @dataclass(eq=True, frozen=True, unsafe_hash=True) class List(Object): items: typing.List[Object] + def serialize(self) -> dict[bytes, object]: + return {b"type": b"List", b"items": [item.serialize() for item in self.items]} + @dataclass(eq=True, frozen=True, unsafe_hash=True) class Assign(Object): name: Var value: Object + def serialize(self) -> dict[bytes, object]: + return self._serialize(type=b"Assign", value=self.value.serialize()) + @dataclass(eq=True, frozen=True, unsafe_hash=True) class Function(Object): arg: Object body: Object + def serialize(self) -> dict[bytes, object]: + return self._serialize(type=b"Function", arg=self.arg.serialize(), body=self.body.serialize()) + @dataclass(eq=True, frozen=True, unsafe_hash=True) class Apply(Object): func: Object arg: Object + def serialize(self) -> dict[bytes, object]: + return self._serialize(type=b"Apply", func=self.func.serialize(), arg=self.arg.serialize()) + @dataclass(eq=True, frozen=True, unsafe_hash=True) class Compose(Object): inner: Object outer: Object + def serialize(self) -> dict[bytes, object]: + return self._serialize(type=b"Compose", inner=self.inner.serialize(), outer=self.outer.serialize()) + @dataclass(eq=True, frozen=True, unsafe_hash=True) class Where(Object): body: Object binding: Object + def serialize(self) -> dict[bytes, object]: + return self._serialize(type=b"Where", body=self.body.serialize(), binding=self.binding.serialize()) + @dataclass(eq=True, frozen=True, unsafe_hash=True) class Assert(Object): value: Object cond: Object + def serialize(self) -> dict[bytes, object]: + return self._serialize(type=b"Assert", value=self.value.serialize(), cond=self.cond.serialize()) + @dataclass(eq=True, frozen=True, unsafe_hash=True) class EnvObject(Object): env: Env + def serialize(self) -> dict[bytes, object]: + return self._serialize( + type=b"EnvObject", value={key.encode("utf-8"): value.serialize() for key, value in self.env.items()} + ) + @dataclass(eq=True, frozen=True, unsafe_hash=True) class MatchCase(Object): pattern: Object body: Object + def serialize(self) -> dict[bytes, object]: + return self._serialize(type=b"MatchCase", pattern=self.pattern.serialize(), body=self.pattern.serialize()) + @dataclass(eq=True, frozen=True, unsafe_hash=True) class MatchFunction(Object): cases: typing.List[MatchCase] + def serialize(self) -> dict[bytes, object]: + return self._serialize(type=b"MatchFunction", cases=[case.serialize() for case in self.cases]) + @dataclass(eq=True, frozen=True, unsafe_hash=True) class NativeFunction(Object): @@ -1841,6 +1884,29 @@ def test_serialize_bool(self) -> None: obj = Bool(True) self.assertEqual(obj.serialize(), {b"type": b"Bool", b"value": True}) + def test_serialize_binary_add(self) -> None: + obj = Binop(BinopKind.ADD, Int(123), Int(456)) + self.assertEqual( + obj.serialize(), + { + b"left": {b"type": b"Int", b"value": 123}, + b"op": b"ADD", + b"right": {b"type": b"Int", b"value": 456}, + b"type": b"Binop", + }, + ) + + def test_serialize_list(self) -> None: + obj = List([Int(1), Int(2)]) + self.assertEqual( + obj.serialize(), + {b"type": b"List", b"items": [{b"type": b"Int", b"value": 1}, {b"type": b"Int", b"value": 2}]}, + ) + + def test_serialize_assign(self) -> None: + obj = Assign(Var("x"), Int(2)) + self.assertEqual(obj.serialize(), {b"type": b"Assign", b"value": {b"type": b"Int", b"value": 2}}) + class SerializeTests(unittest.TestCase): def test_serialize_int(self) -> None: From 7529dcd6de0072d20965c44aee8ce1e0dddb6aea Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Tue, 21 Nov 2023 16:10:37 -0500 Subject: [PATCH 3/8] Add more serialization functions --- scrapscript.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/scrapscript.py b/scrapscript.py index 81c53473..9fb24aae 100755 --- a/scrapscript.py +++ b/scrapscript.py @@ -520,14 +520,16 @@ def serialize(self) -> dict[bytes, object]: return self._serialize(type=b"Assert", value=self.value.serialize(), cond=self.cond.serialize()) +def serialize_env(env: Env) -> dict[bytes, object]: + return {key.encode("utf-8"): value.serialize() for key, value in env.items()} + + @dataclass(eq=True, frozen=True, unsafe_hash=True) class EnvObject(Object): env: Env def serialize(self) -> dict[bytes, object]: - return self._serialize( - type=b"EnvObject", value={key.encode("utf-8"): value.serialize() for key, value in self.env.items()} - ) + return self._serialize(type=b"EnvObject", value=serialize_env(self.env)) @dataclass(eq=True, frozen=True, unsafe_hash=True) @@ -557,17 +559,28 @@ class Closure(Object): env: Env func: Union[Function, MatchFunction] + def serialize(self) -> dict[bytes, object]: + return self._serialize(type=b"Closure", env=serialize_env(self.env), func=self.func.serialize()) + @dataclass(eq=True, frozen=True, unsafe_hash=True) class Record(Object): data: Dict[str, Object] + def serialize(self) -> dict[bytes, object]: + return self._serialize( + type=b"Record", data={key.encode("utf-8"): value.serialize() for key, value in self.data.items()} + ) + @dataclass(eq=True, frozen=True, unsafe_hash=True) class Access(Object): obj: Object at: Object + def serialize(self) -> dict[bytes, object]: + return self._serialize(type=b"Access", obj=self.obj.serialize(), at=self.at.serialize()) + def unpack_int(obj: Object) -> int: if not isinstance(obj, Int): From 2398c7093df1841f1a60ef44bfc656bcd6234c3b Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Wed, 22 Nov 2023 11:33:30 -0500 Subject: [PATCH 4/8] Move type= to _serialize function --- scrapscript.py | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/scrapscript.py b/scrapscript.py index 9fb24aae..3f6944d9 100755 --- a/scrapscript.py +++ b/scrapscript.py @@ -349,7 +349,10 @@ def serialize(self) -> dict[bytes, object]: raise NotImplementedError("{type(self).__name__}.serialize()") def _serialize(self, **kwargs: object) -> dict[bytes, object]: - return {key.encode("utf-8"): value for key, value in kwargs.items()} + return { + b"type": type(self).__name__.encode("utf-8"), + **{key.encode("utf-8"): value for key, value in kwargs.items()}, + } @dataclass(eq=True, frozen=True, unsafe_hash=True) @@ -357,7 +360,7 @@ class Int(Object): value: int def serialize(self) -> dict[bytes, object]: - return self._serialize(type=b"Int", value=self.value) + return self._serialize(value=self.value) @dataclass(eq=True, frozen=True, unsafe_hash=True) @@ -472,7 +475,7 @@ class Assign(Object): value: Object def serialize(self) -> dict[bytes, object]: - return self._serialize(type=b"Assign", value=self.value.serialize()) + return self._serialize(value=self.value.serialize()) @dataclass(eq=True, frozen=True, unsafe_hash=True) @@ -481,7 +484,7 @@ class Function(Object): body: Object def serialize(self) -> dict[bytes, object]: - return self._serialize(type=b"Function", arg=self.arg.serialize(), body=self.body.serialize()) + return self._serialize(arg=self.arg.serialize(), body=self.body.serialize()) @dataclass(eq=True, frozen=True, unsafe_hash=True) @@ -490,7 +493,7 @@ class Apply(Object): arg: Object def serialize(self) -> dict[bytes, object]: - return self._serialize(type=b"Apply", func=self.func.serialize(), arg=self.arg.serialize()) + return self._serialize(func=self.func.serialize(), arg=self.arg.serialize()) @dataclass(eq=True, frozen=True, unsafe_hash=True) @@ -499,7 +502,7 @@ class Compose(Object): outer: Object def serialize(self) -> dict[bytes, object]: - return self._serialize(type=b"Compose", inner=self.inner.serialize(), outer=self.outer.serialize()) + return self._serialize(inner=self.inner.serialize(), outer=self.outer.serialize()) @dataclass(eq=True, frozen=True, unsafe_hash=True) @@ -508,7 +511,7 @@ class Where(Object): binding: Object def serialize(self) -> dict[bytes, object]: - return self._serialize(type=b"Where", body=self.body.serialize(), binding=self.binding.serialize()) + return self._serialize(body=self.body.serialize(), binding=self.binding.serialize()) @dataclass(eq=True, frozen=True, unsafe_hash=True) @@ -517,7 +520,7 @@ class Assert(Object): cond: Object def serialize(self) -> dict[bytes, object]: - return self._serialize(type=b"Assert", value=self.value.serialize(), cond=self.cond.serialize()) + return self._serialize(value=self.value.serialize(), cond=self.cond.serialize()) def serialize_env(env: Env) -> dict[bytes, object]: @@ -529,7 +532,7 @@ class EnvObject(Object): env: Env def serialize(self) -> dict[bytes, object]: - return self._serialize(type=b"EnvObject", value=serialize_env(self.env)) + return self._serialize(value=serialize_env(self.env)) @dataclass(eq=True, frozen=True, unsafe_hash=True) @@ -538,7 +541,7 @@ class MatchCase(Object): body: Object def serialize(self) -> dict[bytes, object]: - return self._serialize(type=b"MatchCase", pattern=self.pattern.serialize(), body=self.pattern.serialize()) + return self._serialize(pattern=self.pattern.serialize(), body=self.pattern.serialize()) @dataclass(eq=True, frozen=True, unsafe_hash=True) @@ -546,7 +549,7 @@ class MatchFunction(Object): cases: typing.List[MatchCase] def serialize(self) -> dict[bytes, object]: - return self._serialize(type=b"MatchFunction", cases=[case.serialize() for case in self.cases]) + return self._serialize(cases=[case.serialize() for case in self.cases]) @dataclass(eq=True, frozen=True, unsafe_hash=True) @@ -560,7 +563,7 @@ class Closure(Object): func: Union[Function, MatchFunction] def serialize(self) -> dict[bytes, object]: - return self._serialize(type=b"Closure", env=serialize_env(self.env), func=self.func.serialize()) + return self._serialize(env=serialize_env(self.env), func=self.func.serialize()) @dataclass(eq=True, frozen=True, unsafe_hash=True) @@ -568,9 +571,7 @@ class Record(Object): data: Dict[str, Object] def serialize(self) -> dict[bytes, object]: - return self._serialize( - type=b"Record", data={key.encode("utf-8"): value.serialize() for key, value in self.data.items()} - ) + return self._serialize(data={key.encode("utf-8"): value.serialize() for key, value in self.data.items()}) @dataclass(eq=True, frozen=True, unsafe_hash=True) @@ -579,7 +580,7 @@ class Access(Object): at: Object def serialize(self) -> dict[bytes, object]: - return self._serialize(type=b"Access", obj=self.obj.serialize(), at=self.at.serialize()) + return self._serialize(obj=self.obj.serialize(), at=self.at.serialize()) def unpack_int(obj: Object) -> int: From b1e88da3ddb73835c0658d3f63c337647a2ad26e Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Wed, 22 Nov 2023 11:43:46 -0500 Subject: [PATCH 5/8] Implement default serializer for only Object fields This makes our lives easier. --- scrapscript.py | 47 +++++++++++++++++++---------------------------- 1 file changed, 19 insertions(+), 28 deletions(-) diff --git a/scrapscript.py b/scrapscript.py index 3f6944d9..53a0b90f 100755 --- a/scrapscript.py +++ b/scrapscript.py @@ -2,6 +2,7 @@ import argparse import base64 import code +import dataclasses import enum import json import logging @@ -346,7 +347,15 @@ def parse(tokens: typing.List[str], p: float = 0) -> "Object": @dataclass(eq=True, frozen=True, unsafe_hash=True) class Object: def serialize(self) -> dict[bytes, object]: - raise NotImplementedError("{type(self).__name__}.serialize()") + cls = type(self) + result: dict[bytes, object] = {b"type": cls.__name__.encode("utf-8")} + for field in dataclasses.fields(cls): + if issubclass(field.type, Object): + value = getattr(self, field.name) + result[field.name.encode("utf-8")] = value.serialize() + else: + raise NotImplementedError("serializing non-Object fields; write your own serialize function") + return result def _serialize(self, **kwargs: object) -> dict[bytes, object]: return { @@ -397,8 +406,7 @@ def serialize(self) -> dict[bytes, object]: @dataclass(eq=True, frozen=True, unsafe_hash=True) class Hole(Object): - def serialize(self) -> dict[bytes, object]: - return {b"type": b"Hole"} + pass Env = Mapping[str, Object] @@ -474,54 +482,36 @@ class Assign(Object): name: Var value: Object - def serialize(self) -> dict[bytes, object]: - return self._serialize(value=self.value.serialize()) - @dataclass(eq=True, frozen=True, unsafe_hash=True) class Function(Object): arg: Object body: Object - def serialize(self) -> dict[bytes, object]: - return self._serialize(arg=self.arg.serialize(), body=self.body.serialize()) - @dataclass(eq=True, frozen=True, unsafe_hash=True) class Apply(Object): func: Object arg: Object - def serialize(self) -> dict[bytes, object]: - return self._serialize(func=self.func.serialize(), arg=self.arg.serialize()) - @dataclass(eq=True, frozen=True, unsafe_hash=True) class Compose(Object): inner: Object outer: Object - def serialize(self) -> dict[bytes, object]: - return self._serialize(inner=self.inner.serialize(), outer=self.outer.serialize()) - @dataclass(eq=True, frozen=True, unsafe_hash=True) class Where(Object): body: Object binding: Object - def serialize(self) -> dict[bytes, object]: - return self._serialize(body=self.body.serialize(), binding=self.binding.serialize()) - @dataclass(eq=True, frozen=True, unsafe_hash=True) class Assert(Object): value: Object cond: Object - def serialize(self) -> dict[bytes, object]: - return self._serialize(value=self.value.serialize(), cond=self.cond.serialize()) - def serialize_env(env: Env) -> dict[bytes, object]: return {key.encode("utf-8"): value.serialize() for key, value in env.items()} @@ -540,9 +530,6 @@ class MatchCase(Object): pattern: Object body: Object - def serialize(self) -> dict[bytes, object]: - return self._serialize(pattern=self.pattern.serialize(), body=self.pattern.serialize()) - @dataclass(eq=True, frozen=True, unsafe_hash=True) class MatchFunction(Object): @@ -579,9 +566,6 @@ class Access(Object): obj: Object at: Object - def serialize(self) -> dict[bytes, object]: - return self._serialize(obj=self.obj.serialize(), at=self.at.serialize()) - def unpack_int(obj: Object) -> int: if not isinstance(obj, Int): @@ -1919,7 +1903,14 @@ def test_serialize_list(self) -> None: def test_serialize_assign(self) -> None: obj = Assign(Var("x"), Int(2)) - self.assertEqual(obj.serialize(), {b"type": b"Assign", b"value": {b"type": b"Int", b"value": 2}}) + self.assertEqual( + obj.serialize(), + {b"type": b"Assign", b"name": {b"name": b"x", b"type": b"Var"}, b"value": {b"type": b"Int", b"value": 2}}, + ) + + def test_serialize_record(self) -> None: + obj = Record({"x": Int(1)}) + self.assertEqual(obj.serialize(), {b"data": {b"x": {b"type": b"Int", b"value": 1}}, b"type": b"Record"}) class SerializeTests(unittest.TestCase): From 7a75d426c6b308815b249283914a7a22ecdaeef9 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Wed, 22 Nov 2023 11:46:43 -0500 Subject: [PATCH 6/8] Add another end-to-end serialization test --- scrapscript.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/scrapscript.py b/scrapscript.py index 53a0b90f..9f47ffb5 100755 --- a/scrapscript.py +++ b/scrapscript.py @@ -1934,6 +1934,13 @@ def test_serialize_bool(self) -> None: obj = Bool(True) self.assertEqual(serialize(obj), b"d4:type4:Bool5:valuei1ee") + def test_serialize_function(self) -> None: + obj = Function(Var("x"), Binop(BinopKind.ADD, Int(1), Var("x"))) + self.assertEqual( + serialize(obj), + b"d3:argd4:name1:x4:type3:Vare4:bodyd4:leftd4:type3:Int5:valuei1ee2:op3:ADD5:rightd4:name1:x4:type3:Vare4:type5:Binope4:type8:Functione", + ) + def eval_command(args: argparse.Namespace) -> None: if args.debug: From b8b0bc0fb0ba0da66447739677f0cd2269e707f4 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Wed, 22 Nov 2023 11:53:47 -0500 Subject: [PATCH 7/8] Add serialize function to stdlib --- scrapscript.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/scrapscript.py b/scrapscript.py index 9f47ffb5..51cffd6a 100755 --- a/scrapscript.py +++ b/scrapscript.py @@ -1824,6 +1824,15 @@ def test_stdlib_quote_pipe(self) -> None: def test_stdlib_quote_reverse_pipe(self) -> None: self.assertEqual(self._run("$$quote <| 3 + 4"), Binop(BinopKind.ADD, Int(3), Int(4))) + def test_stdlib_serialize(self) -> None: + self.assertEqual(self._run("$$serialize 3", STDLIB), Bytes(value=b"d4:type3:Int5:valuei3ee")) + + def test_stdlib_serialize_expr(self) -> None: + self.assertEqual( + self._run("(1+2) |> $$quote |> $$serialize", STDLIB), + Bytes(value=b"d4:leftd4:type3:Int5:valuei1ee2:op3:ADD5:rightd4:type3:Int5:valuei2ee4:type5:Binope"), + ) + class BencodeTests(unittest.TestCase): def test_bencode_int(self) -> None: @@ -2001,6 +2010,7 @@ def jsondecode(obj: Object) -> Object: "$$add": NativeFunction(lambda x: NativeFunction(lambda y: Int(unpack_int(x) + unpack_int(y)))), "$$fetch": NativeFunction(fetch), "$$jsondecode": NativeFunction(jsondecode), + "$$serialize": NativeFunction(lambda obj: Bytes(serialize(obj))), } From bbe7527b82949dd6c9383a25faa35687505bfd45 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Wed, 22 Nov 2023 13:21:03 -0500 Subject: [PATCH 8/8] Update for Python3.8 support --- scrapscript.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/scrapscript.py b/scrapscript.py index 51cffd6a..8e12a787 100755 --- a/scrapscript.py +++ b/scrapscript.py @@ -346,9 +346,9 @@ def parse(tokens: typing.List[str], p: float = 0) -> "Object": @dataclass(eq=True, frozen=True, unsafe_hash=True) class Object: - def serialize(self) -> dict[bytes, object]: + def serialize(self) -> Dict[bytes, object]: cls = type(self) - result: dict[bytes, object] = {b"type": cls.__name__.encode("utf-8")} + result: Dict[bytes, object] = {b"type": cls.__name__.encode("utf-8")} for field in dataclasses.fields(cls): if issubclass(field.type, Object): value = getattr(self, field.name) @@ -357,7 +357,7 @@ def serialize(self) -> dict[bytes, object]: raise NotImplementedError("serializing non-Object fields; write your own serialize function") return result - def _serialize(self, **kwargs: object) -> dict[bytes, object]: + def _serialize(self, **kwargs: object) -> Dict[bytes, object]: return { b"type": type(self).__name__.encode("utf-8"), **{key.encode("utf-8"): value for key, value in kwargs.items()}, @@ -368,7 +368,7 @@ def _serialize(self, **kwargs: object) -> dict[bytes, object]: class Int(Object): value: int - def serialize(self) -> dict[bytes, object]: + def serialize(self) -> Dict[bytes, object]: return self._serialize(value=self.value) @@ -376,7 +376,7 @@ def serialize(self) -> dict[bytes, object]: class String(Object): value: str - def serialize(self) -> dict[bytes, object]: + def serialize(self) -> Dict[bytes, object]: return {b"type": b"String", b"value": self.value.encode("utf-8")} @@ -384,7 +384,7 @@ def serialize(self) -> dict[bytes, object]: class Bytes(Object): value: bytes - def serialize(self) -> dict[bytes, object]: + def serialize(self) -> Dict[bytes, object]: return {b"type": b"Bytes", b"value": self.value} @@ -392,7 +392,7 @@ def serialize(self) -> dict[bytes, object]: class Var(Object): name: str - def serialize(self) -> dict[bytes, object]: + def serialize(self) -> Dict[bytes, object]: return {b"type": b"Var", b"name": self.name.encode("utf-8")} @@ -400,7 +400,7 @@ def serialize(self) -> dict[bytes, object]: class Bool(Object): value: bool - def serialize(self) -> dict[bytes, object]: + def serialize(self) -> Dict[bytes, object]: return {b"type": b"Bool", b"value": self.value} @@ -460,7 +460,7 @@ class Binop(Object): left: Object right: Object - def serialize(self) -> dict[bytes, object]: + def serialize(self) -> Dict[bytes, object]: return { b"type": b"Binop", b"op": self.op.name.encode("utf-8"), @@ -473,7 +473,7 @@ def serialize(self) -> dict[bytes, object]: class List(Object): items: typing.List[Object] - def serialize(self) -> dict[bytes, object]: + def serialize(self) -> Dict[bytes, object]: return {b"type": b"List", b"items": [item.serialize() for item in self.items]} @@ -513,7 +513,7 @@ class Assert(Object): cond: Object -def serialize_env(env: Env) -> dict[bytes, object]: +def serialize_env(env: Env) -> Dict[bytes, object]: return {key.encode("utf-8"): value.serialize() for key, value in env.items()} @@ -521,7 +521,7 @@ def serialize_env(env: Env) -> dict[bytes, object]: class EnvObject(Object): env: Env - def serialize(self) -> dict[bytes, object]: + def serialize(self) -> Dict[bytes, object]: return self._serialize(value=serialize_env(self.env)) @@ -535,7 +535,7 @@ class MatchCase(Object): class MatchFunction(Object): cases: typing.List[MatchCase] - def serialize(self) -> dict[bytes, object]: + def serialize(self) -> Dict[bytes, object]: return self._serialize(cases=[case.serialize() for case in self.cases]) @@ -549,7 +549,7 @@ class Closure(Object): env: Env func: Union[Function, MatchFunction] - def serialize(self) -> dict[bytes, object]: + def serialize(self) -> Dict[bytes, object]: return self._serialize(env=serialize_env(self.env), func=self.func.serialize()) @@ -557,7 +557,7 @@ def serialize(self) -> dict[bytes, object]: class Record(Object): data: Dict[str, Object] - def serialize(self) -> dict[bytes, object]: + def serialize(self) -> Dict[bytes, object]: return self._serialize(data={key.encode("utf-8"): value.serialize() for key, value in self.data.items()})