code release

This commit is contained in:
iperov 2021-07-23 17:34:49 +04:00
commit a902f11f74
354 changed files with 826570 additions and 1 deletions

499
xlib/mp/csw/CSWBase.py Normal file
View file

@ -0,0 +1,499 @@
import multiprocessing
import threading
import time
import traceback
from enum import IntEnum
from xlib import db as lib_db
from xlib.python import Disposable, EventListener
from ..PMPI import PMPI
class Control:
"""
Base class of control elements between 2 processes.
"""
class State(IntEnum):
DISABLED = 0 # the control is not available and unusable (default)
FREEZED = 1 # the control is available, but temporary unusable
ENABLED = 2 # the control is available and usable
def __init__(self):
self._name = None
self._pmpi = None
self._pmpi_gather_call_on_msgs = []
self._pmpi_gather_send_msgs = []
self._state = Control.State.DISABLED
self._state_change_evl = EventListener()
self._call_on_msg('_state', lambda state: self._set_state(state) )
##########
### PMPI
def _call_on_msg(self, name, func):
if self._pmpi is None:
self._pmpi_gather_call_on_msgs.append( (name,func) )
else:
self._pmpi.call_on_msg(f'__{self._name}_{name}__', func)
def _send_msg(self, name, *args, **kwargs):
if self._pmpi is None:
self._pmpi_gather_send_msgs.append( (name,args,kwargs) )
else:
self._pmpi.send_msg(f'__{self._name}_{name}__', *args, **kwargs)
def _set_pmpi(self, pmpi):
self._pmpi = pmpi
for name, func in self._pmpi_gather_call_on_msgs:
self._call_on_msg(name, func)
self._pmpi_gather_call_on_msgs = []
for name, args, kwargs in self._pmpi_gather_send_msgs:
self._send_msg(name, *args, **kwargs)
self._pmpi_gather_send_msgs = []
##########
### STATE
def _set_state(self, state : 'Control.State'):
if not isinstance(state, Control.State):
raise ValueError('state must be an instance of Control.State')
if self._state != state:
self._state = state
self._state_change_evl.call(state)
return True
return False
def call_on_change_state(self, func_or_list):
"""Call when the state of the control is changed"""
self._state_change_evl.add(func_or_list)
def call_on_control_info(self, func_or_list):
"""Call when the state of the control is changed"""
self._control_info_evl.add(func_or_list)
def get_state(self) -> 'Control.State': return self._state
def is_disabled(self): return self._state == Control.State.DISABLED
def is_freezed(self): return self._state == Control.State.FREEZED
def is_enabled(self): return self._state == Control.State.ENABLED
class ControlHost(Control):
def __init__(self):
Control.__init__(self)
self._send_msg('_reset')
def _set_state(self, state : 'Control.State'):
result = super()._set_state(state)
if result:
self._send_msg('_state', self._state)
return result
def disable(self): self._set_state(Control.State.DISABLED)
def freeze(self): self._set_state(Control.State.FREEZED)
def enable(self): self._set_state(Control.State.ENABLED)
class ControlClient(Control):
def __init__(self):
Control.__init__(self)
self._call_on_msg('_reset', self._reset)
def _reset(self):
self._set_state(Control.State.DISABLED)
self._on_reset()
def _on_reset(self):
"""Implement when the Control is resetted to initial state,
the same state like after __init__()
"""
raise NotImplementedError(f'You should implement {self.__class__} _on_reset')
class SheetBase:
def __init__(self):
self._controls = []
def __setattr__(self, var_name, obj):
super().__setattr__(var_name, obj)
if isinstance(obj, Control):
if obj in self._controls:
raise ValueError(f'Control with name {var_name} already in Sheet')
self._controls.append(obj)
obj._name = var_name
class Sheet:
"""
base sheet to control CSW
"""
class Host(SheetBase):
def __init__(self):
super().__init__()
class Worker(SheetBase):
def __init__(self):
super().__init__()
class WorkerState:
def __getstate__(self):
return self.__dict__.copy()
def __setstate__(self, d):
self.__init__()
self.__dict__.update(d)
class DB(lib_db.KeyValueDB):
...
class Base(Disposable):
"""
base class for Controllable Subprocess Worker (CSW)
"""
def __init__(self, sheet):
super().__init__()
self._pmpi = PMPI()
if not isinstance(sheet, SheetBase):
raise ValueError('sheet must be an instance of SheetBase')
self._sheet = sheet
for control in sheet._controls:
control._set_pmpi(self._pmpi)
def get_control_sheet(self): return self._sheet
def _get_name(self): return self.__class__.__name__
def _get_pmpi(self) -> PMPI: return self._pmpi
class Host(Base):
"""
Base host class for CSW.
"""
class _ProcessStatus:
STOPPING = 0
STOPPED = 1
STARTING = 2
STARTED = 3
def __init__(self, db : lib_db.KeyValueDB = None,
sheet_cls = None,
worker_cls = None,
worker_state_cls : WorkerState = None,
worker_start_args = None,
worker_start_kwargs = None,
):
sheet_host_cls = getattr(sheet_cls, 'Host', None)
sheet_worker_cls = getattr(sheet_cls, 'Worker', None)
if sheet_host_cls is None or not issubclass(sheet_host_cls, Sheet.Host):
raise ValueError('sheet_cls.Host must be an instance Sheet.Host')
if sheet_worker_cls is None or not issubclass(sheet_worker_cls, Sheet.Worker):
raise ValueError('sheet_cls.Worker must be an instance Sheet.Worker')
if not issubclass(worker_cls, Worker):
raise ValueError("worker_cls must be subclass of Worker")
if worker_state_cls is None:
worker_state_cls = WorkerState
if not issubclass(worker_state_cls, WorkerState):
raise ValueError("worker_state_cls must be subclass of WorkerState")
if worker_start_args is None:
worker_start_args = []
if worker_start_kwargs is None:
worker_start_kwargs = {}
if db is None:
db = DB()
if not isinstance(db, DB ):
raise ValueError("db must be subclass of DB")
super().__init__(sheet=sheet_host_cls())
self._worker_cls = worker_cls
self._worker_sheet_cls = sheet_worker_cls
self._worker_start_args = worker_start_args
self._worker_start_kwargs = worker_start_kwargs
self._worker_state_cls = worker_state_cls
self._db = db
self._db_key_host_onoff = f'{self._get_name()}_host_onoff'
self._db_key_worker_state = f'{self._get_name()}_worker_state'
state = None
if db is not None:
# Try to load the WorkerState
state = db.get_value (self._db_key_worker_state)
if state is None:
# still None - create new
state = self._worker_state_cls()
self._state = state
self._process_status = Host._ProcessStatus.STOPPED
self._is_busy = False
self._process = None
self._reset_restart = False
self._on_state_change_evl = EventListener()
self.call_on_msg('_start', self._on_worker_start)
self.call_on_msg('_stop', self._on_worker_stop )
self.call_on_msg('_state', self._on_worker_state)
self.call_on_msg('_busy', self._on_worker_busy)
def _on_dispose(self):
self.stop()
while self._process_status != Host._ProcessStatus.STOPPED:
self.process_messages()
super()._on_dispose()
def call_on_msg(self, name, func): self._pmpi.call_on_msg(name, func)
def call_on_state_change(self, func_or_list):
"""
func_or_list callable(csw, started, starting, stopping, stopped, busy)
"""
self._on_state_change_evl.add(func_or_list)
def _on_state_change_evl_call(self):
self._on_state_change_evl.call(self, self.is_started(), self.is_starting(), self.is_stopping(), self.is_stopped(), self.is_busy() )
def send_msg(self, name, *args, **kwargs): self._pmpi.send_msg(name, *args, **kwargs)
def reset_state(self):
"""
reset state to default
"""
if self.is_stopped():
self._state = self._worker_state_cls()
self._save_state()
else:
self._reset_restart = True
self.stop()
def save_on_off_state(self):
"""
save current start/stop state to DB
"""
if self._process_status == Host._ProcessStatus.STARTED or \
self._process_status == Host._ProcessStatus.STOPPED:
# Save only when the process is fully started / stopped
self._db.set_value(self._db_key_host_onoff, self._process_status == Host._ProcessStatus.STARTED )
def restore_on_off_state(self):
"""
restore saved on_off state from db. Default is on.
"""
is_on = self._db.get_value(self._db_key_host_onoff, True)
if is_on:
self.start()
def start(self):
"""
Start the worker.
**kwargs will be passed to Worker.on_start(**kwargs)
returns True if operation is successfully initiated.
"""
if self._process_status != Host._ProcessStatus.STARTED:
if self._process_status == Host._ProcessStatus.STOPPED:
pipe, worker_pipe = multiprocessing.Pipe()
self._pmpi.set_pipe(pipe)
self._process_status = Host._ProcessStatus.STARTING
self._on_state_change_evl_call()
process = self._process = multiprocessing.Process(target=Worker._start_proc,
args=[self._worker_cls, self._worker_sheet_cls, worker_pipe, self._state, self._worker_start_args, self._worker_start_kwargs],
daemon=True)
# Start non-blocking in subthread
threading.Thread(target=lambda: self._process.start(), daemon=True).start()
time.sleep(0.016) # BUG ? remove will raise ImportError: cannot import name 'Popen' tested in Python 3.6
return True
return False
def stop(self, force=False):
"""
Stop the module
arguments:
force(False) bool False: gracefully stop the module(deferred)
True: force terminate(right now)
returns True if operation is successfully initiated.
WARNING !
Do not kill the process, if it is using any multiprocessing syncronization primivites,
because if process is killed while any sync is acquired, it will not be released.
"""
if self._process_status != Host._ProcessStatus.STOPPED:
if force or self._process_status == Host._ProcessStatus.STARTED:
if not force:
self.send_msg('_stop')
self._process_status = Host._ProcessStatus.STOPPING
self._on_state_change_evl_call()
else:
self._process.terminate()
self._process.join()
self._process = None
self._pmpi.set_pipe(None)
# Reset client controls
for control in self.get_control_sheet()._controls:
if isinstance(control, ControlClient):
control._reset()
# Process is physically stopped
self._process_status = Host._ProcessStatus.STOPPED
self._is_busy = False
#print(f'{self._get_name()} is stopped.')
self._on_state_change_evl_call()
return True
return False
def is_started(self): return self._process_status == Host._ProcessStatus.STARTED
def is_starting(self): return self._process_status == Host._ProcessStatus.STARTING
def is_stopped(self): return self._process_status == Host._ProcessStatus.STOPPED
def is_stopping(self): return self._process_status == Host._ProcessStatus.STOPPING
def is_busy(self): return self._is_busy
def _save_state(self):
self._db.set_value( self._db_key_worker_state, self._state)
def _on_worker_start(self):
self._process_status = Host._ProcessStatus.STARTED
#print(f'{self._get_name()} is started.')
self._on_state_change_evl_call()
def _on_worker_stop(self, error : str = None, restart : bool = False):
if error is not None:
print(f'{self._get_name()} error: {error}')
# Stop on error: reset state
self._state = self._worker_state_cls()
self.stop(force=True)
if self._reset_restart:
self._reset_restart = False
self._state = self._worker_state_cls()
restart = True
if restart:
self.start()
def _on_worker_state(self, state):
self._state = state
self._save_state()
def _on_worker_busy(self, is_busy):
self._is_busy = is_busy
self._on_state_change_evl_call()
def process_messages(self):
self._pmpi.process_messages()
if self._process_status == Host._ProcessStatus.STARTED:
if not self._process.is_alive():
self.stop(force=True)
class Worker(Base):
"""
Base Worker class for CSW.
"""
def __init__(self, sheet):
super().__init__(sheet=sheet)
self._started = False
self._run = True
self._req_restart = False
self._req_save_state = False
self._get_pmpi().call_on_msg('_stop', lambda: setattr(self, '_run', False))
def on_start(self, *args, **kwargs):
"""overridable"""
def on_tick(self):
"""
overridable
do a sleep inside your implementation
"""
def on_stop(self):
"""overridable"""
def send_msg(self, name, *args, **kwargs): self._pmpi.send_msg(name, *args, **kwargs)
def call_on_msg(self, name, func): self._pmpi.call_on_msg(name, func)
def restart(self):
"""request to restart Worker"""
self._req_restart = True
self._run = False
def get_state(self) -> WorkerState:
"""
get WorkerState object of Worker.
Inner variables can be modified.
Call save_state to save the WorkerState.
"""
return self._state
def save_state(self):
"""Request to save current state"""
self._req_save_state = True
def set_busy(self, is_busy : bool):
"""
indicate to host that worker is in busy mode now
"""
self.send_msg('_busy', is_busy)
def is_started(self) -> bool:
"""
returns True after on_start()
"""
return self._started
@staticmethod
def _start_proc(cls_, sheet_cls, pipe, state, worker_start_args, worker_start_kwargs):
self = cls_(sheet=sheet_cls())
self._get_pmpi().set_pipe(pipe)
self._state = state
error = None
try:
self.on_start(*worker_start_args, **worker_start_kwargs)
self._started = True
self.send_msg('_start')
while True:
if self._req_save_state:
self._req_save_state = False
self.send_msg('_state', self._state)
if not self._run:
break
self._pmpi.process_messages()
self.on_tick()
self.on_stop()
except Exception as e:
error = f'{str(e)} {traceback.format_exc()}'
self.send_msg('_stop', error=error, restart=self._req_restart)
time.sleep(1.0)

