mwxlib 0.99.0__py3-none-any.whl → 1.7.13__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
mwx/utilus.py CHANGED
@@ -16,8 +16,7 @@ import fnmatch
16
16
  import pkgutil
17
17
  import pydoc
18
18
  import inspect
19
- from inspect import (isclass, ismodule, ismethod, isbuiltin,
20
- isfunction, isgenerator, isframe, iscode, istraceback)
19
+ from inspect import isclass, ismodule, ismethod, isbuiltin, isfunction
21
20
  from pprint import pprint
22
21
 
23
22
 
@@ -37,18 +36,26 @@ def ignore(*category):
37
36
  yield
38
37
 
39
38
 
40
- def warn(message, category=None):
41
- frame = inspect.currentframe().f_back # previous call stack frame
42
- skip = [frame.f_code.co_filename]
43
- stacklevel = 1
44
- while frame.f_code.co_filename in skip:
45
- frame = frame.f_back
46
- stacklevel += 1
39
+ def warn(message, category=None, stacklevel=None):
40
+ if stacklevel is None:
41
+ frame = inspect.currentframe().f_back # previous call stack frame
42
+ skip = [frame.f_code.co_filename]
43
+ stacklevel = 1
44
+ while frame.f_code.co_filename in skip:
45
+ frame = frame.f_back
46
+ if not frame:
47
+ break
48
+ stacklevel += 1
47
49
  return warnings.warn(message, category, stacklevel+1)
48
50
 
49
51
 
50
52
  def atom(v):
51
- return not hasattr(v, '__name__')
53
+ ## Not a class, method, function, module, or any type (class, int, str, etc.).
54
+ if (isclass(v) or ismethod(v) or isfunction(v) or isbuiltin(v)
55
+ or ismodule(v) or isinstance(v, type)):
56
+ return False
57
+ ## Include the case where __name__ is manually defined for a class instance.
58
+ return not hasattr(v, '__name__') or hasattr(v, '__class__')
52
59
 
53
60
 
54
61
  def isobject(v):
@@ -60,7 +67,7 @@ def instance(*types):
60
67
  ## return lambda v: isinstance(v, types)
61
68
  def _pred(v):
62
69
  return isinstance(v, types)
63
- _pred.__name__ = str("instance<{}>".format(','.join(p.__name__ for p in types)))
70
+ _pred.__name__ = "instance<{}>".format(','.join(p.__name__ for p in types))
64
71
  return _pred
65
72
 
66
73
 
@@ -68,7 +75,7 @@ def subclass(*types):
68
75
  ## return lambda v: issubclass(v, types)
69
76
  def _pred(v):
70
77
  return issubclass(v, types)
71
- _pred.__name__ = str("subclass<{}>".format(','.join(p.__name__ for p in types)))
78
+ _pred.__name__ = "subclass<{}>".format(','.join(p.__name__ for p in types))
72
79
  return _pred
73
80
 
74
81
 
@@ -78,7 +85,7 @@ def _Not(p):
78
85
  p = instance(p)
79
86
  def _pred(v):
80
87
  return not p(v)
81
- _pred.__name__ = str("not {}".format(p.__name__))
88
+ _pred.__name__ = "not {}".format(p.__name__)
82
89
  return _pred
83
90
 
84
91
 
@@ -90,7 +97,7 @@ def _And(p, q):
90
97
  q = instance(q)
91
98
  def _pred(v):
92
99
  return p(v) and q(v)
93
- _pred.__name__ = str("{} and {}".format(p.__name__, q.__name__))
100
+ _pred.__name__ = "{} and {}".format(p.__name__, q.__name__)
94
101
  return _pred
95
102
 
96
103
 
@@ -102,7 +109,7 @@ def _Or(p, q):
102
109
  q = instance(q)
103
110
  def _pred(v):
104
111
  return p(v) or q(v)
105
- _pred.__name__ = str("{} or {}".format(p.__name__, q.__name__))
112
+ _pred.__name__ = "{} or {}".format(p.__name__, q.__name__)
106
113
  return _pred
107
114
 
108
115
 
@@ -136,9 +143,6 @@ def apropos(obj, rexpr='', ignorecase=True, alias=None, pred=None, locals=None):
136
143
  """
137
144
  name = alias or typename(obj)
138
145
 
139
- rexpr = (rexpr.replace('\\a','[a-z0-9]') #\a: identifier chars (custom rule)
140
- .replace('\\A','[A-Z0-9]')) #\A:
141
-
142
146
  if isinstance(pred, str):
143
147
  pred = predicate(pred, locals)
144
148
 
@@ -161,7 +165,7 @@ def apropos(obj, rexpr='', ignorecase=True, alias=None, pred=None, locals=None):
161
165
  except re.error as e:
162
166
  print("- re:miss compilation:", e)
163
167
  else:
164
- keys = sorted(filter(p.search, dir(obj)), key=lambda s:s.upper())
168
+ keys = sorted(filter(p.search, dir(obj)), key=lambda s: s.upper())
165
169
  n = 0
166
170
  for key in keys:
167
171
  try:
@@ -174,7 +178,7 @@ def apropos(obj, rexpr='', ignorecase=True, alias=None, pred=None, locals=None):
174
178
  except Exception as e:
175
179
  word = f"#<{e!r}>"
176
180
  if len(word) > 80:
177
- word = word[:80] + '...' # truncate words +3 ellipsis
181
+ word = word[:80] + '...' # truncate words +3 ellipsis
178
182
  print(" {}.{:<36s} {}".format(name, key, word))
179
183
  if pred:
180
184
  print("found {} of {} words with :{}".format(n, len(keys), pred.__name__))
@@ -185,22 +189,26 @@ def apropos(obj, rexpr='', ignorecase=True, alias=None, pred=None, locals=None):
185
189
  def typename(obj, docp=False, qualp=True):
186
190
  """Formatted object type name.
