wizlib 2.0.18__tar.gz → 3.0.0__tar.gz

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.

Potentially problematic release.


This version of wizlib might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: wizlib
3
- Version: 2.0.18
3
+ Version: 3.0.0
4
4
  Summary: Framework for flexible and powerful command-line applications
5
5
  License: MIT
6
6
  Author: Steampunk Wizard
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "wizlib"
3
- version = "2.0.18"
3
+ version = "3.0.0"
4
4
  description = "Framework for flexible and powerful command-line applications"
5
5
  authors = ["Steampunk Wizard <wizlib@steampunkwizard.ca>"]
6
6
  license = "MIT"
@@ -0,0 +1,98 @@
1
+ import sys
2
+ from dataclasses import dataclass
3
+ import os
4
+ from pathlib import Path
5
+
6
+ from wizlib.class_family import ClassFamily
7
+ from wizlib.command import WizHelpCommand
8
+ from wizlib.super_wrapper import SuperWrapper
9
+ from wizlib.parser import WizParser
10
+ from wizlib.ui import UI
11
+
12
+
13
+ RED = '\033[91m'
14
+ RESET = '\033[0m'
15
+
16
+
17
+ class AppCancellation(BaseException):
18
+ pass
19
+
20
+
21
+ class WizApp:
22
+ """Root of all WizLib-based CLI applications. Subclass it. Can be
23
+ instantiated and then run multiple commands."""
24
+
25
+ base_command = None
26
+ name = ''
27
+
28
+ # Set some default types so linting works
29
+ ui: UI
30
+
31
+ @classmethod
32
+ def main(cls): # pragma: nocover
33
+ """Call this from a __main__ entrypoint"""
34
+ cls.start(*sys.argv[1:], debug=os.getenv('DEBUG'))
35
+
36
+ @classmethod
37
+ def start(cls, *args, debug=False):
38
+ """Call this from a Python entrypoint"""
39
+ try:
40
+ parser = WizParser(prog=cls.name)
41
+ for handler in cls.base_command.handlers:
42
+ handler.add_args(parser)
43
+ ns, more = parser.parse_known_args(args)
44
+ app = cls.initialize(**vars(ns))
45
+ more = more if more else [cls.base_command.default]
46
+ app.run(*more)
47
+ except AppCancellation as cancellation:
48
+ if str(cancellation):
49
+ print(str(cancellation), file=sys.stderr)
50
+ except BaseException as error:
51
+ if debug:
52
+ raise error
53
+ else:
54
+ name = type(error).__name__
55
+ print(f"\n{RED}{name}{': ' if str(error) else ''}" +
56
+ f"{error}{RESET}", file=sys.stderr)
57
+ sys.exit(1)
58
+
59
+ @classmethod
60
+ def initialize(cls, **vals):
61
+ """Converts argparse values (strings) into actual handlers and
62
+ instantiates the app"""
63
+ handlers = {}
64
+ for handler in cls.base_command.handlers:
65
+ val = vals[handler.name] if (
66
+ handler.name in vals) else handler.default
67
+ handlers[handler.name] = handler.setup(val)
68
+ return cls(**handlers)
69
+
70
+ def __init__(self, **handlers):
71
+ for name, handler in handlers.items():
72
+ handler.app = self
73
+ setattr(self, name, handler)
74
+ self.parser = WizParser()
75
+ subparsers = self.parser.add_subparsers(dest='command')
76
+ for command in self.base_command.family_members('name'):
77
+ key = command.get_member_attr('key')
78
+ aliases = [key] if key else []
79
+ subparser = subparsers.add_parser(command.name, aliases=aliases)
80
+ command.add_args(subparser)
81
+
82
+ def run(self, *args):
83
+ vals = vars(self.parser.parse_args(args))
84
+ if 'help' in vals:
85
+ return WizHelpCommand(**vals)
86
+ command_name = vals.pop('command')
87
+ command_class = self.base_command.family_member(
88
+ 'name', command_name)
89
+ if not command_class:
90
+ raise Exception(f"Unknown command {command_name}")
91
+ command = command_class(self, **vals)
92
+ result = command.execute()
93
+ if result:
94
+ print(result, end='')
95
+ if sys.stdout.isatty(): # pragma: nocover
96
+ print()
97
+ if command.status:
98
+ print(command.status, file=sys.stderr)
@@ -48,10 +48,11 @@ class ClassFamily:
48
48
  return hits
