wizlib 2.0.10__tar.gz → 2.0.12__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.10
3
+ Version: 2.0.12
4
4
  Summary: Framework for flexible and powerful command-line applications
5
5
  License: MIT
6
6
  Author: Steampunk Wizard
@@ -11,7 +11,9 @@ Classifier: Programming Language :: Python :: 3
11
11
  Classifier: Programming Language :: Python :: 3.11
12
12
  Requires-Dist: PyYAML (>=6.0.1,<7.0.0)
13
13
  Requires-Dist: gnureadline (>=8.1.2,<9.0.0) ; sys_platform == "darwin"
14
+ Requires-Dist: myst-parser (>=2.0.0,<3.0.0)
14
15
  Requires-Dist: pyreadline3 (>=3.4.1,<4.0.0) ; sys_platform == "win32"
16
+ Requires-Dist: readchar (>=4.0.5,<5.0.0)
15
17
  Description-Content-Type: text/markdown
16
18
 
17
19
  # WizLib
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "wizlib"
3
- version = "2.0.10"
3
+ version = "2.0.12"
4
4
  description = "Framework for flexible and powerful command-line applications"
5
5
  authors = ["Steampunk Wizard <wizlib@steampunkwizard.ca>"]
6
6
  license = "MIT"
@@ -11,18 +11,16 @@ python = ">=3.11,<3.12"
11
11
  PyYAML = "^6.0.1"
12
12
  pyreadline3 = { version="^3.4.1", markers="sys_platform=='win32'" }
13
13
  gnureadline = { version="^8.1.2", markers="sys_platform=='darwin'" }
14
+ myst-parser = "^2.0.0"
15
+ readchar = "^4.0.5"
14
16
 
15
17
  [tool.poetry.group.dev.dependencies]
16
18
  coverage = "^7.3.1"
17
19
  pycodestyle = "^2.11.0"
18
20
  autopep8 = "^2.0.4"
21
+ sphinx = "^7.2.6"
22
+ sphinx-bootstrap-theme = "^0.8.1"
19
23
 
20
24
  [build-system]
21
25
  requires = ["poetry-core"]
22
26
  build-backend = "poetry.core.masonry.api"
23
-
24
- [tool.poetry-dynamic-versioning]
25
- enable = false
26
- vcs = "git"
27
- format = "{base}"
28
-
@@ -7,6 +7,8 @@ from wizlib.class_family import ClassFamily
7
7
  from wizlib.command import WizHelpCommand
8
8
  from wizlib.super_wrapper import SuperWrapper
9
9
  from wizlib.parser import WizParser
10
+ from wizlib.ui import UI
11
+ from wizlib.ui.shell_ui import ShellUI
10
12
 
11
13
 
12
14
  RED = '\033[91m'
@@ -31,7 +33,6 @@ class WizApp:
31
33
  try:
32
34
  cls.initialize()
33
35
  app = cls(*args)
34
- # if app.ready:
35
36
  command = app.first_command
36
37
  result = command.execute()
37
38
  if result:
@@ -58,8 +59,8 @@ class WizApp:
58
59
  cls.parser.add_argument(
59
60
  f"--{handler.name}",
60
61
  f"-{handler.name[0]}",
61
- type=handler.named(cls.name),
62
- default='')
62
+ **handler.option_properties(cls))
63
+
63
64
  subparsers = cls.parser.add_subparsers(dest='command')
64
65
  for command in cls.base_command.family_members('name'):
65
66
  key = command.get_member_attr('key')
@@ -68,12 +69,15 @@ class WizApp:
68
69
  command.add_args(subparser)
69
70
 
70
71
  def __init__(self, *args):
72
+ self.shell = ShellUI()
71
73
  args = args if args else [self.base_command.default]
74
+ if not hasattr(self, 'parser'):
75
+ self.__class__.initialize()
72
76
  self.vals = vars(self.parser.parse_args(args))
73
77
  self.first_command = self.get_command(**self.vals)
74
78
 
75
79
  def get_command(self, **vals):
76
- """Run a single command"""
80
+ """Returns a single command"""
77
81
  if 'help' in vals:
78
82
  return WizHelpCommand(**vals)
79
83
  else:
@@ -5,6 +5,7 @@ from wizlib.class_family import ClassFamily
5
5
  from wizlib.config_handler import ConfigHandler
6
6
  from wizlib.input_handler import InputHandler
7
7
  from wizlib.super_wrapper import SuperWrapper
8
+ from wizlib.ui import UI
8
9
 
9
10
 
10
11
  class WizCommand(ClassFamily, SuperWrapper):
@@ -12,6 +13,7 @@ class WizCommand(ClassFamily, SuperWrapper):
12
13
 
13
14
  status = ''
14
15
  handlers = []
16
+ ui: UI = None
15
17
 
16
18
  @classmethod
17
19
  def add_args(self, parser):
@@ -31,6 +33,13 @@ class WizCommand(ClassFamily, SuperWrapper):
31
33
  this and call super().handle_vals()."""
32
34
  pass
33
35
 
36
+ def provided(self, argument):
37
+ """Was an argument provided?"""
38
+ value = None
39
+ if hasattr(self, argument):
40
+ value = getattr(self, argument)
41
+ return True if (value is False) else bool(value)
42
+
34
43
  def execute(self, method, *args, **kwargs):
35
44
  """Actually perform the command - override and wrap this via