187
191
  """
188
- if hasattr(obj, '__name__'): # module, class, method, function, etc.
192
+ if not atom(obj): # module, class, method, function, etc.
189
193
  if qualp:
190
194
  name = getattr(obj, '__qualname__', obj.__name__)
191
195
  else:
192
196
  name = obj.__name__
193
- elif hasattr(obj, '__module__'): # atom -> module.class
197
+ elif hasattr(obj, '__class__'): # class instance -> module.class
194
198
  name = obj.__class__.__name__
195
199
  else:
196
- return pydoc.describe(obj) # atom -> short description
200
+ return pydoc.describe(obj) # atom -> short description
197
201
 
198
202
  modname = getattr(obj, '__module__', None)
199
- if modname and modname != "__main__" and not modname.startswith('mwx'):
200
- name = modname + '.' + name
203
+ if modname:
204
+ if qualp:
205
+ name = modname + '.' + name
206
+ else:
207
+ if not modname.startswith(("__main__", "mwx")):
208
+ name = modname + '..' + name
201
209
 
202
210
  if docp and callable(obj) and obj.__doc__:
203
- name += "<{!r}>".format(obj.__doc__.splitlines()[0]) # concat the first doc line
211
+ name += "<{!r}>".format(obj.__doc__.splitlines()[0]) # concat the first doc line
204
212
  return name
205
213
 
206
214
 
@@ -225,8 +233,8 @@ def where(obj):
225
233
  name = obj.tb_frame.f_code.co_name
226
234
  return "{}:{}:{}".format(filename, lineno, name)
227
235
 
228
- ## if inspect.isbuiltin(obj):
229
- ## return None
236
+ # if inspect.isbuiltin(obj):
237
+ # return None
230
238
 
231
239
  def _where(obj):
232
240
  obj = inspect.unwrap(obj)
@@ -239,18 +247,18 @@ def where(obj):
239
247
 
240
248
  try:
241
249
  try:
242
- return _where(obj) # module, class, method, function, frame, or code
250
+ return _where(obj) # module, class, method, function, frame, or code
243
251
  except TypeError:
244
- return _where(obj.__class__) # otherwise, class of the object
252
+ return _where(obj.__class__) # otherwise, class of the object
245
253
  except Exception:
246
254
  pass
247
255
  ## The source code cannot be retrieved.
248
256
  ## Try to get filename where the object is defined.
249
257
  try:
250
258
  try:
251
- return inspect.getfile(obj) # compiled file?
259
+ return inspect.getfile(obj) # compiled file?
252
260
  except TypeError:
253
- return inspect.getfile(obj.__class__) # or a special class?
261
+ return inspect.getfile(obj.__class__) # or a special class?
254
262
  except Exception:
255
263
  pass
256
264
 
@@ -273,34 +281,50 @@ def mro(obj):
273
281
  def pp(obj):
274
282
  pprint(obj, **pp.__dict__)
275
283
 
276
- if pp:
277
- pp.indent = 1
278
- pp.width = 80 # default 80
279
- pp.depth = None
280
- if sys.version_info >= (3,6):
281
- pp.compact = False
282
- if sys.version_info >= (3,8):
283
- pp.sort_dicts = True
284
+
285
+ pp.indent = 1
286
+ pp.width = 80 # default 80
287
+ pp.depth = None
288
+ pp.compact = False
289
+ pp.sort_dicts = False
290
+
291
+
292
+ ## --------------------------------
293
+ ## Shell internal helper functions.
294
+ ## --------------------------------
295
+
296
+ def fix_fnchars(filename, substr='_'):
297
+ """Replace invalid filename characters with substr."""
298
+ if os.name == 'nt':
299
+ ## Replace Windows-invalid chars [:*?"<>|] with substr.
300
+ ## Do not replace \\ or / to preserve folder structure.
301
+ return re.sub(r'[:*?"<>|]', substr, filename)
302
+ else:
303
+ return filename
284
304
 
285
305
 
286
- def split_paren(text, reverse=False):
287
- """Split text into a head parenthesis and the rest, including the tail.
288
- If reverse is True, search from tail to head.
306
+ def split_words(text, reverse=False):
307
+ """Generates words (python phrase) extracted from text.
308
+ If reverse is True, process from tail to head.
289
309
  """
290
310
  tokens = list(split_tokens(text))
291
311
  if reverse:
292
312
  tokens = tokens[::-1]
293
- words = _extract_paren_from_tokens(tokens, reverse)
294
- paren = ''.join(reversed(words) if reverse else words)
295
- rest = ''.join(reversed(tokens) if reverse else tokens)
296
- if reverse:
297
- return rest, paren
298
- else:
299
- return paren, rest
313
+ while tokens:
314
+ words = []
315
+ while 1:
316
+ word = _extract_words_from_tokens(tokens, reverse)
317
+ if not word:
318
+ break
319
+ words += word
320
+ if words:
321
+ yield ''.join(reversed(words) if reverse else words)
322
+ if tokens:
323
+ yield tokens.pop(0) # sep-token
300
324
 