View file

@ -0,0 +1,172 @@
from collections import Iterable
from typing import List, Union
from xlib.python import EventListener
from .CSWBase import ControlClient, ControlHost
class _DynamicSingleSwitchBase:
def __init__(self):
self._on_selected_evl = EventListener()
self._on_choices_evl = EventListener()
self._call_on_msg('selected_idx', self._on_msg_selected)
self._call_on_msg('choices', self._on_msg_choices)
self._selected_idx = None
self._choices = None
self._choices_len = None
self._choices_names = None
self._none_choice_name = None
def _on_msg_selected(self, selected_idx):
self._set_selected_idx(selected_idx)
def _send_selected_idx(self):
self._send_msg('selected_idx', self.get_selected_idx() )
def _set_selected_idx(self, selected_idx):
if self._selected_idx != selected_idx:
self._selected_idx = selected_idx
self._on_selected_evl.call(selected_idx, self.get_selected_choice() )
return True
return False
def _send_choices(self):
self._send_msg('choices', self._choices, self._choices_names, self._none_choice_name)
def _set_choices(self, choices, choices_names : List[str], none_choice_name : Union[str,None]):
self._choices = choices
self._choices_len = len(choices)
self._choices_names = choices_names
self._none_choice_name = none_choice_name
self._on_choices_evl.call(choices, choices_names, none_choice_name)
def _on_msg_choices(self, choices, choices_names, none_choice_name):
self._set_choices(choices, choices_names, none_choice_name)
def _choice_to_index(self, idx_or_choice):
choices = self._choices
if idx_or_choice.__class__ != int:
try:
idx_or_choice = choices.index(idx_or_choice)
except:
# Choice not in list
return None
if idx_or_choice < 0 or idx_or_choice >= self._choices_len:
# idx out of bounds
return None
return idx_or_choice
def call_on_choices(self, func_or_list):
"""call when choices list is configured"""
self._on_choices_evl.add(func_or_list)
def call_on_selected(self, func):
"""
called when selected
func ( idx : int, choice : object)
"""
self._on_selected_evl.add(func)
def in_choices(self, choice) -> bool: return choice in self._choices
def get_choices(self): return self._choices
def get_choices_names(self) -> List[str]: return self._choices_names
def get_selected_idx(self) -> Union[int, None]: return self._selected_idx
def get_selected_choice(self):
if self._selected_idx is None:
return None
return self._choices[self._selected_idx]
def select(self, idx_or_choice) -> bool:
"""
Select index or choice or None(unselect)
returns False if the value is not correct or already selected
returns True if operation is success
func does not raise any exceptions
"""
if idx_or_choice is not None:
idx_or_choice = self._choice_to_index (idx_or_choice)
if idx_or_choice is None:
return False
result = self._set_selected_idx(idx_or_choice)
if result:
self._send_selected_idx()
return result
def unselect(self) -> bool:
"""
unselect
returns True if operation is success
"""
return self.select(None)
class DynamicSingleSwitch:
"""
DynamicSingleSwitch control dynamically loaded list of choices.
Has None state as unselected.
"""
class Host(ControlHost, _DynamicSingleSwitchBase):
def __init__(self):
ControlHost.__init__(self)
_DynamicSingleSwitchBase.__init__(self)
def _on_msg_selected(self, selected_idx):
if self.is_enabled():
_DynamicSingleSwitchBase._on_msg_selected(self, selected_idx)
self._send_selected_idx()
def set_choices(self, choices, choices_names=None, none_choice_name=None):
"""
set choices, and optional choices_names.
choices_names list/dict/None if list, should match the len of choices
if dict, should return a str by key of choice
if None, choices will be stringfied
none_choice_name('') str/None if not None, shows None choice with name,
by default empty string
"""
# Validate choices
if choices is None:
raise ValueError('Choices cannot be None.')
if not isinstance(choices, Iterable):
raise ValueError('Choices must be Iterable')
if choices_names is None:
choices_names = tuple(str(c) for c in choices)
elif isinstance(choices_names, (list,tuple)):
if len(choices_names) != len(choices):
raise ValueError('mismatch len of choices and choices names')
elif isinstance(choices_names, dict):
choices_names = [ choices_names[x] for x in choices ]
else:
raise ValueError('unsupported type of choices_names')
if not all( isinstance(x, str) for x in choices_names ):
raise ValueError('all values in choices_names must be a str')
choices = tuple(choices)
self._set_choices(choices, choices_names, none_choice_name)
self._send_choices()
class Client(ControlClient, _DynamicSingleSwitchBase):
def __init__(self):
ControlClient.__init__(self)
_DynamicSingleSwitchBase.__init__(self)
def _on_reset(self):
self._set_selected_idx(None)

53
xlib/mp/csw/Error.py Normal file
View file

@ -0,0 +1,53 @@
from typing import Union
from xlib.python import EventListener
from .CSWBase import ControlClient, ControlHost
class Error:
"""
One-way error control.
"""
class Client(ControlClient):
def __init__(self):
ControlClient.__init__(self)
self._on_error_evl = EventListener()
self._call_on_msg('error', self._on_msg_error)
def _on_msg_error(self, text):
self._on_error_evl.call(text)
def call_on_error(self, func_or_list):
"""
Call when the error message arrive
func(text : Union[str,None])
"""
self._on_error_evl.add(func_or_list)
def _on_reset(self):
self._on_msg_error(None)
class Host(ControlHost):
def __init__(self):
ControlHost.__init__(self)
def set_error(self, text : Union[str, None]):
"""
set tex
text str or None
"""
if text is None:
self.disable()
else:
self.enable()
self._send_msg('error', text)

62
xlib/mp/csw/Flag.py Normal file
View file

@ -0,0 +1,62 @@
from xlib.python import EventListener
from .CSWBase import ControlClient, ControlHost
class _FlagBase:
def __init__(self):
self._flag = None
self._on_flag_evl = EventListener()
self._call_on_msg('flag', self._on_msg_flag)
def _on_msg_flag(self, flag):
self._set_flag(flag)
def _send_flag(self):
self._send_msg('flag', self._flag)
def _set_flag(self, flag : bool):
if flag is not None and not isinstance(flag, bool):
raise ValueError('flag must be a bool value or None')
if self._flag != flag:
self._flag = flag
self._on_flag_evl.call(flag if flag is not None else False)
return True
return False
def call_on_flag(self, func):
"""Call when the flag is changed"""
self._on_flag_evl.add(func)
def set_flag(self, flag : bool):
if self._set_flag(flag):
self._send_flag()
def get_flag(self): return self._flag
class Flag:
"""
Flag control.
Values: None : uninitialized/not set
bool : value
"""
class Host(ControlHost, _FlagBase):
def __init__(self):
ControlHost.__init__(self)
_FlagBase.__init__(self)
def _on_msg_flag(self, flag):
if self.is_enabled():
_FlagBase._on_msg_flag(self, flag)
self._send_flag()
class Client(ControlClient, _FlagBase):
def __init__(self):
ControlClient.__init__(self)
_FlagBase.__init__(self)
def _on_reset(self):
self._set_flag(None)

48
xlib/mp/csw/InfoBlock.py Normal file
View file

@ -0,0 +1,48 @@
from typing import Union, List
from xlib.python import EventListener
from .CSWBase import ControlClient, ControlHost
class InfoBlock:
"""
"""
class Client(ControlClient):
def __init__(self):
ControlClient.__init__(self)
self._on_info_evl = EventListener()
self._call_on_msg('info', self._on_msg_info)
def _on_msg_info(self, lines):
self._on_info_evl.call(lines)
def call_on_info(self, func_or_list):
"""
Call when the error message arrive
func( lines : Union[ List[str], None] )
"""
self._on_info_evl.add(func_or_list)
def _on_reset(self):
self._on_msg_info(None)
class Host(ControlHost):
def __init__(self):
ControlHost.__init__(self)
def set_info(self, lines : Union[ List[str], None]):
"""
set info
lines List[str] | None
"""
self._send_msg('info', lines)

48
xlib/mp/csw/InfoLabel.py Normal file
View file

@ -0,0 +1,48 @@
from typing import Union, List
from xlib.python import EventListener
from .CSWBase import ControlClient, ControlHost
class InfoLabel:
"""
"""
class Config:
def __init__(self, label : Union[str, None] = None,
info_icon = False,
info_lines : Union[ List[str], None] = None):
self.label = label
self.info_icon = info_icon
self.info_lines = info_lines
class Client(ControlClient):
def __init__(self):
ControlClient.__init__(self)
self._on_config_evl = EventListener()
self._call_on_msg('_cfg', self._on_msg_config)
def _on_msg_config(self, cfg):
self._on_config_evl.call(cfg)
def call_on_config(self, func_or_list):
"""
"""
self._on_config_evl.add(func_or_list)
def _on_reset(self):
...
# self._on_msg_config( InfoLabel.Config() )
class Host(ControlHost):
def __init__(self):
ControlHost.__init__(self)
def set_config(self, cfg : 'InfoLabel.Config'):
"""
"""
self._send_msg('_cfg', cfg)

