class CloneTests(unittest.TestCase): def setUp(self): self.manager = LogManager() def tearDown(self): self.manager.loggers.clear() self.manager.routing.clear() self.manager.endings.clear() def test_clone_from_log(self): a = self.manager.get_logger("a") b = a.manager.get_logger("a") self.assertIs(a, b) c = a.clone("c") self.assertIsNot(c, a) def test_frame(self): a = self.manager.get_logger("a") a.frame += ["extra"] self.assertIn("extra", a.frame) b = self.manager.get_logger("a") self.assertIs(a, b) c = self.manager.clone(self.manager.get_logger("a"), "c") self.assertIsNot(c, a) self.assertEqual(c.frame, a.frame) def test_routes(self): self.manager = LogManager() n_routes = len(self.manager.routing) a = self.manager.get_logger("a") self.assertEqual(n_routes + 1, len(self.manager.routing)) b = self.manager.get_logger("a") self.assertIs(a, b) self.assertEqual(n_routes + 1, len(self.manager.routing)) self.manager.set_route(b, Logger.Level.INFO, LogAdapter(), sys.stderr) self.assertEqual(n_routes + 1, len(self.manager.routing)) c = self.manager.clone(self.manager.get_logger("a"), "c") self.assertIsNot(c, a) self.assertEqual(n_routes + 2, len(self.manager.routing)) c.set_route(Logger.Level.INFO, LogAdapter(), sys.stderr) self.assertEqual(n_routes + 2, len(self.manager.routing))
def scripts(cls, pkg, metadata, paths=[], **kwargs): """This class method is the preferred way to create SceneScript objects. :param str pkg: The dotted name of the package containing the scripts. :param metadata: A mapping or data object. This parameter permits searching among scripts against particular criteria. Its use is application specific. :param list(str) paths: A sequence of file paths to the scripts relative to the package. You can satisfy all parameter requirements by passing in a :py:class:`~turberfield.dialogue.model.SceneScript.Folder` object like this:: SceneScript.scripts(**folder._asdict()) The method generates a sequence of :py:class:`~turberfield.dialogue.model.SceneScript` objects. """ log_manager = LogManager() log = log_manager.clone(log_manager.get_logger("main"), "turberfield.dialogue.model.scenescript") for path in paths: try: fP = pkg_resources.resource_filename(pkg, path) except ImportError: log.warning("No package called {}".format(pkg)) else: if not os.path.isfile(fP): log.warning("No script file at {}".format( os.path.join(*pkg.split(".") + [path]))) else: yield cls(fP, metadata)
def run_through(script, ensemble, roles=1, strict=False): """ :py:class:`turberfield.dialogue.model.SceneScript`. """ with script as dialogue: selection = dialogue.select(ensemble, roles=roles) if not any(selection.values()) or strict and not all(selection.values()): return try: model = dialogue.cast(selection).run() except (AttributeError, ValueError) as e: log_manager = LogManager() log = log_manager.clone( log_manager.get_logger("main"), "turberfield.dialogue.player.run_through" ) log.warning(". ".join(getattr(e, "args", e) or e)) return else: yield from model
class Model(docutils.nodes.GenericNodeVisitor): """This class registers the necessary extensions to the docutils document model. It also defines the types which are returned on iterating over a scene script file. """ Shot = namedtuple("Shot", ["name", "scene", "items", "path", "line_nr"], defaults=(None, None)) Property = namedtuple( "Property", ["entity", "object", "attr", "val", "path", "line_nr"], defaults=(None, None)) Audio = namedtuple("Audio", [ "package", "resource", "offset", "duration", "loop", "path", "line_nr" ], defaults=(None, None)) Still = namedtuple("Still", list(Audio._fields[:-2]) + ["label", "width", "height", "path", "line_nr"], defaults=(None, None)) Video = namedtuple("Video", list(Still._fields[:-2]) + ["poster", "url", "path", "line_nr"], defaults=(None, None)) Memory = namedtuple( "Memory", ["subject", "object", "state", "text", "html", "path", "line_nr"], defaults=(None, None)) Line = namedtuple("Line", ["persona", "text", "html", "path", "line_nr"], defaults=(None, None)) Condition = namedtuple( "Condition", ["object", "format", "regex", "value", "path", "line_nr"], defaults=(None, None)) def __init__(self, fP, document): super().__init__(document) self.fP = fP self.optional = tuple( i.__name__ for i in (EntityDirective.Declaration, MemoryDirective.Definition, PropertyDirective.Getter, PropertyDirective.Setter, FXDirective.Cue, ConditionDirective.Evaluation)) self.log_manager = LogManager() self.log = self.log_manager.clone(self.log_manager.get_logger("main"), "turberfield.dialogue.model") self.log.frame = [ "{now}", "{level.name:>8}", "{logger.name}", "{1[path]}", "{1[line_nr]:>5}", " {0:<64}", " {token}" ] self.section_level = 0 self.scenes = [None] self.shots = [Model.Shot(None, None, [])] self.speaker = None self.memory = None self.metadata = [] self.escape_table = str.maketrans({ v: "&" + k for k, v in html.entities.html5.items() if k.endswith(";") and len(v) == 1 and v not in "!\"#'()*+,-..:;=@{}~" }) self.text = [] self.html = [] def __iter__(self): for shot in self.shots: for item in shot.items: yield shot, item def close_shot(self, line_nr=None): if self.memory: self.shots[-1].items.append( self.memory._replace(text="".join(self.text), html="\n".join(self.html))) self.memory = None elif self.text: self.shots[-1].items.append( Model.Line(self.speaker, "".join(self.text), "\n".join(self.html), self.fP, line_nr)) self.text.clear() self.html.clear() def get_entity(self, ref): return next((entity for entity in self.document.citations if ref and ref.lower() in entity.attributes["names"]), None) def substitute_property(self, matchObj, line=None): try: defn = self.document.substitution_defs[matchObj.group(1)] getter = next(i for i in defn.children if isinstance(i, PropertyDirective.Getter)) ref, dot, attr = getter["arguments"][0].partition(".") entity = self.get_entity(ref) rv = str(operator.attrgetter(attr)(entity.persona)).strip() except (AttributeError, KeyError, IndexError, StopIteration) as e: self.log.warning("Argument has bad substitution reference", { "path": self.fP, "line_nr": line }, token=matchObj.group(1)) rv = None return rv def default_visit(self, node): self.log.debug(node, {"path": self.fP, "line_nr": node.line}) def default_departure(self, node): pass def depart_block_quote(self, node): self.speaker = None def visit_bullet_list(self, node): self.html.append("<ul>") def depart_bullet_list(self, node): self.html.append("</ul>") self.close_shot(node.line) def visit_citation_reference(self, node): entity = self.get_entity(node.attributes["refname"]) try: self.speaker = entity.persona except AttributeError: self.log.warning("Reference to entity with no persona", { "path": self.fP, "line_nr": node.parent.line }, node=node, entity=entity, token=node.rawsource) self.speaker = node.attributes["refname"] def visit_Cue(self, node): subref_re = re.compile("\|(\w+)\|") subber = functools.partial(self.substitute_property, line=node.parent.line) pkg = node["arguments"][0] rsrc = subref_re.sub(subber, node["arguments"][1]) offset = node["options"].get("offset") duration = node["options"].get("duration") label = subref_re.sub(subber, node["options"].get("label", "")) loop = node["options"].get("loop") width = node["options"].get("width") height = node["options"].get("height") typ = mimetypes.guess_type(rsrc)[0] item = None try: if typ.startswith("audio"): item = Model.Audio(pkg, rsrc, offset, duration, loop, self.fP, node.line) elif typ.startswith("image"): item = Model.Still(pkg, rsrc, offset, duration, loop, label, width, height, self.fP, node.line) elif typ.startswith("video"): item = Model.Video(pkg, rsrc, offset, duration, loop, label, width, height, *(node["options"].get(i, None) for i in ["poster", "url"]), path=self.fP, line_nr=node.line) except AttributeError: pass if item is not None: self.shots[-1].items.append(item) def visit_Definition(self, node): state = node.string_import(node["arguments"][0]) subj = self.get_entity(node["options"].get("subject")) obj = self.get_entity(node["options"].get("object")) self.memory = Model.Memory(subj and subj.persona, obj and obj.persona, state, None, None, self.fP, node.line) def visit_emphasis(self, node): text = node.astext() self.text.append(text.lstrip() if self.text and self.text[-1]. endswith(tuple(string.whitespace)) else text) self.html.append('<em class="text">{0}</em>'.format( text.translate(self.escape_table))) def visit_Evaluation(self, node): ref, dot, format_ = node["arguments"][0].partition(".") entity = self.get_entity(ref) pattern = node["arguments"][-1] regex = None if "(" in pattern: try: regex = re.compile(pattern) value = pattern except Exception as e: self.log.warning("Condition regex error", { "path": self.fP, "line_nr": node.line }, token=pattern, exception=e) if not regex: subber = functools.partial(self.substitute_property, line=node.line) s = re.compile("\|(\w+)\|").sub(subber, pattern) try: value = int(s) if s.isdigit() else node.string_import(s) except ValueError: value = s self.shots[-1].items.append( Model.Condition(entity.persona, format_, regex, value, self.fP, node.line)) def depart_field_name(self, node): self.metadata.append((node.astext(), None)) def depart_field_body(self, node): name, _ = self.metadata.pop(-1) if self.text: self.metadata.append((name, " ".join(self.text))) self.text.clear() def visit_footnote(self, node): self.text = [] def depart_footnote(self, node): try: span = self.html.pop(-1) self.html.append( span.replace('class="text"', 'class="footnote" role="note"')) except InderError: self.log.warning( "Unable to process footnote", { "path": self.fP, "line_nr": node.line }, ) self.close_shot(node.line) def visit_list_item(self, node): self.html.append("<li>") def depart_list_item(self, node): self.html.append("</li>") self.text.append("\n") def visit_literal(self, node): text = node.astext() self.text.append(text.lstrip() if self.text and self.text[-1]. endswith(tuple(string.whitespace)) else text) self.html.append('<pre class="text">{0}</pre>'.format( text.translate(self.escape_table))) def visit_paragraph(self, node): if self.shots and not isinstance( node.parent, (citation, field_body, footnote, list_item)): self.text = [] self.html = ["<p>"] def depart_paragraph(self, node): if self.shots and not isinstance( node.parent, (citation, field_body, footnote, list_item)): self.html.append("</p>\n") self.close_shot(node.line) def depart_raw(self, node): if "html" in node.attributes["format"] and self.shots: if self.shots[-1].items: line = self.shots[-1].items[-1] self.shots[-1].items[-1] = line._replace(html=line.html + "\n" + node.astext()) def visit_reference(self, node): ref_id = self.document.nameids.get(node.get("refname", None), None) if ref_id: target = self.document.ids[ref_id] ref_uri = target["refuri"] else: ref_uri = node["refuri"] text = node.astext() self.text.append(text.lstrip() if self.text and self.text[-1]. endswith(tuple(string.whitespace)) else text) self.html.append('<a href="{0}">{1}</a>'.format( ref_uri, text.translate(self.escape_table))) def visit_section(self, node): self.section_level += 1 def depart_section(self, node): self.section_level -= 1 def visit_Setter(self, node): ref, attr = node["arguments"][0].split(".") entity = self.get_entity(ref) subber = functools.partial(self.substitute_property, line=node.line) s = re.compile("\|(\w+)\|").sub(subber, node["arguments"][1]) try: # Attempt objectwise assignment if RHS is an entity bits = s.partition(".") donor = self.get_entity(bits[0]) val = operator.attrgetter(bits[2])( donor.persona) if bits[2] else donor.persona self.shots[-1].items.append( Model.Property(self.speaker, entity.persona, attr, val, self.fP, node.line)) except AttributeError: pass else: return try: val = int(s) if s.isdigit() else node.string_import(s) except ValueError: val = s try: self.shots[-1].items.append( Model.Property(self.speaker, entity.persona, attr, val, self.fP, node.line)) except AttributeError: warnings.warn("Line {0.parent.line}: " "Entity has no persona ({1}).".format(node, entity)) def visit_strong(self, node): text = node.astext() self.text.append(text.lstrip() if self.text and self.text[-1]. endswith(tuple(string.whitespace)) else text) self.html.append('<strong class="text">{0}</strong>'.format( text.translate(self.escape_table))) def visit_substitution_reference(self, node): try: defn = self.document.substitution_defs[node.attributes["refname"]] except KeyError: self.log.warning("Bad substitution reference", { "path": self.fP, "line_nr": node.line }, token=node.rawsource) raise for tgt in defn.children: if isinstance(tgt, PropertyDirective.Getter): ref, dot, attr = tgt["arguments"][0].partition(".") entity = self.get_entity(ref) if entity is None: obj = Pathfinder.string_import(tgt["arguments"][0], relative=False, sep=".", path=self.fP, line_nr=defn.line) if obj is not None: self.text.append(str(obj).strip()) self.html.append( str(obj).strip().translate(self.escape_table)) elif getattr(entity, "persona", None) is not None: fmt = "".join(("{0.", attr, "}")) val = fmt.format(entity.persona) self.text.append(val.strip()) self.html.append('<span class="ref">{0}</span>'.format( val.translate(self.escape_table))) def visit_Text(self, node): if isinstance(node.parent, docutils.nodes.paragraph): text = node.astext() self.text.append(text.lstrip() if self.text and self.text[-1]. endswith(tuple(string.whitespace)) else text) self.html.append('<span class="text">{0}</span>'.format( text.translate(self.escape_table))) def visit_title(self, node): self.log.debug( "Title level {0.section_level}".format(self), { "path": self.fP, "line_nr": node.line }, token=node.rawsource, ) if self.scenes == [None ] and self.shots == [Model.Shot(None, None, [])]: self.scenes.clear() self.shots.clear() if isinstance(node.parent, docutils.nodes.section): if self.section_level == 1: names = node.parent.attributes[ "names"] + node.parent.attributes["dupnames"] self.scenes.append(names[0]) elif self.section_level == 2: names = node.parent.attributes[ "names"] + node.parent.attributes["dupnames"] self.shots.append( Model.Shot(names[0], self.scenes[-1], [], path=self.fP, line_nr=node.parent.line))
class TerminalHandler: """ The default handler for events from scene script files. It generates output for a console terminal. The class is written to be a callable, stateful object. Its `__call__` method delegates to handlers specific to each type of event. You can subclass it and override those methods to suit your own application. :param terminal: A stream object. :param str dbPath: An optional URL to the internal database. :param float pause: The time in seconds to pause on a line of dialogue. :param float dwell: The time in seconds to dwell on a word of dialogue. :param log: An optional log object. """ pause = turberfield.dialogue.cli.DEFAULT_PAUSE_SECS dwell = turberfield.dialogue.cli.DEFAULT_DWELL_SECS @staticmethod def handle_audio(obj, wait=False): """Handle an audio event. This function plays an audio file. Currently only `.wav` format is supported. :param obj: An :py:class:`~turberfield.dialogue.model.Model.Audio` object. :param bool wait: Force a blocking wait until playback is complete. :return: The supplied object. """ if not simpleaudio: return obj fp = pkg_resources.resource_filename(obj.package, obj.resource) data = wave.open(fp, "rb") nChannels = data.getnchannels() bytesPerSample = data.getsampwidth() sampleRate = data.getframerate() nFrames = data.getnframes() framesPerMilliSecond = nChannels * sampleRate // 1000 offset = framesPerMilliSecond * obj.offset duration = nFrames - offset duration = min( duration, framesPerMilliSecond * obj.duration if obj.duration is not None else duration) data.readframes(offset) frames = data.readframes(duration) for i in range(obj.loop): waveObj = simpleaudio.WaveObject(frames, nChannels, bytesPerSample, sampleRate) playObj = waveObj.play() if obj.loop > 1 or wait: playObj.wait_done() return obj def handle_interlude(self, obj, folder, index, ensemble, loop=None, **kwargs): """Handle an interlude event. Interlude functions permit branching. They return a folder which the application can choose to adopt as the next supplier of dialogue. This handler calls the interlude with the supplied arguments and returns the result. :param obj: A callable object. :param folder: A :py:class:`~turberfield.dialogue.model.SceneScript.Folder` object. :param int index: Indicates which scene script in the folder is being processed. :param ensemble: A sequence of Python objects. :param branches: A sequence of :py:class:`~turberfield.dialogue.model.SceneScript.Folder` objects. from which to pick a branch in the action. :return: A :py:class:`~turberfield.dialogue.model.SceneScript.Folder` object. """ if obj is None: return folder.metadata else: return obj(folder, index, ensemble, loop=loop, **kwargs) def handle_line(self, obj): """Handle a line event. This function displays a line of dialogue. It generates a blocking wait for a period of time calculated from the length of the line. :param obj: A :py:class:`~turberfield.dialogue.model.Model.Line` object. :return: The supplied object. """ if obj.persona is None: return obj name = getattr(obj.persona, "_name", "") print(textwrap.indent( "{t.normal}{name}".format(name=name, t=self.terminal), " " * 2), end="\n", file=self.terminal.stream) print(textwrap.indent( "{t.normal}{obj.text}".format(obj=obj, t=self.terminal), " " * 10), end="\n" * 2, file=self.terminal.stream) interval = self.pause + self.dwell * obj.text.count(" ") time.sleep(interval) return obj def handle_memory(self, obj): """Handle a memory event. This function accesses the internal database. It writes a record containing state information and an optional note. :param obj: A :py:class:`~turberfield.dialogue.model.Model.Memory` object. :return: The supplied object. """ if obj.subject is not None: with self.con as db: SchemaBase.note( db, obj.subject, obj.state, obj.object, text=obj.text, html=obj.html, ) return obj def handle_property(self, obj): """Handle a property event. This function will set an attribute on an object if the event requires it. :param obj: A :py:class:`~turberfield.dialogue.model.Model.Property` object. :return: The supplied object. """ if obj.object is not None: try: setattr(obj.object, obj.attr, obj.val) except AttributeError as e: self.log.error(". ".join(getattr(e, "args", e) or e)) try: print( "{t.dim}{obj.object._name}.{obj.attr} = {obj.val!s}{t.normal}" .format(obj=obj, t=self.terminal), end="\n" * 2, file=self.terminal.stream) except AttributeError as e: self.log.error(". ".join(getattr(e, "args", e) or e)) return obj def handle_scene(self, obj): """Handle a scene event. This function applies a blocking wait at the start of a scene. :param obj: A :py:class:`~turberfield.dialogue.model.Model.Shot` object. :return: The supplied object. """ print("{t.dim}{scene}{t.normal}".format(scene=obj.scene.capitalize(), t=self.terminal), end="\n" * 3, file=self.terminal.stream) time.sleep(self.pause) return obj def handle_scenescript(self, obj): """Handle a scene script event. :param obj: A :py:class:`~turberfield.dialogue.model.SceneScript.Folder` object. :return: The supplied object. """ self.log.debug(obj.fP) return obj def handle_shot(self, obj): """Handle a shot event. :param obj: A :py:class:`~turberfield.dialogue.model.Model.Shot` object. :return: The supplied object. """ print("{t.dim}{shot}{t.normal}".format(shot=obj.name.capitalize(), t=self.terminal), end="\n" * 3, file=self.terminal.stream) return obj def handle_creation(self): with self.con as db: rv = Creation(*SchemaBase.tables.values()).run(db) db.commit() self.log.info("Created {0} tables in {1}.".format( len(rv), self.dbPath)) return rv def handle_references(self, obj): with self.con as db: rv = SchemaBase.populate(db, obj) self.log.info("Populated {0} rows.".format(rv)) return rv def __init__(self, terminal, dbPath=None, pause=pause, dwell=dwell, log=None): self.terminal = terminal self.dbPath = dbPath self.pause = pause self.dwell = dwell self.log_manager = LogManager() self.log = log or self.log_manager.clone( self.log_manager.get_logger("main"), "turberfield.dialogue.handle") self.shot = None self.con = Connection(**Connection.options( paths=[dbPath] if dbPath else [])) self.handle_creation() def __call__(self, obj, *args, loop, **kwargs): if isinstance(obj, Model.Line): try: yield self.handle_line(obj) except AttributeError: pass elif isinstance(obj, Model.Audio): yield self.handle_audio(obj) elif isinstance(obj, Model.Memory): yield self.handle_memory(obj) elif isinstance(obj, Model.Property): yield self.handle_property(obj) elif isinstance(obj, Model.Shot): if self.shot is None or obj.scene != self.shot.scene: yield self.handle_scene(obj) if self.shot is None or obj.name != self.shot.name: yield self.handle_shot(obj) else: yield obj self.shot = obj elif isinstance(obj, SceneScript): yield self.handle_scenescript(obj) elif asyncio.iscoroutinefunction(obj): raise NotImplementedError elif isinstance(obj, MutableSequence): yield self.handle_references(obj) elif (obj is None or isinstance(obj, Callable)) and len(args) == 3: yield self.handle_interlude(obj, *args, loop=loop, **kwargs) else: yield obj