301
325
 
302
- def split_words(text, reverse=False):
303
- """Generates words extracted from text.
326
+ def split_parts(text, reverse=False):
327
+ """Generates portions (words and parens) extracted from text.
304
328
  If reverse is True, process from tail to head.
305
329
  """
306
330
  tokens = list(split_tokens(text))
@@ -311,7 +335,7 @@ def split_words(text, reverse=False):
311
335
  if words:
312
336
  yield ''.join(reversed(words) if reverse else words)
313
337
  else:
314
- yield tokens.pop(0) # sep-token
338
+ yield tokens.pop(0) # sep-token
315
339
 
316
340
 
317
341
  def split_tokens(text, comment=True):
@@ -324,17 +348,18 @@ def split_tokens(text, comment=True):
324
348
  j, k = 1, 0
325
349
  for type, string, start, end, line in tokens:
326
350
  l, m = start
327
- if type in (0,5,6) or not string:
351
+ if type in (tokenize.INDENT, tokenize.DEDENT) or not string:
352
+ ## Empty strings such as NEWLINE and ENDMARKER are also skipped.
328
353
  continue
329
- if type == 61 and not comment:
330
- token = next(tokens) # eats a trailing token
331
- string = token.string # cr/lf or ''
354
+ if type == tokenize.COMMENT and not comment:
355
+ token = next(tokens) # eats a trailing token
356
+ string = token.string # cr/lf or ''
332
357
  if m == 0:
333
358
  continue # line starting with a comment
334
359
  if l > j and m > 0:
335
360
  yield ' ' * m # indent spaces
336
361
  elif m > k:
337
- yield ' ' * (m-k) # white spaces
362
+ yield ' ' * (m-k) # white spaces
338
363
  j, k = end
339
364
  yield string
340
365
  except tokenize.TokenError:
@@ -344,10 +369,6 @@ def split_tokens(text, comment=True):
344
369
  def _extract_words_from_tokens(tokens, reverse=False):
345
370
  """Extracts pythonic expressions from tokens.
346
371
 
