cube-cli 0.3.0__tar.gz → 0.4.1__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.
- {cube_cli-0.3.0 → cube_cli-0.4.1}/PKG-INFO +2 -2
- {cube_cli-0.3.0 → cube_cli-0.4.1}/pyproject.toml +3 -3
- {cube_cli-0.3.0 → cube_cli-0.4.1}/src/cube_cli/command.py +40 -21
- {cube_cli-0.3.0 → cube_cli-0.4.1}/src/cube_cli/repl.py +49 -2
- {cube_cli-0.3.0 → cube_cli-0.4.1}/src/cube_cli/repl_state.py +10 -0
- {cube_cli-0.3.0 → cube_cli-0.4.1}/README.md +0 -0
- {cube_cli-0.3.0 → cube_cli-0.4.1}/src/cube_cli/__init__.py +0 -0
- {cube_cli-0.3.0 → cube_cli-0.4.1}/src/cube_cli/_version.py +0 -0
- {cube_cli-0.3.0 → cube_cli-0.4.1}/src/cube_cli/cube_json.py +0 -0
- {cube_cli-0.3.0 → cube_cli-0.4.1}/src/cube_cli/main.py +0 -0
- {cube_cli-0.3.0 → cube_cli-0.4.1}/src/cube_cli/print.py +0 -0
- {cube_cli-0.3.0 → cube_cli-0.4.1}/src/cube_cli/save_load.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cube-cli
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: A CLI application for
|
|
3
|
+
Version: 0.4.1
|
|
4
|
+
Summary: A CLI application for the 3x3x3 cube
|
|
5
5
|
Requires-Python: >=3.13,<4.0
|
|
6
6
|
Classifier: Programming Language :: Python :: 3
|
|
7
7
|
Classifier: Programming Language :: Python :: 3.13
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "cube-cli"
|
|
3
|
-
version = "0.
|
|
4
|
-
description = "A CLI application for
|
|
3
|
+
version = "0.4.1"
|
|
4
|
+
description = "A CLI application for the 3x3x3 cube"
|
|
5
5
|
authors = []
|
|
6
6
|
readme = "README.md"
|
|
7
7
|
repository = "https://github.com/ygale/cube-cli"
|
|
@@ -16,7 +16,7 @@ cube = "cube_cli.main:main"
|
|
|
16
16
|
|
|
17
17
|
[tool.poetry.group.dev.dependencies]
|
|
18
18
|
pytest = "^8.0"
|
|
19
|
-
mypy = "
|
|
19
|
+
mypy = ">=1.10,<3.0"
|
|
20
20
|
|
|
21
21
|
[build-system]
|
|
22
22
|
requires = ["poetry-core"]
|
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
import re
|
|
3
3
|
from abc import ABC, abstractmethod
|
|
4
4
|
from dataclasses import dataclass
|
|
5
|
-
from typing import Self
|
|
5
|
+
from typing import ClassVar, Self
|
|
6
6
|
|
|
7
7
|
from cube_model import Color, Cube, Side, shuffled, solved
|
|
8
8
|
from cube_model.action import Action, ParseError, act, parse_actions
|
|
9
9
|
from cube_model.navigation import all_colors, side_color
|
|
10
10
|
|
|
11
11
|
from .repl_state import (
|
|
12
|
+
Alias,
|
|
12
13
|
Exit,
|
|
13
14
|
LoadError,
|
|
14
15
|
ReplState,
|
|
@@ -61,13 +62,24 @@ class Command(ABC):
|
|
|
61
62
|
'''Execute the command against the given REPL state.'''
|
|
62
63
|
|
|
63
64
|
@dataclass
|
|
64
|
-
class
|
|
65
|
+
class Tabbable:
|
|
66
|
+
'''Mixin for commands that can be tab-completed in the REPL.'''
|
|
67
|
+
aliases: ClassVar[list[Alias]] = []
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def match(cls, inp: str) -> bool:
|
|
71
|
+
'''Return True if inp equals the name of one of cls's aliases.'''
|
|
72
|
+
return any(inp == alias.name for alias in cls.aliases)
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class Shuffle(Tabbable, Command):
|
|
65
76
|
'''Randomize the cube.'''
|
|
77
|
+
aliases: ClassVar[list[Alias]] = [Alias(name='shuffle', min_chars=2)]
|
|
66
78
|
|
|
67
79
|
@classmethod
|
|
68
80
|
def parse(cls, cmd: str) -> Self | None:
|
|
69
|
-
'''Return a Shuffle if cmd
|
|
70
|
-
if cmd
|
|
81
|
+
'''Return a Shuffle if cmd matches one of its aliases.'''
|
|
82
|
+
if cls.match(cmd):
|
|
71
83
|
return cls()
|
|
72
84
|
return None
|
|
73
85
|
|
|
@@ -81,13 +93,14 @@ class Shuffle(Command):
|
|
|
81
93
|
return None
|
|
82
94
|
|
|
83
95
|
@dataclass
|
|
84
|
-
class Solve(Command):
|
|
96
|
+
class Solve(Tabbable, Command):
|
|
85
97
|
'''Return the cube to its solved state.'''
|
|
98
|
+
aliases: ClassVar[list[Alias]] = [Alias(name='solve', min_chars=2)]
|
|
86
99
|
|
|
87
100
|
@classmethod
|
|
88
101
|
def parse(cls, cmd: str) -> Self | None:
|
|
89
|
-
'''Return a Solve if cmd
|
|
90
|
-
if cmd
|
|
102
|
+
'''Return a Solve if cmd matches one of its aliases.'''
|
|
103
|
+
if cls.match(cmd):
|
|
91
104
|
return cls()
|
|
92
105
|
return None
|
|
93
106
|
|
|
@@ -101,8 +114,9 @@ class Solve(Command):
|
|
|
101
114
|
return None
|
|
102
115
|
|
|
103
116
|
@dataclass
|
|
104
|
-
class Load(Command):
|
|
117
|
+
class Load(Tabbable, Command):
|
|
105
118
|
'''Load cube state from a file.'''
|
|
119
|
+
aliases: ClassVar[list[Alias]] = [Alias(name='load', min_chars=2)]
|
|
106
120
|
filename: str | None
|
|
107
121
|
|
|
108
122
|
@classmethod
|
|
@@ -135,8 +149,9 @@ class Load(Command):
|
|
|
135
149
|
return None
|
|
136
150
|
|
|
137
151
|
@dataclass
|
|
138
|
-
class Save(Command):
|
|
152
|
+
class Save(Tabbable, Command):
|
|
139
153
|
'''Save cube state to a file.'''
|
|
154
|
+
aliases: ClassVar[list[Alias]] = [Alias(name='save', min_chars=2)]
|
|
140
155
|
filename: str | None
|
|
141
156
|
|
|
142
157
|
@classmethod
|
|
@@ -166,13 +181,14 @@ class Save(Command):
|
|
|
166
181
|
return None
|
|
167
182
|
|
|
168
183
|
@dataclass
|
|
169
|
-
class Undo(Command):
|
|
184
|
+
class Undo(Tabbable, Command):
|
|
170
185
|
'''Undo the last command.'''
|
|
186
|
+
aliases: ClassVar[list[Alias]] = [Alias(name='undo', min_chars=2)]
|
|
171
187
|
|
|
172
188
|
@classmethod
|
|
173
189
|
def parse(cls, cmd: str) -> Self | None:
|
|
174
|
-
'''Return an Undo if cmd
|
|
175
|
-
if cmd
|
|
190
|
+
'''Return an Undo if cmd matches one of its aliases.'''
|
|
191
|
+
if cls.match(cmd):
|
|
176
192
|
return cls()
|
|
177
193
|
return None
|
|
178
194
|
|
|
@@ -192,13 +208,14 @@ class Undo(Command):
|
|
|
192
208
|
return None
|
|
193
209
|
|
|
194
210
|
@dataclass
|
|
195
|
-
class Redo(Command):
|
|
211
|
+
class Redo(Tabbable, Command):
|
|
196
212
|
'''Redo the last undone command.'''
|
|
213
|
+
aliases: ClassVar[list[Alias]] = [Alias(name='redo', min_chars=4)]
|
|
197
214
|
|
|
198
215
|
@classmethod
|
|
199
216
|
def parse(cls, cmd: str) -> Self | None:
|
|
200
|
-
'''Return a Redo if cmd
|
|
201
|
-
if cmd
|
|
217
|
+
'''Return a Redo if cmd matches one of its aliases.'''
|
|
218
|
+
if cls.match(cmd):
|
|
202
219
|
return cls()
|
|
203
220
|
return None
|
|
204
221
|
|
|
@@ -218,13 +235,14 @@ class Redo(Command):
|
|
|
218
235
|
return None
|
|
219
236
|
|
|
220
237
|
@dataclass
|
|
221
|
-
class Quit(Command):
|
|
238
|
+
class Quit(Tabbable, Command):
|
|
222
239
|
'''Exit the REPL.'''
|
|
240
|
+
aliases: ClassVar[list[Alias]] = [Alias(name='quit'), Alias(name='q')]
|
|
223
241
|
|
|
224
242
|
@classmethod
|
|
225
243
|
def parse(cls, cmd: str) -> Self | None:
|
|
226
|
-
'''Return a Quit if cmd
|
|
227
|
-
if cmd
|
|
244
|
+
'''Return a Quit if cmd matches one of its aliases.'''
|
|
245
|
+
if cls.match(cmd):
|
|
228
246
|
return cls()
|
|
229
247
|
return None
|
|
230
248
|
|
|
@@ -271,13 +289,14 @@ Commands:
|
|
|
271
289
|
quit, q Exit cube.'''
|
|
272
290
|
|
|
273
291
|
@dataclass
|
|
274
|
-
class Help(Command):
|
|
292
|
+
class Help(Tabbable, Command):
|
|
275
293
|
'''Print a summary of available commands.'''
|
|
294
|
+
aliases: ClassVar[list[Alias]] = [Alias(name='help'), Alias(name='?')]
|
|
276
295
|
|
|
277
296
|
@classmethod
|
|
278
297
|
def parse(cls, cmd: str) -> Self | None:
|
|
279
|
-
'''Return a Help if cmd
|
|
280
|
-
if cmd
|
|
298
|
+
'''Return a Help if cmd matches one of its aliases.'''
|
|
299
|
+
if cls.match(cmd):
|
|
281
300
|
return cls()
|
|
282
301
|
return None
|
|
283
302
|
|
|
@@ -3,11 +3,56 @@ import readline
|
|
|
3
3
|
|
|
4
4
|
from cube_model import Cube, solved
|
|
5
5
|
|
|
6
|
-
from .command import Command, Quit, all_commands
|
|
6
|
+
from .command import Command, Quit, Tabbable, all_commands
|
|
7
7
|
from .print import print_cube
|
|
8
|
-
from .repl_state import Exit, LoadError, ReplState
|
|
8
|
+
from .repl_state import Alias, Exit, LoadError, ReplState
|
|
9
9
|
from .save_load import SaveError, load
|
|
10
10
|
|
|
11
|
+
# All tab completion candidates
|
|
12
|
+
_COMPLETIONS: list[str] = sorted(a.name
|
|
13
|
+
for c in all_commands if issubclass(c, Tabbable)
|
|
14
|
+
for a in c.aliases)
|
|
15
|
+
|
|
16
|
+
# Do not tab complete if it might be a Move.
|
|
17
|
+
# Avoid comprehension due to mypy bug.
|
|
18
|
+
_NON_COMPLETIONS: set[str] = set()
|
|
19
|
+
cmd: type[Command]
|
|
20
|
+
alias: Alias
|
|
21
|
+
for cmd in all_commands:
|
|
22
|
+
if issubclass(cmd, Tabbable):
|
|
23
|
+
for alias in cmd.aliases:
|
|
24
|
+
if alias.min_chars > 1:
|
|
25
|
+
_NON_COMPLETIONS.add(alias.name[:alias.min_chars - 1])
|
|
26
|
+
|
|
27
|
+
# Cached tab completion candidates for a specific input text
|
|
28
|
+
_matches: list[str] = []
|
|
29
|
+
|
|
30
|
+
def _complete(text: str, state: int) -> str | None:
|
|
31
|
+
'''Readline completer for REPL commands.
|
|
32
|
+
|
|
33
|
+
Matches text against _COMPLETIONS case-insensitively, always
|
|
34
|
+
returning lower-case completions. Returns no completions at all
|
|
35
|
+
if text is a prefix of any entry in _NON_COMPLETIONS.
|
|
36
|
+
'''
|
|
37
|
+
global _matches
|
|
38
|
+
text_lower: str = text.lower()
|
|
39
|
+
if state == 0:
|
|
40
|
+
if any(nc.startswith(text_lower) for nc in _NON_COMPLETIONS):
|
|
41
|
+
_matches = []
|
|
42
|
+
else:
|
|
43
|
+
_matches = [c for c in _COMPLETIONS if c.startswith(text_lower)]
|
|
44
|
+
if state < len(_matches):
|
|
45
|
+
return _matches[state]
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
def _setup_readline() -> None:
|
|
49
|
+
'''Register the REPL's tab completer with readline.'''
|
|
50
|
+
readline.set_completer(_complete)
|
|
51
|
+
if readline.__doc__ is not None and 'libedit' in readline.__doc__:
|
|
52
|
+
readline.parse_and_bind('bind ^I rl_complete')
|
|
53
|
+
else:
|
|
54
|
+
readline.parse_and_bind('tab: complete')
|
|
55
|
+
|
|
11
56
|
def parse_command(inp: str) -> Command | None:
|
|
12
57
|
'''Try each command type in order; return the first match.'''
|
|
13
58
|
cmd_type: type[Command]
|
|
@@ -35,6 +80,7 @@ def repl(filename: str | None) -> None:
|
|
|
35
80
|
If filename is given, load it as the initial cube state. If the
|
|
36
81
|
load fails, fall back to a solved cube and report the error.
|
|
37
82
|
'''
|
|
83
|
+
_setup_readline()
|
|
38
84
|
cmd: Command | None
|
|
39
85
|
rs: ReplState = _initial_state(filename)
|
|
40
86
|
while True:
|
|
@@ -53,6 +99,7 @@ def repl(filename: str | None) -> None:
|
|
|
53
99
|
inp: str = input(
|
|
54
100
|
'Command (? for help, q to quit): ').strip()
|
|
55
101
|
except EOFError:
|
|
102
|
+
print()
|
|
56
103
|
cmd = Quit()
|
|
57
104
|
else:
|
|
58
105
|
cmd = parse_command(inp)
|
|
@@ -21,6 +21,16 @@ class LoadError:
|
|
|
21
21
|
filename: str
|
|
22
22
|
msg: str
|
|
23
23
|
|
|
24
|
+
@dataclass
|
|
25
|
+
class Alias:
|
|
26
|
+
'''A name by which a Tabbable command can be invoked in the REPL.
|
|
27
|
+
|
|
28
|
+
min_chars is the minimum number of characters of name that must
|
|
29
|
+
be typed before tab completion will offer it.
|
|
30
|
+
'''
|
|
31
|
+
name: str
|
|
32
|
+
min_chars: int = 1
|
|
33
|
+
|
|
24
34
|
def _invert_action(action: Action) -> Action:
|
|
25
35
|
'''Return the inverse of a single action.
|
|
26
36
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|