예제 #1
0
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))
예제 #2
0
    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)
예제 #3
0
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
예제 #4
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))
예제 #5
0
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