49
49
 
50
50
  @classmethod
51
- def family_attrs(self, attribute):
51
+ def family_attrs(self, attribute): # pragma: nocover
52
52
  """
53
53
  Return a set of all the values of a specific attribute that exist in
54
- the family. The set avoids repetition of values in the result.
54
+ the family. The set avoids repetition of values in the result. This is
55
+ a legacy method, not currently tested or used.
55
56
  """
56
57
  # Put myself into a set if I qualify
57
58
  if self.has_member_attrs(attribute):
@@ -3,11 +3,15 @@ from pathlib import Path
3
3
 
4
4
  from wizlib.class_family import ClassFamily
5
5
  from wizlib.config_handler import ConfigHandler
6
- from wizlib.input_handler import InputHandler
6
+ from wizlib.stream_handler import StreamHandler
7
7
  from wizlib.super_wrapper import SuperWrapper
8
8
  from wizlib.ui import UI
9
9
 
10
10
 
11
+ class CommandCancellation(BaseException):
12
+ pass
13
+
14
+
11
15
  class WizCommand(ClassFamily, SuperWrapper):
12
16
  """Define all the args you want, but stdin always works."""
13
17
 
@@ -21,12 +25,10 @@ class WizCommand(ClassFamily, SuperWrapper):
21
25
  Add global arguments in the base class. Not wrapped."""
22
26
  pass
23
27
 
24
- def __init__(self, **vals):
28
+ def __init__(self, app=None, **vals):
29
+ self.app = app
25
30
  for key in vals:
26
31
  setattr(self, key, vals[key])
27
- for handler in self.handlers:
28
- if handler.name not in vals:
29
- setattr(self, handler.name, handler.setup()())
30
32
 
31
33
  def handle_vals(self):
32
34
  """Clean up vals, calculate any, ask through UI, etc. - override
@@ -43,9 +45,12 @@ class WizCommand(ClassFamily, SuperWrapper):
43
45
  def execute(self, method, *args, **kwargs):
44
46
  """Actually perform the command - override and wrap this via
45
47
  SuperWrapper"""
46
- self.handle_vals()
47
- result = method(self, *args, **kwargs)
48
- return result
48
+ try:
49
+ self.handle_vals()
50
+ result = method(self, *args, **kwargs)
51
+ return result
52
+ except CommandCancellation as cancellation:
53
+ self.status = str(cancellation) if str(cancellation) else None
49
54
 
50
55
 
51
56
  class WizHelpCommand(WizCommand):
@@ -9,6 +9,7 @@ from yaml import Loader
9
9
  from wizlib.handler import Handler
10
10
 
11
11
  from wizlib.error import ConfigHandlerError
12
+ from wizlib.parser import WizParser
12
13
 
13
14
 
14
15
  class ConfigHandler(Handler):
@@ -23,8 +24,8 @@ class ConfigHandler(Handler):
23
24
 
24
25
  name = 'config'
25
26
 
26
- def __init__(self, value=None):
27
- self.file = value
27
+ def __init__(self, file=None):
28
+ self.file = file
28
29
  self.cache = {}
29
30
 
30
31
  @property
@@ -34,10 +35,10 @@ class ConfigHandler(Handler):
34
35
  path = None
35
36
  if self.file:
36
37
  path = Path(self.file)
37
- elif self.appname:
38
- localpath = Path.cwd() / f".{self.appname}.yml"
39
- homepath = Path.home() / f".{self.appname}.yml"
40
- if (envvar := self.env(self.appname + '-config')):
38
+ elif self.app and self.app.name:
39
+ localpath = Path.cwd() / f".{self.app.name}.yml"
40
+ homepath = Path.home() / f".{self.app.name}.yml"
41
+ if (envvar := self.env(self.app.name + '-config')):
41
42
  path = Path(envvar)
42
43
  elif (localpath.is_file()):
43
44
  path = localpath
