wizlib 3.2.0__py3-none-any.whl → 3.4.0__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.
wizlib/config_handler.py CHANGED
@@ -1,13 +1,16 @@
1
1
  from argparse import Namespace
2
+ from functools import cached_property
2
3
  from pathlib import Path
3
4
  import os
4
5
  from dataclasses import dataclass
6
+ import re
7
+ import shlex
8
+ import subprocess
5
9
  from unittest.mock import patch
6
10
 
7
- from yaml import load
8
- from yaml import Loader
9
- from wizlib.handler import Handler
11
+ import yaml
10
12
 
13
+ from wizlib.handler import Handler
11
14
  from wizlib.error import ConfigHandlerError
12
15
  from wizlib.parser import WizParser
13
16
 
@@ -24,14 +27,33 @@ class ConfigHandler(Handler):
24
27
 
25
28
  name = 'config'
26
29
 
27
- def __init__(self, file=None):
30
+ @classmethod
31
+ def setup(cls, val):
32
+ """Allow for alternative setup, passing in an injected data value as a
33
+ dict, bypassing file loading, for testing. Possible long-term
34
+ alternative to .fake() below."""
35
+
36
+ if isinstance(val, str) or isinstance(val, Path):
37
+ return cls(file=val)
38
+ elif isinstance(val, dict) or isinstance(val, list):
39
+ return cls(data=val)
40
+
41
+ def __init__(self, file: str = None, data: dict = None):
42
+ """Initiatlize with either a yaml file path or a data block to inject
43
+ (for testing)"""
28
44
  self.file = file
45
+ self.injected_data = data
29
46
  self.cache = {}
30
47
 
31
- @property
32
- def yaml(self):
33
- if hasattr(self, '_yaml'):
34
- return self._yaml
48
+ @cached_property
49
+ def data(self):
50
+ """Returns the full set of configuration data, typically loaded from a
51
+ yaml file. Is cached in code."""
52
+
53
+ # If yaml_dict was provided, use it directly
54
+ if self.injected_data is not None:
55
+ return self.injected_data
56
+
35
57
  path = None
36
58
  if self.file:
37
59
  path = Path(self.file)
@@ -46,34 +68,41 @@ class ConfigHandler(Handler):
46
68
  path = homepath
47
69
  if path:
48
70
  with open(path) as file:
49
- self._yaml = load(file, Loader=Loader)
50
- return self._yaml
71
+ data = yaml.safe_load(file)
72
+ return data
51
73
 
52
74
  @staticmethod
53
75
  def env(name):
54
76
  if (envvar := name.upper().replace('-', '_')) in os.environ:
55
77
  return os.environ[envvar]
56
78
 
57
- def get(self, key: str):
58
- """Return the value for the requested config entry"""
79
+ def get(self, data_path: str):
80
+ """Return the value for the requested config entry. If the value is a
81
+ string, evaluate it for shell-type expressions using $(...) syntax. Can
82
+ also return a dict or array. Note that the value returned is cached
83
+ against the data_path, so future calls may not address nested paths.
84
+
85
+ data_path: Hyphen-separated path through the yaml/dict hierarchy."""
59
86
 
60
87
  # If we already found the value, return it
61
- if key in self.cache:
62
- return self.cache[key]
88
+ if data_path in self.cache:
89
+ return self.cache[data_path]
63
90
 
64
91
  # Environment variables take precedence
65
- if (result := self.env(key)):
66
- self.cache[key] = result
92
+ if (result := self.env(data_path)):
93
+ self.cache[data_path] = result
67
94
  return result
68
95
 
69
- # Otherwise look at the YAML
70
- if (yaml := self.yaml):
71
- split = key.split('-')
72
- while (val := split.pop(0)) and (val in yaml):
73
- yaml = yaml[val] if val in yaml else None
96
+ # Otherwise look at the YAML or injected data
97
+ if (data := self.data):
98
+ split = data_path.split('-')
99
+ while (key := split.pop(0)) and (key in data):
100
+ data = data[key] if key in data else None
74
101
  if not split:
75
- self.cache[key] = yaml
76
- return yaml
102
+ if isinstance(data, str):
103
+ data = evaluate_string(data)
104
+ self.cache[data_path] = data
105
+ return data
77
106
 
78
107
  @classmethod
79
108
  def fake(cls, **vals):