347
- Extraction continues until the parenthesis is closed
348
- and the following token starts with a char in sep, where
349
- the sep includes `@, ops, delims, and whitespaces, etc.
350
-
351
372
  Returns:
352
373
  A token list extracted including the parenthesis.
353
374
  If reverse is True, the order of the tokens will be reversed.
@@ -359,53 +380,28 @@ def _extract_words_from_tokens(tokens, reverse=False):
359
380
  stack = []
360
381
  words = []
361
382
  for j, c in enumerate(tokens):
383
+ if not c:
384
+ continue
362
385
  if c in p:
363
386
  stack.append(c)
364
387
  elif c in q:
365
- if not stack: # error("open-paren")
388
+ if not stack: # error("open-paren")
366
389
  break
367
- if c != q[p.index(stack.pop())]: # error("mismatch-paren")
390
+ if c != q[p.index(stack.pop())]: # error("mismatch-paren")
368
391
  break
369
- elif not stack and c[0] in sep: # ok; starts with a char in sep
392
+ elif not stack and c[0] in sep: # ok; starts with a char in sep
370
393
  break
371
394
  words.append(c)
372
- else: # if stack: error("unclosed-paren")
395
+ if not stack: # ok
396
+ j += 1 # to remove current token
397
+ break
398
+ else:
399
+ # if stack: error("unclosed-paren")
373
400
  j = None
374
- del tokens[:j] # remove extracted tokens (except the last one)
401
+ del tokens[:j] # remove extracted tokens (except the last one)
375
402
  return words
376
403
 
377
404
 
378
- def _extract_paren_from_tokens(tokens, reverse=False):
379
- """Extracts parenthesis from tokens.
380
-
381
- The first token must be a parenthesis.
382
- Returns:
383
- A token list extracted including the parenthesis,
384
- or an empty list if the parenthesis is not closed.
385
- If reverse is True, the order of the tokens will be reversed.
386
- """
387
- p, q = "({[", ")}]"
388
- if reverse:
389
- p, q = q, p
390
- stack = []
391
- words = []
392
- for j, c in enumerate(tokens):
393
- if c in p:
394
- stack.append(c)
395
- elif c in q:
396
- if not stack: # error("open-paren")
397
- break
398
- if c != q[p.index(stack.pop())]: # error("mismatch-paren")
399
- break
400
- elif j == 0:
401
- break # first char is not paren
402
- words.append(c)
403
- if not stack: # ok
404
- del tokens[:j+1] # remove extracted tokens
405
- return words
406
- return [] # error("unclosed-paren")
407
-
408
-
409
405
  def walk_packages_no_import(path=None, prefix=''):
410
406
  """Yields module info recursively for all submodules on path.
411
407
  If path is None, yields all top-level modules on sys.path.
@@ -441,9 +437,9 @@ def find_modules(force=False, verbose=True):
441
437
 
442
438
  if not force and os.path.exists(fn):
443
439
  with open(fn, 'r') as o:
444
- lm = eval(o.read()) # read and evaluate module list
440
+ lm = eval(o.read()) # read and evaluate module list
445
441
 
446
- ## Check additional packages and modules
442
+ ## Check additional packages and modules.
447
443
  verbose = False
448
444
  for info in walk_packages_no_import(['.']):
449
445
  _callback('.', info.name)
@@ -462,7 +458,7 @@ def find_modules(force=False, verbose=True):
462
458
 
463
459
  lm.sort(key=str.upper)
464
460
  with open(fn, 'w') as o:
465
- pprint(lm, stream=o) # write module list
461
+ pprint(lm, stream=o) # write module list
466
462
  print("The results were written in {!r}.".format(fn))
467
463
  return lm
468
464
 
@@ -478,19 +474,19 @@ def get_rootpath(fn):
478
474
 
479
475
 
480
476
  ## --------------------------------
481
- ## Finite State Machine
477
+ ## Finite State Machine.
482
478
  ## --------------------------------
483
479
 
484
480
  class SSM(dict):
485
- """Single State Machine/Context of FSM
481
+ """Single State Machine/Context of FSM.
486
482
  """
487
483
  def __call__(self, event, *args, **kwargs):
488
484
  for act in self[event]:
489
485
  act(*args, **kwargs)
490
-
486
+
491
487
  def __repr__(self):
492
488
  return "<{} object at 0x{:X}>".format(self.__class__.__name__, id(self))
493
-
489
+
494
490
  def __str__(self):
495
491
  def _lstr(v):
496
492
  def _name(a):
@@ -499,7 +495,7 @@ class SSM(dict):
499
495
  return repr(a)
500
496
  return ', '.join(_name(a) for a in v)
501
497
  return '\n'.join("{:>32} : {}".format(str(k), _lstr(v)) for k, v in self.items())
502
-
498
+
503
499
  def bind(self, event, action=None):
504
500
  """Append a transaction to the context."""
505
501
  assert callable(action) or action is None
@@ -511,7 +507,7 @@ class SSM(dict):
511
507
  if action not in transaction:
512
508
  transaction.append(action)
513
509
  return action
514
-
510
+
515
511
  def unbind(self, event, action=None):
516
512
  """Remove a transaction from the context."""
517
513
  assert callable(action) or action is None
@@ -528,7 +524,7 @@ class SSM(dict):
528
524
 
529
525
 
530
526
  class FSM(dict):
531
- """Finite State Machine
527
+ """Finite State Machine.
532
528
 
533
529
  Args:
534
530
  contexts: map of context <DNA>
@@ -552,27 +548,28 @@ class FSM(dict):
552
548
  [8] +++ (max verbose level) to put all args and kwargs.
553
549
 
554
550
  Note:
555
- A default=None is given as an argument of the init.
556
- If there is only one state, that state will be the default.
551
+ default=None is given as an argument to ``__init__``.
552
+ If there is only one state, that state is used as the default.
557
553
 
558
554
  Note:
559
555
  There is no enter/exit event handler.
560
556
  """
561
557
  debug = 0
562
-
563
- default_state = None
558
+
559
+ default_state = None # Used for define/undefine methods.
560
+
564
561
  current_state = property(lambda self: self.__state)
565
562
  previous_state = property(lambda self: self.__prev_state)
566
-
563
+
567
564
  current_event = property(lambda self: self.__event)
568
565
  previous_event = property(lambda self: self.__prev_event)
569
-
566
+
570
567
  @current_state.setter
571
568
  def current_state(self, state):
572
569
  self.__state = state
573
570
  self.__event = '*forced*'
574
571
  self.__debcall__(self.__event)
575
-
572
+
576
573
  def clear(self, state):
577
574
  """Reset current and previous states."""
578
575
  self.__state = state
@@ -580,28 +577,28 @@ class FSM(dict):
580
577
  self.__event = None
581
578
  self.__prev_event = None
582
579
  self.__matched_pattern = None
583
-
580
+
584
581
  def __init__(self, contexts=None, default=None):
585
- dict.__init__(self) # update dict, however, it does not clear
586
- dict.clear(self) # if and when __init__ is called, all contents are cleared
582
+ dict.__init__(self) # update dict, however, it does not clear
583
+ dict.clear(self) # if and when __init__ is called, all contents are cleared
587
584
  if contexts is None:
588
585
  contexts = {}
589
- if default is None: # if no default given, reset the first state as the default
586
+ if default is None: # if no default given, reset the first state as the default
590
587
  if self.default_state is None:
591
588
  default = next((k for k in contexts if k is not None), None)
592
589
  self.default_state = default
593
- self.clear(default) # the first clear creates object localvars
590
+ self.clear(default) # the first clear creates object localvars
594
591
  self.update(contexts)
595
-
592
+
596
593
  def __missing__(self, key):
597
594
  raise Exception("FSM logic-error: undefined state {!r}".format(key))
598
-
595
+
599
596
  def __repr__(self):
600
597
  return "<{} object at 0x{:X}>".format(self.__class__.__name__, id(self))
601
-
598
+
602
599
  def __str__(self):
603
600
  return '\n'.join("[ {!r} ]\n{!s}".format(k, v) for k, v in self.items())
604
-
601
+
605
602
  def __call__(self, event, *args, **kwargs):
606
603
  """Handle the event.
607
604
 
@@ -615,8 +612,8 @@ class FSM(dict):
615
612
  - process the event (no actions) -> []
616
613
  - no event:transaction -> None
617
614
  """
618
- recept = False # Is transaction performed?
619
- retvals = [] # retvals of actions
615
+ recept = False # Is transaction performed?
616
+ retvals = [] # retvals of actions
620
617
  self.__event = event
621
618
  if None in self:
622
619
  org = self.__state
@@ -624,17 +621,17 @@ class FSM(dict):
624
621
  try:
625
622
  self.__state = None
626
623
  self.__prev_state = None
627
- ret = self.call(event, *args, **kwargs) # None process
624
+ ret = self.call(event, *args, **kwargs) # None process
628
625
  if ret is not None:
629
626
  recept = True
630
627
  retvals += ret
631
628
  finally:
632
- if self.__state is None: # restore original
629
+ if self.__state is None: # restore original
633
630
  self.__state = org
634
631
  self.__prev_state = prev
635
632
 
636
633
  if self.__state is not None:
637
- ret = self.call(event, *args, **kwargs) # normal process
634
+ ret = self.call(event, *args, **kwargs) # normal process
638
635
  if ret is not None:
639
636
  recept = True
640
637
  retvals += ret
@@ -644,7 +641,7 @@ class FSM(dict):
644
641
  self.__prev_state = self.__state
645
642
  if recept:
646
643
  return retvals
647
-
644
+
648
645
  def fork(self, event, *args, **kwargs):
649
646
  """Invoke the event handlers (internal use only).
650
647
 
@@ -655,7 +652,7 @@ class FSM(dict):
655
652
  ret = self.call(event, *args, **kwargs)
656
653
  self.__prev_event = self.__event
657
654
  return ret
658
-
655
+
659
656
  def call(self, event, *args, **kwargs):
660
657
  """Invoke the event handlers (internal use only).
661
658
 
@@ -672,16 +669,16 @@ class FSM(dict):
672
669
  context = self[self.__state]
673
670
  if event in context:
674
671
  transaction = context[event]
675
- self.__prev_state = self.__state # save previous state
676
- self.__state = transaction[0] # the state transits here
677
- self.__debcall__(event, *args, **kwargs) # check after transition
672
+ self.__prev_state = self.__state # save previous state
673
+ self.__state = transaction[0] # the state transits here
674
+ self.__debcall__(event, *args, **kwargs) # check after transition
678
675
  retvals = []
679
676
  for act in transaction[1:]:
680
677
  ## Save the event before each action (for nested call).
681
678
  if self.__matched_pattern is None:
682
679
  self.__event = event
683
680
  try:
684
- ret = act(*args, **kwargs) # call actions after transition
681
+ ret = act(*args, **kwargs) # call actions after transition
685
682
  retvals.append(ret)
686
683
  except BdbQuit:
687
684
  pass
@@ -697,15 +694,15 @@ class FSM(dict):
697
694
  self.__matched_pattern = None
698
695
  return retvals
699
696
 
700
- if isinstance(event, str): # matching test using fnmatch
697
+ if isinstance(event, str): # matching test using fnmatch
701
698
  for pat in context:
702
699
  if fnmatch.fnmatchcase(event, pat):
703
700
  self.__matched_pattern = pat
704
- return self.call(pat, *args, **kwargs) # recursive call
701
+ return self.call(pat, *args, **kwargs) # recursive call
705
702
 
706
- self.__debcall__(event, *args, **kwargs) # check when no transition
707
- return None # no event, no action
708
-
703
+ self.__debcall__(event, *args, **kwargs) # check when no transition
704
+ return None # no event, no action
705
+
709
706
  def __debcall__(self, pattern, *args, **kwargs):
710
707
  v = self.debug
711
708
  if v and self.__state is not None:
@@ -720,7 +717,7 @@ class FSM(dict):
720
717
  a = '' if not actions else ('=> ' + actions),
721
718
  c = '*' if self.__prev_state != self.__state else ' '))
722
719
 
723
- elif v > 3: # state is None
720
+ elif v > 3: # state is None
724
721
  transaction = self[None].get(pattern) or []
725
722
  actions = ', '.join(typename(a, qualp=0) for a in transaction[1:])
726
723
  if actions or v > 4:
@@ -728,13 +725,13 @@ class FSM(dict):
728
725
  self.__event,
729
726
  a = '' if not actions else ('=> ' + actions)))
730
727
 
731
- if v > 7: # max verbose level puts all args
728
+ if v > 7: # max verbose level puts all args
732
729
  self.log("\t:", args, kwargs)
733
-
730
+
734
731
  @staticmethod
735
732
  def log(*args):
736
733
  print(*args, file=sys.__stdout__)
737
-
734
+
738
735
  @staticmethod
739
736
  def dump(*args):
740
737
  fn = get_rootpath("deb-dump.log")
@@ -742,7 +739,7 @@ class FSM(dict):
742
739
  print(time.strftime('!!! %Y/%m/%d %H:%M:%S'), file=o)
743
740
  print(*args, traceback.format_exc(), sep='\n', file=o)
744
741
  print(*args, traceback.format_exc(), sep='\n', file=sys.__stderr__)
745
-
742
+
746
743
  @staticmethod
747
744
  def duplicate(context):
748
745
  """Duplicate the transaction:list in the context.
@@ -750,63 +747,63 @@ class FSM(dict):
750
747
  This method is used for the contexts given to :append and :update
751
748
  so that the original transaction (if they are lists) is not removed.
752
749
  """
753
- return {event:transaction[:] for event, transaction in context.items()}
754
-
750
+ return {event: transaction[:] for event, transaction in context.items()}
751
+
755
752
  def validate(self, state):
756
753
  """Sort and move to end items with key which includes ``*?[]``."""
757
754
  context = self[state]
758
755
  ast = []
759
756
  bra = []
760
- for event in list(context): # context mutates during iteration
757
+ for event in list(context): # context mutates during iteration
761
758
  if re.search(r"\[.+\]", event):
762
- bra.append((event, context.pop(event))) # event key has '[]'
759
+ bra.append((event, context.pop(event))) # event key has '[]'
763
760
  elif '*' in event or '?' in event:
764
- ast.append((event, context.pop(event))) # event key has '*?'
761
+ ast.append((event, context.pop(event))) # event key has '*?'
765
762
 
766
- temp = sorted(context.items()) # normal event key
763
+ temp = sorted(context.items()) # normal event key
767
764
  context.clear()
768
765
  context.update(temp)
769
766
  context.update(sorted(bra, reverse=1))
770
- context.update(sorted(ast, reverse=1, key=lambda v:len(v[0])))
771
-
767
+ context.update(sorted(ast, reverse=1, key=lambda v: len(v[0])))
768
+
772
769
  def update(self, contexts):
773
770
  """Update each context or Add new contexts."""
774
771
  for k, v in contexts.items():
775
772
  if k in self:
776
773
  self[k].update(self.duplicate(v))
777
774
  else:
778
- self[k] = SSM(self.duplicate(v)) # new context
775
+ self[k] = SSM(self.duplicate(v)) # new context
779
776
  self.validate(k)
780
-
777
+
781
778
  def append(self, contexts):
782
779
  """Append new contexts."""
783
780
  for k, v in contexts.items():
784
781
  if k in self:
785
782
  for event, transaction in v.items():
786
783
  if event not in self[k]:
787
- self[k][event] = transaction[:] # copy the event:transaction
784
+ self[k][event] = transaction[:] # copy the event:transaction
788
785
  continue
789
786
  for act in transaction[1:]:
790
787
  self.bind(event, act, k, transaction[0])
791
788
  else:
792
- self[k] = SSM(self.duplicate(v)) # new context
789
+ self[k] = SSM(self.duplicate(v)) # new context
793
790
  self.validate(k)
794
-
791
+
795
792
  def remove(self, contexts):
796
793
  """Remove old contexts."""
797
794
  for k, v in contexts.items():
798
795
  if k in self:
799
796
  for event, transaction in v.items():
800
797
  if self[k].get(event) == transaction:
801
- self[k].pop(event) # remove the event:transaction
798
+ self[k].pop(event) # remove the event:transaction
802
799
  continue
803
800
  for act in transaction[1:]:
804
801
  self.unbind(event, act, k)
805
- ## cleanup
806
- for k, v in list(self.items()): # self mutates during iteration
802
+ ## Cleanup.
803
+ for k, v in list(self.items()): # self mutates during iteration
807
804
  if not v:
808
805
  del self[k]
809
-
806
+
810
807
  def bind(self, event, action=None, state=None, state2=None):
811
808
  """Append a transaction to the context.
812
809
 
@@ -820,24 +817,24 @@ class FSM(dict):
820
817
 
821
818
  if state not in self:
822
819
  warn(f"- FSM [{state!r}] context newly created.")
823
- self[state] = SSM() # new context
820
+ self[state] = SSM() # new context
824
821
 
825
- context = self[state]
826
822
  if state2 is None:
827
823
  state2 = state
828
824
 
825
+ context = self[state]
829
826
  if event in context:
830
827
  if state2 != context[event][0]:
831
828
  warn(f"- FSM transaction may conflict ({event!r} : {state!r} --> {state2!r}).\n"
832
829
  f" The state {state2!r} is different from the original state.")
833
830
  pass
834
- context[event][0] = state2 # update transition
831
+ context[event][0] = state2 # update transition
835
832
  else:
836
833
  if state2 not in self:
837
834
  warn(f"- FSM transaction may contradict ({event!r} : {state!r} --> {state2!r}).\n"
838
835
  f" The state {state2!r} is not found in the contexts.")
839
836
  pass
840
- context[event] = [state2] # new event:transaction
837
+ context[event] = [state2] # new event:transaction
841
838
 
842
839
  transaction = context[event]
843
840
  if action is None:
@@ -850,7 +847,7 @@ class FSM(dict):
850
847
  warn(f"- FSM cannot append new transaction ({state!r} : {event!r}).\n"
851
848
  f" The transaction must be a list, not a tuple.")
852
849
  return action
853
-
850
+
854
851
  def unbind(self, event, action=None, state=None):
855
852
  """Remove a transaction from the context.
856
853
 
@@ -885,6 +882,41 @@ class FSM(dict):
885
882
  f" The transaction must be a list, not a tuple")
886
883
  return False
887
884
 
885
+ def binds(self, event, action=None, state=None, state2=None):
886
+ """Append a one-time transaction to the context.
887
+
888
+ Like `bind`, but unbinds itself after being called once.
889
+ """
890
+ if action is None:
891
+ return lambda f: self.binds(event, f, state, state2)
892
+
893
+ @wraps(action)
894
+ def _act(*v, **kw):
895
+ try:
896
+ return action(*v, **kw)
897
+ finally:
898
+ self.unbind(event, _act, state)
899
+ return self.bind(event, _act, state, state2)
900
+
901
+ def define(self, event, action=None, /, *args, **kwargs):
902
+ """Define event action.
903
+
904
+ Note:
905
+ The funcall kwargs `doc` and `alias` are reserved as kw-only-args.
906
+ """
907
+ state = self.default_state
908
+ if action is None:
909
+ self[state].pop(event, None) # cf. undefine
910
+ return lambda f: self.define(event, f, *args, **kwargs)
911
+
912
+ f = funcall(action, *args, **kwargs)
913
+ self.update({state: {event: [state, f]}})
914
+ return action
915
+
916
+ def undefine(self, event):
917
+ """Delete event context."""
918
+ self.define(event, None)
919
+
888
920
 
889
921
  class TreeList:
890
922
  """Interface class for tree list control.
@@ -900,41 +932,41 @@ class TreeList:
900
932
  """
901
933
  def __init__(self, ls=None):
902
934
  self.__items = ls or []
903
-
935
+
904
936
  def __call__(self, k):
905
937
  return TreeList(self[k])
906
-
938
+
907
939
  def __len__(self):
908
940
  return len(self.__items)
909
-
941
+
910
942
  def __contains__(self, k):
911
943
  return self._getf(self.__items, k)
912
-
944
+
913
945
  def __iter__(self):
914
946
  return self.__items.__iter__()
915
-
947
+
916
948
  def __getitem__(self, k):
917
949
  if isinstance(k, str):
918
950
  return self._getf(self.__items, k)
919
951
  return self.__items.__getitem__(k)
920
-
952
+
921
953
  def __setitem__(self, k, v):
922
954
  if isinstance(k, str):
923
955
  return self._setf(self.__items, k, v)
924
956
  return self.__items.__setitem__(k, v)
925
-
957
+
926
958
  def __delitem__(self, k):
927
959
  if isinstance(k, str):
928
960
  return self._delf(self.__items, k)
929
961
  return self.__items.__delitem__(k)
930
-
962
+
931
963
  def _find_item(self, ls, key):
932
964
  for x in ls:
933
965
  if isinstance(x, (tuple, list)) and x and x[0] == key:
934
966
  if len(x) < 2:
935
967
  raise ValueError(f"No value for {key=!r}")
936
968
  return x
937
-
969
+
938
970
  def _getf(self, ls, key):
939
971
  if '/' in key:
940
972
  a, b = key.split('/', 1)
@@ -945,7 +977,7 @@ class TreeList:
945
977
  li = self._find_item(ls, key)
946
978
  if li is not None:
947
979
  return li[-1]
948
-
980
+
949
981
  def _setf(self, ls, key, value):
950
982
  if '/' in key:
951
983
  a, b = key.split('/', 1)
@@ -953,19 +985,19 @@ class TreeList:
953
985
  if la is not None:
954
986
  return self._setf(la, b, value)
955
987
  p, key = key.rsplit('/', 1)
956
- return self._setf(ls, p, [[key, value]]) # ls[p].append([key, value])
988
+ return self._setf(ls, p, [[key, value]]) # ls[p].append([key, value])
957
989
  try:
958
990
  li = self._find_item(ls, key)
959
991
  if li is not None:
960
992
  try:
961
- li[-1] = value # assign value to item (ls must be a list)
993
+ li[-1] = value # assign value to item (ls must be a list)
962
994
  except TypeError:
963
- li[-1][:] = value # assign value to items:list
995
+ li[-1][:] = value # assign value to items:list
964
996
  else:
965
- ls.append([key, value]) # append to items:list
997
+ ls.append([key, value]) # append to items:list
966
998
  except (ValueError, TypeError, AttributeError) as e:
967
999
  warn(f"- TreeList {e!r}: {key=!r}")
968
-
1000
+
969
1001
  def _delf(self, ls, key):
970
1002
  if '/' in key:
971
1003
  p, key = key.rsplit('/', 1)
@@ -974,26 +1006,44 @@ class TreeList:
974
1006
 
975
1007
 
976
1008
  def get_fullargspec(f):
977
- argv = []
978
- defaults = {}
979
- varargs = None
980
- varkwargs = None
981
- kwonlyargs = []
982
- kwonlydefaults = {}
1009
+ """Get the names and default values of a callable object's parameters.
1010
+ If the object is a built-in function, it tries to get argument
1011
+ information from the docstring. If it fails, it returns None.
1012
+
1013
+ Returns:
1014
+ args: a list of the parameter names.
1015
+ varargs: the name of the * parameter or None.
1016
+ varkwargs: the name of the ** parameter or None.
1017
+ defaults: a dict mapping names from args to defaults.
1018
+ kwonlyargs: a list of keyword-only parameter names.
1019
+ kwonlydefaults: a dict mapping names from kwonlyargs to defaults.
1020
+
1021
+ Note:
1022
+ `self` parameter is not reported for bound methods.
1023
+
1024
+ cf. inspect.getfullargspec
1025
+ """
1026
+ argv = [] # <before /> 0:POSITIONAL_ONLY
1027
+ # <before *> 1:POSITIONAL_OR_KEYWORD
1028
+ varargs = None # <*args> 2:VAR_POSITIONAL
1029
+ varkwargs = None # <**kwargs> 4:VAR_KEYWORD
1030
+ defaults = {} #
1031
+ kwonlyargs = [] # <after *> 3:KEYWORD_ONLY
1032
+ kwonlydefaults = {} #
983
1033
  try:
984
1034
  sig = inspect.signature(f)
985
1035
  for k, v in sig.parameters.items():
986
- if v.kind <= 1: # POSITIONAL_ONLY / POSITIONAL_OR_KEYWORD
1036
+ if v.kind in (v.POSITIONAL_ONLY, v.POSITIONAL_OR_KEYWORD):
987
1037
  argv.append(k)
988
1038
  if v.default != v.empty:
989
1039
  defaults[k] = v.default
990
- elif v.kind == 2: # VAR_POSITIONAL (*args)
1040
+ elif v.kind == v.VAR_POSITIONAL:
991
1041
  varargs = k
992
- elif v.kind == 3: # KEYWORD_ONLY
1042
+ elif v.kind == v.KEYWORD_ONLY:
993
1043
  kwonlyargs.append(k)
994
1044
  if v.default != v.empty:
995
1045
  kwonlydefaults[k] = v.default
996
- elif v.kind == 4: # VAR_KEYWORD (**kwargs)
1046
+ elif v.kind == v.VAR_KEYWORD:
997
1047
  varkwargs = k
998
1048
  except ValueError:
999
1049
  ## Builtin functions don't have an argspec that we can get.
@@ -1004,25 +1054,39 @@ def get_fullargspec(f):
1004
1054
  ##
1005
1055
  ## ...(details)...
1006
1056
  ## ```