@@ -77,11 +78,6 @@ class ConfigHandler(Handler):
77
78
  @classmethod
78
79
  def fake(cls, **vals):
79
80
  """Return a fake ConfigHandler with forced values, for testing"""
80
- handler = cls()
81
-
82
- def fake_env(name):
83
- key = name.replace('-', '_')
84
- if key in vals:
85
- return vals[key]
86
- handler.env = fake_env
87
- return handler
81
+ self = cls()
82
+ self.cache = {k.replace('_', '-'): vals[k] for k in vals}
83
+ return self
@@ -0,0 +1,24 @@
1
+ from argparse import Action
2
+
3
+ from wizlib.parser import WizParser
4
+
5
+
6
+ class Handler:
7
+ """Base class for handlers"""
8
+
9
+ default = ''
10
+ app = None
11
+
12
+ @classmethod
13
+ def add_args(cls, parser: WizParser):
14
+ parser.add_argument(
15
+ f"--{cls.name}",
16
+ f"-{cls.name[0]}",
17
+ default=cls.default)
18
+
19
+ @classmethod
20
+ def setup(cls, val):
21
+ return cls(val)
22
+
23
+ def __init__(self, val=None):
24
+ pass
@@ -2,13 +2,14 @@ from pathlib import Path
2
2
  import sys
3
3
 
4
4
  from wizlib.handler import Handler
5
+ from wizlib.parser import WizParser
5
6
 
6
7
  INTERACTIVE = sys.stdin.isatty()
7
8
 
8
9
 
9
- class InputHandler(Handler):
10
+ class StreamHandler(Handler):
10
11
 
11
- name = 'input'
12
+ name = 'stream'
12
13
  text: str = ''
13
14
 
14
15
  def __init__(self, file=None, stdin=True):
@@ -22,7 +23,7 @@ class InputHandler(Handler):
22
23
 
23
24
  @classmethod
24
25
  def fake(cls, value):
25
- """Return a fake InputHandler with forced values, for testing"""
26
+ """Return a fake StreamHandler with forced values, for testing"""
26
27
  handler = cls(stdin=False)
27
28
  handler.text = value
28
29
  return handler
@@ -37,6 +37,14 @@ from tempfile import NamedTemporaryFile
37
37
  from wizlib.class_family import ClassFamily
38
38
 
39
39
 
40
+ class Emphasis(Enum):
41
+ """Semantic style"""
42
+ INFO = 1
43
+ GENERAL = 2
44
+ PRINCIPAL = 3
45
+ ERROR = 4
46
+
47
+
40
48
  @dataclass
41
49
  class Choice:
