Example #1
0
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)
Example #2
0
    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 = []
Example #3
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)
Example #4
0
 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
Example #5
0
 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())
Example #6
0
 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__()
Example #7
0
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())
Example #8
0
    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
Example #9
0
    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))
Example #10
0
    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()
Example #11
0
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
Example #12
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
Example #13
0
class EndpointRegistrationTests(unittest.TestCase):

    def setUp(self):
        self.manager = LogManager()
        self.assertIn(sys.stderr, self.manager.endings.values())

    def tearDown(self):
        self.manager.loggers.clear()
        self.manager.routing.clear()
        self.manager.endings.clear()

    def test_register_stderr(self):
        d = {}
        rv = self.manager.register_endpoint(sys.stderr, registry=d)
        self.assertEqual(d, {sys.stderr.name: sys.stderr})

    def test_register_stream(self):
        stream = io.StringIO()
        d = {}
        rv = self.manager.register_endpoint(stream, registry=d)
        self.assertEqual(d, {id(stream): stream})
Example #14
0
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)
Example #15
0
    def string_import(arg, relative=False, sep=None, path=None, line_nr=None):
        log = LogManager().get_logger("turberfield.dialogue.model").clone(
            "turberfield.dialogue.directives")
        arg = arg.strip()
        if not arg:
            log.warning(
                "Empty argument",
                {
                    "path": path,
                    "line_nr": line_nr
                },
            )
            return None

        try:
            return int(arg)
        except ValueError:
            pass

        bits = arg.split(".")
        if sep is None:
            index = min(n for n, i in enumerate(bits) if i and i[0].isupper())
        elif sep != ".":
            index = min(n for n, i in enumerate(bits) if i and sep in i) + 1
            bits = arg.replace(sep, ".").split(".")
        else:
            index = -1

        start = 1 if relative else 0
        modName = ".".join(bits[start:index])
        try:
            # Try importing an installed module
            mod = importlib.import_module(modName)
        except ImportError:
            # Try importing a source file at this location
            mN = bits[index - 1]
            spec = importlib.util.spec_from_file_location(
                mN,
                os.path.abspath(modName.replace(".", os.sep)) + ".py")
            if spec is None:
                return None
            mod = importlib.util.module_from_spec(spec)
            spec.loader.exec_module(mod)
            sys.modules[mN] = mod

        obj = mod
        for name in bits[index:]:
            try:
                obj = getattr(obj, name)
            except AttributeError:
                log.warning("Object missing an attribute", {
                    "path": path,
                    "line_nr": line_nr
                },
                            token=name)
                return None

        return obj
Example #16
0
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
Example #17
0
    def populate(cls, con, items, log=None):
        log = log or LogManager().get_logger(
            "turberfield.dialogue.schema.populate")
        states = [i for i in items if type(i) is enum.EnumMeta]
        entities = [i for i in items if i not in states]
        rv = 0
        for state in states:
            for defn in state:
                try:
                    Insertion(cls.tables["state"],
                              data={
                                  "class": defn.__objclass__.__name__,
                                  "name": defn.name,
                                  "value": defn.value
                              }).run(con)
                except sqlite3.IntegrityError as e:
                    log.warning(e)
                    con.rollback()
                except Exception as e:
                    log.error(e)
                    con.rollback()
                else:
                    rv += 1

        for entity in entities:
            try:
                Insertion(cls.tables["entity"],
                          data={
                              "name":
                              getattr(
                                  entity, "_name",
                                  getattr(entity, "name",
                                          entity.__class__.__name__))
                          }).run(con)
            except sqlite3.IntegrityError as e:
                log.warning(e)
                con.rollback()
            except Exception as e:
                log.error(e)
                con.rollback()
            else:
                rv += 1
        return rv
Example #18
0
 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="")))
Example #19
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))
Example #20
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
Example #21
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))
Example #22
0
 def setUp(self):
     self.manager = LogManager()
Example #23
0
 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")))
Example #24
0
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
Example #25
0
 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)]
     )
Example #26
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)))
Example #27
0
 def setUp(self):
     self.manager = LogManager()
     self.assertIn(sys.stderr, self.manager.endings.values())
Example #28
0
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
Example #29
0
 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={})))