@@ -81,3 +110,17 @@ class ConfigHandler(Handler):
81
110
  self = cls()
82
111
  self.cache = {k.replace('_', '-'): vals[k] for k in vals}
83
112
  return self
113
+
114
+
115
+ def os_process(match):
116
+ """Run a subprocess in shell form"""
117
+ command_string = match.group(1).strip()
118
+ command = shlex.split(command_string)
119
+ result = subprocess.run(command, capture_output=True)
120
+ return result.stdout.decode().strip()
121
+
122
+
123
+ def evaluate_string(yaml: str) -> str:
124
+ """Evaluate shell commands in string values"""
125
+ text = yaml.strip()
126
+ return re.sub(r'\$\((.*?)\)', os_process, text)
wizlib/io.py CHANGED
@@ -6,22 +6,32 @@ import sys
6
6
  import readchar
7
7
 
8
8
  from wizlib.parser import WizArgumentError
9
+ from wizlib.ui.shell import ESC
9
10
 
10
11
 
11
- ISATTY = all(s.isatty() for s in (sys.stdin, sys.stdout, sys.stderr))
12
+ # Is it OK to read tty input? Patch this for testing
12
13
 
14
+ TTY_OK = True
13
15
 
14
- def isatty():
15
- return ISATTY
16
+ # ISATTY = all(s.isatty() for s in (sys.stdin, sys.stdout, sys.stderr))
16
17
 
17
18
 
18
- def stream():
19
- return '' if ISATTY else sys.stdin.read()
19
+ # def isatty():
20
+ # return ISATTY
20
21
 
21
22
 
22
- def ttyin():
23
- if ISATTY:
24
- return readchar.readkey()
23
+ # def stream():
24
+ # return '' if ISATTY else sys.stdin.read()
25
+
26
+
27
+ def ttyin(): # pragma: nocover
28
+ """Read a character from the tty (via readchar). Patch this for testing."""
29
+ if TTY_OK:
30
+ key = readchar.readkey()
31
+ # Handle specialized escape sequences
32
+ if key == ESC + '[1;':
33
+ key = key + readchar.readkey() + readchar.readkey()
34
+ return key
25
35
  else:
26
36
  raise WizArgumentError(
27
37
  'Command designed for interactive use')
wizlib/stream_handler.py CHANGED
@@ -1,6 +1,4 @@
1
- # In a non-interactive, non-testing mode, route input from stdin to the output.
2
- # When testing, read from the object provided (probably a StreamIO)
3
-
1
+ import os
4
2
  from pathlib import Path
5
3
  import sys
6
4
 
@@ -10,33 +8,29 @@ import wizlib.io
10
8
 
11
9
 
12
10
  class StreamHandler(Handler):
13
- """Handle non-interactive input, such as via a pipe in a shell. Only runs
14
- when not in a tty."""
11
+ """Handle non-interactive input, such as via a pipe in a shell. Only
12
+ applies when not in a tty. Doesn't actually stream anything and should
13
+ probably be called PipedInputHandler in a future major upgrade."""
15
14
 
16
15
  name = 'stream'
17
- text: str = ''
18
16
 
19
17
  def __init__(self, file=None):
20
- self.file = file
21
-
22
- def read(self):
23
- """Play like a file, sorta"""
24
- if self.file:
25
- return Path(self.file).read_text()
18
+ if file:
19
+ self._text = Path(file).read_text()
20
+ elif sys.stdin.isatty():
21
+ self._text = ''
26
22
  else:
27
- return wizlib.io.stream()
23
+ self._text = sys.stdin.read()
24
+ # Reset sys.stdin to tty for possible interactions
25
+ # if os.path.exists(os.ctermid()):
26
+ try:
27
+ sys.stdin = open(os.ctermid(), 'r')
28
+ except OSError:
29
+ pass
28
30
 
29
31
  @property
30
32
  def text(self):
31
- """Legacy approach - deprecated"""
32
- return self.read()
33
-
34
- # def __str__(self):
35
- # return self.text
36
-
37
- # @classmethod
38
- # def fake(cls, value):
39
- # """Return a fake StreamHandler with forced values, for testing"""
40
- # handler = cls(stdin=False)
41
- # handler.text = value
42
- # return handler
33
+ return self._text
34
+
35
+ def read(self):
36
+ return self.text
wizlib/test_case.py CHANGED
@@ -9,7 +9,8 @@ class WizLibTestCase(TestCase):
9
9
  """Wrap your test cases in this class to use the patches correctly"""
