noirbizarre/json4humans

View on GitHub
json4humans/types.py

Summary

Maintainability
A
2 hrs
Test Coverage
A
99%
"""
This modules contains all shared and reausable types supporting style preservation.

Most of the time you won't have to instanciate them manually.
"""

from __future__ import annotations

from collections import OrderedDict
from collections.abc import Iterable
from enum import Enum
from typing import Generic, TypeVar


class JSONType:
    """
    Base class for parsed types with style and metadata preservation.
    """

    json_before: list[WSC]
    """Whitespaces and comments sequence before the object."""
    json_after: list[WSC]
    """Whitespaces and comments sequence after the object."""

    def __init__(
        self, *_, before: list[WSC | str] | None = None, after: list[WSC | str] | None = None, **__
    ):
        from . import wsc

        self.json_before = wsc.parse_list(before)
        self.json_after = wsc.parse_list(after)

    def __repr__(self) -> str:
        if attrs := getattr(self, "__dict__"):
            kwargs = ", ".join(f"{k}={v}" for k, v in attrs.items())
            return f"{self.__class__.__name__}({super().__repr__()}, {kwargs})"
        return f"{self.__class__.__name__}({super().__repr__()})"


class Container(JSONType):
    """
    Base class for containers with style and metadata preservation.
    """

    json_container_head: list[WSC]
    """Whitespaces and comments sequence in the head of the container."""
    json_container_tail: list[WSC]
    """Whitespaces and comments sequence in the tail of the container."""
    json_container_trailing_coma: bool
    """Wether this container have a trailing coma or not."""

    def __init__(
        self,
        *args,
        before: list[WSC | str] | None = None,
        after: list[WSC | str] | None = None,
        head: list[WSC | str] | None = None,
        tail: list[WSC | str] | None = None,
        trailing_coma: bool = False,
        **kwargs,
    ):
        super().__init__(*args, before=before, after=after, **kwargs)
        from . import wsc

        self.json_container_head = wsc.parse_list(head)
        self.json_container_tail = wsc.parse_list(tail)
        self.json_container_trailing_coma = trailing_coma


class WhiteSpace(str):
    """Stores a sequence of whitespaces"""

    def __repr__(self) -> str:
        return f"WhiteSpace({super().__repr__()})"


class Comment(str):
    """Store a comment"""

    def __repr__(self) -> str:
        return f"{self.__class__.__name__}({super().__repr__()})"


WSC = WhiteSpace | Comment
"""A whitespace or a comment"""


class BlockStyleComment(Comment):
    """Stores a block-style comment (ie. starting with `/*` and ending with `*/`)"""

    pass


class LineStyleComment(Comment):
    """Stores a line-style comment (ie. starting with `//` and ending at the end of the current line)"""

    pass


class HashStyleComment(Comment):
    """Stores a hash-style comment (ie. starting with `#` and ending at the end of the current line)"""

    pass


class Object(OrderedDict, Container):
    """A JSON Object with order and style preservation"""

    pass


class Array(list["Value"], Container):
    """A JSON Array with style preservation"""

    def __init__(
        self,
        items: Iterable,
        *,
        before: list[WSC | str] | None = None,
        after: list[WSC | str] | None = None,
        head: list[WSC | str] | None = None,
        tail: list[WSC | str] | None = None,
        trailing_coma: bool = False,
        **kwargs,
    ):
        list.__init__(self, items)
        Container.__init__(
            self, before=before, after=after, head=head, tail=tail, trailing_coma=trailing_coma
        )


class Identifier(str, JSONType):
    "A quoteless string without special characters"
    pass


Ident = Identifier


class Quote(Enum):
    """Known quotes formats"""

    SINGLE = "'"
    DOUBLE = '"'


class String(str, JSONType):
    """A JSON String with style preservation"""

    quote: Quote
    """Quote character wrapping the string"""

    linebreaks: list[int]
    """Escaped line breaks positions"""

    def __new__(cls, value, *args, **kwargs):
        return super().__new__(cls, value)

    def __init__(
        self,
        _,
        quote: str | Quote = Quote.DOUBLE,
        linebreaks: list[int] | None = None,
        before: list[WSC | str] | None = None,
        after: list[WSC | str] | None = None,
    ):
        super().__init__(before=before, after=after)
        self.quote = Quote(quote) if isinstance(quote, str) else quote
        self.linebreaks = linebreaks or []


Key = String | Identifier | str


class Number(JSONType):
    """
    Base class for all Number types and representations.
    """

    prefixed: bool
    """
    Is the number prefixed by an explicit sign
    """

    def __new__(cls, value, *args, **kwargs):
        number = super().__new__(cls, value)
        number.prefixed = kwargs.get("prefixed", False)
        return number


class Integer(Number, int):
    """
    A JSON integer compatible with Python's `int`.
    """

    def __str__(self) -> str:
        return int.__repr__(self)


class HexInteger(Integer):
    """
    A JSON integer compatible with Python's `int` and represented in its hexadecimal form.
    """

    def __str__(self) -> str:
        return hex(self)


class Float(Number, float):
    """
    A JSON float compatible with Python's `float`.
    """

    leading_point: bool
    significand: int | None

    def __new__(cls, value, *args, **kwargs):
        number = super().__new__(cls, value, prefixed=kwargs.get("prefixed", False))
        number.leading_point = kwargs.get("leading_point", False)
        number.significand = kwargs.get("significand")
        return number

    def __str__(self) -> str:
        raw = float.__repr__(self)
        if self.leading_point and raw.startswith("0"):
            return raw[1:]
        if self.significand is not None:
            pos = raw.index(".") + 1
            raw = raw[: pos + self.significand]
        return raw


AnyNumber = Integer | Float

T = TypeVar("T")


class Literal(JSONType, Generic[T]):
    """
    Represents a JSON Literal and wraps the equivalent value in Python.
    """

    value: T
    """
    The Python equivalent value.
    """

    def __init__(self, value: T, **kwargs):
        super().__init__(**kwargs)
        self.value = value

    def __eq__(self, obj: object) -> bool:
        return self.value.__eq__(obj)

    def __hash__(self) -> int:
        return self.value.__hash__()


Value = Object | Array | String | Number | Literal | bool | None
"""
A type alias matching the JSON Value.
"""

Member = tuple[Key, Value]
"""
A Key-Value pair in an [Object][json4humans.types.Object]
"""


class TupleWithTrailingComa(tuple[T, ...]):
    trailing_coma: bool

    def __new__(cls, items, *args, **kwargs):
        # explicitly only pass value to the tuple constructor
        return super().__new__(cls, items)

    def __init__(self, items: Iterable[T], trailing_coma: bool = False):
        self.trailing_coma = trailing_coma