superparsing 0.0.0.dev2__tar.gz → 0.1.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.
Files changed (25) hide show
  1. {superparsing-0.0.0.dev2/superparsing.egg-info → superparsing-0.1.0}/PKG-INFO +1 -1
  2. {superparsing-0.0.0.dev2 → superparsing-0.1.0}/pyproject.toml +1 -1
  3. superparsing-0.1.0/src/superparsing/__init__.py +1 -0
  4. superparsing-0.1.0/src/superparsing/core/__init__.py +227 -0
  5. superparsing-0.1.0/src/superparsing/py.typed +0 -0
  6. {superparsing-0.0.0.dev2 → superparsing-0.1.0/src/superparsing.egg-info}/PKG-INFO +1 -1
  7. superparsing-0.1.0/src/superparsing.egg-info/SOURCES.txt +20 -0
  8. superparsing-0.1.0/src/superparsing.egg-info/top_level.txt +1 -0
  9. superparsing-0.1.0/tests/test_0.py +94 -0
  10. superparsing-0.1.0/tests/test_comprehensive_TestParseArgs.py +138 -0
  11. superparsing-0.1.0/tests/test_comprehensive_TestSubCommand.py +57 -0
  12. superparsing-0.1.0/tests/test_comprehensive_TestSuperFlag.py +88 -0
  13. superparsing-0.1.0/tests/test_comprehensive_TestSuperParserBasics.py +53 -0
  14. superparsing-0.1.0/tests/test_comprehensive_TestSuperParserText.py +81 -0
  15. superparsing-0.1.0/tests/test_patch.py +29 -0
  16. superparsing-0.0.0.dev2/make/env.py +0 -85
  17. superparsing-0.0.0.dev2/superparsing.egg-info/SOURCES.txt +0 -12
  18. superparsing-0.0.0.dev2/superparsing.egg-info/top_level.txt +0 -1
  19. superparsing-0.0.0.dev2/tests/test_1984.py +0 -10
  20. {superparsing-0.0.0.dev2 → superparsing-0.1.0}/LICENSE.txt +0 -0
  21. {superparsing-0.0.0.dev2 → superparsing-0.1.0}/MANIFEST.in +0 -0
  22. {superparsing-0.0.0.dev2 → superparsing-0.1.0}/README.rst +0 -0
  23. {superparsing-0.0.0.dev2 → superparsing-0.1.0}/run_tests.py +0 -0
  24. {superparsing-0.0.0.dev2 → superparsing-0.1.0}/setup.cfg +0 -0
  25. {superparsing-0.0.0.dev2 → superparsing-0.1.0/src}/superparsing.egg-info/dependency_links.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: superparsing
3
- Version: 0.0.0.dev2
3
+ Version: 0.1.0
4
4
  Summary: This project does superparsing.
5
5
  Author-email: Johannes <johannes.programming@gmail.com>
6
6
  License-Expression: MIT