36
45
  SuperWrapper"""
@@ -6,6 +6,14 @@ class Handler:
6
6
 
7
7
  appname = None
8
8
 
9
+ @classmethod
10
+ def option_properties(cls, app):
11
+ """Argparse keyword arguments for this optional arg"""
12
+ return {
13
+ 'type': cls.named(app.name),
14
+ 'default': ''
15
+ }
16
+
9
17
  @classmethod
10
18
  def named(cls, name):
11
19
  """Subclass of the handler that holds the app name as a closure"""
@@ -3,6 +3,8 @@ import sys
3
3
 
4
4
  from wizlib.handler import Handler
5
5
 
6
+ INTERACTIVE = sys.stdin.isatty()
7
+
6
8
 
7
9
  class InputHandler(Handler):
8
10
 
@@ -12,7 +14,7 @@ class InputHandler(Handler):
12
14
  def __init__(self, file=None, stdin=True):
13
15
  if file:
14
16
  self.text = Path(file).read_text()
15
- elif stdin and (not sys.stdin.isatty()):
17
+ elif stdin and (not INTERACTIVE):
16
18
  self.text = sys.stdin.read()
17
19
 
18
20
  def __str__(self):
@@ -0,0 +1,182 @@
1
+ # Abstract classes for UIs. The idea here is to allow many different user
2
+ # interfaces (shell, curses, Slack, etc) to drive the same data model without
3
+ # the data model needing specific UI knowledge.
4
+ #
5
+ # The UI keeps a reference to the Handler. The Handler will bring a Command
6
+ # with it, and this is the main root command. In the case of a Shell-type UI,
7
+ # that is the command to be executed. In the case of an interactive UI, it's
8
+ # really just a placeholder; the UI will instantiate other commands as it
9
+ # proceeds. But the Handler won't know about those.
10
+ #
11
+ # Commands might call back to the UI for confirmations or arguments that are
12
+ # previously omitted, using the get_ methods.
13
+
14
+ #
15
+ # UI end classes must implement the following interface:
16
+ #
17
+ # __init__(handler): Takes the handler and hangs on to it. Implemented in the
18
+ # main base class.
19
+ #
20
+ # start(): No arguments. Actually performs the operation of the UI. It might be
21
+ # short running (in the case of a shell UI) or long-running (in the case of an
22
+ # interactive UI).
23
+ #
24
+ # output(intro=""): Output some multi-line text explaining an action, usually a
25
+ # list of items being acted upon.
26
+ #
27
+ # get_string(prompt, default=""): For arguments that are omitted, get a string
28
+ # from the user. The prompt is just a word (like "Description") telling the
29
+ # user what to input.
30
+ #
31
+ # get_confirmation(verb): For delete-oriented commands to confirm with the user
32
+ # before proceeding. Verb is what we're asking the user to confirm. Description
33
+ # can be multiple lines, as with get_string. Returns a boolean saying whether
34
+ # the action is confirmed.
35
+
36
+ import os
37
+ import re
38
+ import shutil
39
+ import subprocess
40
+ from dataclasses import dataclass, field
41
+ from enum import Enum
42
+ from io import StringIO
43
+ from pathlib import Path
44
+ from tempfile import NamedTemporaryFile
45
+
46
+ from wizlib.class_family import ClassFamily
47
+
48
+
49
+ @dataclass
50
+ class Choice:
51
+ """One option of several"""
52
+
53
+ keys: list[str] = field(default_factory=list)
54
+ words: list[str] = field(default_factory=list)
55
+ action: object = None
56
+
57
+ @property
58
+ def key(self):
59
+ return self.keys[0]
60
+
61
+ @property
62
+ def word(self):
63
+ return self.words[0]
64
+
65
+ def hit_key(self, key):
66
+ return key in self.keys
67
+
68
+ def hit_word(self, word):
69
+ return word in self.words
70
+
71
+ def value(self):
72
+ return self.word if (self.action is None) else self.action
73
+
74
+ # def call(self, *args, **kwargs):
75
+ # if callable(self.action):
76
+ # return self.action(*args, **kwargs)
77
+
78
+
79
+ @dataclass
80
+ class Prompt:
81
+ """Tell the user what kind of input to provide"""
82
+
83
+ intro: str = ""
84
+ default: str = ""
85
+ choices: list = field(default_factory=list)
86
+
87
+ @property
88
+ def prompt_string(self):
89
+ """Simple prompt string for stdio, no colours"""
90
+ result = []
91
+ if self.intro:
92
+ result.append(self.intro)
93
+ if self.default:
94
+ result.append(f"[{self.default}]")
95
+ for choice in self.choices:
96
+ if choice.word == self.default:
97
+ continue
98
+ pre, it, post = choice.word.partition(choice.key)
99
+ result.append(f"{pre}({it}){post}")
100
+ return " ".join(result) + ": "
101
+
102
+
103
+ @dataclass
104
+ class Chooser(Prompt):
105
+ """Hold a set of choices and get the result"""
106
+
107
+ def add_choice(self, *args, **kwargs):
108
+ choice = Choice(*args, **kwargs)
109
+ self.choices.append(choice)
110
+
111
+ def choice_by_key(self, key):
112
+ chosenlist = [o.value for o in self.choices if o.hit_key(key)]
113
+ return self._choose(chosenlist)
114
+
115
+ def choice_by_word(self, word):
116
+ chosenlist = [o.value for o in self.choices if o.hit_word(word)]
117
+ return self._choose(chosenlist)
118
+
119
+ def _choose(self, chosenlist):
120
+ choice = next(iter(chosenlist), None)
121
+ if callable(choice):
122
+ return choice()
123
+ else:
124
+ return choice
125
+
126
+
127
+ @dataclass
128
+ class UI(ClassFamily):
129
+ pass
130
+
131
+ # handler: object
132
+
133
+ # # TODO: Make start an abstract method
134
+ # def start(self):
135
+ # pass
136
+
137
+ # # TODO: Make get_option an abstract method
138
+ # def get_option(self, chooser: Chooser):
139
+ # """Get an option from a list from the user"""
140
+ # pass
141
+
142
+ # # TODO: Make get_string an abstract method
143
+ # def get_string(self, intro, default=""):
144
+ # """Get a string value from the user"""
145
+ # pass
146
+
147
+
148
+ # # Terminal UIs include Shell and Curses, since both will rely on a
149
+ # # terminal-based editor for the e.g. Manage command.
150
+
151
+
152
+ # class TerminalUI(UI):
153
+
154
+ # # A convenience method to get a full textual prompt for a string input
155
+
156
+ # def full_prompt(self, prompt, default=None):
157
+ # if default:
158
+ # return f'{prompt} [{default}]: '
159
+ # else:
160
+ # return f'{prompt}: '
161
+
162
+ # # @staticmethod
163
+ # # def edit_text(text):
164
+ # # commands = [['sensible-editor'], ['open', '-W']]
165
+ # # with NamedTemporaryFile(mode="w+") as tempfile:
166
+ # # tempfile.write(text)
167
+ # # tempfile.seek(0)
168
+ # # command = [os.environ.get('EDITOR')]
169
+ # # if not command[0] or not shutil.which(command[0]):
170
+ # # iterator = (c for c in commands if shutil.which(c[0]))
171
+ # # command = next(filter(None, iterator), None)
172
+ # # if not command:
173
+ # # raise RuntimeError(
174
+ # # "A text editor at the $EDITOR " +
175
+ # # "environment variable is required")
176
+ # # subprocess.run(command + [tempfile.name])
177
+ # # tempfile.seek(0)
178
+ # # return tempfile.read()
179
+
180
+
181
+ # class UserCancelError(Exception):
182
+ # pass
@@ -0,0 +1,31 @@
1
+ import sys
2
+
3
+ from readchar import readkey
4
+ from wizlib.rlinput import rlinput
5
+ from wizlib.ui import UI
6
+
7
+
8
+ class ShellUI(UI):
9
+
10
+ """The UI to execute one command passed in through the shell. There will be
11
+ limited interactivity, if the user omits an argument on the command line,
12
+ but otherwise this is a run and done situation.
13
+ """
14
+
15
+ name = "shell"
16
+
17
+ def send(self, value: str):
18
+ """Output some text"""
19
+ if value:
20
+ print(value, file=sys.stderr)
21
+
22
+ def get_option(self, chooser):
23
+ """Get a choice from the user with a single keystroke"""
24
+ # key = rlinput(chooser.prompt_string)
25
+ print(chooser.prompt_string, end='', file=sys.stderr, flush=True)
26
+ if sys.stdin.isatty():
27
+ key = readkey()
28
+ else:
29
+ key = input()
30
+ print(file=sys.stderr, flush=True)
31
+ return chooser.choice_by_key(key)
@@ -0,0 +1,32 @@
1
+ from pathlib import Path
2
+ import sys
3
+
4
+ from wizlib.handler import Handler
5
+ from wizlib.ui import UI
6
+
7
+ # INTERACTIVE = sys.stdin.isatty()
8
+
9
+
10
+ class UIHandler(Handler):
11
+ """A sort of proxy-handler for the UI class family, which drives user
12
+ interactions (if any) during and between command execution. In this case,
13
+ the handler only contains class-level methods, because the action actually
14
+ returns the UI itself for the command to use."""
15
+
16
+ name = 'ui'
17
+
18
+ @classmethod
19
+ def option_properties(cls, app):
20
+ """Argparse keyword arguments for this optional arg"""
21
+ return {
22
+ # 'choices': ['shell'],
23
+ 'default': 'shell',
24
+ 'help': 'Only option currently is "shell" (the default)',
25
+ 'type': cls.ui
26
+ }
27
+
28
+ @classmethod
29
+ def ui(cls, uitype):
30
+ """Instead of instantiating the handler, return the chosen UI
31
+ object."""
32
+ return UI.family_member('name', uitype)()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes