@ -24,7 +24,7 @@ parameters given by the setting SOUND_QUALITY. This script requires
 | 
			
		||||
Sox (and soxi).
 | 
			
		||||
"""
 | 
			
		||||
import atexit
 | 
			
		||||
import concurrent.futures as futures
 | 
			
		||||
from concurrent import futures
 | 
			
		||||
import logging
 | 
			
		||||
import time
 | 
			
		||||
import os
 | 
			
		||||
@ -142,9 +142,9 @@ class MonitorHandler(PatternMatchingEventHandler):
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    pool = None
 | 
			
		||||
    jobs = {}
 | 
			
		||||
    jobs = None
 | 
			
		||||
 | 
			
		||||
    def __init__(self, subdir, pool, **sync_kw):
 | 
			
		||||
    def __init__(self, subdir, pool, jobs=None, **sync_kw):
 | 
			
		||||
        """
 | 
			
		||||
        :param str subdir: sub-directory in program dirs to monitor \
 | 
			
		||||
            (SOUND_ARCHIVES_SUBDIR or SOUND_EXCERPTS_SUBDIR);
 | 
			
		||||
@ -154,6 +154,7 @@ class MonitorHandler(PatternMatchingEventHandler):
 | 
			
		||||
        """
 | 
			
		||||
        self.subdir = subdir
 | 
			
		||||
        self.pool = pool
 | 
			
		||||
        self.jobs = jobs or {}
 | 
			
		||||
        self.sync_kw = sync_kw
 | 
			
		||||
 | 
			
		||||
        patterns = [
 | 
			
		||||
@ -199,29 +200,27 @@ class MonitorHandler(PatternMatchingEventHandler):
 | 
			
		||||
 | 
			
		||||
class SoundMonitor:
 | 
			
		||||
    """Monitor for filesystem changes in order to synchronise database and
 | 
			
		||||
    analyse files."""
 | 
			
		||||
    analyse files of a provided program."""
 | 
			
		||||
 | 
			
		||||
    def report(self, program=None, component=None, logger=logging, *content):
 | 
			
		||||
        if not component:
 | 
			
		||||
    def report(self, program=None, component=None, *content, logger=logging):
 | 
			
		||||
        content = " ".join([str(c) for c in content])
 | 
			
		||||
        logger.info(
 | 
			
		||||
                "%s: %s", str(program), " ".join([str(c) for c in content])
 | 
			
		||||
            )
 | 
			
		||||
        else:
 | 
			
		||||
            logger.info(
 | 
			
		||||
                "%s, %s: %s",
 | 
			
		||||
                str(program),
 | 
			
		||||
                str(component),
 | 
			
		||||
                " ".join([str(c) for c in content]),
 | 
			
		||||
            f"{program}: {content}"
 | 
			
		||||
            if not component
 | 
			
		||||
            else f"{program}, {component}: {content}"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def scan(self, logger=logging):
 | 
			
		||||
        """For all programs, scan dirs."""
 | 
			
		||||
        """For all programs, scan dirs.
 | 
			
		||||
 | 
			
		||||
        Return scanned directories.
 | 
			
		||||
        """
 | 
			
		||||
        logger.info("scan all programs...")
 | 
			
		||||
        programs = Program.objects.filter()
 | 
			
		||||
 | 
			
		||||
        dirs = []
 | 
			
		||||
        for program in programs:
 | 
			
		||||
            logger.info("#%d %s", program.id, program.title)
 | 
			
		||||
            logger.info(f"#{program.id} {program.title}")
 | 
			
		||||
            self.scan_for_program(
 | 
			
		||||
                program,
 | 
			
		||||
                settings.SOUND_ARCHIVES_SUBDIR,
 | 
			
		||||
@ -234,7 +233,7 @@ class SoundMonitor:
 | 
			
		||||
                logger=logger,
 | 
			
		||||
                type=Sound.TYPE_EXCERPT,
 | 
			
		||||
            )
 | 
			
		||||
            dirs.append(os.path.join(program.abspath))
 | 
			
		||||
            dirs.append(program.abspath)
 | 
			
		||||
        return dirs
 | 
			
		||||
 | 
			
		||||
    def scan_for_program(
 | 
			
		||||
@ -272,7 +271,12 @@ class SoundMonitor:
 | 
			
		||||
            if sound.check_on_file():
 | 
			
		||||
                SoundFile(sound.file.path).sync(sound=sound, **sync_kwargs)
 | 
			
		||||
 | 
			
		||||
    _running = False
 | 
			
		||||
 | 
			
		||||
    def monitor(self, logger=logging):
 | 
			
		||||
        if self._running:
 | 
			
		||||
            raise RuntimeError("already running")
 | 
			
		||||
 | 
			
		||||
        """Run in monitor mode."""
 | 
			
		||||
        with futures.ThreadPoolExecutor() as pool:
 | 
			
		||||
            archives_handler = MonitorHandler(
 | 
			
		||||
@ -307,5 +311,13 @@ class SoundMonitor:
 | 
			
		||||
 | 
			
		||||
            atexit.register(leave)
 | 
			
		||||
 | 
			
		||||
            while True:
 | 
			
		||||
            self._running = True
 | 
			
		||||
            while self._running:
 | 
			
		||||
                time.sleep(1)
 | 
			
		||||
 | 
			
		||||
            leave()
 | 
			
		||||
            atexit.unregister(leave)
 | 
			
		||||
 | 
			
		||||
    def stop(self):
 | 
			
		||||
        """Stop monitor() loop."""
 | 
			
		||||
        self._running = False
 | 
			
		||||
 | 
			
		||||
@ -44,20 +44,45 @@ InterfaceTarget = namedtuple(
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class WrapperMixin:
 | 
			
		||||
    def __init__(self, target=None, ns=None, ns_attr=None, **kwargs):
 | 
			
		||||
    type_interface = None
 | 
			
		||||
    """For instance of class wrapped by an Interface, this is the wrapping
 | 
			
		||||
    interface of the class."""
 | 
			
		||||
    instances = None
 | 
			
		||||
    ns = None
 | 
			
		||||
    ns_attr = None
 | 
			
		||||
 | 
			
		||||
    def __init__(
 | 
			
		||||
        self, target=None, ns=None, ns_attr=None, type_interface=None, **kwargs
 | 
			
		||||
    ):
 | 
			
		||||
        self.target = target
 | 
			
		||||
        if ns:
 | 
			
		||||
            self.inject(ns, ns_attr)
 | 
			
		||||
        if self.type_interface:
 | 
			
		||||
            self._set_type_interface(type_interface)
 | 
			
		||||
        super().__init__(**kwargs)
 | 
			
		||||
 | 
			
		||||
    def _set_type_interface(self, type_interface):
 | 
			
		||||
        if self.type_interface:
 | 
			
		||||
            raise RuntimeError("a type interface is already assigned")
 | 
			
		||||
 | 
			
		||||
        self.type_interface = type_interface
 | 
			
		||||
        if not type_interface.instances:
 | 
			
		||||
            type_interface.instances = [self]
 | 
			
		||||
        else:
 | 
			
		||||
            type_interface.instances.append(self)
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def ns_target(self):
 | 
			
		||||
        """Actual namespace's target (using ns.ns_attr)"""
 | 
			
		||||
        if self.ns and self.ns_attr:
 | 
			
		||||
            return getattr(self.ns, self.ns_attr, None)
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
    def inject(self, ns=None, ns_attr=None):
 | 
			
		||||
        if ns and ns_attr:
 | 
			
		||||
        """Inject interface into namespace at given key."""
 | 
			
		||||
        if not (ns and ns_attr):
 | 
			
		||||
            raise ValueError("ns and ns_attr must be provided together")
 | 
			
		||||
 | 
			
		||||
        ns_target = getattr(ns, ns_attr, None)
 | 
			
		||||
        if self.target is ns_target:
 | 
			
		||||
            return
 | 
			
		||||
@ -66,16 +91,17 @@ class WrapperMixin:
 | 
			
		||||
                "self target already injected. It must be "
 | 
			
		||||
                "`release` before `inject`."
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        self.target = ns_target
 | 
			
		||||
            setattr(ns, ns_attr, self.parent)
 | 
			
		||||
        elif not ns or not ns_attr:
 | 
			
		||||
            raise ValueError("ns and ns_attr must be provided together")
 | 
			
		||||
        setattr(ns, ns_attr, self.interface)
 | 
			
		||||
 | 
			
		||||
        self.ns = ns
 | 
			
		||||
        self.ns_attr = ns_attr
 | 
			
		||||
 | 
			
		||||
    def release(self):
 | 
			
		||||
        if self.ns_target is self:
 | 
			
		||||
            setattr(self.target.namespace, self.target.name, self.target)
 | 
			
		||||
        """Remove injection from previously injected parent, reset target."""
 | 
			
		||||
        if self.ns_target is self.interface:
 | 
			
		||||
            setattr(self.ns, self.ns_attr, self.target)
 | 
			
		||||
        self.target = None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -83,7 +109,9 @@ class SpoofMixin:
 | 
			
		||||
    traces = None
 | 
			
		||||
 | 
			
		||||
    def __init__(self, funcs=None, **kwargs):
 | 
			
		||||
        self.reset(funcs or {})
 | 
			
		||||
        self.reset(
 | 
			
		||||
            funcs or {},
 | 
			
		||||
        )
 | 
			
		||||
        super().__init__(**kwargs)
 | 
			
		||||
 | 
			
		||||
    def reset(self, funcs=None):
 | 
			
		||||
@ -152,23 +180,19 @@ class SpoofMixin:
 | 
			
		||||
        """
 | 
			
		||||
        func = self.funcs[name]
 | 
			
		||||
        if callable(func):
 | 
			
		||||
            return func(*a, **kw)
 | 
			
		||||
            return func(self, *a, **kw)
 | 
			
		||||
        return func
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class InterfaceMeta(SpoofMixin, WrapperMixin):
 | 
			
		||||
    calls = None
 | 
			
		||||
    """Calls done."""
 | 
			
		||||
 | 
			
		||||
    def __init__(self, parent, **kwargs):
 | 
			
		||||
        self.parent = parent
 | 
			
		||||
class Interface:
 | 
			
		||||
    class IMeta(SpoofMixin, WrapperMixin):
 | 
			
		||||
        def __init__(self, interface, **kwargs):
 | 
			
		||||
            self.interface = interface
 | 
			
		||||
            super().__init__(**kwargs)
 | 
			
		||||
 | 
			
		||||
        def __getitem__(self, name):
 | 
			
		||||
            return self.traces[name]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Interface:
 | 
			
		||||
    _imeta = None
 | 
			
		||||
    """This contains a InterfaceMeta instance related to Interface one.
 | 
			
		||||
 | 
			
		||||
@ -182,7 +206,7 @@ class Interface:
 | 
			
		||||
            _imeta_kw.setdefault("funcs", _funcs)
 | 
			
		||||
        if _target is not None:
 | 
			
		||||
            _imeta_kw.setdefault("target", _target)
 | 
			
		||||
        self._imeta = InterfaceMeta(self, **_imeta_kw)
 | 
			
		||||
        self._imeta = self.IMeta(self, **_imeta_kw)
 | 
			
		||||
        self.__dict__.update(kwargs)
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
@ -195,19 +219,29 @@ class Interface:
 | 
			
		||||
        return cls(**kwargs)
 | 
			
		||||
 | 
			
		||||
    def _irelease(self):
 | 
			
		||||
        """Shortcut to `self._imeta.release`."""
 | 
			
		||||
        self._imeta.release()
 | 
			
		||||
 | 
			
		||||
    def _trace(self, *args, **kw):
 | 
			
		||||
        """Shortcut to `self._imeta.get_trace`."""
 | 
			
		||||
        return self._imeta.get_trace(*args, **kw)
 | 
			
		||||
 | 
			
		||||
    def _traces(self, *args, **kw):
 | 
			
		||||
        """Shortcut to `self._imeta.get_traces`."""
 | 
			
		||||
        return self._imeta.get_traces(*args, **kw)
 | 
			
		||||
 | 
			
		||||
    def __call__(self, *args, **kwargs):
 | 
			
		||||
        target = self._imeta.target
 | 
			
		||||
        print("is it class?", self, target, inspect.isclass(target))
 | 
			
		||||
        if inspect.isclass(target):
 | 
			
		||||
            target = target(*args, **kwargs)
 | 
			
		||||
            return type(self)(target, _imeta_kw={"funcs": self._imeta.funcs})
 | 
			
		||||
            return type(self)(
 | 
			
		||||
                target,
 | 
			
		||||
                _imeta_kw={"type_interface": self, "funcs": self._imeta.funcs},
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        if "__call__" in self._imeta.funcs:
 | 
			
		||||
            return self._imeta.call("__call__", args, kwargs)
 | 
			
		||||
 | 
			
		||||
        self._imeta.add("__call__", args, kwargs)
 | 
			
		||||
        return self._imeta.target(*args, **kwargs)
 | 
			
		||||
@ -216,3 +250,7 @@ class Interface:
 | 
			
		||||
        if attr in self._imeta.funcs:
 | 
			
		||||
            return lambda *args, **kwargs: self._imeta.call(attr, args, kwargs)
 | 
			
		||||
        return getattr(self._imeta.target, attr)
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        iface = super().__str__()
 | 
			
		||||
        return f"{iface}::{self._imeta.target}"
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,10 @@
 | 
			
		||||
from concurrent import futures
 | 
			
		||||
import logging
 | 
			
		||||
import pytest
 | 
			
		||||
 | 
			
		||||
from django.utils import timezone as tz
 | 
			
		||||
 | 
			
		||||
from aircox.conf import settings
 | 
			
		||||
from aircox.models import Sound
 | 
			
		||||
from aircox.controllers import sound_monitor
 | 
			
		||||
from aircox.test import Interface, interface
 | 
			
		||||
@ -25,8 +27,8 @@ def logger():
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture
 | 
			
		||||
def interfaces(logger):
 | 
			
		||||
    return {
 | 
			
		||||
def interfaces():
 | 
			
		||||
    items = {
 | 
			
		||||
        "SoundFile": Interface.inject(
 | 
			
		||||
            sound_monitor,
 | 
			
		||||
            "SoundFile",
 | 
			
		||||
@ -41,8 +43,11 @@ def interfaces(logger):
 | 
			
		||||
                "sleep": None,
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
        "datetime": Interface.inject(sound_monitor, "datetime", {now: now}),
 | 
			
		||||
        "datetime": Interface.inject(sound_monitor, "datetime", {"now": now}),
 | 
			
		||||
    }
 | 
			
		||||
    yield items
 | 
			
		||||
    for item in items.values():
 | 
			
		||||
        item._irelease()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture
 | 
			
		||||
@ -65,6 +70,23 @@ def modified_task(interfaces):
 | 
			
		||||
    return sound_monitor.ModifiedTask()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture
 | 
			
		||||
def monitor_handler(interfaces):
 | 
			
		||||
    pool = Interface(
 | 
			
		||||
        None,
 | 
			
		||||
        {
 | 
			
		||||
            "submit": lambda imeta, *a, **kw: Interface(
 | 
			
		||||
                None,
 | 
			
		||||
                {
 | 
			
		||||
                    "add_done_callback": None,
 | 
			
		||||
                    "done": False,
 | 
			
		||||
                },
 | 
			
		||||
            )
 | 
			
		||||
        },
 | 
			
		||||
    )
 | 
			
		||||
    return sound_monitor.MonitorHandler("/tmp", pool=pool, sync_kw=13)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestTask:
 | 
			
		||||
    def test___init__(self, task):
 | 
			
		||||
        assert task.timestamp is not None
 | 
			
		||||
@ -111,7 +133,7 @@ class TestModifiedTask:
 | 
			
		||||
        dt_now = now + modified_task.timeout_delta - tz.timedelta(hours=10)
 | 
			
		||||
        datetime = Interface.inject(sound_monitor, "datetime", {"now": dt_now})
 | 
			
		||||
 | 
			
		||||
        def sleep(n):
 | 
			
		||||
        def sleep(imeta, n):
 | 
			
		||||
            datetime._imeta.funcs[
 | 
			
		||||
                "now"
 | 
			
		||||
            ] = modified_task.timestamp + tz.timedelta(hours=10)
 | 
			
		||||
@ -129,31 +151,124 @@ class TestModifiedTask:
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestMonitorHandler:
 | 
			
		||||
    def test_monitor___init__(self):
 | 
			
		||||
        pass
 | 
			
		||||
    def test_on_created(self, monitor_handler, event):
 | 
			
		||||
        monitor_handler._submit = monitor_handler.pool.submit
 | 
			
		||||
        monitor_handler.on_created(event)
 | 
			
		||||
        trace_args, trace_kwargs = monitor_handler.pool._trace("submit")
 | 
			
		||||
        assert isinstance(trace_args[0], sound_monitor.CreateTask)
 | 
			
		||||
        assert trace_args[1:] == (event, "new")
 | 
			
		||||
        assert trace_kwargs == monitor_handler.sync_kw
 | 
			
		||||
 | 
			
		||||
    def test_on_created(self):
 | 
			
		||||
        pass
 | 
			
		||||
    def test_on_deleted(self, monitor_handler, event):
 | 
			
		||||
        monitor_handler._submit = monitor_handler.pool.submit
 | 
			
		||||
        monitor_handler.on_deleted(event)
 | 
			
		||||
        trace_args, _ = monitor_handler.pool._trace("submit")
 | 
			
		||||
        assert isinstance(trace_args[0], sound_monitor.DeleteTask)
 | 
			
		||||
        assert trace_args[1:] == (event, "del")
 | 
			
		||||
 | 
			
		||||
    def test_on_deleted(self):
 | 
			
		||||
        pass
 | 
			
		||||
    def test_on_moved(self, monitor_handler, event):
 | 
			
		||||
        monitor_handler._submit = monitor_handler.pool.submit
 | 
			
		||||
        monitor_handler.on_moved(event)
 | 
			
		||||
        trace_args, trace_kwargs = monitor_handler.pool._trace("submit")
 | 
			
		||||
        assert isinstance(trace_args[0], sound_monitor.MoveTask)
 | 
			
		||||
        assert trace_args[1:] == (event, "mv")
 | 
			
		||||
        assert trace_kwargs == monitor_handler.sync_kw
 | 
			
		||||
 | 
			
		||||
    def test_on_moved(self):
 | 
			
		||||
        pass
 | 
			
		||||
    def test_on_modified(self, monitor_handler, event):
 | 
			
		||||
        monitor_handler._submit = monitor_handler.pool.submit
 | 
			
		||||
        monitor_handler.on_modified(event)
 | 
			
		||||
        trace_args, trace_kwargs = monitor_handler.pool._trace("submit")
 | 
			
		||||
        assert isinstance(trace_args[0], sound_monitor.ModifiedTask)
 | 
			
		||||
        assert trace_args[1:] == (event, "up")
 | 
			
		||||
        assert trace_kwargs == monitor_handler.sync_kw
 | 
			
		||||
 | 
			
		||||
    def test_on_modified(self):
 | 
			
		||||
        pass
 | 
			
		||||
    def test__submit(self, monitor_handler, event):
 | 
			
		||||
        handler = Interface()
 | 
			
		||||
        handler, created = monitor_handler._submit(
 | 
			
		||||
            handler, event, "prefix", kw=13
 | 
			
		||||
        )
 | 
			
		||||
        assert created
 | 
			
		||||
        assert handler.future._trace("add_done_callback")
 | 
			
		||||
        assert monitor_handler.pool._trace("submit") == (
 | 
			
		||||
            (handler, event),
 | 
			
		||||
            {"kw": 13},
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test__submit(self):
 | 
			
		||||
        pass
 | 
			
		||||
        key = f"prefix:{event.src_path}"
 | 
			
		||||
        assert monitor_handler.jobs.get(key) == handler
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture
 | 
			
		||||
def monitor_interfaces():
 | 
			
		||||
    items = {
 | 
			
		||||
        "atexit": Interface.inject(
 | 
			
		||||
            sound_monitor, "atexit", {"register": None, "leave": None}
 | 
			
		||||
        ),
 | 
			
		||||
        "observer": Interface.inject(
 | 
			
		||||
            sound_monitor,
 | 
			
		||||
            "Observer",
 | 
			
		||||
            {
 | 
			
		||||
                "schedule": None,
 | 
			
		||||
                "start": None,
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
    }
 | 
			
		||||
    yield items
 | 
			
		||||
    for item in items.values():
 | 
			
		||||
        item.release()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture
 | 
			
		||||
def monitor():
 | 
			
		||||
    yield sound_monitor.SoundMonitor()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SoundMonitor:
 | 
			
		||||
    def test_report(self):
 | 
			
		||||
        pass
 | 
			
		||||
    def test_report(self, monitor, program, logger):
 | 
			
		||||
        monitor.report(program, "component", "content", logger=logger)
 | 
			
		||||
        msg = f"{program}, component: content"
 | 
			
		||||
        assert logger._trace("info", args=True) == (msg,)
 | 
			
		||||
 | 
			
		||||
    def test_scan(self):
 | 
			
		||||
        pass
 | 
			
		||||
    def test_scan(self, monitor, program, logger):
 | 
			
		||||
        interface = Interface(None, {"scan_for_program": None})
 | 
			
		||||
        monitor.scan_for_program = interface.scan_for_program
 | 
			
		||||
        dirs = monitor.scan(logger)
 | 
			
		||||
 | 
			
		||||
    def test_monitor(self):
 | 
			
		||||
        pass
 | 
			
		||||
        assert logger._traces("info") == (
 | 
			
		||||
            "scan all programs...",
 | 
			
		||||
            f"#{program.id} {program.title}",
 | 
			
		||||
        )
 | 
			
		||||
        assert dirs == [program.abspath]
 | 
			
		||||
        assert interface._traces("scan_for_program") == (
 | 
			
		||||
            ((program, settings.SOUND_ARCHIVES_SUBDIR), {"logger": logger})(
 | 
			
		||||
                (program, settings.SOUND_EXCERPTS_SUBDIR), {"logger": logger}
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_monitor(self, monitor, monitor_interfaces, logger):
 | 
			
		||||
        def sleep(*args, **kwargs):
 | 
			
		||||
            monitor.stop()
 | 
			
		||||
 | 
			
		||||
        time = Interface.inject(sound_monitor, "time", {"sleep": sleep})
 | 
			
		||||
        monitor.monitor(logger=logger)
 | 
			
		||||
        time._irelease()
 | 
			
		||||
 | 
			
		||||
        observers = monitor_interfaces["observer"].instances
 | 
			
		||||
        observer = observers and observers[0]
 | 
			
		||||
        assert observer
 | 
			
		||||
        schedules = observer._traces("schedule")
 | 
			
		||||
        for (handler, *_), kwargs in schedules:
 | 
			
		||||
            assert isinstance(handler, sound_monitor.MonitorHandler)
 | 
			
		||||
            assert isinstance(handler.pool, futures.ThreadPoolExecutor)
 | 
			
		||||
            assert (handler.subdir, handler.type) in (
 | 
			
		||||
                (settings.SOUND_ARCHIVES_SUBDIR, Sound.TYPE_ARCHIVE),
 | 
			
		||||
                (settings.SOUND_EXCERPTS_SUBDIR, Sound.TYPE_EXCERPT),
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        assert observer._trace("start")
 | 
			
		||||
 | 
			
		||||
        atexit = monitor_interfaces["atexit"]
 | 
			
		||||
        assert atexit._trace("register")
 | 
			
		||||
        assert atexit._trace("unregister")
 | 
			
		||||
 | 
			
		||||
        assert observers
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user