Source code for mtc.base.parser
import io
from typing import Any, Self
from .utils import printable_bytes_truncate
[docs]
class ParserError(Exception): pass
[docs]
class Parser:
"""
The basic building block of the rest of the project. It provides a standard interface to serialize and deserialize
an object to bytes (hence the name parser). Do not instantiate this class directly.
"""
def __new__(cls, *args, **kwargs):
"""perform validation right after object initialization so subclasses don't have to explicitly call it"""
obj = super().__new__(cls)
obj.__init__(*args, **kwargs) # type: ignore
obj.validate()
return obj
class ValidationError(ParserError): pass
class ParsingError(ParserError):
def __init__(self, start: int, end: int, *args):
super().__init__(*args)
self.start = start
self.end = end
def __str__(self) -> str:
return f"Error {self.start}:{self.end} " + super().__str__()
[docs]
def __init__(self, /, value: Any) -> None:
"""
All subclasses initializers must have the same signature. This function must be idempotent due to how validation
is handled.
"""
self.value = value
raise NotImplementedError("Do not use this class directly")
[docs]
def to_bytes(self) -> bytes:
"""Serialize the object to bytes"""
raise NotImplementedError("Implemented in subclass only")
[docs]
@classmethod
def parse(cls, stream: io.BufferedIOBase) -> Self:
"""
Deserialize the first object found from the stream. Raises :class:`ParsingError` if the stream cannot be parsed.
"""
raise NotImplementedError("Do not use this class directly")
[docs]
@classmethod
def skip(cls, stream: io.BufferedIOBase) -> None:
"""
skips the corresponding section in the stream, while doing minimum processing possible. This is especially
useful when a large quantity of objects are serialized into a file.
"""
raise NotImplementedError("Implemented in subclass only")
[docs]
def __repr__(self) -> str:
return f"<{self.__class__.__name__} {self.value}>"
[docs]
def __str__(self) -> str:
"""By default, returns the serialized bytes in hex format"""
return self.to_bytes().hex()
[docs]
def __eq__(self, other: object) -> bool:
"""Compares two :class:`Parser` object. They are equal if and only if their serialized bytes are equal"""
if isinstance(other, Parser):
return self.value == other.value
return False
[docs]
def __len__(self) -> int:
"""Returns the length of the serialized bytes"""
return len(self.to_bytes())
[docs]
def __hash__(self) -> int:
"""Returns the hash of the serialized bytes"""
return hash(self.to_bytes())
[docs]
def print(self) -> str:
"""
Returns a string representation of the pretty-formatted byte structure of the object. Despite its name, this
method does not write anything to stdout because it is sometimes recursively called in subclasses.
For example, calling
"""
b = self.to_bytes()
return f"{len(b)} {self.__class__.__name__} {printable_bytes_truncate(b, 80)}"
[docs]
def validate(self) -> None:
"""
Performs basic validation on the data contained in the class and raises :class:`ValidationError` if data is
inconsistent. This function is a no-op on :class:`Parser` and should be implemented in subclasses if needed
"""
pass
[docs]
@staticmethod
def disable_validation() -> None:
"""
Disable validation for all :class:`Parser` objects for the duration of the program. This operation cannot be reversed without
restarting the program. Calling this method in subclasses is the same as calling it on :class:`Parser`
"""
Parser.__new__ = lambda cls, *args, **kwargs: object.__new__(cls) # types:ignore
__all__ = ["Parser", "ParserError"]