42
50
  """A single option, equivalent to a single radio button or select option in
@@ -129,71 +137,9 @@ class Chooser(Prompt):
129
137
  def choices_by_text(self, text):
130
138
  return [o.value for o in self.choices if o.hit_text(text)]
131
139
 
132
- # def choice_by_word(self, word):
133
- # chosenlist = [o.value for o in self.choices if o.hit_word(word)]
134
- # return self._choose(chosenlist)
135
-
136
- # def _choose(self, chosenlist):
137
- # choice = next(iter(chosenlist), None)
138
- # if callable(choice):
139
- # return choice()
140
- # else:
141
- # return choice
142
-
143
140
 
144
141
  @dataclass
145
142
  class UI(ClassFamily):
146
- pass
147
-
148
- # handler: object
149
-
150
- # # TODO: Make start an abstract method
151
- # def start(self):
152
- # pass
153
-
154
- # # TODO: Make get_option_mini an abstract method
155
- # def get_option_mini(self, chooser: Chooser):
156
- # """Get an option from a list from the user"""
157
- # pass
158
-
159
- # # TODO: Make get_string an abstract method
160
- # def get_string(self, intro, default=""):
161
- # """Get a string value from the user"""
162
- # pass
163
-
164
-
165
- # # Terminal UIs include Shell and Curses, since both will rely on a
166
- # # terminal-based editor for the e.g. Manage command.
167
-
168
-
169
- # class TerminalUI(UI):
170
-
171
- # # A convenience method to get a full textual prompt for a string input
172
-
173
- # def full_prompt(self, prompt, default=None):
174
- # if default:
175
- # return f'{prompt} [{default}]: '
176
- # else:
177
- # return f'{prompt}: '
178
-
179
- # # @staticmethod
180
- # # def edit_text(text):
181
- # # commands = [['sensible-editor'], ['open', '-W']]
182
- # # with NamedTemporaryFile(mode="w+") as tempfile:
183
- # # tempfile.write(text)
184
- # # tempfile.seek(0)
185
- # # command = [os.environ.get('EDITOR')]
186
- # # if not command[0] or not shutil.which(command[0]):
187
- # # iterator = (c for c in commands if shutil.which(c[0]))
188
- # # command = next(filter(None, iterator), None)
189
- # # if not command:
190
- # # raise RuntimeError(
191
- # # "A text editor at the $EDITOR " +
192
- # # "environment variable is required")
193
- # # subprocess.run(command + [tempfile.name])
194
- # # tempfile.seek(0)
195
- # # return tempfile.read()
196
-
197
143
 
198
- # class UserCancelError(Exception):
199
- # pass
144
+ def send(self, value: str = '', emphasis: Emphasis = Emphasis.GENERAL):
145
+ pass
@@ -0,0 +1,40 @@
1
+ # We use an odd misc of nonprintable ASCII, ANSI escape sequences, with some
2
+ # custom preferences. So define them all here.
3
+
4
+
5
+ from enum import StrEnum
6
+
7
+
8
+ def sequence(hexes: str) -> str:
9
+ return bytes.fromhex(hexes).decode()
10
+
11
+
12
+ ESC = sequence("1b")
13
+
14
+ # TODO: Is any of this available in the standard library?
15
+
16
+
17
+ class S(StrEnum):
18
+ LEFT = sequence("1b5b44")
19
+ RIGHT = sequence("1b5b43")
20
+ BACKSPACE = sequence("7f")
21
+ BEGINNING = sequence("01")
22
+ END = sequence("05")
23
+ RETURN = sequence("0a")
24
+ TAB = sequence("09")
25
+ SHIFT_TAB = sequence("1b5b5a")
26
+ LEFT_WORD = sequence("1b62")
27
+ RIGHT_WORD = sequence("1b66")
28
+ KILL = sequence("0b")
29
+ RESET = ESC + "[0m"
30
+ FAINT = ESC + "[2m"
31
+ BOLD = ESC + "[1m"
32
+ SEPARATORS = ' -_.,'
33
+ SPACE = ' '
34
+ RED = ESC + '[31m'
35
+ GREEN = ESC + '[32m'
36
+ YELLOW = ESC + '[33m'
37
+ BLUE = ESC + '[34m'
38
+ MAGENTA = ESC + '[35m'
39
+ CYAN = ESC + '[36m'
40
+ CLEAR = ESC + '[2J'
@@ -4,6 +4,8 @@ import re
4
4
 
5
5
  from readchar import readkey
6
6
 
7
+ from wizlib.ui.shell import S
8
+
7
9
 
8
10
  if (sys.platform == "win32"):
9
11
  import ctypes
@@ -32,8 +34,8 @@ def cursorPos(): # pragma: nocover
32
34
  termios.tcsetattr(sys.stdin, termios.TCSAFLUSH, _)
33
35
  try:
34
36
  _ = ""
35
- sys.stdout.write("\x1b[6n")
36
- sys.stdout.flush()
37
+ sys.stdouS.write("\x1b[6n")
38
+ sys.stdouS.flush()
37
39
  while not (_ := _ + sys.stdin.read(1)).endswith('R'):
38
40
  True
39
41
  res = re.match(r".*\[(?P<y>\d*);(?P<x>\d*)R", _)
@@ -48,37 +50,13 @@ def cursorPos(): # pragma: nocover
48
50
  return (-1, -1)
49
51
 
50
52
 
51
- def sequence(hexes: str) -> str:
52
- return bytes.fromhex(hexes).decode()
53
-
54
-
55
- LEFT = sequence("1b5b44")
56
- RIGHT = sequence("1b5b43")
57
- BACKSPACE = sequence("7f")
58
- BEGINNING = sequence("01")
59
- END = sequence("05")
60
- RETURN = sequence("0a")
61
- ESC = sequence("1b")
62
- TAB = sequence("09")
63
- SHIFT_TAB = sequence("1b5b5a")
64
- LEFT_WORD = sequence("1b62")
65
- RIGHT_WORD = sequence("1b66")
66
- KILL = sequence("0b")
67
-
68
- RESET = ESC + "[0m"
69
- FAINT = ESC + "[2m"
70
- BOLD = ESC + "[1m"
71
-
72
- WORD_SEPARATORS = ' -_.,'
73
-
74
-
75
53
  def write(key):
76
54
  sys.stderr.write(key)
77
55
  sys.stderr.flush()
78
56
 
79
57
 
80
58
  # "Fill" refers to the lighter text in the editor, to the right of (or instead
81
- # of) user-typed text.
59
+ # of) user-typed texS.
82
60
  #
83
61
  # States:
84
62
  # - USER: User has typed something, so no fill
@@ -108,63 +86,63 @@ class ShellLineEditor: # pragma: nocover
108
86
  self.fillstate = FillState.DEFAULT
109
87
 
110
88
  def edit(self):
111
- write(RESET)
89
+ write(S.RESET)
112
90
  while True:
113
91
  self.write_fill()
114
92
  key = readkey()
115
93
  self.clear_fill()
116
- if key == RETURN:
94
+ if key == S.RETURN:
117
95
  break
118
96
  if key.isprintable():
119
- write(BOLD + key + self.buf[self.pos:] +
120
- ('\b' * (len(self.buf) - self.pos)) + RESET)
97
+ write(S.BOLD + key + self.buf[self.pos:] +
98
+ ('\b' * (len(self.buf) - self.pos)) + S.RESET)
121
99
  self.buf = self.buf[:self.pos] + key + self.buf[self.pos:]
122
100
  self.pos += 1
123
101
  self.fillstate = FillState.USER
124
- elif (key in [BACKSPACE, KILL]) and self.has_fill:
102
+ elif (key in [S.BACKSPACE, S.KILL]) and self.has_fill:
125
103
  # Backspace clears the fill
126
104
  self.fillstate = FillState.BLANK
127
- elif (key == BACKSPACE) and (self.pos > 0):
128
- write(BOLD + '\b' + self.buf[self.pos:] + ' ' +
129
- ('\b' * (1 + len(self.buf) - self.pos)) + RESET)
105
+ elif (key == S.BACKSPACE) and (self.pos > 0):
106
+ write(S.BOLD + '\b' + self.buf[self.pos:] + ' ' +
107
+ ('\b' * (1 + len(self.buf) - self.pos)) + S.RESET)
130
108
  self.buf = self.buf[:self.pos-1] + self.buf[self.pos:]
131
109
  self.pos -= 1
132
110
  self.fillstate = FillState.USER if (
133
111
  self.pos > 0) else FillState.DEFAULT
134
- elif key == LEFT and self.pos > 0:
112
+ elif key == S.LEFT and self.pos > 0:
135
113
  self.move_left()
136
114
  self.fillstate = FillState.USER
137
- elif key == RIGHT and self.pos < len(self.buf):
115
+ elif key == S.RIGHT and self.pos < len(self.buf):
138
116
  self.move_right()
139
117
  self.fillstate = FillState.USER
140
- elif key == BEGINNING and self.pos > 0:
118
+ elif key == S.BEGINNING and self.pos > 0:
141
119
  self.move_beginning()
142
120
  self.fillstate = FillState.USER
143
- elif key == END and self.has_fill:
121
+ elif key == S.END and self.has_fill:
144
122
  self.accept_fill()
145
123
  self.fillstate = FillState.USER
146
- elif key == END and self.pos < len(self.buf):
124
+ elif key == S.END and self.pos < len(self.buf):
147
125
  self.move_end_buf()
148
126
  self.fillstate = FillState.USER
149
- elif key == TAB and (choices := self.valid_choices):
127
+ elif key == S.TAB and (choices := self.valid_choices):
150
128
  self.index = (self.index + 1) % len(choices)
151
129
  self.fillstate = FillState.TAB
152
- elif key == SHIFT_TAB and self.index > -1:
130
+ elif key == S.SHIFT_TAB and self.index > -1:
153
131
  self.index = (self.index - 1) % len(self.valid_choices)
154
132
  self.fillstate = FillState.TAB
155
- elif key == LEFT_WORD and self.pos > 0:
133
+ elif key == S.LEFT_WORD and self.pos > 0:
156
134
  while (self.pos > 0) and self.is_sep(self.pos - 1):
157
135
  self.move_left()
158
136
  while (self.pos > 0) and not self.is_sep(self.pos - 1):
159
137
  self.move_left()
160
138
  self.fillstate = FillState.USER
161
- elif key == RIGHT_WORD and self.pos < len(self.buf):
139
+ elif key == S.RIGHT_WORD and self.pos < len(self.buf):
162
140
  while (self.pos < len(self.buf)) and self.is_sep(self.pos):
163
141
  self.move_right()
164
142
  while (self.pos < len(self.buf)) and not self.is_sep(self.pos):
165
143
  self.move_right()
166
144
  self.fillstate = FillState.USER
167
- elif key == KILL:
145
+ elif key == S.KILL:
168
146
  chars = len(self.buf) - self.pos
169
147
  write(' ' * chars + '\b' * chars)
170
148
  self.buf = self.buf[:self.pos]
@@ -172,11 +150,11 @@ class ShellLineEditor: # pragma: nocover
172
150
  else:
173
151
  pass
174
152
  self.accept_fill()
175
- write(RETURN)
153
+ write(S.RETURN)
176
154
  return self.buf
177
155
 
178
156
  def is_sep(self, pos):
179
- return (self.buf[pos] in WORD_SEPARATORS)
157
+ return (self.buf[pos] in S.SEPARATORS)
180
158
 
181
159
  @property
182
160
  def has_fill(self):
@@ -184,16 +162,16 @@ class ShellLineEditor: # pragma: nocover
184
162
 
185
163
  def accept_fill(self):
186
164
  if self.has_fill:
187
- write(BOLD + self.fill + RESET)
165
+ write(S.BOLD + self.fill + S.RESET)
188
166
  self.buf += self.fill
189
167
  self.pos = len(self.buf)
190
168
 
191
169
  def move_left(self):
192
- write(LEFT)
170
+ write(S.LEFT)
193
171
  self.pos -= 1
194
172
 
195
173
  def move_right(self):
196
- write(RIGHT)
174
+ write(S.RIGHT)
197
175
  self.pos += 1
198
176
 
199
177
  def move_beginning(self):
@@ -207,7 +185,7 @@ class ShellLineEditor: # pragma: nocover
207
185
  def write_fill(self):
208
186
  if self.fillstate in [FillState.DEFAULT, FillState.TAB]:
209
187
  self.move_end_buf()
210
- write(FAINT + self.fill + '\b' * len(self.fill) + RESET)
188
+ write(S.FAINT + self.fill + '\b' * len(self.fill) + S.RESET)
211
189
 
212
190
  def clear_fill(self):
213
191
  if self.has_fill:
@@ -227,7 +205,7 @@ class ShellLineEditor: # pragma: nocover
227
205
  @property
228
206
  def last_word(self):
229
207
  index = next((c for c in reversed(range(len(self.buf)))
230
- if self.buf[c] in WORD_SEPARATORS), -1)
208
+ if self.buf[c] == S.SPACE), -1)
231
209
  return self.buf[index+1:]
232
210
 
233
211
  @property
@@ -0,0 +1,67 @@
1
+ from enum import StrEnum
2
+ import sys
3
+
4
+ from readchar import readkey
5
+
6
+ from wizlib.rlinput import rlinput
7
+ from wizlib.ui import UI, Chooser, Emphasis
8
+ from wizlib.ui.shell.line_editor import ShellLineEditor
9
+ from wizlib.ui.shell import S
10
+
11
+ INTERACTIVE = sys.stdin.isatty()
12
+
13
+ COLOR = {
14
+ Emphasis.INFO: S.BLUE,
15
+ Emphasis.GENERAL: S.CYAN,
16
+ Emphasis.PRINCIPAL: S.YELLOW,
17
+ Emphasis.ERROR: S.RED
18
+ }
19
+
20
+
21
+ class ShellUI(UI):
22
+
23
+ """The UI to execute one command passed in through the shell. There will be
24
+ limited interactivity, if the user omits an argument on the command line,
25
+ but otherwise this is a run and done situation.
26
+ """
27
+
28
+ name = "shell"
29
+
30
+ def send(self, value: str = '', emphasis: Emphasis = Emphasis.GENERAL):
31
+ """Output some text"""
32
+ sys.stderr.write(COLOR[emphasis] + value + S.RESET + '\n')
33
+ sys.stderr.flush()
34
+
35
+ def ask(self, value: str):
36
+ """Prompt for input"""
37
+ if value:
38
+ sys.stderr.write(S.GREEN + value + S.RESET)
39
+ sys.stderr.flush()
40
+
41
+ def get_option(self, chooser: Chooser):
42
+ """Get a choice from the user with a single keystroke"""
43
+ while True:
44
+ self.ask(chooser.prompt_string)
45
+ if INTERACTIVE:
46
+ key = readkey()
47
+ else:
48
+ key = sys.stdin.read(1)
49
+ out = chooser.default if key == '\n' else \
50
+ key if key.isprintable() else ''
51
+ choice = chooser.choice_by_key(key)
52
+ emphasis = Emphasis.ERROR if (
53
+ choice is None) else Emphasis.PRINCIPAL
54
+ self.send(out, emphasis=emphasis)
55
+ if choice is not None:
56
+ break
57
+ return choice
58
+
59
+ def get_text(self, prompt='', choices=[], default=''):
60
+ """Allow the user to input an arbitrary line of text, with possible tab
61
+ completion"""
62
+ self.ask(prompt)
63
+ if INTERACTIVE:
64
+ value = ShellLineEditor(choices, default).edit()
65
+ else:
66
+ value = input()
67
+ return value
@@ -15,9 +15,10 @@ class UIHandler(Handler):
15
15
  default = 'shell'
16
16
 
17
17
  @classmethod
18
- def setup(cls, name=None):
19
- def ui(uitype='shell'):
20
- """Instead of instantiating the handler, return a new instance of
21
- the chosen UI object."""
22
- return UI.family_member('name', uitype)()
23
- return ui
18
+ def setup(cls, uitype):
19
+ return UI.family_member('name', uitype)()
20
+
21
+ @classmethod
22
+ def fake(cls, val=None):
23
+ """Use mock_keys"""
24
+ return
@@ -1,7 +1,7 @@
1
1
  from unittest.mock import Mock, patch
2
2
 
3
3
 
4
- RK = 'wizlib.ui.shell_line_editor.readkey'
4
+ RK = 'wizlib.ui.shell.line_editor.readkey'
5
5
 
6
6
 
7
7
  def mock_keys(keys: str):
@@ -1,88 +0,0 @@
1
- import sys
2
- from dataclasses import dataclass
3
- import os
4
- from pathlib import Path
5
-
6
- from wizlib.class_family import ClassFamily
7
- from wizlib.command import WizHelpCommand
8
- from wizlib.super_wrapper import SuperWrapper
9
- from wizlib.parser import WizParser
10
- from wizlib.ui import UI
11
- from wizlib.ui.shell_ui import ShellUI
12
-
13
-
14
- RED = '\033[91m'
15
- RESET = '\033[0m'
16
-
17
-
18
- class WizApp:
19
- """Root of all WizLib-based CLI applications. Subclass it. Can be
20
- instantiated and then run multiple commands."""
21
-
22
- base_command = None
23
- name = ''
24
-
25
- @classmethod
26
- def main(cls): # pragma: nocover
27
- """Call this from a __main__ entrypoint"""
28
- cls.run(*sys.argv[1:], debug=os.getenv('DEBUG'))
29
-
30
- @classmethod
31
- def run(cls, *args, debug=False):
32
- """Call this from a Python entrypoint"""
33
- try:
34
- cls.initialize()
35
- app = cls(*args)
36
- command = app.first_command
37
- result = command.execute()
38
- if result:
39
- print(result, file=sys.stdout, end='')
40
- if sys.stdout.isatty(): # pragma: nocover
41
- print()
42
- if command.status:
43
- print(command.status, file=sys.stderr)
44
- except Exception as error:
45
- if debug:
46
- raise error
47
- else:
48
- print(f"\n{RED}{type(error).__name__}: " +
49
- f"{error}{RESET}\n", file=sys.stderr)
50
- sys.exit(1)
51
-
52
- @classmethod
53
- def initialize(cls):
54
- """Set up the app class to parse arguments"""
55
- cls.parser = WizParser(
56
- prog=cls.name,
57
- exit_on_error=False)
58
- for handler in cls.base_command.handlers:
59
- cls.parser.add_argument(
60
- f"--{handler.name}",
61
- f"-{handler.name[0]}",
62
- **handler.option_properties(cls))
63
-
64
- subparsers = cls.parser.add_subparsers(dest='command')
65
- for command in cls.base_command.family_members('name'):
66
- key = command.get_member_attr('key')
67
- aliases = [key] if key else []
68
- subparser = subparsers.add_parser(command.name, aliases=aliases)
69
- command.add_args(subparser)
70
-
71
- def __init__(self, *args):
72
- args = args if args else [self.base_command.default]
73
- if not hasattr(self, 'parser'):
74
- self.__class__.initialize()
75
- self.vals = vars(self.parser.parse_args(args))
76
- self.first_command = self.get_command(**self.vals)
77
-
78
- def get_command(self, **vals):
79
- """Returns a single command"""
80
- if 'help' in vals:
81
- return WizHelpCommand(**vals)
82
- else:
83
- command_name = vals.pop('command')
84
- command_class = self.base_command.family_member(
85
- 'name', command_name)
86
- if not command_class:
87
- raise Exception(f"Unknown command {command_name}")
88
- return command_class(**vals)
@@ -1,25 +0,0 @@
1
- from argparse import Action
2
-
3
-
4
- class Handler:
5
- """Base class for handlers"""
6
-
7
- appname = None
8
- default = ''
9
-
10
- @classmethod
11
- def option_properties(cls, app):
12
- """Argparse keyword arguments for this optional arg"""
13
- return {
14
- 'type': cls.setup(app.name),
15
- 'default': cls.default
16
- }
17
-
18
- @classmethod
19
- def setup(cls, name=None):
20
- """Return a callable that returns an instance of the handler object
21
- that's referenced by commands. In the default case, an instance of the
22
- handler class itself that knows the name of the app."""
23
- class NamedHandler(cls):
24
- appname = name
25
- return NamedHandler
@@ -1,42 +0,0 @@
1
- import sys
2
-
3
- from readchar import readkey
4
- from wizlib.rlinput import rlinput
5
- from wizlib.ui import UI
6
- from wizlib.ui.shell_line_editor import ShellLineEditor
7
-
8
- INTERACTIVE = sys.stdin.isatty()
9
-
10
-
11
- class ShellUI(UI):
12
-
13
- """The UI to execute one command passed in through the shell. There will be
14
- limited interactivity, if the user omits an argument on the command line,
15
- but otherwise this is a run and done situation.
16
- """
17
-
18
- name = "shell"
19
-
20
- def send(self, value: str):
21
- """Output some text"""
22
- if value:
23
- print(value, file=sys.stderr)
24
-
25
- def get_option_mini(self, chooser):
26
- """Get a choice from the user with a single keystroke"""
27
- # key = rlinput(chooser.prompt_string)
28
- print(chooser.prompt_string, end='', file=sys.stderr, flush=True)
29
- if INTERACTIVE:
30
- key = readkey()
31
- else:
32
- key = sys.stdin.read(1)
33
- print(file=sys.stderr, flush=True)
34
- return chooser.choice_by_key(key)
35
-
36
- def get_text(self, prompt='', choices=[], default=''):
37
- """Allow the user to input an arbitrary line of text, with possible tab
38
- completion"""
39
- sys.stderr.write(prompt)
40
- sys.stderr.flush()
41
- value = ShellLineEditor(choices, default).edit()
42
- return value
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes