PythonIota 1.2.0__py3-none-any.whl → 1.7.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.
pythoniota/__init__.py CHANGED
@@ -3,12 +3,14 @@ from pythoniota._bitflag import BitFlag
3
3
  from pythoniota._safe_eval import safe_eval
4
4
  from pythoniota.enum import (
5
5
  IotaEnum, IotaBitFlags, IotaStringEnum,
6
- FlagScope, iota_enum, _EnumMemberProxy,
6
+ FlagScope, iota_enum, unique, _EnumMemberProxy,
7
7
  )
8
8
  from pythoniota.sequence import IotaSequence as iota
9
9
  from pythoniota import recipes
10
+ from pythoniota import regex
11
+ from pythoniota import cli
10
12
 
11
- __version__ = "1.2.0"
13
+ __version__ = "1.7.0"
12
14
  __all__ = [
13
15
  "Iota",
14
16
  "IotaEnum",
@@ -18,6 +20,9 @@ __all__ = [
18
20
  "BitFlag",
19
21
  "FlagScope",
20
22
  "iota_enum",
23
+ "unique",
21
24
  "safe_eval",
22
25
  "recipes",
26
+ "regex",
27
+ "cli",
23
28
  ]
@@ -0,0 +1,27 @@
1
+ """A from-scratch, zero-dependency command-line argument parser.
2
+
3
+ A small, faithful subset of the standard library's ``argparse``:
4
+
5
+ from pythoniota import cli
6
+
7
+ p = cli.ArgumentParser(prog="greet", description="say hello")
8
+ p.add_argument("name")
9
+ p.add_argument("-n", "--times", type=int, default=1)
10
+ p.add_argument("-v", "--verbose", action="store_true")
11
+ args = p.parse_args(["world", "--times", "3", "-v"])
12
+ args.name, args.times, args.verbose # -> ('world', 3, True)
13
+
14
+ Supported: positional and optional arguments, ``nargs`` (``N`` / ``?`` / ``*``
15
+ / ``+``), ``type`` / ``choices`` / ``default`` / ``required``, actions
16
+ ``store`` / ``store_true`` / ``store_false`` / ``store_const`` / ``append`` /
17
+ ``count``, ``--opt=value`` and clustered short flags (``-abc``), the ``--``
18
+ end-of-options marker, an auto-generated ``-h/--help``, and sub-commands via
19
+ ``add_subparsers``. Bad user input prints usage and raises ``SystemExit``.
20
+ """
21
+
22
+ from ._argument import Argument
23
+ from ._errors import ArgumentError
24
+ from ._namespace import Namespace
25
+ from ._parser import ArgumentParser
26
+
27
+ __all__ = ["ArgumentParser", "Namespace", "Argument", "ArgumentError"]
@@ -0,0 +1,70 @@
1
+ """A single argument specification (the result of one ``add_argument`` call)."""
2
+
3
+ INF = float("inf")
4
+
5
+ # Actions that consume no value from the command line.
6
+ _ZERO_ACTIONS = ("store_true", "store_false", "store_const", "count", "help")
7
+
8
+
9
+ class Argument:
10
+ def __init__(self, option_strings, dest, action, nargs, const, default,
11
+ type, choices, required, metavar, help):
12
+ self.option_strings = option_strings # [] for positionals
13
+ self.dest = dest
14
+ self.action = action
15
+ self.nargs = nargs
16
+ self.const = const
17
+ self.default = default
18
+ self.type = type
19
+ self.choices = choices
20
+ self.required = required
21
+ self.metavar = metavar
22
+ self.help = help
23
+
24
+ @property
25
+ def is_positional(self):
26
+ return not self.option_strings
27
+
28
+ def takes_value(self):
29
+ return self.action not in _ZERO_ACTIONS
30
+
31
+ def get_default(self):
32
+ if self.action == "store_true":
33
+ return False
34
+ if self.action == "store_false":
35
+ return True
36
+ return self.default
37
+
38
+ def count_range(self):
39
+ """(min, max) number of command-line tokens this argument consumes."""
40
+ if not self.takes_value():
41
+ return (0, 0)
42
+ n = self.nargs
43
+ if n is None:
44
+ return (1, 1)
45
+ if n == "?":
46
+ return (0, 1)
47
+ if n == "*":
48
+ return (0, INF)
49
+ if n == "+":
50
+ return (1, INF)
51
+ return (n, n) # an int
52
+
53
+ def display_name(self):
54
+ return "/".join(self.option_strings) if self.option_strings else self.metavar
55
+
56
+ def convert_one(self, raw, parser):
57
+ if self.type is not None:
58
+ try:
59
+ value = self.type(raw)
60
+ except (ValueError, TypeError):
61
+ tname = getattr(self.type, "__name__", str(self.type))
62
+ parser.error(f"argument {self.display_name()}: "
63
+ f"invalid {tname} value: {raw!r}")
64
+ else:
65
+ value = raw
66
+ if self.choices is not None and value not in self.choices:
67
+ choices = ", ".join(repr(c) for c in self.choices)
68
+ parser.error(f"argument {self.display_name()}: "
69
+ f"invalid choice: {value!r} (choose from {choices})")
70
+ return value
@@ -0,0 +1,9 @@
1
+ """Exceptions raised while *building* a parser (bad add_argument calls).
2
+
3
+ Parse-time problems (bad user input) go through ``ArgumentParser.error`` and
4
+ raise ``SystemExit`` instead, matching the standard library.
5
+ """
6
+
7
+
8
+ class ArgumentError(Exception):
9
+ """A malformed argument specification."""
@@ -0,0 +1,93 @@
1
+ """Usage/help text generation. Layout is clean and readable; it deliberately
2
+ does not try to reproduce the standard library's column math byte-for-byte."""
3
+
4
+
5
+ def _value_pattern(arg):
6
+ """The ``METAVAR`` portion of an argument's usage, honoring nargs."""
7
+ if not arg.takes_value():
8
+ return ""
9
+ m, n = arg.metavar, arg.nargs
10
+ if n is None:
11
+ return m
12
+ if n == "?":
13
+ return f"[{m}]"
14
+ if n == "*":
15
+ return f"[{m} ...]"
16
+ if n == "+":
17
+ return f"{m} [{m} ...]"
18
+ return " ".join([m] * n) # an int
19
+
20
+
21
+ def _optional_usage(arg):
22
+ flag = arg.option_strings[0]
23
+ value = _value_pattern(arg)
24
+ piece = f"{flag} {value}" if value else flag
25
+ return piece if arg.required else f"[{piece}]"
26
+
27
+
28
+ def _positional_usage(arg):
29
+ m = arg.metavar
30
+ n = arg.nargs
31
+ if n == "?":
32
+ return f"[{m}]"
33
+ if n == "*":
34
+ return f"[{m} ...]"
35
+ if n == "+":
36
+ return f"{m} [{m} ...]"
37
+ if isinstance(n, int):
38
+ return " ".join([m] * n)
39
+ return m
40
+
41
+
42
+ def format_usage(parser):
43
+ parts = [f"usage: {parser.prog}"]
44
+ for arg in parser._arguments:
45
+ if arg.option_strings:
46
+ parts.append(_optional_usage(arg))
47
+ for arg in parser._arguments:
48
+ if not arg.option_strings:
49
+ parts.append(_positional_usage(arg))
50
+ if parser._subparsers is not None:
51
+ names = ",".join(parser._subparsers.choices)
52
+ parts.append(f"{{{names}}} ...")
53
+ return " ".join(parts) + "\n"
54
+
55
+
56
+ def _row(invocation, help):
57
+ if not help:
58
+ return f" {invocation}\n"
59
+ if len(invocation) <= 20:
60
+ return f" {invocation:<20} {help}\n"
61
+ return f" {invocation}\n{'':<24}{help}\n"
62
+
63
+
64
+ def format_help(parser):
65
+ out = [format_usage(parser)]
66
+ if parser.description:
67
+ out.append("\n" + parser.description + "\n")
68
+
69
+ positionals = [a for a in parser._arguments if not a.option_strings]
70
+ optionals = [a for a in parser._arguments if a.option_strings]
71
+
72
+ if positionals or parser._subparsers is not None:
73
+ out.append("\npositional arguments:\n")
74
+ for arg in positionals:
75
+ out.append(_row(arg.metavar, arg.help))
76
+ if parser._subparsers is not None:
77
+ sp = parser._subparsers
78
+ out.append(_row("{" + ",".join(sp.choices) + "}", sp.help))
79
+ for name in sp.choices:
80
+ out.append(_row(" " + name, sp._help.get(name)))
81
+
82
+ if optionals:
83
+ out.append("\noptions:\n")
84
+ for arg in optionals:
85
+ invocation = ", ".join(arg.option_strings)
86
+ value = _value_pattern(arg)
87
+ if value:
88
+ invocation += " " + value
89
+ out.append(_row(invocation, arg.help))
90
+
91
+ if parser.epilog:
92
+ out.append("\n" + parser.epilog + "\n")
93
+ return "".join(out)
@@ -0,0 +1,17 @@
1
+ """The object ``parse_args`` fills in — attribute access over parsed values."""
2
+
3
+
4
+ class Namespace:
5
+ def __init__(self, **kwargs):
6
+ for key, value in kwargs.items():
7
+ setattr(self, key, value)
8
+
9
+ def __eq__(self, other):
10
+ return isinstance(other, Namespace) and vars(self) == vars(other)
11
+
12
+ def __contains__(self, key):
13
+ return key in self.__dict__
14
+
15
+ def __repr__(self):
16
+ items = ", ".join(f"{k}={v!r}" for k, v in self.__dict__.items())
17
+ return f"Namespace({items})"
@@ -0,0 +1,321 @@
1
+ """A from-scratch, ``argparse``-style command-line parser.
2
+
3
+ The parser turns ``add_argument`` calls into :class:`Argument` specs, then does
4
+ a single left-to-right scan of ``argv``: options are consumed as they appear,
5
+ leftover tokens are matched against the positional specs by their (min, max)
6
+ counts. Errors print a usage line and raise ``SystemExit(2)`` — same contract
7
+ as the standard library, so it drops into scripts and tests unchanged.
8
+ """
9
+
10
+ import os
11
+ import sys
12
+
13
+ from ._argument import Argument, INF
14
+ from ._errors import ArgumentError
15
+ from ._namespace import Namespace
16
+ from . import _formatter
17
+
18
+
19
+ class _SubParsers:
20
+ """The object returned by ``add_subparsers``; holds the command table."""
21
+
22
+ def __init__(self, parent, dest, required, help, metavar):
23
+ self.parent = parent
24
+ self.dest = dest
25
+ self.required = required
26
+ self.help = help
27
+ self.metavar = metavar
28
+ self.choices = {} # name -> ArgumentParser
29
+ self._help = {} # name -> help string
30
+
31
+ def add_parser(self, name, help=None, **kwargs):
32
+ kwargs.setdefault("prog", f"{self.parent.prog} {name}")
33
+ parser = ArgumentParser(**kwargs)
34
+ self.choices[name] = parser
35
+ self._help[name] = help
36
+ return parser
37
+
38
+
39
+ class ArgumentParser:
40
+ def __init__(self, prog=None, description=None, epilog=None,
41
+ add_help=True, prefix_chars="-"):
42
+ self.prog = prog or os.path.basename(sys.argv[0] or "prog")
43
+ self.description = description
44
+ self.epilog = epilog
45
+ self.prefix = prefix_chars[0]
46
+ self._arguments = []
47
+ self._optionmap = {} # option string -> Argument
48
+ self._subparsers = None
49
+ self._seen = set()
50
+ if add_help:
51
+ self.add_argument(self.prefix + "h", self.prefix * 2 + "help",
52
+ action="help", help="show this help message and exit")
53
+
54
+ # -- building ----------------------------------------------------------- #
55
+ def add_argument(self, *names, **kwargs):
56
+ action = kwargs.get("action", "store")
57
+ if names and names[0].startswith(self.prefix):
58
+ option_strings = list(names)
59
+ dest = kwargs.get("dest")
60
+ if dest is None:
61
+ longs = [n for n in names if n.startswith(self.prefix * 2)]
62
+ base = (longs[0] if longs else names[0]).lstrip(self.prefix)
63
+ dest = base.replace("-", "_")
64
+ required = kwargs.get("required", False)
65
+ else:
66
+ if len(names) != 1:
67
+ raise ArgumentError("positional arguments take a single name")
68
+ option_strings = []
69
+ dest = kwargs.get("dest", names[0])
70
+ required = kwargs.get("nargs") not in ("?", "*")
71
+
72
+ const = kwargs.get("const")
73
+ if action == "store_const" and const is None:
74
+ raise ArgumentError("action 'store_const' requires 'const='")
75
+
76
+ metavar = kwargs.get("metavar")
77
+ if metavar is None:
78
+ metavar = dest.upper() if option_strings else dest
79
+
80
+ arg = Argument(
81
+ option_strings=option_strings,
82
+ dest=dest,
83
+ action=action,
84
+ nargs=kwargs.get("nargs"),
85
+ const=const,
86
+ default=kwargs.get("default"),
87
+ type=kwargs.get("type"),
88
+ choices=kwargs.get("choices"),
89
+ required=required,
90
+ metavar=metavar,
91
+ help=kwargs.get("help"),
92
+ )
93
+ self._arguments.append(arg)
94
+ for opt in option_strings:
95
+ self._optionmap[opt] = arg
96
+ return arg
97
+
98
+ def add_subparsers(self, dest=None, required=False, help=None, metavar=None):
99
+ if self._subparsers is not None:
100
+ raise ArgumentError("cannot have multiple subparser sets")
101
+ self._subparsers = _SubParsers(self, dest, required, help, metavar)
102
+ return self._subparsers
103
+
104
+ # -- parsing ------------------------------------------------------------ #
105
+ def parse_args(self, argv=None, namespace=None):
106
+ ns, extras = self.parse_known_args(argv, namespace)
107
+ if extras:
108
+ self.error("unrecognized arguments: " + " ".join(extras))
109
+ return ns
110
+
111
+ def parse_known_args(self, argv=None, namespace=None):
112
+ if argv is None:
113
+ argv = sys.argv[1:]
114
+ argv = list(argv)
115
+ if namespace is None:
116
+ namespace = Namespace()
117
+ self._seen = set()
118
+
119
+ for arg in self._arguments:
120
+ if arg.action != "help" and not hasattr(namespace, arg.dest):
121
+ setattr(namespace, arg.dest, arg.get_default())
122
+
123
+ extras = [] # (original_index, token)
124
+ if self._subparsers is not None:
125
+ return self._parse_with_subparsers(argv, namespace, extras)
126
+
127
+ positionals_raw = [] # (original_index, token)
128
+ i, after_ddash = 0, False
129
+ while i < len(argv):
130
+ tok = argv[i]
131
+ if not after_ddash and tok == self.prefix * 2:
132
+ after_ddash = True
133
+ i += 1
134
+ elif not after_ddash and self._is_option(tok):
135
+ i = self._consume_optional(argv, i, namespace, extras)
136
+ else:
137
+ positionals_raw.append((i, tok))
138
+ i += 1
139
+
140
+ self._match_positionals(positionals_raw, namespace, extras)
141
+ self._check_required(namespace)
142
+ return namespace, self._order_extras(extras)
143
+
144
+ # -- option scanning ---------------------------------------------------- #
145
+ def _is_option(self, tok):
146
+ if not tok.startswith(self.prefix) or tok == self.prefix:
147
+ return False
148
+ if tok.startswith(self.prefix * 2):
149
+ return True
150
+ return not tok[1].isdigit() # '-5' is a value, '-x' an option
151
+
152
+ def _consume_optional(self, argv, i, namespace, extras):
153
+ tok = argv[i]
154
+ if tok.startswith(self.prefix * 2):
155
+ name, eq, val = tok.partition("=")
156
+ arg = self._optionmap.get(name)
157
+ if arg is None:
158
+ extras.append((i, tok))
159
+ return i + 1
160
+ return self._apply_option(arg, name, val if eq else None,
161
+ argv, i + 1, namespace)
162
+
163
+ # short cluster, e.g. -v, -vvv, -abc, -o value, -ovalue
164
+ j = 1
165
+ while j < len(tok):
166
+ oname = self.prefix + tok[j]
167
+ arg = self._optionmap.get(oname)
168
+ if arg is None:
169
+ extras.append((i, tok))
170
+ return i + 1
171
+ if not arg.takes_value():
172
+ self._store_zero(arg, oname, namespace)
173
+ j += 1
174
+ continue
175
+ rest = tok[j + 1:]
176
+ return self._apply_option(arg, oname, rest or None,
177
+ argv, i + 1, namespace)
178
+ return i + 1
179
+
180
+ def _apply_option(self, arg, name, explicit, argv, i, namespace):
181
+ if not arg.takes_value():
182
+ if explicit is not None:
183
+ self.error(f"argument {name}: ignored explicit argument {explicit!r}")
184
+ self._store_zero(arg, name, namespace)
185
+ return i
186
+
187
+ lo, hi = arg.count_range()
188
+ values = []
189
+ if explicit is not None:
190
+ values.append(explicit)
191
+ while len(values) < hi and i < len(argv):
192
+ nxt = argv[i]
193
+ if nxt == self.prefix * 2 or self._is_option(nxt):
194
+ break
195
+ values.append(nxt)
196
+ i += 1
197
+ if len(values) < lo:
198
+ expected = f"at least {lo}" if hi == INF else str(lo)
199
+ self.error(f"argument {name}: expected {expected} argument(s)")
200
+ self._store_values(arg, values, namespace)
201
+ return i
202
+
203
+ # -- storing ------------------------------------------------------------ #
204
+ def _store_zero(self, arg, name, namespace):
205
+ if arg.action == "help":
206
+ self.print_help()
207
+ self.exit(0)
208
+ elif arg.action == "store_true":
209
+ setattr(namespace, arg.dest, True)
210
+ elif arg.action == "store_false":
211
+ setattr(namespace, arg.dest, False)
212
+ elif arg.action == "store_const":
213
+ setattr(namespace, arg.dest, arg.const)
214
+ elif arg.action == "count":
215
+ current = getattr(namespace, arg.dest, None) or 0
216
+ setattr(namespace, arg.dest, current + 1)
217
+ self._seen.add(arg.dest)
218
+
219
+ def _store_values(self, arg, raw_values, namespace):
220
+ converted = [arg.convert_one(v, self) for v in raw_values]
221
+ if arg.nargs is None:
222
+ value = converted[0]
223
+ elif arg.nargs == "?":
224
+ if converted:
225
+ value = converted[0]
226
+ else:
227
+ value = arg.const if arg.option_strings else arg.default
228
+ else: # int, '*', '+'
229
+ value = converted
230
+
231
+ if arg.action == "append":
232
+ current = getattr(namespace, arg.dest, None)
233
+ lst = list(current) if current else []
234
+ lst.append(value)
235
+ setattr(namespace, arg.dest, lst)
236
+ else:
237
+ setattr(namespace, arg.dest, value)
238
+ self._seen.add(arg.dest)
239
+
240
+ def _match_positionals(self, pairs, namespace, extras):
241
+ positionals = [a for a in self._arguments if a.is_positional]
242
+ toks = [t for _, t in pairs]
243
+ idx, n = 0, len(toks)
244
+ for k, arg in enumerate(positionals):
245
+ lo, hi = arg.count_range()
246
+ remaining_min = sum(p.count_range()[0] for p in positionals[k + 1:])
247
+ avail = n - idx - remaining_min
248
+ if avail < lo:
249
+ self.error("the following arguments are required: " + arg.metavar)
250
+ take = int(min(hi, avail))
251
+ chunk = toks[idx:idx + take]
252
+ idx += take
253
+ self._store_values(arg, chunk, namespace)
254
+ if idx < n:
255
+ extras.extend(pairs[idx:])
256
+
257
+ @staticmethod
258
+ def _order_extras(extras):
259
+ return [tok for _, tok in sorted(extras, key=lambda pair: pair[0])]
260
+
261
+ def _check_required(self, namespace):
262
+ missing = [a.option_strings[0] for a in self._arguments
263
+ if a.option_strings and a.required and a.dest not in self._seen]
264
+ if missing:
265
+ self.error("the following arguments are required: " + ", ".join(missing))
266
+
267
+ # -- subcommands -------------------------------------------------------- #
268
+ def _parse_with_subparsers(self, argv, namespace, extras):
269
+ sp = self._subparsers
270
+ i, after_ddash = 0, False
271
+ while i < len(argv):
272
+ tok = argv[i]
273
+ if not after_ddash and tok == self.prefix * 2:
274
+ after_ddash = True
275
+ i += 1
276
+ elif not after_ddash and self._is_option(tok):
277
+ i = self._consume_optional(argv, i, namespace, extras)
278
+ else:
279
+ break
280
+ self._check_required(namespace)
281
+
282
+ if i >= len(argv):
283
+ if sp.required:
284
+ self.error("the following arguments are required: "
285
+ + (sp.metavar or sp.dest or "command"))
286
+ return namespace, self._order_extras(extras)
287
+
288
+ command = argv[i]
289
+ if command not in sp.choices:
290
+ choices = ", ".join(repr(c) for c in sp.choices)
291
+ self.error(f"argument command: invalid choice: {command!r} "
292
+ f"(choose from {choices})")
293
+ if sp.dest:
294
+ setattr(namespace, sp.dest, command)
295
+ _, child_extras = sp.choices[command].parse_known_args(argv[i + 1:], namespace)
296
+ return namespace, self._order_extras(extras) + child_extras
297
+
298
+ # -- help / errors ------------------------------------------------------ #
299
+ def format_usage(self):
300
+ return _formatter.format_usage(self)
301
+
302
+ def format_help(self):
303
+ return _formatter.format_help(self)
304
+
305
+ def print_usage(self, file=None):
306
+ (file or sys.stdout).write(self.format_usage())
307
+
308
+ def print_help(self, file=None):
309
+ (file or sys.stdout).write(self.format_help())
310
+
311
+ def error(self, message):
312
+ self.print_usage(sys.stderr)
313
+ self.exit(2, f"{self.prog}: error: {message}\n")
314
+
315
+ def exit(self, status=0, message=None):
316
+ if message:
317
+ sys.stderr.write(message)
318
+ raise SystemExit(status)
319
+
320
+ def __repr__(self):
321
+ return f"ArgumentParser(prog={self.prog!r})"
pythoniota/enum.py CHANGED
@@ -113,20 +113,32 @@ class IotaNamespace(dict): # type: ignore[type-arg]
113
113
  def __init__(self) -> None:
114
114
  super().__init__()
115
115
  self._counter = 0
116
+ self._step = 1
116
117
  self._member_names: list[str] = []
117
118
 
119
+ def _skip(self, n: int = 1) -> None:
120
+ """Advance the iota counter by `n` steps without defining a member (Go's blank `_`)."""
121
+ self._counter += n * self._step
122
+ return None
123
+
118
124
  def __getitem__(self, key: str) -> Any:
119
125
  if key == "iota":
120
126
  val = _IotaCounter(self._counter)
121
- self._counter += 1
127
+ self._counter += self._step
122
128
  return val
129
+ if key == "skip":
130
+ return self._skip
123
131
  return super().__getitem__(key)
124
132
 
125
133
  def __setitem__(self, key: str, value: Any) -> None:
134
+ if key == "_iota_start_":
135
+ self._counter = int(value)
136
+ elif key == "_iota_step_":
137
+ self._step = int(value)
126
138
  super().__setitem__(key, value)
127
139
  if (
128
140
  not key.startswith("_")
129
- and key != "iota"
141
+ and key not in ("iota", "skip")
130
142
  and isinstance(value, (int, float, str, _IotaCounter))
131
143
  ):
132
144
  if key not in self._member_names:
@@ -182,7 +194,7 @@ class IotaEnumMeta(type):
182
194
  members[mname] = flag
183
195
  type.__setattr__(cls, mname, flag)
184
196
  elif is_ordered:
185
- for order, (mname, mval) in builtins_enumerate(members.items()):
197
+ for order, (mname, mval) in enumerate(members.items()):
186
198
  proxy = _EnumMemberProxy(mname, mval, cls, order)
187
199
  members[mname] = proxy
188
200
  type.__setattr__(cls, mname, proxy)
@@ -388,6 +400,14 @@ class IotaEnum(metaclass=IotaEnumMeta):
388
400
  return alias
389
401
  return None
390
402
 
403
+ @classmethod
404
+ def has(cls, name: str) -> bool:
405
+ return name in cls._members_ # type: ignore[attr-defined]
406
+
407
+ @classmethod
408
+ def get(cls, name: str, default: Any = None) -> Any:
409
+ return cls._members_.get(name, default) # type: ignore[attr-defined]
410
+
391
411
  @classmethod
392
412
  def alias(cls, alias_name: str, target_name: str) -> None:
393
413
  if target_name not in cls._members_: # type: ignore[attr-defined]
@@ -397,13 +417,21 @@ class IotaEnum(metaclass=IotaEnumMeta):
397
417
 
398
418
  @classmethod
399
419
  def to_dict(cls) -> dict[str, Any]:
400
- return dict(cls._members_) # type: ignore[attr-defined]
420
+ d = {}
421
+ for k, v in cls._members_.items(): # type: ignore[attr-defined]
422
+ d[k] = v._value if isinstance(v, _EnumMemberProxy) else v
423
+ return d
401
424
 
402
425
  @classmethod
403
426
  def to_json(cls, **kwargs: Any) -> str:
404
427
  d = {}
405
428
  for k, v in cls._members_.items(): # type: ignore[attr-defined]
406
- d[k] = int(v) if isinstance(v, (BitFlag, float)) and not isinstance(v, str) else v
429
+ if isinstance(v, _EnumMemberProxy):
430
+ d[k] = v._value
431
+ elif isinstance(v, BitFlag):
432
+ d[k] = int(v)
433
+ else:
434
+ d[k] = v
407
435
  return json.dumps(d, **kwargs)
408
436
 
409
437
  @classmethod
@@ -443,6 +471,14 @@ class IotaBitFlags(metaclass=IotaEnumMeta):
443
471
  return name
444
472
  return None
445
473
 
474
+ @classmethod
475
+ def has(cls, name: str) -> bool:
476
+ return name in cls._members_ # type: ignore[attr-defined]
477
+
478
+ @classmethod
479
+ def get(cls, name: str, default: Any = None) -> Any:
480
+ return cls._members_.get(name, default) # type: ignore[attr-defined]
481
+
446
482
  @classmethod
447
483
  def to_dict(cls) -> dict[str, int]:
448
484
  return {k: int(v) for k, v in cls._members_.items()} # type: ignore[attr-defined]
@@ -536,5 +572,22 @@ def iota_enum(cls: type | None = None, *, ordered: bool = False) -> Any:
536
572
  return _wrap
537
573
 
538
574
 
539
- import builtins
540
- builtins_enumerate = builtins.enumerate
575
+ def _member_value(val: Any) -> Any:
576
+ if isinstance(val, _EnumMemberProxy):
577
+ return val._value
578
+ if isinstance(val, BitFlag):
579
+ return int(val)
580
+ return val
581
+
582
+
583
+ def unique(cls: type) -> type:
584
+ """Class decorator that rejects an enum with duplicate member values."""
585
+ seen: dict[Any, str] = {}
586
+ for name, val in cls._members_.items(): # type: ignore[attr-defined]
587
+ v = _member_value(val)
588
+ if v in seen:
589
+ raise ValueError(
590
+ f"duplicate value {v!r} in {cls.__name__}: {seen[v]} and {name}"
591
+ )
592
+ seen[v] = name
593
+ return cls