1007
- docs = [ln for ln in inspect.getdoc(f).splitlines() if ln]
1008
- m = re.search(r"(\w+)\((.*)\)", docs[0])
1009
- if m:
1010
- ## Note: Cannot process args containing commas.
1011
- ## e.g., v='Hello, world"
1012
- name, argspec = m.groups()
1013
- for v in argspec.strip().split(','):
1014
- m = re.search(r"(\w+)", v)
1057
+ doc = inspect.getdoc(f)
1058
+ for word in split_parts(doc or ''): # Search pattern for `func(argspec)`.
1059
+ if word.startswith('('):
1060
+ argspec = word[1:-1]
1061
+ break
1062
+ else:
1063
+ return None # no argument spec information
1064
+ if argspec:
1065
+ argparts = ['']
1066
+ for part in split_parts(argspec): # Separate argument parts with commas.
1067
+ if not part.strip():
1068
+ continue
1069
+ if part != ',':
1070
+ argparts[-1] += part
1071
+ else:
1072
+ argparts.append('')
1073
+ for v in argparts:
1074
+ m = re.match(r"(\w+):?", v) # argv + kwonlyargs
1015
1075
  if m:
1016
1076
  argv.append(m.group(1))
1017
- defaults = dict(re.findall(r"(\w+)\s*=\s*([\w' ]+)", argspec))
1018
- else:
1019
- return None
1077
+ m = re.match(r"(\w+)(?::\w+)?=(.+)", v) # defaults + kwonlydefaults
1078
+ if m:
1079
+ defaults.update([m.groups()])
1080
+ elif v.startswith('**'): # <**kwargs>
1081
+ varkwargs = v[2:]
1082
+ elif v.startswith('*'): # <*args>
1083
+ varargs = v[1:]
1020
1084
  return (argv, varargs, varkwargs,
1021
1085
  defaults, kwonlyargs, kwonlydefaults)
