diff --git a/README.md b/README.md index 7aee89b..96eed9a 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,7 @@ While in pre-release they are subject to change. | Any | `typing.Any` | | Unions (A\|B\|C) | `typing.Union[A\|B\|C]` | | Regex | `pkl.Regex` | +| Bytes | `bytes` | ## Contributing Contributions are welcome! If you'd like to contribute, please fork the repository and submit a pull request. For major changes, please open an issue first to discuss what you would like to change. diff --git a/scripts/download_binary.py b/scripts/download_binary.py index 83e4a02..5922ac8 100644 --- a/scripts/download_binary.py +++ b/scripts/download_binary.py @@ -3,7 +3,7 @@ import requests -VERSION = "0.25.2" +VERSION = "0.30.0" BASE_PATH = "https://github.com/apple/pkl/releases/download/" filenames = [ diff --git a/src/pkl/parser.py b/src/pkl/parser.py index 6b1676e..02f0833 100644 --- a/src/pkl/parser.py +++ b/src/pkl/parser.py @@ -38,6 +38,7 @@ class ResultType(Enum): CODE_REGEX = 0xB CODE_CLASS = 0xC CODE_TYPEALIAS = 0xD +CODE_BYTES = 0xF CODE_PROPERTY = 0x10 CODE_ENTRY = 0x11 CODE_ELEMENT = 0x12 @@ -114,6 +115,7 @@ def __init__( CODE_REGEX: self.parse_regex, CODE_CLASS: self.parse_class, CODE_TYPEALIAS: self.parse_typealias, + CODE_BYTES: self.parse_bytes, CODE_PROPERTY: self.parse_property, CODE_ENTRY: self.parse_entry, CODE_ELEMENT: self.parse_element, @@ -155,7 +157,7 @@ def get_dataclass_class(self, class_name: str, keys: List[str], no_cache: bool = return dynamic_class def parse_typed_dynamic(self, obj): - _, full_class_name, module_uri, members = obj + full_class_name, module_uri, members = obj[1], obj[2], obj[3] member_types = set(m[0] for m in members) property_list = list(map(self.handle_type, members)) @@ -202,7 +204,7 @@ def parse_set(self, obj): return set(obj[1]) def parse_duration(self, obj): - _, value, unit = obj + value, unit = obj[1], obj[2] return Duration(value, unit) @@ -224,14 +226,17 @@ def parse_class(self, obj): def parse_typealias(self, obj): return + def parse_bytes(self, obj): + return obj[1] + def parse_property(self, obj): - _, key, value = obj + key, value = obj[1], obj[2] return {key: self.handle_type(value)} def parse_entry(self, obj): - _, key, value = obj + key, value = obj[1], obj[2] return {key: self.handle_type(value)} def parse_element(self, obj): - _, index, value = obj + index, value = obj[1], obj[2] return {index: self.handle_type(value)} diff --git a/tests/test_parser_binary_encoding.py b/tests/test_parser_binary_encoding.py new file mode 100644 index 0000000..99a518d --- /dev/null +++ b/tests/test_parser_binary_encoding.py @@ -0,0 +1,41 @@ +"""Unit tests for pkl.parser.Parser against the pkl-binary wire encoding. + +These exercise the decoder directly against hand-built MessagePack-style +arrays (as documented in bindings-specification/binary-encoding.adoc), so +they don't need a real `pkl` binary/subprocess. +""" + +from pkl.parser import CODE_BYTES, CODE_DURATION, Parser + + +def test_bytes_decodes_to_python_bytes(): + """`Bytes` (type code 0x0F) added in Pkl 0.29; wire encoding documented + as [0x0F, ]. Previously unhandled, so the parser silently returned + {"type": "Unknown", "value": obj} instead of the actual bytes. + """ + parser = Parser() + obj = [CODE_BYTES, b"\x01\x02\x03"] + + result = parser.handle_type(obj) + + assert result == b"\x01\x02\x03" + assert isinstance(result, bytes) + + +def test_decoder_ignores_unknown_trailing_slots(): + """Pkl 0.30's binary-encoding.adoc formalizes forward compatibility: + "Additional slots may be added to types in future Pkl releases. + Decoders *must* be designed to defensively discard values beyond the + number of known slots for a type[...]". + + Fixed-arity unpacking (`_, value, unit = obj`) would raise + "too many values to unpack" on a future Pkl release that appends a + slot; decoders must tolerate and ignore extras instead. + """ + parser = Parser() + obj = [CODE_DURATION, 5.0, "s", "some-future-slot"] + + result = parser.handle_type(obj) + + assert result.value == 5.0 + assert result.unit == "s"