10
10
 
11
11
  def setUp(self):
12
- self.notty = patch('wizlib.io.isatty', Mock(return_value=False))
12
+ """Test cases should never use true interaction"""
13
+ self.notty = patch('wizlib.io.TTY_OK', False)
13
14
  self.notty.start()
14
15
 
15
16
  def tearDown(self):
@@ -18,13 +19,16 @@ class WizLibTestCase(TestCase):
18
19
  @staticmethod
19
20
  def patch_stream(val: str):
20
21
  """Patch stream input such as pipes for stream handler"""
21
- mock = Mock(return_value=val)
22
- return patch('wizlib.io.stream', mock)
22
+ # mock = Mock(return_value=val)
23
+ # return patch('wizlib.io.stream', mock)
24
+ # return patch('wizlib.stream_handler.StreamHandler.read', mock)
25
+ return patch('wizlib.stream_handler.StreamHandler.text', val)
23
26
 
24
27
  @staticmethod
25
- def patch_ttyin(val: str):
28
+ def patch_ttyin(val:str='\n'):
26
29
  """Patch input typed by a user in shell ui"""
27
30
  mock = Mock(side_effect=val)
31
+ # mock = Mock(return_value = val)
28
32
  return patch('wizlib.io.ttyin', mock)
29
33
 
30
34
  @staticmethod
@@ -38,3 +38,9 @@ class S(StrEnum):
38
38
  MAGENTA = ESC + '[35m'
39
39
  CYAN = ESC + '[36m'
40
40
  CLEAR = ESC + '[2J'
41
+
42
+ # Alternative keys that can be configured in a terminal emulator
43
+ CUSTOM_END = ESC + '[1;5C'
44
+ CUSTOM_BEGINNING = ESC + '[1;5D'
45
+ CUSTOM_RIGHTWORD = ESC + '[1;3C'
46
+ CUSTOM_LEFTWORD = ESC + '[1;3D'
@@ -5,47 +5,47 @@ import re
5
5
  from wizlib.ui.shell import S
6
6
  import wizlib.io
7
7
 