107
xlib/mp/csw/Number.py Normal file
View file

@ -0,0 +1,107 @@
import numpy as np
from xlib.python import EventListener
from .CSWBase import ControlClient, ControlHost
class _NumberBase:
def __init__(self):
self._number = None
self._on_number_evl = EventListener()
self._call_on_msg('number', self._on_msg_number)
def _on_msg_number(self, number):
self._set_number(number)
def _send_number(self):
self._send_msg('number', self._number)
def _set_number(self, number, block_event=False):
if number is not None:
if isinstance(number, (int, np.int, np.int8, np.int16, np.int32, np.int64)):
number = int(number)
elif isinstance(number, (float, np.float, np.float16, np.float32, np.float64)):
number = float(number)
else:
raise ValueError('number must be an instance of int/float')
if self._number != number:
self._number = number
if not block_event:
self._on_number_evl.call(number if number is not None else 0)
return True
return False
def call_on_number(self, func_or_list):
"""Call when the number is changed."""
self._on_number_evl.add(func_or_list)
def set_number(self, number, block_event=False):
"""
block_event(False) bool on_number event will not be called on this side
"""
if self._set_number(number, block_event=block_event):
self._send_number()
def get_number(self): return self._number
class Number:
"""
Number control.
Values:
None : uninitialized state
int/float : value
"""
class Config:
"""
allow_instant_update mean that the user widget can
send the value immediatelly during change,
for example - scrolling the spinbox
"""
def __init__(self, min=None, max=None, step=None, decimals=None, zero_is_auto : bool =False, allow_instant_update : bool =False, read_only : bool =False):
self.min = min
self.max = max
self.step = step
self.decimals = decimals
self.zero_is_auto : bool = zero_is_auto
self.allow_instant_update : bool = allow_instant_update
self.read_only : bool = read_only
class Host(ControlHost, _NumberBase):
def __init__(self):
ControlHost.__init__(self)
_NumberBase.__init__(self)
self._config = Number.Config()
def _on_msg_number(self, number):
if self.is_enabled():
_NumberBase._on_msg_number(self, number)
self._send_number()
def get_config(self) -> 'Number.Config':
return self._config
def set_config(self, config : 'Number.Config'):
self._config = config
self._send_msg('config', config)
class Client(ControlClient, _NumberBase):
def __init__(self):
ControlClient.__init__(self)
_NumberBase.__init__(self)
self._on_config_evl = EventListener()
self._call_on_msg('config', self._on_msg_config)
def _on_reset(self):
self._set_number(None)
def _on_msg_config(self, cfg : 'Number.Config'):
self._on_config_evl.call(cfg)
def call_on_config(self, func): self._on_config_evl.add(func)

