✨ Small spelling fix
This commit is contained in:
@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
This file contains definitions for a MEOW pattern based off of file events,
|
This file contains definitions for a MEOW pattern based off of file events,
|
||||||
along with an appropriate monitor for said events.
|
along with an appropriate monitor for said events.
|
||||||
|
|
||||||
Author(s): David Marchant
|
Author(s): David Marchant
|
||||||
@ -49,16 +49,16 @@ WATCHDOG_HASH = "file_hash"
|
|||||||
|
|
||||||
WATCHDOG_EVENT_KEYS = {
|
WATCHDOG_EVENT_KEYS = {
|
||||||
WATCHDOG_BASE: str,
|
WATCHDOG_BASE: str,
|
||||||
WATCHDOG_HASH: str,
|
WATCHDOG_HASH: str,
|
||||||
**EVENT_KEYS
|
**EVENT_KEYS
|
||||||
}
|
}
|
||||||
|
|
||||||
def create_watchdog_event(path:str, rule:Any, base:str, time:float,
|
def create_watchdog_event(path:str, rule:Any, base:str, time:float,
|
||||||
hash:str, extras:Dict[Any,Any]={})->Dict[Any,Any]:
|
hash:str, extras:Dict[Any,Any]={})->Dict[Any,Any]:
|
||||||
"""Function to create a MEOW event dictionary."""
|
"""Function to create a MEOW event dictionary."""
|
||||||
return create_event(
|
return create_event(
|
||||||
EVENT_TYPE_WATCHDOG,
|
EVENT_TYPE_WATCHDOG,
|
||||||
path,
|
path,
|
||||||
rule,
|
rule,
|
||||||
time,
|
time,
|
||||||
extras={
|
extras={
|
||||||
@ -70,9 +70,6 @@ def create_watchdog_event(path:str, rule:Any, base:str, time:float,
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
def valid_watchdog_event(event:Dict[str,Any])->None:
|
|
||||||
valid_meow_dict(event, "Watchdog event", WATCHDOG_EVENT_KEYS)
|
|
||||||
|
|
||||||
|
|
||||||
class FileEventPattern(BasePattern):
|
class FileEventPattern(BasePattern):
|
||||||
# The path at which events will trigger this pattern
|
# The path at which events will trigger this pattern
|
||||||
@ -81,11 +78,11 @@ class FileEventPattern(BasePattern):
|
|||||||
triggering_file:str
|
triggering_file:str
|
||||||
# Which types of event the pattern responds to
|
# Which types of event the pattern responds to
|
||||||
event_mask:List[str]
|
event_mask:List[str]
|
||||||
def __init__(self, name:str, triggering_path:str, recipe:str,
|
def __init__(self, name:str, triggering_path:str, recipe:str,
|
||||||
triggering_file:str, event_mask:List[str]=_DEFAULT_MASK,
|
triggering_file:str, event_mask:List[str]=_DEFAULT_MASK,
|
||||||
parameters:Dict[str,Any]={}, outputs:Dict[str,Any]={},
|
parameters:Dict[str,Any]={}, outputs:Dict[str,Any]={},
|
||||||
sweep:Dict[str,Any]={}):
|
sweep:Dict[str,Any]={}):
|
||||||
"""FileEventPattern Constructor. This is used to match against file
|
"""FileEventPattern Constructor. This is used to match against file
|
||||||
system events, as caught by the python watchdog module."""
|
system events, as caught by the python watchdog module."""
|
||||||
super().__init__(name, recipe, parameters, outputs, sweep)
|
super().__init__(name, recipe, parameters, outputs, sweep)
|
||||||
self._is_valid_triggering_path(triggering_path)
|
self._is_valid_triggering_path(triggering_path)
|
||||||
@ -96,70 +93,70 @@ class FileEventPattern(BasePattern):
|
|||||||
self.event_mask = event_mask
|
self.event_mask = event_mask
|
||||||
|
|
||||||
def _is_valid_triggering_path(self, triggering_path:str)->None:
|
def _is_valid_triggering_path(self, triggering_path:str)->None:
|
||||||
"""Validation check for 'triggering_path' variable from main
|
"""Validation check for 'triggering_path' variable from main
|
||||||
constructor."""
|
constructor."""
|
||||||
valid_string(
|
valid_string(
|
||||||
triggering_path,
|
triggering_path,
|
||||||
VALID_PATH_CHARS+'*',
|
VALID_PATH_CHARS+'*',
|
||||||
min_length=1,
|
min_length=1,
|
||||||
hint="FileEventPattern.triggering_path"
|
hint="FileEventPattern.triggering_path"
|
||||||
)
|
)
|
||||||
if len(triggering_path) < 1:
|
if len(triggering_path) < 1:
|
||||||
raise ValueError (
|
raise ValueError (
|
||||||
f"triggiering path '{triggering_path}' is too short. "
|
f"triggering path '{triggering_path}' is too short. "
|
||||||
"Minimum length is 1"
|
"Minimum length is 1"
|
||||||
)
|
)
|
||||||
|
|
||||||
def _is_valid_triggering_file(self, triggering_file:str)->None:
|
def _is_valid_triggering_file(self, triggering_file:str)->None:
|
||||||
"""Validation check for 'triggering_file' variable from main
|
"""Validation check for 'triggering_file' variable from main
|
||||||
constructor."""
|
constructor."""
|
||||||
valid_string(
|
valid_string(
|
||||||
triggering_file,
|
triggering_file,
|
||||||
VALID_VARIABLE_NAME_CHARS,
|
VALID_VARIABLE_NAME_CHARS,
|
||||||
hint="FileEventPattern.triggering_file"
|
hint="FileEventPattern.triggering_file"
|
||||||
)
|
)
|
||||||
|
|
||||||
def _is_valid_recipe(self, recipe:str)->None:
|
def _is_valid_recipe(self, recipe:str)->None:
|
||||||
"""Validation check for 'recipe' variable from main constructor.
|
"""Validation check for 'recipe' variable from main constructor.
|
||||||
Called within parent BasePattern constructor."""
|
Called within parent BasePattern constructor."""
|
||||||
valid_string(
|
valid_string(
|
||||||
recipe,
|
recipe,
|
||||||
VALID_RECIPE_NAME_CHARS,
|
VALID_RECIPE_NAME_CHARS,
|
||||||
hint="FileEventPattern.recipe"
|
hint="FileEventPattern.recipe"
|
||||||
)
|
)
|
||||||
|
|
||||||
def _is_valid_parameters(self, parameters:Dict[str,Any])->None:
|
def _is_valid_parameters(self, parameters:Dict[str,Any])->None:
|
||||||
"""Validation check for 'parameters' variable from main constructor.
|
"""Validation check for 'parameters' variable from main constructor.
|
||||||
Called within parent BasePattern constructor."""
|
Called within parent BasePattern constructor."""
|
||||||
valid_dict(
|
valid_dict(
|
||||||
parameters,
|
parameters,
|
||||||
str,
|
str,
|
||||||
Any,
|
Any,
|
||||||
strict=False,
|
strict=False,
|
||||||
min_length=0,
|
min_length=0,
|
||||||
hint="FileEventPattern.parameters"
|
hint="FileEventPattern.parameters"
|
||||||
)
|
)
|
||||||
for k in parameters.keys():
|
for k in parameters.keys():
|
||||||
valid_string(
|
valid_string(
|
||||||
k,
|
k,
|
||||||
VALID_VARIABLE_NAME_CHARS,
|
VALID_VARIABLE_NAME_CHARS,
|
||||||
hint=f"FileEventPattern.parameters[{k}]"
|
hint=f"FileEventPattern.parameters[{k}]"
|
||||||
)
|
)
|
||||||
|
|
||||||
def _is_valid_output(self, outputs:Dict[str,str])->None:
|
def _is_valid_output(self, outputs:Dict[str,str])->None:
|
||||||
"""Validation check for 'output' variable from main constructor.
|
"""Validation check for 'output' variable from main constructor.
|
||||||
Called within parent BasePattern constructor."""
|
Called within parent BasePattern constructor."""
|
||||||
valid_dict(
|
valid_dict(
|
||||||
outputs,
|
outputs,
|
||||||
str,
|
str,
|
||||||
str,
|
str,
|
||||||
strict=False,
|
strict=False,
|
||||||
min_length=0,
|
min_length=0,
|
||||||
hint="FileEventPattern.outputs"
|
hint="FileEventPattern.outputs"
|
||||||
)
|
)
|
||||||
for k in outputs.keys():
|
for k in outputs.keys():
|
||||||
valid_string(
|
valid_string(
|
||||||
k,
|
k,
|
||||||
VALID_VARIABLE_NAME_CHARS,
|
VALID_VARIABLE_NAME_CHARS,
|
||||||
hint=f"FileEventPattern.outputs[{k}]"
|
hint=f"FileEventPattern.outputs[{k}]"
|
||||||
)
|
)
|
||||||
@ -167,9 +164,9 @@ class FileEventPattern(BasePattern):
|
|||||||
def _is_valid_event_mask(self, event_mask)->None:
|
def _is_valid_event_mask(self, event_mask)->None:
|
||||||
"""Validation check for 'event_mask' variable from main constructor."""
|
"""Validation check for 'event_mask' variable from main constructor."""
|
||||||
valid_list(
|
valid_list(
|
||||||
event_mask,
|
event_mask,
|
||||||
str,
|
str,
|
||||||
min_length=1,
|
min_length=1,
|
||||||
hint="FileEventPattern.event_mask"
|
hint="FileEventPattern.event_mask"
|
||||||
)
|
)
|
||||||
for mask in event_mask:
|
for mask in event_mask:
|
||||||
@ -193,18 +190,18 @@ class WatchdogMonitor(BaseMonitor):
|
|||||||
debug_level:int
|
debug_level:int
|
||||||
# Where print messages are sent
|
# Where print messages are sent
|
||||||
_print_target:Any
|
_print_target:Any
|
||||||
def __init__(self, base_dir:str, patterns:Dict[str,FileEventPattern],
|
def __init__(self, base_dir:str, patterns:Dict[str,FileEventPattern],
|
||||||
recipes:Dict[str,BaseRecipe], autostart=False, settletime:int=1,
|
recipes:Dict[str,BaseRecipe], autostart=False, settletime:int=1,
|
||||||
name:str="", print:Any=sys.stdout, logging:int=0)->None:
|
name:str="", print:Any=sys.stdout, logging:int=0)->None:
|
||||||
"""WatchdogEventHandler Constructor. This uses the watchdog module to
|
"""WatchdogEventHandler Constructor. This uses the watchdog module to
|
||||||
monitor a directory and all its sub-directories. Watchdog will provide
|
monitor a directory and all its sub-directories. Watchdog will provide
|
||||||
the monitor with an caught events, with the monitor comparing them
|
the monitor with an caught events, with the monitor comparing them
|
||||||
against its rules, and informing the runner of match."""
|
against its rules, and informing the runner of match."""
|
||||||
super().__init__(patterns, recipes, name=name)
|
super().__init__(patterns, recipes, name=name)
|
||||||
self._is_valid_base_dir(base_dir)
|
self._is_valid_base_dir(base_dir)
|
||||||
self.base_dir = base_dir
|
self.base_dir = base_dir
|
||||||
check_type(settletime, int, hint="WatchdogMonitor.settletime")
|
check_type(settletime, int, hint="WatchdogMonitor.settletime")
|
||||||
self._print_target, self.debug_level = setup_debugging(print, logging)
|
self._print_target, self.debug_level = setup_debugging(print, logging)
|
||||||
self.event_handler = WatchdogEventHandler(self, settletime=settletime)
|
self.event_handler = WatchdogEventHandler(self, settletime=settletime)
|
||||||
self.monitor = Observer()
|
self.monitor = Observer()
|
||||||
self.monitor.schedule(
|
self.monitor.schedule(
|
||||||
@ -212,7 +209,7 @@ class WatchdogMonitor(BaseMonitor):
|
|||||||
self.base_dir,
|
self.base_dir,
|
||||||
recursive=True
|
recursive=True
|
||||||
)
|
)
|
||||||
print_debug(self._print_target, self.debug_level,
|
print_debug(self._print_target, self.debug_level,
|
||||||
"Created new WatchdogMonitor instance", DEBUG_INFO)
|
"Created new WatchdogMonitor instance", DEBUG_INFO)
|
||||||
|
|
||||||
if autostart:
|
if autostart:
|
||||||
@ -220,14 +217,14 @@ class WatchdogMonitor(BaseMonitor):
|
|||||||
|
|
||||||
def start(self)->None:
|
def start(self)->None:
|
||||||
"""Function to start the monitor."""
|
"""Function to start the monitor."""
|
||||||
print_debug(self._print_target, self.debug_level,
|
print_debug(self._print_target, self.debug_level,
|
||||||
"Starting WatchdogMonitor", DEBUG_INFO)
|
"Starting WatchdogMonitor", DEBUG_INFO)
|
||||||
self._apply_retroactive_rules()
|
self._apply_retroactive_rules()
|
||||||
self.monitor.start()
|
self.monitor.start()
|
||||||
|
|
||||||
def stop(self)->None:
|
def stop(self)->None:
|
||||||
"""Function to stop the monitor."""
|
"""Function to stop the monitor."""
|
||||||
print_debug(self._print_target, self.debug_level,
|
print_debug(self._print_target, self.debug_level,
|
||||||
"Stopping WatchdogMonitor", DEBUG_INFO)
|
"Stopping WatchdogMonitor", DEBUG_INFO)
|
||||||
self.monitor.stop()
|
self.monitor.stop()
|
||||||
|
|
||||||
@ -235,7 +232,7 @@ class WatchdogMonitor(BaseMonitor):
|
|||||||
"""Function to determine if a given event matches the current rules."""
|
"""Function to determine if a given event matches the current rules."""
|
||||||
src_path = event.src_path
|
src_path = event.src_path
|
||||||
|
|
||||||
prepend = "dir_" if event.is_directory else "file_"
|
prepend = "dir_" if event.is_directory else "file_"
|
||||||
event_types = [prepend+i for i in event.event_type]
|
event_types = [prepend+i for i in event.event_type]
|
||||||
|
|
||||||
# Remove the base dir from the path as trigger paths are given relative
|
# Remove the base dir from the path as trigger paths are given relative
|
||||||
@ -248,12 +245,12 @@ class WatchdogMonitor(BaseMonitor):
|
|||||||
self._rules_lock.acquire()
|
self._rules_lock.acquire()
|
||||||
try:
|
try:
|
||||||
for rule in self._rules.values():
|
for rule in self._rules.values():
|
||||||
|
|
||||||
# Skip events not within the event mask
|
# Skip events not within the event mask
|
||||||
if any(i in event_types for i in rule.pattern.event_mask) \
|
if any(i in event_types for i in rule.pattern.event_mask) \
|
||||||
!= True:
|
!= True:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Use regex to match event paths against rule paths
|
# Use regex to match event paths against rule paths
|
||||||
target_path = rule.pattern.triggering_path
|
target_path = rule.pattern.triggering_path
|
||||||
recursive_regexp = translate(target_path)
|
recursive_regexp = translate(target_path)
|
||||||
@ -273,10 +270,10 @@ class WatchdogMonitor(BaseMonitor):
|
|||||||
rule,
|
rule,
|
||||||
self.base_dir,
|
self.base_dir,
|
||||||
event.time_stamp,
|
event.time_stamp,
|
||||||
get_hash(event.src_path, SHA256)
|
get_hash(event.src_path, SHA256)
|
||||||
)
|
)
|
||||||
print_debug(self._print_target, self.debug_level,
|
print_debug(self._print_target, self.debug_level,
|
||||||
f"Event at {src_path} hit rule {rule.name}",
|
f"Event at {src_path} hit rule {rule.name}",
|
||||||
DEBUG_INFO)
|
DEBUG_INFO)
|
||||||
# Send the event to the runner
|
# Send the event to the runner
|
||||||
self.send_event_to_runner(meow_event)
|
self.send_event_to_runner(meow_event)
|
||||||
@ -288,17 +285,17 @@ class WatchdogMonitor(BaseMonitor):
|
|||||||
self._rules_lock.release()
|
self._rules_lock.release()
|
||||||
|
|
||||||
def _is_valid_base_dir(self, base_dir:str)->None:
|
def _is_valid_base_dir(self, base_dir:str)->None:
|
||||||
"""Validation check for 'base_dir' variable from main constructor. Is
|
"""Validation check for 'base_dir' variable from main constructor. Is
|
||||||
automatically called during initialisation."""
|
automatically called during initialisation."""
|
||||||
valid_dir_path(base_dir, must_exist=True)
|
valid_dir_path(base_dir, must_exist=True)
|
||||||
|
|
||||||
def _is_valid_patterns(self, patterns:Dict[str,FileEventPattern])->None:
|
def _is_valid_patterns(self, patterns:Dict[str,FileEventPattern])->None:
|
||||||
"""Validation check for 'patterns' variable from main constructor. Is
|
"""Validation check for 'patterns' variable from main constructor. Is
|
||||||
automatically called during initialisation."""
|
automatically called during initialisation."""
|
||||||
valid_dict(patterns, str, FileEventPattern, min_length=0, strict=False)
|
valid_dict(patterns, str, FileEventPattern, min_length=0, strict=False)
|
||||||
|
|
||||||
def _is_valid_recipes(self, recipes:Dict[str,BaseRecipe])->None:
|
def _is_valid_recipes(self, recipes:Dict[str,BaseRecipe])->None:
|
||||||
"""Validation check for 'recipes' variable from main constructor. Is
|
"""Validation check for 'recipes' variable from main constructor. Is
|
||||||
automatically called during initialisation."""
|
automatically called during initialisation."""
|
||||||
valid_dict(recipes, str, BaseRecipe, min_length=0, strict=False)
|
valid_dict(recipes, str, BaseRecipe, min_length=0, strict=False)
|
||||||
|
|
||||||
@ -309,7 +306,7 @@ class WatchdogMonitor(BaseMonitor):
|
|||||||
return [BaseRecipe]
|
return [BaseRecipe]
|
||||||
|
|
||||||
def _apply_retroactive_rule(self, rule:Rule)->None:
|
def _apply_retroactive_rule(self, rule:Rule)->None:
|
||||||
"""Function to determine if a rule should be applied to the existing
|
"""Function to determine if a rule should be applied to the existing
|
||||||
file structure, were the file structure created/modified now."""
|
file structure, were the file structure created/modified now."""
|
||||||
self._rules_lock.acquire()
|
self._rules_lock.acquire()
|
||||||
try:
|
try:
|
||||||
@ -337,7 +334,7 @@ class WatchdogMonitor(BaseMonitor):
|
|||||||
time(),
|
time(),
|
||||||
get_hash(globble, SHA256)
|
get_hash(globble, SHA256)
|
||||||
)
|
)
|
||||||
print_debug(self._print_target, self.debug_level,
|
print_debug(self._print_target, self.debug_level,
|
||||||
f"Retroactive event for file at at {globble} hit rule "
|
f"Retroactive event for file at at {globble} hit rule "
|
||||||
f"{rule.name}", DEBUG_INFO)
|
f"{rule.name}", DEBUG_INFO)
|
||||||
# Send it to the runner
|
# Send it to the runner
|
||||||
@ -349,7 +346,7 @@ class WatchdogMonitor(BaseMonitor):
|
|||||||
self._rules_lock.release()
|
self._rules_lock.release()
|
||||||
|
|
||||||
def _apply_retroactive_rules(self)->None:
|
def _apply_retroactive_rules(self)->None:
|
||||||
"""Function to determine if any rules should be applied to the existing
|
"""Function to determine if any rules should be applied to the existing
|
||||||
file structure, were the file structure created/modified now."""
|
file structure, were the file structure created/modified now."""
|
||||||
for rule in self._rules.values():
|
for rule in self._rules.values():
|
||||||
self._apply_retroactive_rule(rule)
|
self._apply_retroactive_rule(rule)
|
||||||
@ -366,8 +363,8 @@ class WatchdogEventHandler(PatternMatchingEventHandler):
|
|||||||
# A lock to solve race conditions on '_recent_jobs'
|
# A lock to solve race conditions on '_recent_jobs'
|
||||||
_recent_jobs_lock:threading.Lock
|
_recent_jobs_lock:threading.Lock
|
||||||
def __init__(self, monitor:WatchdogMonitor, settletime:int=1):
|
def __init__(self, monitor:WatchdogMonitor, settletime:int=1):
|
||||||
"""WatchdogEventHandler Constructor. This inherits from watchdog
|
"""WatchdogEventHandler Constructor. This inherits from watchdog
|
||||||
PatternMatchingEventHandler, and is used to catch events, then filter
|
PatternMatchingEventHandler, and is used to catch events, then filter
|
||||||
out excessive events at the same location."""
|
out excessive events at the same location."""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.monitor = monitor
|
self.monitor = monitor
|
||||||
@ -376,14 +373,14 @@ class WatchdogEventHandler(PatternMatchingEventHandler):
|
|||||||
self._recent_jobs_lock = threading.Lock()
|
self._recent_jobs_lock = threading.Lock()
|
||||||
|
|
||||||
def threaded_handler(self, event):
|
def threaded_handler(self, event):
|
||||||
"""Function to determine if the given event shall be sent on to the
|
"""Function to determine if the given event shall be sent on to the
|
||||||
monitor. After each event we wait for '_settletime', to catch
|
monitor. After each event we wait for '_settletime', to catch
|
||||||
subsequent events at the same location, so as to not swamp the system
|
subsequent events at the same location, so as to not swamp the system
|
||||||
with repeated events."""
|
with repeated events."""
|
||||||
|
|
||||||
self._recent_jobs_lock.acquire()
|
self._recent_jobs_lock.acquire()
|
||||||
try:
|
try:
|
||||||
if event.src_path in self._recent_jobs:
|
if event.src_path in self._recent_jobs:
|
||||||
if event.time_stamp > self._recent_jobs[event.src_path][0]:
|
if event.time_stamp > self._recent_jobs[event.src_path][0]:
|
||||||
self._recent_jobs[event.src_path][0] = event.time_stamp
|
self._recent_jobs[event.src_path][0] = event.time_stamp
|
||||||
self._recent_jobs[event.src_path][1].add(event.event_type)
|
self._recent_jobs[event.src_path][1].add(event.event_type)
|
||||||
@ -395,7 +392,7 @@ class WatchdogEventHandler(PatternMatchingEventHandler):
|
|||||||
[event.time_stamp, {event.event_type}]
|
[event.time_stamp, {event.event_type}]
|
||||||
|
|
||||||
# If we have a closed event then short-cut the wait and send event
|
# If we have a closed event then short-cut the wait and send event
|
||||||
# immediately
|
# immediately
|
||||||
if event.event_type == FILE_CLOSED_EVENT:
|
if event.event_type == FILE_CLOSED_EVENT:
|
||||||
self.monitor.match(event)
|
self.monitor.match(event)
|
||||||
self._recent_jobs_lock.release()
|
self._recent_jobs_lock.release()
|
||||||
@ -423,9 +420,9 @@ class WatchdogEventHandler(PatternMatchingEventHandler):
|
|||||||
self.monitor.match(event)
|
self.monitor.match(event)
|
||||||
|
|
||||||
def handle_event(self, event):
|
def handle_event(self, event):
|
||||||
"""Handler function, called by all specific event functions. Will
|
"""Handler function, called by all specific event functions. Will
|
||||||
attach a timestamp to the event immediately, and attempt to start a
|
attach a timestamp to the event immediately, and attempt to start a
|
||||||
threaded_handler so that the monitor can resume monitoring as soon as
|
threaded_handler so that the monitor can resume monitoring as soon as
|
||||||
possible."""
|
possible."""
|
||||||
event.time_stamp = time()
|
event.time_stamp = time()
|
||||||
|
|
||||||
@ -440,7 +437,7 @@ class WatchdogEventHandler(PatternMatchingEventHandler):
|
|||||||
waiting_for_threaded_resources = False
|
waiting_for_threaded_resources = False
|
||||||
except threading.ThreadError:
|
except threading.ThreadError:
|
||||||
sleep(1)
|
sleep(1)
|
||||||
|
|
||||||
def on_created(self, event):
|
def on_created(self, event):
|
||||||
"""Function called when a file created event occurs."""
|
"""Function called when a file created event occurs."""
|
||||||
self.handle_event(event)
|
self.handle_event(event)
|
||||||
@ -456,7 +453,7 @@ class WatchdogEventHandler(PatternMatchingEventHandler):
|
|||||||
def on_deleted(self, event):
|
def on_deleted(self, event):
|
||||||
"""Function called when a file deleted event occurs."""
|
"""Function called when a file deleted event occurs."""
|
||||||
self.handle_event(event)
|
self.handle_event(event)
|
||||||
|
|
||||||
def on_closed(self, event):
|
def on_closed(self, event):
|
||||||
"""Function called when a file closed event occurs."""
|
"""Function called when a file closed event occurs."""
|
||||||
self.handle_event(event)
|
self.handle_event(event)
|
||||||
|
Reference in New Issue
Block a user