8
- if (sys.platform == "win32"):
9
- import ctypes
10
- from ctypes import wintypes
11
- else:
12
- import termios
13
-
14
- # https://stackoverflow.com/questions/35526014/how-can-i-get-the-cursors-position-in-an-ansi-terminal
15
-
16
-
17
- def cursorPos(): # pragma: nocover
18
- if (sys.platform == "win32"):
19
- OldStdinMode = ctypes.wintypes.DWORD()
20
- OldStdoutMode = ctypes.wintypes.DWORD()
21
- kernel32 = ctypes.windll.kernel32
22
- kernel32.GetConsoleMode(
23
- kernel32.GetStdHandle(-10), ctypes.byref(OldStdinMode))
24
- kernel32.SetConsoleMode(kernel32.GetStdHandle(-10), 0)
25
- kernel32.GetConsoleMode(
26
- kernel32.GetStdHandle(-11), ctypes.byref(OldStdoutMode))
27
- kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
28
- else:
29
- OldStdinMode = termios.tcgetattr(sys.stdin)
30
- _ = termios.tcgetattr(sys.stdin)
31
- _[3] = _[3] & ~(termios.ECHO | termios.ICANON)
32
- termios.tcsetattr(sys.stdin, termios.TCSAFLUSH, _)
33
- try:
34
- _ = ""
35
- sys.stdouS.write("\x1b[6n")
36
- sys.stdouS.flush()
37
- while not (_ := _ + sys.stdin.read(1)).endswith('R'):
38
- True
39
- res = re.match(r".*\[(?P<y>\d*);(?P<x>\d*)R", _)
40
- finally:
41
- if (sys.platform == "win32"):
42
- kernel32.SetConsoleMode(kernel32.GetStdHandle(-10), OldStdinMode)
43
- kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), OldStdoutMode)
44
- else:
45
- termios.tcsetattr(sys.stdin, termios.TCSAFLUSH, OldStdinMode)
46
- if (res):
47
- return (res.group("x"), res.group("y"))
48
- return (-1, -1)
8
+ # if (sys.platform == "win32"):
9
+ # import ctypes
10
+ # from ctypes import wintypes
11
+ # else:
12
+ # import termios
13
+
14
+ # # https://stackoverflow.com/questions/35526014/how-can-i-get-the-cursors-position-in-an-ansi-terminal
15
+
16
+
17
+ # def cursorPos(): # pragma: nocover
18
+ # if (sys.platform == "win32"):
19
+ # OldStdinMode = ctypes.wintypes.DWORD()
20
+ # OldStdoutMode = ctypes.wintypes.DWORD()
21
+ # kernel32 = ctypes.windll.kernel32
22
+ # kernel32.GetConsoleMode(
23
+ # kernel32.GetStdHandle(-10), ctypes.byref(OldStdinMode))
24
+ # kernel32.SetConsoleMode(kernel32.GetStdHandle(-10), 0)
25
+ # kernel32.GetConsoleMode(
26
+ # kernel32.GetStdHandle(-11), ctypes.byref(OldStdoutMode))
27
+ # kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
28
+ # else:
29
+ # OldStdinMode = termios.tcgetattr(sys.stdin)
30
+ # _ = termios.tcgetattr(sys.stdin)
31
+ # _[3] = _[3] & ~(termios.ECHO | termios.ICANON)
32
+ # termios.tcsetattr(sys.stdin, termios.TCSAFLUSH, _)
33
+ # try:
34
+ # _ = ""
35
+ # sys.stdouS.write("\x1b[6n")
36
+ # sys.stdouS.flush()
37
+ # while not (_ := _ + sys.stdin.read(1)).endswith('R'):
38
+ # True
39
+ # res = re.match(r".*\[(?P<y>\d*);(?P<x>\d*)R", _)
40
+ # finally:
41
+ # if (sys.platform == "win32"):
42
+ # kernel32.SetConsoleMode(kernel32.GetStdHandle(-10), OldStdinMode)
43
+ # kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), OldStdoutMode)
44
+ # else:
45
+ # termios.tcsetattr(sys.stdin, termios.TCSAFLUSH, OldStdinMode)
46
+ # if (res):
47
+ # return (res.group("x"), res.group("y"))
48
+ # return (-1, -1)
49
49
 
50
50
 
51
51
  def write(key):
@@ -92,10 +92,7 @@ class ShellLineEditor: # pragma: nocover
92
92
  if key == S.RETURN:
93
93
  break
94
94
  if key.isprintable():
95
- write(S.BOLD + key + self.buf[self.pos:] +
96
- ('\b' * (len(self.buf) - self.pos)) + S.RESET)
97
- self.buf = self.buf[:self.pos] + key + self.buf[self.pos:]
98
- self.pos += 1
95
+ self.write_key(key)
99
96
  self.fillstate = FillState.USER
100
97
  elif (key in [S.BACKSPACE, S.KILL]) and self.has_fill:
101
98
  # Backspace clears the fill
@@ -113,13 +110,13 @@ class ShellLineEditor: # pragma: nocover
113
110
  elif key == S.RIGHT and self.pos < len(self.buf):
114
111
  self.move_right()
115
112
  self.fillstate = FillState.USER
116
- elif key == S.BEGINNING and self.pos > 0:
113
+ elif (key in [S.BEGINNING, S.CUSTOM_BEGINNING]) and self.pos > 0:
117
114
  self.move_beginning()
118
115
  self.fillstate = FillState.USER
119
- elif key == S.END and self.has_fill:
116
+ elif (key in [S.END, S.CUSTOM_END]) and self.has_fill:
120
117
  self.accept_fill()
121
118
  self.fillstate = FillState.USER
122
- elif key == S.END and self.pos < len(self.buf):
119
+ elif (key in [S.END, S.CUSTOM_END]) and self.pos < len(self.buf):
123
120
  self.move_end_buf()
124
121
  self.fillstate = FillState.USER
125
122
  elif key == S.TAB and (choices := self.valid_choices):
@@ -128,13 +125,25 @@ class ShellLineEditor: # pragma: nocover
128
125
  elif key == S.SHIFT_TAB and self.index > -1:
129
126
  self.index = (self.index - 1) % len(self.valid_choices)
130
127
  self.fillstate = FillState.TAB
131
- elif key == S.LEFT_WORD and self.pos > 0:
128
+ elif (key in [S.LEFT_WORD, S.CUSTOM_LEFTWORD]) and self.pos > 0:
132
129
  while (self.pos > 0) and self.is_sep(self.pos - 1):