144
xlib/mp/csw/Paths.py Normal file
View file

@ -0,0 +1,144 @@
from collections import Iterable
from enum import IntEnum
from pathlib import Path
from typing import List, Union
from xlib.python import EventListener
from .CSWBase import ControlClient, ControlHost
class _PathBase:
def __init__(self):
self._paths = []
self._on_paths_evl = EventListener()
self._call_on_msg('paths', self._on_msg_paths)
def _on_msg_paths(self, path):
self._set_paths(path)
def _send_paths(self):
self._send_msg('paths', self._paths)
def _set_paths(self, path_or_list, block_event=False):
if isinstance(path_or_list, Iterable) and \
not isinstance(path_or_list, str):
path_or_list = list(path_or_list)
else:
path_or_list = [path_or_list]
for i,path in enumerate(path_or_list):
if isinstance(path, str):
path_or_list[i] = Path(path)
elif not isinstance(path, Path):
raise ValueError(f'value {path} must be an instance of str or Path')
if self._paths != path_or_list:
prev_paths = self._paths
self._paths = path_or_list
if not block_event:
self._on_paths_evl.call(path_or_list, prev_paths)
return True
return False
def call_on_paths(self, func_or_list):
"""
Call when the path is changed
func(path_list, prev_path_list)
"""
self._on_paths_evl.add(func_or_list)
def set_paths(self, path_or_list, block_event=False):
"""
path_or_list Path/str or list of Paths/str or []
or None which is same as []
"""
if path_or_list is None:
path_or_list = []
if self._set_paths(path_or_list, block_event=block_event):
self._send_paths()
def get_paths(self): return self._paths
class Paths:
"""
Paths control.
Values: [] not set
list of [1+] Paths
"""
class Config:
class Type(IntEnum):
NONE = 0
ANY_FILE = 1
EXISTING_FILE = 2
EXISTING_FILES = 3
DIRECTORY = 4
def __init__(self, type = None, is_save = False, caption = None, suffixes = None, directory_path = None):
if type is None:
type = Paths.Config.Type.NONE
self._type = type
self._is_save = is_save
self._caption = caption
self._suffixes = suffixes
self._directory_path = directory_path
def get_type(self) -> 'Paths.Config.Type': return self._type
def is_save(self) -> bool: return self._is_save
def get_caption(self) -> Union[str, None]: return self._caption
def get_suffixes(self) -> Union[List[str], None]: return self._suffixes
def get_directory_path(self) -> Union[Path, None]: return self._directory_path
@staticmethod
def AnyFile(is_save=False, caption=None, suffixes=None):
return Paths.Config(Paths.Config.Type.ANY_FILE, is_save, caption, suffixes)
@staticmethod
def ExistingFile(is_save=False, caption=None, suffixes=None):
return Paths.Config(Paths.Config.Type.EXISTING_FILE, is_save, caption, suffixes)
@staticmethod
def ExistingFiles(caption=None, suffixes=None):
return Paths.Config(Paths.Config.Type.EXISTING_FILES, False, caption, suffixes)
@staticmethod
def Directory(caption=None, directory_path=None):
return Paths.Config(Paths.Config.Type.DIRECTORY, False, caption, None, directory_path=directory_path)
class Host(ControlHost, _PathBase):
def __init__(self):
ControlHost.__init__(self)
_PathBase.__init__(self)
self._config = Paths.Config()
def _on_msg_paths(self, path):
if self.is_enabled():
_PathBase._on_msg_paths(self, path)
self._send_paths()
def set_config(self, config : 'Paths.Config'):
self._config = config
self._send_msg('config', config)
class Client(ControlClient, _PathBase):
def __init__(self):
ControlClient.__init__(self)
_PathBase.__init__(self)
self._on_config_evl = EventListener()
self._call_on_msg('config', self._on_msg_config)
def _on_msg_config(self, config : 'Paths.Config'):
self._on_config_evl.call(config)
def call_on_config(self, func): self._on_config_evl.add(func)
def _on_reset(self):
self._set_paths([])