@@ -27,7 +27,7 @@ license-files = [
27
27
  name = "superparsing"
28
28
  readme = "README.rst"
29
29
  requires-python = ">=3.11"
30
- version = "0.0.0.dev2"
30
+ version = "0.1.0"
31
31
 
32
32
  [project.urls]
33
33
  Download = "https://pypi.org/project/superparsing/#files"
@@ -0,0 +1 @@
1
+ from superparsing.core import *
@@ -0,0 +1,227 @@
1
+ import getopt
2
+ import importlib.metadata
3
+ import sys
4
+ from collections.abc import Iterable
5
+ from dataclasses import dataclass, field
6
+ from functools import partial
7
+ from typing import Any, Final, Optional, Self
8
+
9
+ __all__ = ["SuperParseError", "SuperFlag", "SubCommand", "SuperParser"]
10
+
11
+ INDENT: Final[int] = 2
12
+ SHIFT: Final[int] = 8
13
+ ARGS_MESSAGE: Final[str] = (
14
+ "These remaining args are parsed again by the chosen subcommand."
15
+ )
16
+
17
+
18
+ class SuperParseError(ValueError):
19
+ pass
20
+
21
+
22
+ @dataclass
23
+ class SuperFlag:
24
+ keys: Iterable[object] = ()
25
+ help: object = ""
26
+ message: Optional[object] = None
27
+
28
+ def _keys(self: Self) -> list[str]:
29
+ return list(map(str, self.keys))
30
+
31
+ def _message(self: Self) -> Optional[str]:
32
+ if self.message is None:
33
+ return None
34
+ else:
35
+ return str(self.message)
36
+
37
+ def _usage(self: Self) -> str:
38
+ ans: list[str]
39
+ ans = self._keys()
40
+ if ans:
41
+ return "[" + ", ".join(ans) + "]"
42
+ else:
43
+ return ""
44
+
45
+ def intro(self: Self) -> str:
46
+ ans: list[str]
47
+ ans = self._keys()
48
+ if ans:
49
+ return ", ".join(ans) + ":"
50
+ else:
51
+ return ""
52
+
53
+ def longopts(self: Self) -> tuple[str, ...]:
54
+ ans: list[str]
55
+ ans = list()
56
+ for flag in map(str, self.keys):
57
+ if flag.startswith("--"):
58
+ ans.append(flag[2:].split("=")[0])
59
+ return tuple(ans)
60
+
61
+ def shortopts(self: Self) -> str:
62
+ ans: str
63
+ ans = ""
64
+ for flag in map(str, self.keys):
65
+ if len(flag) == 2 and flag[0] == "-":
66
+ ans += flag[1]
67
+ return ans
68
+
69
+
70
+ @dataclass
71
+ class SubCommand:
72
+ name: object
73
+ aliases: Iterable[object] = ()
74
+ help: object = ""
75
+
76
+ def _usage(self: Self) -> str:
77
+ return ", ".join(map(str, [self.name] + list(self.aliases)))
78
+
79
+ def intro(self: Self) -> str:
80
+ ans: str
81
+ aliases_: list[str]
82
+ aliases_ = list(map(str, self.aliases))
83
+ ans = str(self.name)
84
+ if not aliases_:
85
+ return ans + ":"
86
+ ans += "("
87
+ ans += ", ".join(aliases_)
88
+ ans += "):"
89
+ return ans
90
+
91
+
92
+ @dataclass
93
+ class SuperParser:
94
+ fromfile_prefix_chars: object = field(default="", kw_only=True)
95
+ helpFlag: SuperFlag = field(
96
+ default_factory=partial(SuperFlag, help="print this message and exit"),
97
+ kw_only=True,
98
+ )
99
+ prog: Optional[str] = field(default=None, kw_only=True)
100
+ subCommands: list[SubCommand] = field(default_factory=list, kw_only=True)
101
+ versionFlag: SuperFlag = field(
102
+ default_factory=partial(SuperFlag, help="print version and exit"),
103
+ kw_only=True,
104
+ )
105
+
106
+ def _keys_message(self: Self) -> Optional[str]:
107
+ ans: str
108
+ shift: int
109
+ help_intro = self.helpFlag.intro()
110
+ version_intro = self.versionFlag.intro()
111
+ if help_intro + version_intro == "":
112
+ return None
113
+ ans = "keys:"
114
+ shift = max(SHIFT, len(help_intro), len(version_intro))
115
+ shift += INDENT
116
+ if help_intro:
117
+ ans += "\n"
118
+ ans += " " * INDENT
119
+ ans += help_intro.ljust(shift)
120
+ ans += str(self.helpFlag.help)
121
+ if version_intro:
122
+ ans += "\n"
123
+ ans += " " * INDENT
124
+ ans += version_intro.ljust(shift)
125
+ ans += str(self.versionFlag.help)
126
+ return ans
127
+
128
+ def _prog(self: Self) -> str:
129
+ if self.prog is None:
130
+ return str(sys.argv[0])
131
+ else:
132
+ return str(self.prog)
133
+
134
+ def _subCommands_message(self: Self) -> Optional[str]:
135
+ intros = list(map(SubCommand.intro, self.subCommands))
136
+ if not intros:
137
+ return None
138
+ ans = "subcommands:"
139
+ shift = max(SHIFT, *map(len, intros))
140
+ shift += INDENT
141
+ for intro, cmd in zip(intros, self.subCommands):
142
+ ans += "\n" + " " * INDENT + intro.ljust(shift) + str(cmd.help)
143
+ return ans
144
+
145
+ def add_subCommand(self: Self, **kwargs: Any) -> SubCommand:
146
+ self.subCommands.append(SubCommand(**kwargs))
147
+ return self.subCommands[-1]
148
+
149
+ def help(self: Self) -> str:
150
+ ans: list[Optional[str]]
151
+ intro = self.helpFlag._message()
152
+ if intro is not None:
153
+ return intro
154
+ ans = list()
155
+ ans.append(self.usage())
156
+ ans.append(self._keys_message())
157
+ ans.append(self._subCommands_message())
158
+ ans.append("args:\n" + " " * INDENT + ARGS_MESSAGE)
159
+ filtered = [part for part in ans if part is not None]
160
+ return "\n\n".join(filtered)
161
+
162
+ def parse_args(
163
+ self: Self, args: Optional[Iterable[object]] = None, /
164
+ ) -> list[str]:
165
+ args_: list[str]
166
+ cmd: SubCommand
167
+ longopts: tuple[str, ...]
168
+ optitems: list[tuple[str, str]]
169
+ shortopts: str
170
+ args_ = list()
171
+ for arg in map(str, sys.argv[1:] if args is None else args):
172
+ prefix_chars = str(self.fromfile_prefix_chars)
173
+ if arg == "" or arg[0] not in prefix_chars:
174
+ args_.append(arg)
175
+ continue
176
+ with open(arg[1:], "r") as stream:
177
+ args_.extend(stream.read().splitlines())
178
+ shortopts = self.helpFlag.shortopts() + self.versionFlag.shortopts()
179
+ longopts = self.helpFlag.longopts() + self.versionFlag.longopts()
180
+ try:
181
+ optitems, args_ = getopt.getopt(
182
+ args=args_, shortopts=shortopts, longopts=longopts
183
+ )
184
+ except Exception:
185
+ raise SuperParseError("Failed getopt!")
186
+ if set(self.helpFlag._keys()).intersection(dict(optitems).keys()):
187
+ _print(self.help())
188
+ return list()
189
+ if set(self.versionFlag._keys()).intersection(dict(optitems).keys()):
190
+ _print(self.version())
191
+ return list()
192
+ if not args_:
193
+ raise SuperParseError("Subcommand missing!")
194
+ for cmd in self.subCommands:
195
+ if args_[0] == str(cmd.name):
196
+ return args_
197
+ for cmd in self.subCommands:
198
+ if args_[0] in map(str, cmd.aliases):
199
+ args_[0] = str(cmd.name)
200
+ return args_
201
+ raise SuperParseError("Subcommand %r unknown!" % args_[0])
202
+
203
+ def usage(self: Self) -> str:
204
+ ans: str
205
+ piece: str
206
+ ans = "usage:"
207
+ ans += " " + self._prog()
208
+ for flag in (self.helpFlag, self.versionFlag):
209
+ piece = flag._usage()
210
+ if piece:
211
+ ans += " "
212
+ ans += piece
213
+ ans += " {"
214
+ ans += ", ".join(map(SubCommand._usage, self.subCommands))
215
+ ans += "} [arg ...]"
216
+ return ans
217
+
218
+ def version(self: Self) -> str:
219
+ if self.versionFlag.message is not None:
220
+ return str(self.versionFlag.message)
221
+ else:
222
+ return f"{self._prog()}, version {importlib.metadata.version(self._prog())}"
223
+
224
+
225
+ def _print(value: Optional[str]) -> None:
226
+ if value is not None:
227
+ print(value)
File without changes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: superparsing
3
- Version: 0.0.0.dev2
3
+ Version: 0.1.0
4
4
  Summary: This project does superparsing.
5
5
  Author-email: Johannes <johannes.programming@gmail.com>
6
6
  License-Expression: MIT
@@ -0,0 +1,20 @@
1
+ LICENSE.txt
2
+ MANIFEST.in
3
+ README.rst
4
+ pyproject.toml
5
+ run_tests.py
6
+ setup.cfg
7
+ src/superparsing/__init__.py
8
+ src/superparsing/py.typed
9
+ src/superparsing.egg-info/PKG-INFO
10
+ src/superparsing.egg-info/SOURCES.txt
11
+ src/superparsing.egg-info/dependency_links.txt
12
+ src/superparsing.egg-info/top_level.txt
13
+ src/superparsing/core/__init__.py
14
+ tests/test_0.py
15
+ tests/test_comprehensive_TestParseArgs.py
16
+ tests/test_comprehensive_TestSubCommand.py
17
+ tests/test_comprehensive_TestSuperFlag.py
18
+ tests/test_comprehensive_TestSuperParserBasics.py
19
+ tests/test_comprehensive_TestSuperParserText.py
20
+ tests/test_patch.py
@@ -0,0 +1 @@
1
+ superparsing
@@ -0,0 +1,94 @@
1
+ import os
2
+ import tempfile
3
+ import unittest
4
+ from typing import Self
5
+
6
+ from superparsing import SubCommand, SuperFlag, SuperParseError, SuperParser
7
+
8
+
9
+ class TestSuperFlag(unittest.TestCase):
10
+
11
+ def test_intro(self: Self) -> None:
12
+ flag = SuperFlag(keys=["-h", "--help"])
13
+ self.assertEqual(flag.intro(), "-h, --help:")
14
+
15
+ def test_shortopts(self: Self) -> None:
16
+ flag = SuperFlag(keys=["-h", "-v", "--help"])
17
+ self.assertEqual(flag.shortopts(), "hv")
18
+
19
+ def test_longopts(self: Self) -> None:
20
+ flag = SuperFlag(keys=["-h", "--help", "--version=1"])
21
+ self.assertEqual(flag.longopts(), ("help", "version"))
22
+
23
+
24
+ class TestSubCommand(unittest.TestCase):
25
+
26
+ def test_intro_without_alias(self: Self) -> None:
27
+ cmd = SubCommand(name="run")
28
+ self.assertEqual(cmd.intro(), "run:")
29
+
30
+ def test_intro_with_aliases(self: Self) -> None:
31
+ cmd = SubCommand(name="run", aliases=["r", "execute"])
32
+ self.assertEqual(cmd.intro(), "run(r, execute):")
33
+
34
+
35
+ class TestSuperParser(unittest.TestCase):
36
+
37
+ def setUp(self: Self) -> None:
38
+ self.parser = SuperParser(
39
+ prog="tool",
40
+ helpFlag=SuperFlag(keys=["-h", "--help"]),
41
+ versionFlag=SuperFlag(keys=["-v", "--version"]),
42
+ )
43
+ self.parser.add_subCommand(
44
+ name="run", aliases=["r"], help="run command"
45
+ )
46
+ self.parser.add_subCommand(name="test", help="test command")
47
+
48
+ def test_usage(self: Self) -> None:
49
+ usage = self.parser.usage()
50
+ self.assertIn("usage:", usage)
51
+ self.assertIn("tool", usage)
52
+ self.assertIn("run, r", usage)
53
+
54
+ def test_help_output(self: Self) -> None:
55
+ text = self.parser.help()
56
+ self.assertIn("usage:", text)
57
+ self.assertIn("subcommands:", text)
58
+ self.assertIn("args:", text)
59
+
60
+ def test_parse_valid_subcommand(self: Self) -> None:
61
+ result = self.parser.parse_args(["run"])
62
+ self.assertEqual(result, ["run"])
63
+
64
+ def test_parse_alias(self: Self) -> None:
65
+ result = self.parser.parse_args(["r"])
66
+ self.assertEqual(result, ["run"])
67
+
68
+ def test_parse_unknown_subcommand(self: Self) -> None:
69
+ with self.assertRaises(SuperParseError):
70
+ self.parser.parse_args(["unknown"])
71
+
72
+ def test_parse_missing_subcommand(self: Self) -> None:
73
+ with self.assertRaises(SuperParseError):
74
+ self.parser.parse_args([])
75
+
76
+ def test_version_custom_message(self: Self) -> None:
77
+ parser = SuperParser(
78
+ prog="tool", versionFlag=SuperFlag(message="custom version")
79
+ )
80
+ self.assertEqual(parser.version(), "custom version")
81
+
82
+ def test_fromfile_prefix_chars(self: Self) -> None:
83
+ with tempfile.TemporaryDirectory() as tmpdir:
84
+ fname = os.path.join(tmpdir, "a.txt")
85
+ with open(fname, "w") as f:
86
+ f.write("run\narg1\narg2")
87
+ parser = SuperParser(prog="tool", fromfile_prefix_chars="@")
88
+ parser.add_subCommand(name="run")
89
+ result = parser.parse_args([f"@{fname}"])
90
+ self.assertEqual(result, ["run", "arg1", "arg2"])
91
+
92
+
93
+ if __name__ == "__main__":
94
+ unittest.main()
@@ -0,0 +1,138 @@
1
+ import contextlib
2
+ import io
3
+ import os
4
+ import sys
5
+ import tempfile
6
+ import unittest
7
+ from collections.abc import Callable
8
+ from typing import Self
9
+ from unittest import mock
10
+
11
+ from superparsing import SuperParseError, SuperParser
12
+
13
+
14
+ def capture_stdout(
15
+ func: Callable[..., object],
16
+ *args: object,
17
+ **kwargs: object,
18
+ ) -> tuple[object, str]:
19
+ """Run ``func`` and return (return_value, captured_stdout_str)."""
20
+ buf = io.StringIO()
21
+ with contextlib.redirect_stdout(buf):
22
+ result = func(*args, **kwargs)
23
+ return result, buf.getvalue()
24
+
25
+
26
+ # ---------------------------------------------------------------------------
27
+ # SuperParser: parse_args
28
+ # ---------------------------------------------------------------------------
29
+ class TestParseArgs(unittest.TestCase):
30
+ def make(self: Self) -> SuperParser:
31
+ p = SuperParser(prog="tool")
32
+ p.helpFlag.keys = ["-h", "--help"]
33
+ p.versionFlag.keys = ["-v", "--version"]
34
+ p.versionFlag.message = "tool 1.0"
35
+ p.add_subCommand(name="build", aliases=["b", "mk"], help="build")
36
+ p.add_subCommand(name="clean", help="clean")
37
+ return p
38
+
39
+ def test_plain_subcommand(self: Self) -> None:
40
+ p = self.make()
41
+ self.assertEqual(p.parse_args(["build"]), ["build"])
42
+
43
+ def test_subcommand_with_trailing_args(self: Self) -> None:
44
+ p = self.make()
45
+ self.assertEqual(
46
+ p.parse_args(["build", "--force", "target"]),
47
+ ["build", "--force", "target"],
48
+ )
49
+
50
+ def test_alias_is_rewritten_to_canonical_name(self: Self) -> None:
51
+ p = self.make()
52
+ self.assertEqual(p.parse_args(["b", "x"]), ["build", "x"])
53
+ self.assertEqual(p.parse_args(["mk"]), ["build"])
54
+
55
+ def test_getopt_stops_at_first_positional(self: Self) -> None:
56
+ # flags after the subcommand are passed through, NOT consumed as help
57
+ p = self.make()
58
+ _, out = capture_stdout(p.parse_args, ["build", "-h"])
59
+ # parse_args returns the list (help not triggered)
60
+ self.assertEqual(p.parse_args(["build", "-h"]), ["build", "-h"])
61
+ self.assertEqual(out, "")
62
+
63
+ def test_help_short_flag_prints_and_returns_empty(self: Self) -> None:
64
+ p = self.make()
65
+ result, out = capture_stdout(p.parse_args, ["-h"])
66
+ self.assertEqual(result, [])
67
+ self.assertIn("usage: tool", out)
68
+
69
+ def test_help_long_flag(self: Self) -> None:
70
+ p = self.make()
71
+ result, out = capture_stdout(p.parse_args, ["--help"])
72
+ self.assertEqual(result, [])
73
+ self.assertIn("subcommands:", out)
74
+
75
+ def test_version_flag_prints_and_returns_empty(self: Self) -> None:
76
+ p = self.make()
77
+ result, out = capture_stdout(p.parse_args, ["-v"])
78
+ self.assertEqual(result, [])
79
+ self.assertEqual(out.strip(), "tool 1.0")
80
+
81
+ def test_help_takes_precedence_over_version(self: Self) -> None:
82
+ p = self.make()
83
+ result, out = capture_stdout(p.parse_args, ["-v", "-h"])
84
+ self.assertEqual(result, [])
85
+ self.assertIn("usage:", out)
86
+ self.assertNotIn("tool 1.0", out)
87
+
88
+ def test_missing_subcommand_raises(self: Self) -> None:
89
+ p = self.make()
90
+ with self.assertRaises(SuperParseError) as ctx:
91
+ p.parse_args([])
92
+ self.assertIn("Subcommand missing", str(ctx.exception))
93
+
94
+ def test_unknown_subcommand_raises(self: Self) -> None:
95
+ p = self.make()
96
+ with self.assertRaises(SuperParseError) as ctx:
97
+ p.parse_args(["nope"])
98
+ self.assertIn("unknown", str(ctx.exception))
99
+
100
+ def test_reads_sys_argv_when_args_none(self: Self) -> None:
101
+ p = self.make()
102
+ with mock.patch.object(sys, "argv", ["tool", "clean", "now"]):
103
+ self.assertEqual(p.parse_args(), ["clean", "now"])
104
+
105
+ def test_non_string_args_are_coerced(self: Self) -> None:
106
+ p = self.make()
107
+ # map(str, ...) means numeric tokens are stringified before matching
108
+ self.assertEqual(p.parse_args(["build", 5]), ["build", "5"])
109
+
110
+ def test_fromfile_prefix_expands_file(self: Self) -> None:
111
+ p = self.make()
112
+ p.fromfile_prefix_chars = "@"
113
+ with tempfile.TemporaryDirectory() as d:
114
+ path = os.path.join(d, "args.txt")
115
+ with open(path, "w") as fh:
116
+ fh.write("build\n--flag\nvalue\n")
117
+ self.assertEqual(
118
+ p.parse_args(["@" + path]),
119
+ ["build", "--flag", "value"],
120
+ )
121
+
122
+ def test_fromfile_disabled_by_default(self: Self) -> None:
123
+ # default fromfile_prefix_chars == "" -> "@file" treated literally
124
+ p = self.make()
125
+ with self.assertRaises(SuperParseError):
126
+ # "@whatever" is an unknown subcommand, not a file read
127
+ p.parse_args(["@whatever"])
128
+
129
+ def test_empty_string_arg_is_not_treated_as_file(self: Self) -> None:
130
+ p = self.make()
131
+ p.fromfile_prefix_chars = "@"
132
+ # leading "" must not crash on arg[0]; it's a positional/unknown subcmd
133
+ with self.assertRaises(SuperParseError):
134
+ p.parse_args([""])
135
+
136
+
137
+ if __name__ == "__main__":
138
+ unittest.main(verbosity=2)
@@ -0,0 +1,57 @@
1
+ import contextlib
2
+ import io
3
+ import unittest
4
+ from collections.abc import Callable
5
+ from typing import Self
6
+
7
+ from superparsing import SubCommand
8
+
9
+
10
+ def capture_stdout(
11
+ func: Callable[..., object],
12
+ *args: object,
13
+ **kwargs: object,
14
+ ) -> tuple[object, str]:
15
+ """Run ``func`` and return (return_value, captured_stdout_str)."""
16
+ buf = io.StringIO()
17
+ with contextlib.redirect_stdout(buf):
18
+ result = func(*args, **kwargs)
19
+ return result, buf.getvalue()
20
+
21
+
22
+ # ---------------------------------------------------------------------------
23
+ # SubCommand
24
+ # ---------------------------------------------------------------------------
25
+ class TestSubCommand(unittest.TestCase):
26
+ def test_defaults(self: Self) -> None:
27
+ c = SubCommand(name="run")
28
+ self.assertEqual(c.name, "run")
29
+ self.assertEqual(tuple(c.aliases), ())
30
+ self.assertEqual(c.help, "")
31
+
32
+ def test_usage_no_aliases(self: Self) -> None:
33
+ self.assertEqual(SubCommand(name="run")._usage(), "run")
34
+
35
+ def test_usage_with_aliases(self: Self) -> None:
36
+ c = SubCommand(name="build", aliases=["b", "make"])
37
+ self.assertEqual(c._usage(), "build, b, make")
38
+
39
+ def test_usage_coerces_non_strings(self: Self) -> None:
40
+ c = SubCommand(name=1, aliases=[2, 3])
41
+ self.assertEqual(c._usage(), "1, 2, 3")
42
+
43
+ def test_intro_no_aliases(self: Self) -> None:
44
+ self.assertEqual(SubCommand(name="run").intro(), "run:")
45
+
46
+ def test_intro_with_aliases(self: Self) -> None:
47
+ c = SubCommand(name="build", aliases=["b", "make"])
48
+ self.assertEqual(c.intro(), "build(b, make):")
49
+
50
+ def test_intro_single_alias(self: Self) -> None:
51
+ self.assertEqual(
52
+ SubCommand(name="build", aliases=["b"]).intro(), "build(b):"
53
+ )
54
+
55
+
56
+ if __name__ == "__main__":
57
+ unittest.main(verbosity=2)
@@ -0,0 +1,88 @@
1
+ import contextlib
2
+ import io
3
+ import unittest
4
+ from collections.abc import Callable
5
+ from typing import Self
6
+
7
+ from superparsing import SuperFlag
8
+
9
+
10
+ def capture_stdout(
11
+ func: Callable[..., object],
12
+ *args: object,
13
+ **kwargs: object,
14
+ ) -> tuple[object, str]:
15
+ """Run ``func`` and return (return_value, captured_stdout_str)."""
16
+ buf = io.StringIO()
17
+ with contextlib.redirect_stdout(buf):
18
+ result = func(*args, **kwargs)
19
+ return result, buf.getvalue()
20
+
21
+
22
+ # ---------------------------------------------------------------------------
23
+ # SuperFlag
24
+ # ---------------------------------------------------------------------------
25
+ class TestSuperFlag(unittest.TestCase):
26
+ def test_defaults(self: Self) -> None:
27
+ f = SuperFlag()
28
+ self.assertEqual(tuple(f.keys), ())
29
+ self.assertEqual(f.help, "")
30
+ self.assertIsNone(f.message)
31
+
32
+ def test_keys_stringifies(self: Self) -> None:
33
+ f = SuperFlag(keys=["-h", "--help"])
34
+ self.assertEqual(f._keys(), ["-h", "--help"])
35
+
36
+ def test_keys_stringifies_non_strings(self: Self) -> None:
37
+ # keys is typed Iterable[object]; non-strings must be coerced via str()
38
+ f = SuperFlag(keys=[1, 2.5, None])
39
+ self.assertEqual(f._keys(), ["1", "2.5", "None"])
40
+
41
+ def test_message_none(self: Self) -> None:
42
+ self.assertIsNone(SuperFlag().message)
43
+ self.assertIsNone(SuperFlag()._message())
44
+
45
+ def test_message_coerced(self: Self) -> None:
46
+ self.assertEqual(SuperFlag(message=42)._message(), "42")
47
+ self.assertEqual(SuperFlag(message="hi")._message(), "hi")
48
+
49
+ def test_usage_with_keys(self: Self) -> None:
50
+ self.assertEqual(
51
+ SuperFlag(keys=["-h", "--help"])._usage(), "[-h, --help]"
52
+ )
53
+
54
+ def test_usage_empty(self: Self) -> None:
55
+ self.assertEqual(SuperFlag()._usage(), "")
56
+
57
+ def test_intro_with_keys(self: Self) -> None:
58
+ self.assertEqual(
59
+ SuperFlag(keys=["-h", "--help"]).intro(), "-h, --help:"
60
+ )
61
+
62
+ def test_intro_empty(self: Self) -> None:
63
+ self.assertEqual(SuperFlag().intro(), "")
64
+
65
+ def test_longopts(self: Self) -> None:
66
+ f = SuperFlag(keys=["-h", "--help", "--version"])
67
+ self.assertEqual(f.longopts(), ("help", "version"))
68
+
69
+ def test_longopts_strips_equals(self: Self) -> None:
70
+ f = SuperFlag(keys=["--name=VALUE"])
71
+ self.assertEqual(f.longopts(), ("name",))
72
+
73
+ def test_longopts_ignores_short_and_bare(self: Self) -> None:
74
+ f = SuperFlag(keys=["-h", "h", "help", "--x"])
75
+ self.assertEqual(f.longopts(), ("x",))
76
+
77
+ def test_shortopts(self: Self) -> None:
78
+ f = SuperFlag(keys=["-h", "-v", "--help"])
79
+ self.assertEqual(f.shortopts(), "hv")
80
+
81
+ def test_shortopts_ignores_long_and_malformed(self: Self) -> None:
82
+ f = SuperFlag(keys=["--help", "-ab", "x", "-"])
83
+ # only exactly "-X" (len 2, leading dash) counts
84
+ self.assertEqual(f.shortopts(), "")
85
+
86
+
87
+ if __name__ == "__main__":
88
+ unittest.main(verbosity=2)
@@ -0,0 +1,53 @@
1
+ import contextlib
2
+ import io
3
+ import unittest
4
+ from collections.abc import Callable
5
+ from typing import Self
6
+
7
+ from superparsing import SubCommand, SuperParser
8
+
9
+
10
+ def capture_stdout(
11
+ func: Callable[..., object],
12
+ *args: object,
13
+ **kwargs: object,
14
+ ) -> tuple[object, str]:
15
+ """Run ``func`` and return (return_value, captured_stdout_str)."""
16
+ buf = io.StringIO()
17
+ with contextlib.redirect_stdout(buf):
18
+ result = func(*args, **kwargs)
19
+ return result, buf.getvalue()
20
+
21
+
22
+ class TestSuperParserBasics(unittest.TestCase):
23
+ def test_default_flags_have_help_text(self: Self) -> None:
24
+ p = SuperParser()
25
+ self.assertEqual(p.helpFlag.help, "print this message and exit")
26
+ self.assertEqual(p.versionFlag.help, "print version and exit")
27
+ # default flags carry no keys until the user assigns them
28
+ self.assertEqual(tuple(p.helpFlag.keys), ())
29
+ self.assertEqual(tuple(p.versionFlag.keys), ())
30
+
31
+ def test_instances_do_not_share_mutable_state(self: Self) -> None:
32
+ # default_factory must produce fresh objects per instance
33
+ p1 = SuperParser()
34
+ p2 = SuperParser()
35
+ self.assertIsNot(p1.helpFlag, p2.helpFlag)
36
+ self.assertIsNot(p1.versionFlag, p2.versionFlag)
37
+ self.assertIsNot(p1.subCommands, p2.subCommands)
38
+ p1.subCommands.append(SubCommand(name="x"))
39
+ self.assertEqual(p2.subCommands, [])
40
+
41
+ def test_add_subCommand_returns_and_appends(self: Self) -> None:
42
+ p = SuperParser()
43
+ c = p.add_subCommand(name="run", aliases=["r"], help="go")
44
+ self.assertIsInstance(c, SubCommand)
45
+ self.assertIs(p.subCommands[-1], c)
46
+ self.assertEqual(c.name, "run")
47
+
48
+ def test_subcommands_message_none_when_empty(self: Self) -> None:
49
+ self.assertIsNone(SuperParser()._subCommands_message())
50
+
51
+
52
+ if __name__ == "__main__":
53
+ unittest.main(verbosity=2)
@@ -0,0 +1,81 @@
1
+ import contextlib
2
+ import io
3
+ import unittest
4
+ from collections.abc import Callable
5
+ from typing import Self
6
+
7
+ from superparsing import SuperParser
8
+
9
+
10
+ def capture_stdout(
11
+ func: Callable[..., object],
12
+ *args: object,
13
+ **kwargs: object,
14
+ ) -> tuple[object, str]:
15
+ """Run ``func`` and return (return_value, captured_stdout_str)."""
16
+ buf = io.StringIO()
17
+ with contextlib.redirect_stdout(buf):
18
+ result = func(*args, **kwargs)
19
+ return result, buf.getvalue()
20
+
21
+
22
+ # ---------------------------------------------------------------------------
23
+ # SuperParser: usage / help / version text
24
+ # ---------------------------------------------------------------------------
25
+ class TestSuperParserText(unittest.TestCase):
26
+ def make(self: Self) -> SuperParser:
27
+ p = SuperParser(prog="tool")
28
+ p.helpFlag.keys = ["-h", "--help"]
29
+ p.versionFlag.keys = ["-v", "--version"]
30
+ p.add_subCommand(name="build", aliases=["b"], help="build the project")
31
+ p.add_subCommand(name="clean", help="remove artifacts")
32
+ return p
33
+
34
+ def test_usage_full(self: Self) -> None:
35
+ p = self.make()
36
+ self.assertEqual(
37
+ p.usage(),
38
+ "usage: tool [-h, --help] [-v, --version] {build, b, clean} [arg ...]",
39
+ )
40
+
41
+ def test_usage_no_flags_no_subcommands(self: Self) -> None:
42
+ p = SuperParser(prog="tool")
43
+ self.assertEqual(p.usage(), "usage: tool {} [arg ...]")
44
+
45
+ def test_help_contains_all_sections(self: Self) -> None:
46
+ p = self.make()
47
+ h = p.help()
48
+ self.assertTrue(h.startswith("usage: tool"))
49
+ self.assertIn("keys:", h)
50
+ self.assertIn("subcommands:", h)
51
+ self.assertIn("args:", h)
52
+ # sections separated by blank lines
53
+ self.assertIn("\n\n", h)
54
+
55
+ def test_help_message_override(self: Self) -> None:
56
+ p = self.make()
57
+ p.helpFlag.message = "custom help blob"
58
+ self.assertEqual(p.help(), "custom help blob")
59
+
60
+ def test_help_omits_missing_sections(self: Self) -> None:
61
+ # no flags, no subcommands -> only usage + args sections
62
+ p = SuperParser(prog="tool")
63
+ h = p.help()
64
+ self.assertIn("usage:", h)
65
+ self.assertIn("args:", h)
66
+ self.assertNotIn("keys:", h)
67
+ self.assertNotIn("subcommands:", h)
68
+
69
+ def test_version_from_message(self: Self) -> None:
70
+ p = SuperParser(prog="tool")
71
+ p.versionFlag.message = "tool 2.3.4"
72
+ self.assertEqual(p.version(), "tool 2.3.4")
73
+
74
+ def test_version_message_coerced(self: Self) -> None:
75
+ p = SuperParser(prog="tool")
76
+ p.versionFlag.message = 99
77
+ self.assertEqual(p.version(), "99")
78
+
79
+
80
+ if __name__ == "__main__":
81
+ unittest.main(verbosity=2)
@@ -0,0 +1,29 @@
1
+ import unittest
2
+ from typing import Self
3
+ from unittest.mock import MagicMock, patch
4
+
5
+ from superparsing import SuperFlag, SuperParser
6
+
7
+
8
+ class TestSuperParser(unittest.TestCase):
9
+
10
+ def setUp(self: Self) -> None:
11
+ self.parser = SuperParser(
12
+ prog="tool",
13
+ helpFlag=SuperFlag(keys=["-h", "--help"]),
14
+ versionFlag=SuperFlag(keys=["-v", "--version"]),
15
+ )
16
+ self.parser.add_subCommand(
17
+ name="run", aliases=["r"], help="run command"
18
+ )
19
+ self.parser.add_subCommand(name="test", help="test command")
20
+
21
+ @patch("importlib.metadata.version")
22
+ def test_version_default(self: Self, mock_version: MagicMock) -> None:
23
+ mock_version.return_value = "2.0.0"
24
+ parser = SuperParser(prog="tool")
25
+ self.assertEqual(parser.version(), "tool, version 2.0.0")
26
+
27
+
28
+ if __name__ == "__main__":
29
+ unittest.main()
@@ -1,85 +0,0 @@
1
- import argparse
2
- import json
3
- import subprocess
4
- from typing import Any, Optional
5
-
6
- __all__ = ["main", "run"]
7
-
8
-
9
- def env_create(env: str, python: Optional[str]) -> subprocess.CompletedProcess: # type: ignore[type-arg]
10
- args: list[str]
11
- args = [
12
- "conda",
13
- "create",
14
- "--name",
15
- env,
16
- "--yes",
17
- "--channel",
18
- "conda-forge",
19
- "--override-channels",
20
- ]
21
- if python is not None:
22
- args.append("python=" + python)
23
- return subprocess.run(
24
- args,
25
- check=True,
26
- capture_output=True,
27
- text=True,
28
- )
29
-
30
-
31
- def env_list() -> list[str]:
32
- ans: list[str]
33
- data: Any
34
- result: subprocess.CompletedProcess # type: ignore[type-arg]
35
- try:
36
- result = subprocess.run(
37
- ["conda", "env", "list", "--json"],
38
- check=True,
39
- capture_output=True,
40
- text=True,
41
- )
42
- except (subprocess.CalledProcessError, FileNotFoundError):
43
- return []
44
- data = json.loads(result.stdout)
45
- ans = list()
46
- for details in data["envs_details"].values():
47
- ans.append(str(details["name"]))
48
- return ans
49
-
50
-
51
- def env_remove(env: str) -> subprocess.CompletedProcess: # type: ignore[type-arg]
52
- args: list[str]
53
- args = ["conda", "env", "remove", "-y", "-n", env]
54
- return subprocess.run(args, check=True)
55
-
56
-
57
- def main(args: Optional[list[str]] = None, /) -> None:
58
- kwargs: dict[str, Any]
59
- parser: argparse.ArgumentParser
60
- parser = argparse.ArgumentParser(fromfile_prefix_chars="@")
61
- # parser.add_argument("--errfile", default="-")
62
- # parser.add_argument("--outfile", default="-")
63
- parser.add_argument("--python")
64
- parser.add_argument("--recreate", action="store_true")
65
- parser.add_argument("envs", default=[], nargs="*")
66
- kwargs = vars(parser.parse_args(args))
67
- run(*kwargs.pop("envs"), **kwargs)
68
-
69
-
70
- def run(
71
- *envs: str, python: Optional[str] = None, recreate: bool = False
72
- ) -> None:
73
- env: str
74
- envs_: list[str]
75
- envs_ = env_list()
76
- for env in envs:
77
- if env in envs_ and not recreate:
78
- continue
79
- if env in envs_:
80
- env_remove(env)
81
- env_create(env, python=python)
82
-
83
-
84
- if __name__ == "__main__":
85
- main()
@@ -1,12 +0,0 @@
1
- LICENSE.txt
2
- MANIFEST.in
3
- README.rst
4
- pyproject.toml
5
- run_tests.py
6
- setup.cfg
7
- make/env.py
8
- superparsing.egg-info/PKG-INFO
9
- superparsing.egg-info/SOURCES.txt
10
- superparsing.egg-info/dependency_links.txt
11
- superparsing.egg-info/top_level.txt
12
- tests/test_1984.py
@@ -1,10 +0,0 @@
1
- import unittest
2
-
3
-
4
- class Test1984(unittest.TestCase):
5
- def test_two_plus_two(self):
6
- self.assertEqual(2 + 2, 4)
7
-
8
-
9
- if __name__ == "__main__":
10
- unittest.main()