1022
1086
 
1023
1087
 
1024
1088
  def funcall(f, *args, doc=None, alias=None, **kwargs):
1025
- """Decorator of event handler
1089
+ """Decorator of event handler.
1026
1090
 
1027
1091
  Check if the event argument can be omitted and if any other
1028
1092
  required arguments are specified in args and kwargs.
@@ -1033,10 +1097,10 @@ def funcall(f, *args, doc=None, alias=None, **kwargs):
1033
1097
  >>> Act1 = lambda *v,**kw: f(*(v+args), **(kwargs|kw))
1034
1098
  >>> Act2 = lambda *v,**kw: f(*args, **(kwargs|kw))
1035
1099
 
1036
- Returns Act1 (accepts event arguments) if event arguments
1100
+ `Act1` is returned (accepts event arguments) if event arguments
1037
1101
  cannot be omitted or if there are any remaining arguments
1038
1102
  that must be explicitly specified.
1039
- Otherwise, returns Act2 (ignores event arguments).
1103
+ Otherwise, `Act2` is returned (ignores event arguments).
1040
1104
  """
1041
1105
  assert callable(f)
1042
1106
  assert isinstance(doc, (str, type(None)))
@@ -1045,18 +1109,19 @@ def funcall(f, *args, doc=None, alias=None, **kwargs):
1045
1109
  @wraps(f)
1046
1110
  def _Act(*v, **kw):
1047
1111
  kwargs.update(kw)
1048
- return f(*v, *args, **kwargs) # function with event args
1112
+ return f(*v, *args, **kwargs) # function with event args
1049
1113
 
1050
1114
  @wraps(f)
1051
1115
  def _Act2(*v, **kw):
1052
1116
  kwargs.update(kw)
1053
- return f(*args, **kwargs) # function with no explicit args
1117
+ return f(*args, **kwargs) # function with no explicit args
1054
1118
 
1055
1119
  action = _Act
1056
1120
  try:
1057
1121
  (argv, varargs, varkwargs, defaults,
1058
1122
  kwonlyargs, kwonlydefaults) = get_fullargspec(f)
1059
1123
  except Exception:
1124
+ warn(f"Failed to get the signature of {f}.")
1060
1125
  return f
1061
1126
  if not varargs:
1062
1127
  N = len(argv)