133
130
  self.move_left()
134
131
  while (self.pos > 0) and not self.is_sep(self.pos - 1):
135
132
  self.move_left()
136
133
  self.fillstate = FillState.USER
137
- elif key == S.RIGHT_WORD and self.pos < len(self.buf):
134
+ elif (key in [S.RIGHT_WORD, S.CUSTOM_RIGHTWORD]) and self.has_fill:
135
+ fill = self.fill
136
+ self.fillstate = FillState.USER
137
+ while fill:
138
+ char = fill[0]
139
+ self.write_key(char)
140
+ fill = fill[1:]
141
+ if char in S.SEPARATORS:
142
+ self.fillstate = FillState.TAB
143
+ self.index = self.valid_choices.index(self.buf + fill)
144
+ break
145
+ elif (key in [S.RIGHT_WORD, S.CUSTOM_RIGHTWORD]) and \
146
+ self.pos < len(self.buf):
138
147
  while (self.pos < len(self.buf)) and self.is_sep(self.pos):
139
148
  self.move_right()
140
149
  while (self.pos < len(self.buf)) and not self.is_sep(self.pos):
@@ -151,6 +160,12 @@ class ShellLineEditor: # pragma: nocover
151
160
  write(S.RETURN)
152
161
  return self.buf
153
162
 
163
+ def write_key(self, key):
164
+ write(S.BOLD + key + self.buf[self.pos:] +
165
+ ('\b' * (len(self.buf) - self.pos)) + S.RESET)
166
+ self.buf = self.buf[:self.pos] + key + self.buf[self.pos:]
167
+ self.pos += 1
168
+
154
169
  def is_sep(self, pos):
155
170
  return (self.buf[pos] in S.SEPARATORS)
156
171
 
wizlib/ui/shell_ui.py CHANGED
@@ -4,6 +4,7 @@ import sys
4
4
  from wizlib.ui import UI, Chooser, Emphasis
5
5
  from wizlib.ui.shell.line_editor import ShellLineEditor
6
6
  from wizlib.ui.shell import S
7
+ from wizlib.ui.text_wrapper import StreamingTextWrapper
7
8
  import wizlib.io
8
9
 
