Skip to content

Commit

Permalink
Drop custom deserializer code for ast nodes and use native dataclasses.
Browse files Browse the repository at this point in the history
  • Loading branch information
seandstewart committed Mar 3, 2021
1 parent 9d03039 commit 2e1d054
Show file tree
Hide file tree
Showing 7 changed files with 266 additions and 205 deletions.
6 changes: 1 addition & 5 deletions iambic/ast/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,4 @@ class InputType(str, enum.Enum):
DATA = "data"


typic.register(deserializer=node_deserializer, check=isnodetype)
typic.register(
deserializer=log_deserializer, check=lambda o: o == LogueBodyT # type: ignore
)
typic.resolve()
protocol = typic.protocol(ResolvedNodeT) # type: ignore
140 changes: 83 additions & 57 deletions iambic/ast/node.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
from __future__ import annotations

import dataclasses
import functools
from operator import attrgetter
from typing import (
ClassVar,
Optional,
Union,
Tuple,
Any,
Mapping,
Type,
List,
Expand Down Expand Up @@ -52,9 +54,6 @@
"GenericNode",
"NodeID",
"NodeType",
"node_deserializer",
"log_deserializer",
"isnodetype",
)


Expand All @@ -70,7 +69,8 @@ def sort_body(body: Iterable[_T]) -> Tuple[_T, ...]:
return tuple(sorted(body, key=indexgetter))


@typic.klass(unsafe_hash=True, order=True, slots=True)
@typic.slotted
@dataclasses.dataclass(unsafe_hash=True, order=True)
class Act:
"""A representation of a single Act in a Play."""

Expand All @@ -95,7 +95,8 @@ def from_node(cls, node: "GenericNode") -> "Act":
return cls(index=node.index, text=node.match_text, num=num)


@typic.klass(unsafe_hash=True, slots=True)
@typic.slotted
@dataclasses.dataclass(unsafe_hash=True, order=True)
class Scene:
"""A representation of a single Scene in a play."""

Expand All @@ -105,8 +106,8 @@ class Scene:
num: int
setting: Optional[str] = None
act: Optional[NodeID] = None
body: "SceneBodyT" = typic.field(compare=False, hash=False, default=()) # type: ignore
personae: "PersonaeIDT" = typic.field(compare=False, hash=False, default=()) # type: ignore
body: SceneBodyT = typic.field(compare=False, hash=False, default=()) # type: ignore
personae: PersonaeIDT = typic.field(compare=False, hash=False, default=()) # type: ignore

@typic.cached_property
def id(self) -> NodeID:
Expand Down Expand Up @@ -143,7 +144,8 @@ def from_node(cls, node: "GenericNode") -> "Scene":
)


@typic.klass(unsafe_hash=True, slots=True)
@typic.slotted
@dataclasses.dataclass(unsafe_hash=True, order=True)
class Prologue:
"""A representation of a single Prologue in a play.
Expand All @@ -157,7 +159,7 @@ class Prologue:
setting: Optional[str] = None
act: Optional[NodeID] = None
body: "LogueBodyT" = typic.field(compare=False, hash=False, default=()) # type: ignore
personae: "PersonaeIDT" = typic.field(compare=False, hash=False, default=()) # type: ignore
personae: PersonaeIDT = typic.field(compare=False, hash=False, default=()) # type: ignore
as_act: bool = typic.field(init=False) # type: ignore

def __post_init__(self):
Expand Down Expand Up @@ -191,7 +193,8 @@ def from_node(cls, node: "GenericNode") -> "Prologue":
)


@typic.klass(unsafe_hash=True, slots=True)
@typic.slotted
@dataclasses.dataclass(unsafe_hash=True, order=True)
class Epilogue(Prologue):
"""A representation of a single Epilogue in a play.
Expand All @@ -202,7 +205,8 @@ class Epilogue(Prologue):
type: ClassVar[NodeType] = NodeType.EPIL


@typic.klass(unsafe_hash=True, slots=True)
@typic.slotted
@dataclasses.dataclass(unsafe_hash=True, order=True)
class Intermission:
"""A representation of an Intermission in a play."""

Expand All @@ -226,7 +230,8 @@ def col(self):
return titleize(self.text)


@typic.klass(unsafe_hash=True, slots=True)
@typic.slotted
@dataclasses.dataclass(unsafe_hash=True, order=True)
class Persona:
"""A representation of a single character in a Play."""

Expand Down Expand Up @@ -260,15 +265,16 @@ def from_node(cls, node: "GenericNode") -> "Persona":
)


@typic.klass(unsafe_hash=True, slots=True)
@typic.slotted
@dataclasses.dataclass(unsafe_hash=True, order=True)
class Entrance:
"""A representation of an entrance for character(s) in a Scene."""

type: ClassVar[NodeType] = NodeType.ENTER
index: int
text: str
scene: NodeID
personae: "PersonaeIDT" = ()
personae: PersonaeIDT = ()

@typic.cached_property
def id(self) -> NodeID:
Expand All @@ -281,14 +287,16 @@ def from_node(cls, node: "GenericNode") -> "Entrance":
return cls(index=node.index, text=node.match_text, scene=node.parent)


@typic.klass(unsafe_hash=True, slots=True)
@typic.slotted
@dataclasses.dataclass(unsafe_hash=True, order=True)
class Exit(Entrance):
"""A representation of an exit for character(s) in a Scene."""

type: ClassVar[NodeType] = NodeType.EXIT


@typic.klass(unsafe_hash=True, slots=True)
@typic.slotted
@dataclasses.dataclass(unsafe_hash=True, order=True)
class Action:
"""A representation of a stage direction related to a specific character."""

Expand All @@ -314,7 +322,8 @@ def from_node(cls, node: "GenericNode") -> "Action":
)


@typic.klass(unsafe_hash=True, slots=True)
@typic.slotted
@dataclasses.dataclass(unsafe_hash=True, order=True)
class Direction:
"""A representation of a stage direction."""

Expand All @@ -335,7 +344,8 @@ def from_node(cls, node: "GenericNode") -> "Direction":
return cls(action=node.match_text, scene=node.parent, index=node.index)


@typic.klass(unsafe_hash=True, slots=True)
@typic.slotted
@dataclasses.dataclass(unsafe_hash=True, order=True)
class Dialogue:
"""A representation of a line of dialogue for a character in a scene."""

Expand Down Expand Up @@ -367,14 +377,15 @@ def from_node(cls, node: "GenericNode") -> "Dialogue":
)


@typic.klass(unsafe_hash=True, slots=True)
@typic.slotted
@dataclasses.dataclass(unsafe_hash=True, order=True)
class Speech:
"""A representation of an unbroken piece of dialogue related to a single character."""

type: ClassVar[NodeType] = NodeType.SPCH
persona: NodeID
scene: NodeID
body: "SpeechBodyT"
body: SpeechBodyT
index: int

def __post_init__(self):
Expand Down Expand Up @@ -407,7 +418,8 @@ def id(self) -> NodeID:
return NodeID(uri)


@typic.klass(unsafe_hash=True, slots=True)
@typic.slotted
@dataclasses.dataclass(unsafe_hash=True, order=True)
class Metadata:
"""General information about a given play."""

Expand Down Expand Up @@ -447,12 +459,13 @@ def asmeta(self): # pragma: nocover
return dikt


@typic.klass(unsafe_hash=True, slots=True)
@typic.slotted
@dataclasses.dataclass(unsafe_hash=True, order=True)
class Play:
"""A representation of a play in its entirety."""

type: ClassVar[NodeType] = NodeType.PLAY
body: "PlayBodyT" = ()
body: PlayBodyT = ()
personae: Tuple[Persona, ...] = ()
meta: Metadata = typic.field(default_factory=Metadata) # type: ignore

Expand All @@ -476,16 +489,57 @@ def linecount(self) -> int:
break
return count

