def __init__( self, fun: Callable[..., Any], name: str=None, nargs=None, min: int=None, max: int=None, **kw ) -> None: self._fun = fun self._argspec = inspect.getfullargspec(fun) # type: ignore self._params = List.wrap(self._argspec.args) self._param_count = self._params.length - ( 1 if self._params.head.contains('self') else 0 ) self._name = Maybe(name) self._nargs = Maybe(try_int(nargs)) self._min = Maybe(min) self._max = Maybe(max) self._kw = kw
def _send(self, data, msg: Message): return Maybe.from_call(self.loop_process, data, msg, exc=TransitionFailed) | TransitionResult.empty(data)
def __init__(self, name: str, parent: "Machine" = None) -> None: self.name = name self.parent = Maybe(parent) self._setup_handlers()
class Machine(Logging): _data_type = Data def __init__(self, name: str, parent: "Machine" = None) -> None: self.name = name self.parent = Maybe(parent) self._setup_handlers() def _setup_handlers(self): handlers = inspect.getmembers(self, lambda a: hasattr(a, _machine_attr)) handler_map = List.wrap(handlers).smap(Handler.create).map(lambda a: (a.message, a)) self._message_handlers = Map(handler_map) @property def _default_handler(self): return Handler("unhandled", None, self.unhandled) def process(self, data: Data, msg) -> TransitionResult: self.prepare(msg) handler = self._resolve_handler(msg) result = self._execute_transition(handler, data, msg) return self._dispatch_transition_result(data, result) def prepare(self, msg): pass def _dispatch_transition_result(self, data, result): return result / self._process_result(data) | TransitionResult.empty(data) def loop_process(self, data, msg): sender = lambda z, m: z.accum(self.loop_process(z.data, m)) return self.process(data, msg).fold(sender) def _resolve_handler(self, msg): return self._message_handlers.get(type(msg)).get_or_else(lambda: self._default_handler) def _execute_transition(self, handler, data, msg): try: return handler.run(data, msg) except Exception as e: return self._handle_transition_error(handler, msg, e) def _handle_transition_error(self, handler, msg, e): err = 'transition "{}" failed for {} in {}' self.log.exception(err.format(handler.name, msg, self.name)) if tryp.development: raise TransitionFailed() from e return Empty() @curried def _process_result(self, old_data: Data, result) -> TransitionResult: if isinstance(result, Coroutine): return CoroTransitionResult(data=old_data, coro=result) elif isinstance(result, TransitionResult): return result elif isinstance(result, self._data_type): return TransitionResult.empty(result) elif isinstance(result, Message) or not is_seq(result): result = List(result) datas, rest = List.wrap(result).split_type(self._data_type) strict, rest = rest.split_type(Message) coro, rest = rest.split(iscoroutine) msgs = strict + coro.map(Coroutine).map(_.pub) if rest: tpl = "invalid transition result parts in {}: {}" msg = tpl.format(self.name, rest) if tryp.development: raise MachineError(msg) else: self.log.error(msg) new_data = datas.head | old_data return self._create_result(new_data, msgs) def _create_result(self, data, msgs): pub, resend = msgs.split_type(Publish) pub_msgs = pub.map(_.message) return StrictTransitionResult(data=data, pub=pub_msgs, resend=resend) @may def unhandled(self, data: Data, msg: Message): pass def _command_by_message_name(self, name: str): msg_name = camelcaseify(name) return self._message_handlers.find_key(lambda a: a.__name__ in [name, msg_name]) def command(self, name: str, args: list): return ( self._command_by_message_name(name) .map(lambda a: StateCommand(a[0])) .map(_.call("dispatch", self, args)) .or_else(F(self._invalid_command, name)) ) def _invalid_command(self, name): self.log.error('plugin "{}" has no command "{}"'.format(self.name, name)) return Empty() @may_handle(NvimIOTask) def message_nvim_io(self, data: Data, msg): msg.io.unsafe_perform_io(self.vim) @may_handle(RunTask) def message_run_task(self, data: Data, msg): success = lambda r: r if isinstance(r, Message) else None return msg.task.unsafe_perform_sync().cata(F(Error) >> _.pub, success) @may_handle(DataTask) def message_data_task(self, data: Data, msg): return either_msg(msg.cons(Task.now(data)).unsafe_perform_sync()) def bubble(self, msg): self.parent.cata(_.bubble, lambda: self.send)(msg)
def call(self, name, *a, **kw): return ( Maybe.from_call(self.vim.call, name, *a, exc=NvimError, **kw) .to_either(lambda: 'vim call failed: {}'.format( format_funcall(name, a, kw))) )
class Command(Logging): def __init__( self, fun: Callable[..., Any], name: str=None, nargs=None, min: int=None, max: int=None, **kw ) -> None: self._fun = fun self._argspec = inspect.getfullargspec(fun) # type: ignore self._params = List.wrap(self._argspec.args) self._param_count = self._params.length - ( 1 if self._params.head.contains('self') else 0 ) self._name = Maybe(name) self._nargs = Maybe(try_int(nargs)) self._min = Maybe(min) self._max = Maybe(max) self._kw = kw @property def nargs(self): return self._nargs\ .map(lambda a: '+' if numeric(a) and a > 1 else a)\ .get_or_else(self._infer_nargs) @property def _infer_nargs(self): if self.min > 1: return '+' elif self.min == 1: return 1 if self.max.contains(1) else '+' else: if self.max.contains(0): return 0 if self.max.contains(1): return '?' else: return '*' @property def min(self): return self._min.get_or_else(self._infer_min) @property def _infer_min(self): s = self._argspec return self._param_count - len(s.defaults or []) @property def max(self): return self._max.or_else(self._infer_max) @property # type: ignore @may def _infer_max(self): s = self._argspec if not (s.varargs or s.varkw): return self._param_count @property def name(self): return self._name.get_or_else(self._infer_name) @property def _infer_name(self): return camelcaseify(self._fun.__name__) @property # type: ignore @may def exact_count(self): if self.max.contains(self.min): return self.min def check_length(self, args): l = len(args) return ( self.min <= l and not self.max.exists(_ < l) ) @property def count_spec(self): return self.exact_count\ .map(lambda a: 'exactly {}'.format(a) if a else 'none')\ .get_or_else( Just(self.min) .zip(self.max) .smap('between {} and {}'.format) .get_or_else('at least {}'.format(self.min)) ) def error(self, args): msg = 'argument count for command "{}" is {}, must be {} ({})' err = msg.format(self.name, len(args), self.count_spec, args) self.log.error(err) return err def _call_fun(self, obj, *args): return self._fun(obj, *args) def dispatch(self, obj, rpc_args): args = List.wrap(rpc_args).lift(0).get_or_else(List()) if self.check_length(args): return self._call_fun(obj, *args) else: return self.error(args) @property def neovim_cmd(self): @neovim.command(self.name, nargs=self.nargs, **self._kw) def neovim_cmd_wrapper(obj, *rpc_args): return self.dispatch(obj, rpc_args) return neovim_cmd_wrapper