9
10
  COLOR = {
@@ -23,10 +24,35 @@ class ShellUI(UI):
23
24
 
24
25
  name = "shell"
25
26
 
26
- def send(self, value: str = '', emphasis: Emphasis = Emphasis.GENERAL):
27
- """Output some text"""
28
- sys.stderr.write(COLOR[emphasis] + value + S.RESET + '\n')
29
- sys.stderr.flush()
27
+ def __init__(self):
28
+ super().__init__()
29
+ self._wrapper = None
30
+
31
+ def send(self, value: str = '', emphasis: Emphasis = Emphasis.GENERAL,
32
+ newline: bool = True, wrap: int = 0):
33
+ """Output some text
34
+
35
+ Args:
36
+ value: Text to output
37
+ emphasis: Color/emphasis style
38
+ newline: Whether to append a newline
39
+ wrap: Column width to wrap at (0 = no wrapping)
40
+ """
41
+ if wrap > 0:
42
+ # Initialize wrapper if needed or if width changed
43
+ if self._wrapper is None or self._wrapper.width != wrap:
44
+ self._wrapper = StreamingTextWrapper(
45
+ width=wrap, output_stream=sys.stderr)
46
+
47
+ # Use streaming wrapper
48
+ self._wrapper.write_streaming(value, COLOR[emphasis])
49
+ if newline:
50
+ self._wrapper.write_newline()
51
+ else:
52
+ # Original behavior - no wrapping
53
+ end = '\n' if newline else ''
54
+ sys.stderr.write(COLOR[emphasis] + value + S.RESET + end)
55
+ sys.stderr.flush()
30
56
 
31
57
  def ask(self, value: str):
32
58
  """Prompt for input"""
@@ -0,0 +1,103 @@
1
+ """
2
+ Streaming text wrapper that provides word wrapping with backspace correction
3
+ for a typewriter-like streaming experience.
4
+ """
5
+
6
+ import sys
7
+ from enum import Enum
8
+ from wizlib.ui import Emphasis
9
+
10
+
11
+ class StreamingTextWrapper:
12
+ """Handles streaming text output with word wrapping that uses backspace
13
+ to correct words that would overflow the line boundary.
14
+
15
+ Provides a typewriter-like streaming experience where characters are
16
+ written immediately, but words are moved to the next line if they
17
+ would overflow.
18
+ """
19
+
20
+ def __init__(self, width: int = 80, output_stream=None):
21
+ """Initialize the wrapper.
22
+
23
+ Args:
24
+ width: Column width to wrap at
25
+ output_stream: Stream to write to (defaults to sys.stderr)
26
+ """
27
+ self.width = width
28
+ self.output_stream = output_stream or sys.stderr
29
+ self._current_col = 0
30
+ self._word_buffer = [] # Buffer for current word being streamed
31
+
32
+ def _write_char(self, char: str, color_code: str = ''):
33
+ """Write a single character with optional color."""
34
+ if color_code:
35
+ self.output_stream.write(color_code + char + '\033[0m')
36
+ else:
37
+ self.output_stream.write(char)
38
+ self.output_stream.flush()
39
+
40
+ def _backspace_buffer(self):
41
+ """Backspace over the current word buffer."""
42
+ for _ in range(len(self._word_buffer)):
43
+ self.output_stream.write('\b \b') # backspace, space, backspace
44
+ self.output_stream.flush()
45
+
46
+ def write_streaming(self, text: str, color_code: str = ''):
47
+ """Write text with streaming word wrapping.
48
+
49
+ Args:
50
+ text: Text to output
51
+ color_code: ANSI color code to apply
52
+ """
53
+ for char in text:
54
+ if char == '\n':
55
+ # Explicit newline - flush buffer and reset
56
+ self._word_buffer = []
57
+ self._write_char(char)
58
+ self._current_col = 0
59
+ elif char in ' \t':
60
+ # Word boundary - write the space and clear buffer
61
+ self._word_buffer = []
62
+ self._write_char(char, color_code)
63
+ self._current_col += 1
64
+ else:
65
+ # Regular character - add to buffer and write immediately
66
+ self._word_buffer.append(char)
67
+ self._write_char(char, color_code)
68
+ self._current_col += 1
69
+
70
+ # Check if this character pushed us over the width
71
+ if self._current_col > self.width:
72
+ # Only backspace and wrap if the word can fit on a new line
73
+ # If the word itself is longer than width, let it continue
74
+ if len(self._word_buffer) <= self.width:
75
+ # We've gone over - backspace the whole current word
76
+ self._backspace_buffer()
77
+ self._current_col -= len(self._word_buffer)
78
+
79
+ # Write newline
80
+ self._write_char('\n')
81
+ self._current_col = 0
82
+
83
+ # Rewrite word on new line
84
+ for c in self._word_buffer:
85
+ self._write_char(c, color_code)
86
+ self._current_col += 1
87
+
88
+ def write_newline(self):
89
+ """Write a newline and reset position."""
90
+ self.output_stream.write('\n')
91
+ self.output_stream.flush()
92
+ self._current_col = 0
93
+ self._word_buffer = []
94
+
95
+ def reset_position(self):
96
+ """Reset the column position (useful for explicit positioning)."""
97
+ self._current_col = 0
98
+ self._word_buffer = []
99
+
100
+ @property
101
+ def current_column(self):
102
+ """Get the current column position."""
103
+ return self._current_col
@@ -0,0 +1,27 @@
1
+ Metadata-Version: 2.4
2
+ Name: wizlib
3
+ Version: 3.4.0
4
+ Summary: Framework for flexible and powerful command-line applications
5
+ License: MIT
6
+ Author: Steampunk Wizard
7
+ Author-email: wizlib@steampunkwizard.ca
8
+ Requires-Python: >=3.11,<4.0
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Requires-Dist: PyYAML (>=6.0.1,<7.0.0)
16
+ Requires-Dist: readchar (>=4.0.5,<5.0.0)
17
+ Description-Content-Type: text/markdown
18
+
19
+
20
+ # WizLib
21
+
22
+ Build configurable CLI tools easily in Python (a framework)
23
+
24
+ <a style="font-weight: bold; font-size: 1.2em;" href="https://wizlib.steamwiz.io">Documentation on SteamWiz.io</a>
25
+
26
+ <a style="font-weight: 300; font-size: 0.8em;" href="https://www.flaticon.com/free-icons/wizard" title="wizard icons">Wizard icon by Freepik-Flaticon</a>
27
+
@@ -0,0 +1,21 @@
1
+ wizlib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ wizlib/app.py,sha256=ke0NbaYC6ArmdtLTySl6VIGIGicxTsIuzW9wag6b7rU,4180
3
+ wizlib/class_family.py,sha256=tORSVAaPeWTQMcz2DX-MQClj1GQR3vCmkALPXxHa_pk,4506
4
+ wizlib/command.py,sha256=NO1558EYuXxfkpSmX6ljjzae8n8g4w6yytZKTJtigvo,1708
5
+ wizlib/config_handler.py,sha256=jKQV-6iP3V3XLx-swLWUmuAaMHsFp9DN_dd6dMeaBhk,4301
6
+ wizlib/error.py,sha256=ypwdMOYhtgKWd48ccfOX8idmCXmm-Skwx3gkPwqJB3c,46
7
+ wizlib/handler.py,sha256=Oz80aPhDyeY9tdppZ1dvtN-19JU5ydEDVW6jtppVoD4,446
8
+ wizlib/io.py,sha256=MWzqqXdmWmOsLs7n2KTcDodNd5YbfHMl5WuVoqsRm0Y,875
9
+ wizlib/parser.py,sha256=yLHV0fENeApFomCRWa3I6sB1x4lk1ag4vKejWVsic64,1550
10
+ wizlib/stream_handler.py,sha256=M8uZkzMFYCEmobq3Ji9S0Z-9fYA8v997oMyax110pAc,963
11
+ wizlib/super_wrapper.py,sha256=msitlfFfEwnrskzTtQBEY975sh9TQPicdLVo67imuqU,315
12
+ wizlib/test_case.py,sha256=-hhrkXST465PTr7CZfztjFD5OXoW60HeFRxV5WHsdN0,1250
13
+ wizlib/ui/__init__.py,sha256=ve_p_g4aBujh4jIJMgKkJ6cE5PT0aeY5AgRlneDswGg,4241
14
+ wizlib/ui/shell/__init__.py,sha256=zT_GjUpKRWAbZ_0rwbHL580vlaKLMesTDdvDApiuKDU,1154
15
+ wizlib/ui/shell/line_editor.py,sha256=l3iPtmYsavBGnQXoV-UHMWfzUqOfsNID6-ySpTF3SIc,8163
16
+ wizlib/ui/shell_ui.py,sha256=zsqmfL5LTnH_yqfqHUmBKhkZdb5cjRWL96vmfTrAn3U,2855
17
+ wizlib/ui/text_wrapper.py,sha256=WYL59E9cMRv0gI2Yhrc5GJTvstRZO86uYUD7lmDiwjM,3812
18
+ wizlib/ui_handler.py,sha256=JoZadtw9DKAtGvHKP3_BJF2NaYqmcQYNdsY4PeRnOjg,634
19
+ wizlib-3.4.0.dist-info/METADATA,sha256=h-cK4RnxEsEfWip8YnSD7Q14FIBN43fswcO6tUuT6u8,995
20
+ wizlib-3.4.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
21
+ wizlib-3.4.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.9.1
2
+ Generator: poetry-core 2.2.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,45 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: wizlib
3
- Version: 3.2.0
4
- Summary: Framework for flexible and powerful command-line applications
5
- License: MIT
6
- Author: Steampunk Wizard
7
- Author-email: wizlib@steampunkwizard.ca
8
- Requires-Python: >=3.11,<3.12
9
- Classifier: License :: OSI Approved :: MIT License
10
- Classifier: Programming Language :: Python :: 3
11
- Classifier: Programming Language :: Python :: 3.11
12
- Requires-Dist: PyYAML (>=6.0.1,<7.0.0)
13
- Requires-Dist: gnureadline (>=8.1.2,<9.0.0) ; sys_platform == "darwin"
14
- Requires-Dist: myst-parser (>=2.0.0,<3.0.0)
15
- Requires-Dist: pyreadline3 (>=3.4.1,<4.0.0) ; sys_platform == "win32"
16
- Requires-Dist: readchar (>=4.0.5,<5.0.0)
17
- Description-Content-Type: text/markdown
18
-
19
-
20
- # WizLib
21
-
22
- A framework for command-line devops and personal productivity tools
23
-
24
- WizLib wraps the built-in ArgumentParser with a set of functions, classes, and conventions that enables:
25
-
26
- - Easy addition of loosely coupled new subcommands
27
- - Unified access to configuration files with a normalized approach
28
- - Standardized use of stdin, stdout, and stderr
29
- - Plugin-type system for handling alternate UIs (such as curses or even a web UI)
30
- - Usable completion support for user input
31
- - Abstracts some of the argparse complexity
32
- - Applies conventions to application code structure
33
- - Supports test-driven development and CICD
34
-
35
- ## Getting started
36
-
37
- - [Home page](https://wizlib.ca) with documentation
38
- - [Sample project](https://gitlab.com/wizlib/sample) to get started
39
- - [Related projects](https://gitlab.com/wizlib) for real-world examples
40
-
41
- ---
42
-
43
- Logo by [Freepik](https://www.freepik.com/?_gl=1*1y9rvc9*test_ga*Mjc1MTIzODYxLjE2ODA3OTczNTg.*test_ga_523JXC6VL7*MTY4MDc5NzM1OC4xLjEuMTY4MDc5NzQxNS4zLjAuMA..*fp_ga*Mjc1MTIzODYxLjE2ODA3OTczNTg.*fp_ga_1ZY8468CQB*MTY4MDc5NzM1OC4xLjEuMTY4MDc5NzQxNS4zLjAuMA..)
44
-
45
-
@@ -1,20 +0,0 @@
1
- wizlib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- wizlib/app.py,sha256=ke0NbaYC6ArmdtLTySl6VIGIGicxTsIuzW9wag6b7rU,4180
3
- wizlib/class_family.py,sha256=tORSVAaPeWTQMcz2DX-MQClj1GQR3vCmkALPXxHa_pk,4506
4
- wizlib/command.py,sha256=NO1558EYuXxfkpSmX6ljjzae8n8g4w6yytZKTJtigvo,1708
5
- wizlib/config_handler.py,sha256=hoDavSMiGM_7PAHI8XIwC8nxPWOZDk302ryyjluoLGg,2588
6
- wizlib/error.py,sha256=ypwdMOYhtgKWd48ccfOX8idmCXmm-Skwx3gkPwqJB3c,46
7
- wizlib/handler.py,sha256=Oz80aPhDyeY9tdppZ1dvtN-19JU5ydEDVW6jtppVoD4,446
8
- wizlib/io.py,sha256=vZ1pIMtPFUZxbOv10HFDezEmlPgwPlUVP6RPwtfM9ec,506
9
- wizlib/parser.py,sha256=yLHV0fENeApFomCRWa3I6sB1x4lk1ag4vKejWVsic64,1550
10
- wizlib/stream_handler.py,sha256=7y1ckUoqTXxaCFI-XEixdBFZQZDxOv2jEEwtjWHZsUA,1066
11
- wizlib/super_wrapper.py,sha256=msitlfFfEwnrskzTtQBEY975sh9TQPicdLVo67imuqU,315
12
- wizlib/test_case.py,sha256=T5cX7yWy94hf8JuherDeBCH9Gve5tx2x_PZOte4udC0,1017
13
- wizlib/ui/__init__.py,sha256=ve_p_g4aBujh4jIJMgKkJ6cE5PT0aeY5AgRlneDswGg,4241
14
- wizlib/ui/shell/__init__.py,sha256=sPrYe4bG_Xf7Nwssx_dqXVk9jeyYBFUjh4oLdlSOeRY,943
15
- wizlib/ui/shell/line_editor.py,sha256=vXXsCS_i4ZCjP0su2X9W_yD1CS6MiazPAtxpiaXQ6Jc,7413
16
- wizlib/ui/shell_ui.py,sha256=jre7E_5vP_SceNH7GYenfXZpFf4h9Sbh9cWZZycTYk8,1911
17
- wizlib/ui_handler.py,sha256=JoZadtw9DKAtGvHKP3_BJF2NaYqmcQYNdsY4PeRnOjg,634
18
- wizlib-3.2.0.dist-info/METADATA,sha256=rKb5cRkQEpv_YjanZXOcPoGyDAz_86TjB67UNor42IM,1779
19
- wizlib-3.2.0.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
20
- wizlib-3.2.0.dist-info/RECORD,,