@functools.lru_cache(maxsize=1)
def asjsonld(self):
fn, ln = self.meta.author.split()
data = {
"@context": "https://schema.org",
"@type": "Play",
"name": self.meta.title,
"author": {
"@type": "Person",
"givenName": fn,
"familyName": ln,
"birthDate": "1564-04-23",
"birthPlace": {
"@type": "Place",
"address": "Stratford-upon-Avon, Warwickshire, England",
},
"deathDate": "1616-04-23",
"deathPlace": {
"@type": "Place",
"address": "Stratford-upon-Avon, Warwickshire, England",
},
},
"maintainer": {
"@type": "Organization",
"name": "Bardly.org",
"url": "https://bardly.org",
},
"size": {
"@type": "QuantitativeValue",
"unitCode": "N2",
"value": self.linecount,
},
"license": self.meta.rights,
"inLanguage": self.meta.language,
"keywords": [*self.meta.tags],
"character": [{"@type": "Person", "name": c.name} for c in self.personae],
}

return data


@typic.klass(unsafe_hash=True, slots=True)
@typic.slotted
@dataclasses.dataclass(unsafe_hash=True, order=True)
class GenericNode:
"""The root-object of a script.
A script ``Node`` represents a single line of text in a script.
"""

__resolver_map__: ClassVar[
Mapping[NodeType, Type["ResolvedNodeT"]]
Mapping[NodeType, Type[ResolvedNodeT]]
] = typic.FrozenDict(
{
NodeType.ACT: Act,
Expand All @@ -508,14 +562,14 @@ class GenericNode:
type: NodeType
text: str
index: int
# Additional typic.fields which may be present
# Additional fields which may be present
lineno: Optional[int] = None
linepart: Optional[int] = None
# Given by text parser
# If reading from JSON, we don't have/need this,
# it will be provided inherently by the data-structure
# on resolution-time.
match: typic.FrozenDict = typic.field(default_factory=typic.FrozenDict) # type: ignore
match: typic.FrozenDict = dataclasses.field(default_factory=typic.FrozenDict)
parent: Optional[NodeID] = None
act: Optional[NodeID] = None
scene: Optional[NodeID] = None
Expand Down Expand Up @@ -552,6 +606,7 @@ def match_text(self) -> str:
Prologue,
Epilogue,
Persona,
Play,
Entrance,
Exit,
Action,
Expand All @@ -570,32 +625,3 @@ def match_text(self) -> str:
PlayNodeT = Union[Act, Epilogue, Prologue]
PlayBodyT = Tuple[PlayNodeT, ...]
PersonaeIDT = Tuple[NodeID, ...]

_RESOLVABLE = set(GenericNode.__resolver_map__.values())


def node_deserializer(value: Any) -> Optional[ResolvedNodeT]:
if type(value) in _RESOLVABLE or value is None:
return value
if isinstance(value, GenericNode):
return value.resolved

if not isinstance(value, Mapping):
value = typic.transmute(dict, value)

handler: Type[ResolvedNodeT] = GenericNode.__resolver_map__[value.pop("type")]
resolved: ResolvedNodeT = typic.transmute(handler, value) # type: ignore
return resolved


def log_deserializer(value):
return typic.protocol(Tuple[ResolvedNodeT, ...]).transmute(value) # type: ignore


@functools.lru_cache(maxsize=None)
def isnodetype(obj: Type, *, __candidates=frozenset(_RESOLVABLE)) -> bool:
is_valid = obj is GenericNode or (
getattr(obj, "__origin__", None) is Union
and {*getattr(obj, "__args__", ())}.issubset(__candidates)
)
return is_valid
16 changes: 3 additions & 13 deletions iambic/parse/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,9 @@
# -*- coding: UTF-8 -*-
import functools

import typic

from iambic import ast


class Parser:
__resolver_map__ = ast.GenericNode.__resolver_map__

@functools.lru_cache()
def parse(self, data: str) -> ast.ResolvedNodeT:
return typic.transmute(ast.ResolvedNodeT, data) # type: ignore

__call__ = parse


parser = Parser()
@functools.lru_cache()
def parser(data: str, *, __trans=ast.protocol.transmute) -> ast.ResolvedNodeT:
return __trans(data)
Loading

0 comments on commit 2e1d054

Please sign in to comment.