84
xlib/mp/csw/Progress.py Normal file
View file

@ -0,0 +1,84 @@
from typing import Union
from xlib.python import EventListener
from .CSWBase import ControlClient, ControlHost
class _ProgressBase:
def __init__(self):
self._progress = None
self._on_progress_evl = EventListener()
self._call_on_msg('progress', self._on_msg_progress)
def _on_msg_progress(self, progress):
self._set_progress(progress)
def _set_progress(self, progress, block_event=False):
if progress is not None:
progress = int(progress)
if self._progress != progress:
self._progress = progress
if not block_event:
self._on_progress_evl.call(progress if progress is not None else 0)
return True
return False
def call_on_progress(self, func_or_list):
"""Call when the progress is changed."""
self._on_progress_evl.add(func_or_list)
def get_progress(self): return self._progress
class Progress:
"""
Progress control with 0..100 int value
Values:
None : uninitialized state
int/float : value
"""
class Config:
def __init__(self, title=None):
self._title = title
def get_title(self) -> Union[str, None]:
return self._title
class Host(ControlHost, _ProgressBase):
def __init__(self):
ControlHost.__init__(self)
_ProgressBase.__init__(self)
self._config = Progress.Config()
def _send_progress(self):
self._send_msg('progress', self._progress)
def set_progress(self, progress, block_event=False):
"""
progress number 0..100
block_event(False) on_progress event will not be called on this side
"""
if self._set_progress(progress, block_event=block_event):
self._send_progress()
def set_config(self, config : 'Progress.Config'):
self._send_msg('config', config)
class Client(ControlClient, _ProgressBase):
def __init__(self):
ControlClient.__init__(self)
_ProgressBase.__init__(self)
self._on_config_evl = EventListener()
self._call_on_msg('config', self._on_msg_config)
def _on_reset(self):
self._set_progress(None)
def _on_msg_config(self, config):
self._on_config_evl.call(config)
def call_on_config(self, func):
self._on_config_evl.add(func)

