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