class LogStreamTests(unittest.TestCase): def setUp(self): self.stream = io.StringIO() self.stream.name = "test stream" self.manager = LogManager( defaults=[LogManager.Route(None, Logger.Level.INFO, LogAdapter(), self.stream)] ) def tearDown(self): self.manager.loggers.clear() self.manager.routing.clear() self.manager.endings.clear() def test_log_blocked(self): logger = self.manager.get_logger("unit.test.log") logger.log(logger.Level.DEBUG, "Debug message") self.assertFalse(self.stream.getvalue()) def test_log_printed(self): logger = self.manager.get_logger("unit.test.log") logger.log(logger.Level.INFO, "Info message") self.assertIn("Info message", self.stream.getvalue()) def test_log_newlines(self): logger = self.manager.get_logger("unit.test.log") logger.log(logger.Level.INFO, "Message") logger.log(logger.Level.INFO, "Message") lines = self.stream.getvalue().splitlines() self.assertEqual(2, len(lines)) def test_bad_default_level(self): manager = LogManager( defaults=[LogManager.Route(None, None, LogAdapter(), self.stream)] ) logger = manager.get_logger("unit.test.log") logger.log(logger.Level.INFO, "Info message") self.assertFalse(self.stream.getvalue()) def test_bad_adapter(self): manager = LogManager( defaults=[LogManager.Route(None, Logger.Level.INFO, None, self.stream)] ) logger = manager.get_logger("unit.test.log") logger.log(logger.Level.INFO, "Info message") self.assertFalse(self.stream.getvalue()) def test_bad_endpoint(self): manager = LogManager( defaults=[LogManager.Route(None, Logger.Level.INFO, LogAdapter(), None)] ) logger = manager.get_logger("unit.test.log") logger.log(logger.Level.INFO, "Info message") self.assertFalse(self.stream.getvalue())
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)
class LogPathTests(LocationTests, unittest.TestCase): def setUp(self): super().setUp() uid = uuid.uuid4() self.path = pathlib.Path(self.locn.name, f"{uid.hex}.log") self.manager = LogManager( self.path, defaults=[LogManager.Route(None, Logger.Level.INFO, LogAdapter(), self.path)] ) self.manager.__enter__() def tearDown(self): self.manager.__exit__(None, None, None) super().tearDown() def test_register_path(self): d = {} rv = self.manager.register_endpoint(self.path, registry=d) self.assertIs(rv, self.path) self.assertIn(self.path, d) self.assertIsInstance(d[self.path], io.TextIOBase) def test_log_written(self): logger = self.manager.get_logger("unit.test.log") logger.log(logger.Level.INFO, "Info message") self.assertIn(self.path, self.manager.endings) self.assertTrue(self.path.exists()) self.assertIn("Info message", self.path.read_text()) self.assertTrue(any( self.path.resolve() == k.endpoint_name for k, v in self.manager.pairings ), self.manager.pairings)
def test_bad_endpoint(self): manager = LogManager( defaults=[LogManager.Route(None, Logger.Level.INFO, LogAdapter(), None)] ) logger = manager.get_logger("unit.test.log") logger.log(logger.Level.INFO, "Info message") self.assertFalse(self.stream.getvalue())
def cgi_producer(args, stream=None): log_manager = LogManager() log = log_manager.get_logger("turberfield") log.info(args) try: handler = CGIHandler(Terminal(stream=stream), None if args.db == "None" else args.db, float(args.pause), float(args.dwell)) except Exception as e: log.error(e) raise print("Content-type:text/event-stream", file=handler.terminal.stream) print(file=handler.terminal.stream) folders, references = resolve_objects(args) Assembly.register(*(i if isinstance(i, type) else type(i) for i in references)) log.info(folders) try: yield from rehearse(folders, references, handler, int(args.repeat), int(args.roles), args.strict) except Exception as e: log.error(e) raise else: log.debug(references)
class CGIHandler(TerminalHandler): def handle_audio(self, obj): path = pkg_resources.resource_filename(obj.package, obj.resource) pos = path.find("lib", len(sys.prefix)) if pos != -1: print("event: audio", "data: ../{0}\n".format(path[pos:]), sep="\n", end="\n", file=self.terminal.stream) self.terminal.stream.flush() return obj def handle_line(self, obj): if obj.persona is None: return obj print("event: line", "data: {0}\n".format(Assembly.dumps(obj)), sep="\n", end="\n", file=self.terminal.stream) self.terminal.stream.flush() interval = self.pause + self.dwell * obj.text.count(" ") time.sleep(interval) return obj def handle_property(self, obj): self.log_manager = LogManager() self.log = self.log_manager.get_logger("turberfield") self.log.info(obj) 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)) print("event: property", "data: {0}\n".format(Assembly.dumps(obj)), sep="\n", end="\n", file=self.terminal.stream) self.terminal.stream.flush() time.sleep(self.pause) return obj def handle_scene(self, obj): time.sleep(self.pause) return obj def handle_scenescript(self, obj): return obj def handle_shot(self, obj): return obj
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 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
def main(args): log_manager = LogManager() log = log_manager.get_logger("main") if args.log_path: log.set_route(args.log_level, LogAdapter(), sys.stderr) log.set_route(log.Level.NOTSET, LogAdapter(), args.log_path) else: log.set_route(args.log_level, LogAdapter(), sys.stderr) folders, references = resolve_objects(args) matcher = Matcher(folders) performer = Performer(folders, references) interlude = None handler = HTMLHandler(dwell=args.dwell, pause=args.pause) items = [] folder = True log.info("Reading sources...") while folder and not performer.stopped: for i in range(args.repeat + 1): if performer.script: log.info("Script {0.fP}".format(performer.script)) folder, index, script, selection, interlude = performer.next( folders, references, strict=args.strict, roles=args.roles) for item in performer.run(strict=args.strict, roles=args.roles): items.extend(list(handler(item))) if interlude is not None: metadata = interlude(folder, index, references) folder = next(matcher.options(metadata)) log.info("Writing {0} items to output...".format(len(items))) print(handler.to_html(metadata=performer.metadata)) log.info("Done.") return 0
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 SceneScript: """Gives access to a Turberfield scene script (.rst) file. This class allows discovery and classification of scene files prior to loading them in memory. Once loaded, it allows entity selection based on the role definitions in the file. Casting a selection permits the script to be iterated as a sequence of dialogue items. """ Folder = namedtuple( "Folder", ["pkg", "description", "metadata", "paths", "interludes"]) settings = Values(defaults=dict( character_level_inline_markup=False, debug=False, error_encoding="utf-8", error_encoding_error_handler="backslashreplace", halt_level=4, auto_id_prefix="", id_prefix="", language_code="en", pep_references=1, report_level=2, rfc_references=1, strict_visitor=False, tab_width=4, warning_stream=sys.stderr, raw_enabled=True, file_insertion_enabled=True, input_encoding="utf-8", input_encoding_error_handler="replace", line_length_limit=float("inf"), )) docutils.parsers.rst.directives.register_directive("entity", EntityDirective) docutils.parsers.rst.directives.register_directive("property", PropertyDirective) docutils.parsers.rst.directives.register_directive("fx", FXDirective) docutils.parsers.rst.directives.register_directive("memory", MemoryDirective) docutils.parsers.rst.directives.register_directive("condition", ConditionDirective) @classmethod 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) @staticmethod def read(text, name=None): """Read a block of text as a docutils document. :param str text: Scene script text. :param str name: An optional name for the document. :return: A document object. """ doc = docutils.utils.new_document(name, SceneScript.settings) parser = docutils.parsers.rst.Parser() parser.parse(text, doc) return doc def __init__(self, fP, metadata=None, doc=None): self.log_manager = LogManager() self.log = self.log_manager.get_logger( "turberfield.dialogue.model.scenescript") self.fP = fP self.metadata = metadata self.doc = doc def __enter__(self): with open(self.fP, "r") as script: self.doc = self.read(script.read()) return self def __exit__(self, exc_type, exc_value, traceback): return False def select(self, personae, relative=False, roles=1): """Select a persona for each entity declared in the scene. :param personae: A sequence of Personae. :param bool relative: Affects imports from namespace packages. Used for testing only. :param int roles: The maximum number of roles allocated to each persona. :return: An OrderedDict of {Entity: Persona}. """ def constrained(entity): return (len(entity["options"].get("types", [])) + len(entity["options"].get("states", []))) rv = OrderedDict() performing = defaultdict(set) pool = list(personae) self.log.debug(pool, {"path": self.fP}) entities = OrderedDict([("".join(entity.attributes["names"]), entity) for entity in sorted(group_by_type(self.doc)[ EntityDirective.Declaration], key=constrained, reverse=True)]) for e in entities.values(): types = tuple( filter(None, (e.string_import(t, relative) for t in e["options"].get("types", [])))) states = tuple( filter( None, (int(t) if t.isdigit() else e.string_import(t, relative) for t in e["options"].get("states", [])))) otherRoles = {i.lower() for i in e["options"].get("roles", [])} typ = types or object persona = next( (i for i in pool if isinstance(i, typ) and getattr(i, "get_state", not states) and all( str(i.get_state(type(s))).startswith(str(s)) for s in states) and (performing[i].issubset(otherRoles) or not otherRoles)), None) rv[e] = persona performing[persona].update(set(e.attributes["names"])) if not otherRoles or list(rv.values()).count(persona) == roles: try: pool.remove(persona) except ValueError: self.log.debug( "No persona for type {0} and states {1} with {2} {3}.". format(typ, states, roles, "role" if roles == 1 else "roles"), {"path": self.fP}) return rv def cast(self, mapping): """Allocate the scene script a cast of personae for each of its entities. :param mapping: A dictionary of {Entity, Persona} :return: The SceneScript object. """ # See 'citation' method in # http://docutils.sourceforge.net/docutils/parsers/rst/states.py for c, p in mapping.items(): self.doc.note_citation(c) self.doc.note_explicit_target(c, c) c.persona = p self.log.debug( "{0} to be played by {1}".format(c["names"][0].capitalize(), p), {"path": self.fP}) return self def run(self): """Parse the script file. :rtype: :py:class:`~turberfield.dialogue.model.Model` """ model = Model(self.fP, self.doc) self.doc.walkabout(model) return model
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
def test_format_key_error(self): manager = LogManager() logger = manager.get_logger("test_key_error") logger.frame += ["{obj[foo]}"] self.assertTrue(list(logger.format("test message", obj={"foo": "bar"}))) self.assertTrue(list(logger.format("test message", obj={})))
def test_format_index_error(self): manager = LogManager() logger = manager.get_logger("test_index_error") logger.frame += ["{data[1]}"] self.assertTrue(list(logger.format("test message", data="0123"))) self.assertTrue(list(logger.format("test message", data="")))
def test_format_attribute_error(self): manager = LogManager() logger = manager.get_logger("test_attribute_error") logger.frame += ["{id.hex}"] self.assertTrue(list(logger.format("test message", id=uuid.uuid4()))) self.assertTrue(list(logger.format("test message", id="no_hex_attribute")))
def main(args): log_manager = LogManager() log = log_manager.get_logger("main") if args.log_path: log.set_route(args.log_level, LogAdapter(), sys.stderr) log.set_route(log.Level.NOTSET, LogAdapter(), args.log_path) else: log.set_route(args.log_level, LogAdapter(), sys.stderr) if args.web: os.chdir(sys.prefix) log.warning("Web mode: running scripts from directory {0}".format( os.getcwd())) params = [(k, getattr(args, k)) for k in ("log_level", "log_path", "port", "session", "locn", "references", "pause", "dwell", "repeat", "roles", "strict")] params.extend([("folder", i) for i in args.folder]) log.info(params) opts = urllib.parse.urlencode(params) url = "http://localhost:{0}/{1}/turberfield-rehearse?{2}".format( args.port, args.locn, opts) log.info(url) webbrowser.open_new_tab(url) Handler = http.server.CGIHTTPRequestHandler Handler.cgi_directories = ["/{0}".format(args.locn)] httpd = http.server.HTTPServer(("", args.port), Handler) log.info("serving at port {0}".format(args.port)) try: httpd.serve_forever() except KeyboardInterrupt: log.info("Shutdown.") return 0 elif "SERVER_NAME" in os.environ: form = cgi.FieldStorage() params = { key: getattr(form[key], "value", form.getlist(key)) if key in form else None for key in vars(args).keys() } params["folder"] = form.getlist("folder") log_manager = LogManager() log = log_manager.get_logger("turberfield") log.info("params") log.info(params) cgitb.enable() args = argparse.Namespace(**params) if not args.session: log.info("Consumer view.") print(cgi_consumer(args)) else: log.info("Producer view.") list(cgi_producer(args)) while True: log.info("Sleeping...") time.sleep(3) else: for line in presenter(args): log.debug(line) return 0
def test_format_type_error(self): manager = LogManager() logger = manager.get_logger("test_key_error") logger.frame += ["{1[0]", "{obj[foo]}"] self.assertTrue(list(logger.format("test message", None, obj=None)))