27
xlib/mp/csw/Signal.py Normal file
View file

@ -0,0 +1,27 @@
from xlib.python import EventListener
from .CSWBase import ControlClient, ControlHost
class Signal:
class Host(ControlHost):
def __init__(self):
super().__init__()
self._signal_evl = EventListener()
self._call_on_msg('signal', self._on_msg_signal)
def call_on_signal(self, func): self._signal_evl.add(func)
def signal(self):
self._on_msg_signal()
def _on_msg_signal(self):
if self.is_enabled():
self._signal_evl.call()
class Client(ControlClient):
def signal(self):
self._send_msg('signal')
def _on_reset(self):
...

67
xlib/mp/csw/Text.py Normal file
View file

@ -0,0 +1,67 @@
from xlib.python import EventListener
from .CSWBase import ControlClient, ControlHost
class _TextBase:
def __init__(self):
self._text = None
self._on_text_evl = EventListener()
self._call_on_msg('text', self._on_msg_text)
def _on_msg_text(self, text):
self._set_text(text)
def _send_text(self):
self._send_msg('text', self._text)
def _set_text(self, text : str):
if text is not None and not isinstance(text, str):
raise ValueError('text must be str or None')
if self._text != text:
self._text = text
self._on_text_evl.call(text)
return True
return False
def call_on_text(self, func_or_list):
"""
Call when the text is changed
func(text : Union[str,None])
"""
self._on_text_evl.add(func_or_list)
def set_text(self, text : str):
if self._set_text(text):
self._send_text()
def get_text(self): return self._text
class Text:
"""
Text control.
Values:
None : uninitialized state
str : value
"""
class Host(ControlHost, _TextBase):
def __init__(self):
ControlHost.__init__(self)
_TextBase.__init__(self)
def _on_msg_text(self, text):
if self.is_enabled():
_TextBase._on_msg_text(self, text)
self._send_text()
class Client(ControlClient, _TextBase):
def __init__(self):
ControlClient.__init__(self)
_TextBase.__init__(self)
def _on_reset(self):
self._set_text(None)

17
xlib/mp/csw/__init__.py Normal file
View file

@ -0,0 +1,17 @@
"""
Controllable Subprocess Worker
"""
from .CSWBase import (DB, Control, ControlClient, ControlHost, Host, Sheet,
Worker, WorkerState)
from .DynamicSingleSwitch import DynamicSingleSwitch
from .Error import Error
from .Flag import Flag
from .InfoBlock import InfoBlock
from .InfoLabel import InfoLabel
from .Number import Number
from .Paths import Paths
from .Progress import Progress
from .Signal import Signal
from .Text import Text