numclassify 0.1.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.
- numclassify/__init__.py +63 -0
- numclassify/__main__.py +4 -0
- numclassify/_core/__init__.py +0 -0
- numclassify/_core/combinatorial.py +392 -0
- numclassify/_core/digital.py +403 -0
- numclassify/_core/divisors.py +756 -0
- numclassify/_core/figurate.py +357 -0
- numclassify/_core/number_theory.py +533 -0
- numclassify/_core/powers.py +349 -0
- numclassify/_core/primes.py +2100 -0
- numclassify/_core/recreational.py +245 -0
- numclassify/_core/sequences.py +488 -0
- numclassify/_registry.py +417 -0
- numclassify/cli.py +525 -0
- numclassify-0.1.0.dist-info/METADATA +220 -0
- numclassify-0.1.0.dist-info/RECORD +19 -0
- numclassify-0.1.0.dist-info/WHEEL +4 -0
- numclassify-0.1.0.dist-info/entry_points.txt +2 -0
- numclassify-0.1.0.dist-info/licenses/LICENSE +21 -0
numclassify/cli.py
ADDED
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
"""
|
|
2
|
+
numclassify.cli
|
|
3
|
+
~~~~~~~~~~~~~~~
|
|
4
|
+
Command-line interface for numclassify.
|
|
5
|
+
|
|
6
|
+
Entry point: ``numclassify`` (configured in pyproject.toml).
|
|
7
|
+
|
|
8
|
+
Commands
|
|
9
|
+
--------
|
|
10
|
+
check <number> [--full] [--json]
|
|
11
|
+
Classify a single number.
|
|
12
|
+
|
|
13
|
+
range <start> <end> [--filter <name>] [--json]
|
|
14
|
+
Inspect a range of integers.
|
|
15
|
+
|
|
16
|
+
find <type_name> [--limit N] [--json]
|
|
17
|
+
Find the first N integers satisfying a named type.
|
|
18
|
+
|
|
19
|
+
info <type_name>
|
|
20
|
+
Show registry metadata for a type.
|
|
21
|
+
|
|
22
|
+
list [--category <cat>]
|
|
23
|
+
List all registered types, optionally filtered by category.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import argparse
|
|
29
|
+
import json
|
|
30
|
+
import sys
|
|
31
|
+
from typing import Any, Dict, List, NoReturn, Optional, Tuple
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
# Terminal colour / Unicode helpers
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
# _USE_COLOR and _USE_UNICODE are set to False at import time; main() may
|
|
38
|
+
# flip them to True after reconfiguring stdout.
|
|
39
|
+
|
|
40
|
+
_USE_COLOR: bool = False
|
|
41
|
+
_USE_UNICODE: bool = False
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _c(text: str, *codes: int) -> str:
|
|
45
|
+
"""Wrap *text* in ANSI escape codes when stdout is a colour TTY."""
|
|
46
|
+
if not _USE_COLOR:
|
|
47
|
+
return text
|
|
48
|
+
seq = ";".join(str(c) for c in codes)
|
|
49
|
+
return f"\033[{seq}m{text}\033[0m"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _bold(text: str) -> str:
|
|
53
|
+
return _c(text, 1)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _green(text: str) -> str:
|
|
57
|
+
return _c(text, 32)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _red(text: str) -> str:
|
|
61
|
+
return _c(text, 31)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _yellow(text: str) -> str:
|
|
65
|
+
return _c(text, 33)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _cyan(text: str) -> str:
|
|
69
|
+
return _c(text, 36)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _check_mark() -> str:
|
|
73
|
+
"""Return ✓ on Unicode-capable TTYs, plain [YES] otherwise."""
|
|
74
|
+
return "✓" if _USE_UNICODE else "[YES]"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _rule(width: int) -> str:
|
|
78
|
+
"""Return a horizontal rule of *width* chars."""
|
|
79
|
+
return ("─" if _USE_UNICODE else "-") * width
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ---------------------------------------------------------------------------
|
|
83
|
+
# Error handling
|
|
84
|
+
# ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
def _die(msg: str, code: int = 1) -> NoReturn:
|
|
87
|
+
"""Print *msg* to stderr and exit with *code*."""
|
|
88
|
+
print(_red(f"Error: {msg}"), file=sys.stderr)
|
|
89
|
+
sys.exit(code)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# ---------------------------------------------------------------------------
|
|
93
|
+
# Lazy imports
|
|
94
|
+
# ---------------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
def _lazy_import() -> Tuple[Any, Dict[str, Any], Any]:
|
|
97
|
+
"""Import numclassify lazily so CLI starts quickly and errors are clean."""
|
|
98
|
+
try:
|
|
99
|
+
import numclassify as nc
|
|
100
|
+
from numclassify._registry import REGISTRY, find_in_range
|
|
101
|
+
return nc, REGISTRY, find_in_range
|
|
102
|
+
except Exception as exc:
|
|
103
|
+
_die(f"Failed to import numclassify: {exc}")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _normalize(name: str) -> str:
|
|
107
|
+
"""Normalise a type name: lower-case, spaces → underscores."""
|
|
108
|
+
return name.strip().lower().replace(" ", "_").replace("-", "_")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _resolve_type(name: str, registry: Dict[str, Any]) -> str:
|
|
112
|
+
"""Return the registry key for *name* or call _die."""
|
|
113
|
+
key = _normalize(name)
|
|
114
|
+
if key not in registry:
|
|
115
|
+
_die(
|
|
116
|
+
f"Unknown type: '{name}'. "
|
|
117
|
+
f"Use 'numclassify list' to see all types."
|
|
118
|
+
)
|
|
119
|
+
return key
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# ---------------------------------------------------------------------------
|
|
123
|
+
# Computed extras for `check`
|
|
124
|
+
# ---------------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
def _computed_extras(n: int) -> Dict[str, int]:
|
|
127
|
+
"""Return a small dict of directly computed values for display."""
|
|
128
|
+
try:
|
|
129
|
+
from numclassify.digital import digit_sum, digital_root
|
|
130
|
+
ds = digit_sum(n)
|
|
131
|
+
dr = digital_root(n)
|
|
132
|
+
except Exception:
|
|
133
|
+
ds = sum(int(d) for d in str(abs(n))) if n != 0 else 0
|
|
134
|
+
dr = ds # fallback
|
|
135
|
+
try:
|
|
136
|
+
from numclassify.divisors import count_divisors
|
|
137
|
+
nd = count_divisors(n)
|
|
138
|
+
except Exception:
|
|
139
|
+
nd = sum(1 for i in range(1, abs(n) + 1) if n % i == 0) if n != 0 else 0
|
|
140
|
+
return {"digit_sum": ds, "digital_root": dr, "num_divisors": nd}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# ---------------------------------------------------------------------------
|
|
144
|
+
# Sub-command handlers
|
|
145
|
+
# ---------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
def cmd_check(args: argparse.Namespace) -> None:
|
|
148
|
+
"""Handle: numclassify check <number> [--full] [--json]."""
|
|
149
|
+
nc, registry, _ = _lazy_import()
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
n = int(args.number)
|
|
153
|
+
except ValueError:
|
|
154
|
+
_die(f"'{args.number}' is not a valid integer.")
|
|
155
|
+
|
|
156
|
+
if args.json:
|
|
157
|
+
true_props = nc.get_true_properties(n)
|
|
158
|
+
false_props = [
|
|
159
|
+
k for k in registry
|
|
160
|
+
if k not in set(_normalize(p) for p in true_props)
|
|
161
|
+
]
|
|
162
|
+
payload = {
|
|
163
|
+
"number": n,
|
|
164
|
+
"true_properties": sorted(true_props),
|
|
165
|
+
"false_properties": sorted(false_props),
|
|
166
|
+
}
|
|
167
|
+
print(json.dumps(payload, indent=2))
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
if args.full:
|
|
171
|
+
nc.print_properties(n)
|
|
172
|
+
return
|
|
173
|
+
|
|
174
|
+
# Default: clean summary
|
|
175
|
+
true_props = nc.get_true_properties(n)
|
|
176
|
+
header = f"Properties of {n}"
|
|
177
|
+
print(_bold(header))
|
|
178
|
+
print(_bold(_rule(len(header))))
|
|
179
|
+
|
|
180
|
+
if not true_props:
|
|
181
|
+
print(" (no registered properties satisfied)")
|
|
182
|
+
else:
|
|
183
|
+
for name in sorted(true_props):
|
|
184
|
+
print(f" {_green(_check_mark())} {name}")
|
|
185
|
+
|
|
186
|
+
# Computed extras
|
|
187
|
+
extras = _computed_extras(n)
|
|
188
|
+
extra_str = " ".join(f"{k}={v}" for k, v in extras.items())
|
|
189
|
+
print()
|
|
190
|
+
print(_cyan(f"Computed: {extra_str}"))
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def cmd_range(args: argparse.Namespace) -> None:
|
|
194
|
+
"""Handle: numclassify range <start> <end> [--filter <name>] [--json]."""
|
|
195
|
+
nc, registry, find_in_range = _lazy_import()
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
start = int(args.start)
|
|
199
|
+
end = int(args.end)
|
|
200
|
+
except ValueError:
|
|
201
|
+
_die("start and end must be integers.")
|
|
202
|
+
|
|
203
|
+
if start > end:
|
|
204
|
+
_die("start must be ≤ end.")
|
|
205
|
+
|
|
206
|
+
span = end - start + 1
|
|
207
|
+
if span > 100_000:
|
|
208
|
+
print(
|
|
209
|
+
_yellow(f"Warning: range has {span} numbers — this may be slow."),
|
|
210
|
+
file=sys.stderr,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
if args.filter:
|
|
214
|
+
key = _resolve_type(args.filter, registry)
|
|
215
|
+
func = registry[key].func
|
|
216
|
+
results = find_in_range(func, start, end)
|
|
217
|
+
|
|
218
|
+
if args.json:
|
|
219
|
+
print(json.dumps(results))
|
|
220
|
+
return
|
|
221
|
+
|
|
222
|
+
label = registry[key].name if hasattr(registry[key], "name") else args.filter
|
|
223
|
+
if results:
|
|
224
|
+
print(
|
|
225
|
+
_bold(f"{label} in range {start}..{end}:")
|
|
226
|
+
+ f" {results}"
|
|
227
|
+
)
|
|
228
|
+
else:
|
|
229
|
+
print(f"No numbers in [{start}, {end}] satisfy '{args.filter}'.")
|
|
230
|
+
else:
|
|
231
|
+
rows: List[Dict[str, Any]] = []
|
|
232
|
+
for n in range(start, end + 1):
|
|
233
|
+
c = nc.count_properties(n)
|
|
234
|
+
rows.append({"number": n, "property_count": c})
|
|
235
|
+
|
|
236
|
+
if args.json:
|
|
237
|
+
print(json.dumps(rows, indent=2))
|
|
238
|
+
return
|
|
239
|
+
|
|
240
|
+
for row in rows:
|
|
241
|
+
n = row["number"]
|
|
242
|
+
c = row["property_count"]
|
|
243
|
+
noun = "property" if c == 1 else "properties"
|
|
244
|
+
print(f" {n}: {c} {noun}")
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def cmd_find(args: argparse.Namespace) -> None:
|
|
248
|
+
"""Handle: numclassify find <type_name> [--limit N] [--json]."""
|
|
249
|
+
nc, registry, find_in_range = _lazy_import()
|
|
250
|
+
|
|
251
|
+
key = _resolve_type(args.type_name, registry)
|
|
252
|
+
limit = args.limit
|
|
253
|
+
if limit < 1:
|
|
254
|
+
_die("--limit must be a positive integer.")
|
|
255
|
+
|
|
256
|
+
func = registry[key].func
|
|
257
|
+
upper = 100_000
|
|
258
|
+
results = find_in_range(func, 0, upper)[:limit]
|
|
259
|
+
|
|
260
|
+
label = registry[key].name if hasattr(registry[key], "name") else args.type_name
|
|
261
|
+
|
|
262
|
+
if args.json:
|
|
263
|
+
print(json.dumps(results))
|
|
264
|
+
return
|
|
265
|
+
|
|
266
|
+
if results:
|
|
267
|
+
print(_bold(f"First {len(results)} {label} numbers:") + f" {results}")
|
|
268
|
+
else:
|
|
269
|
+
print(f"No '{label}' numbers found up to {upper:,}.")
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def cmd_info(args: argparse.Namespace) -> None:
|
|
273
|
+
"""Handle: numclassify info <type_name>."""
|
|
274
|
+
_, registry, _ = _lazy_import()
|
|
275
|
+
|
|
276
|
+
key = _resolve_type(args.type_name, registry)
|
|
277
|
+
entry = registry[key]
|
|
278
|
+
|
|
279
|
+
# Gracefully handle registries that use different attribute names
|
|
280
|
+
name = getattr(entry, "name", key)
|
|
281
|
+
category = getattr(entry, "category", "—")
|
|
282
|
+
oeis = getattr(entry, "oeis", None)
|
|
283
|
+
description = getattr(entry, "description", None)
|
|
284
|
+
example = getattr(entry, "example", None)
|
|
285
|
+
|
|
286
|
+
col_w = 14
|
|
287
|
+
def row(label: str, value: Optional[str]) -> None:
|
|
288
|
+
if value:
|
|
289
|
+
print(f" {_bold(label + ':'): <{col_w + 8}} {value}")
|
|
290
|
+
|
|
291
|
+
print()
|
|
292
|
+
row("Name", name)
|
|
293
|
+
row("Category", str(category).capitalize() if category else None)
|
|
294
|
+
row("OEIS", oeis)
|
|
295
|
+
row("Description", description)
|
|
296
|
+
row("Example", str(example) if example else None)
|
|
297
|
+
print()
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def cmd_list(args: argparse.Namespace) -> None:
|
|
301
|
+
"""Handle: numclassify list [--category <cat>]."""
|
|
302
|
+
_, registry, _ = _lazy_import()
|
|
303
|
+
|
|
304
|
+
# Group entries by category
|
|
305
|
+
from collections import defaultdict
|
|
306
|
+
groups: Dict[str, List[str]] = defaultdict(list)
|
|
307
|
+
|
|
308
|
+
for key, entry in registry.items():
|
|
309
|
+
cat = str(getattr(entry, "category", "other")).upper()
|
|
310
|
+
name = getattr(entry, "name", key)
|
|
311
|
+
groups[cat].append(name)
|
|
312
|
+
|
|
313
|
+
target_cat: Optional[str] = None
|
|
314
|
+
if args.category:
|
|
315
|
+
# Accept any capitalisation; also try appending S so "prime" → "PRIMES"
|
|
316
|
+
needle = args.category.upper().rstrip("S")
|
|
317
|
+
target_cat = next(
|
|
318
|
+
(k for k in groups if k.upper().rstrip("S") == needle),
|
|
319
|
+
None,
|
|
320
|
+
)
|
|
321
|
+
if target_cat is None:
|
|
322
|
+
available = ", ".join(sorted(groups.keys()))
|
|
323
|
+
_die(
|
|
324
|
+
f"Unknown category '{args.category}'. "
|
|
325
|
+
f"Available: {available}"
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
for cat in sorted(groups.keys()):
|
|
329
|
+
if target_cat and cat != target_cat:
|
|
330
|
+
continue
|
|
331
|
+
names = sorted(groups[cat])
|
|
332
|
+
print(_bold(f"\n{cat} ({len(names)} types)"))
|
|
333
|
+
# Print names in a wrapped paragraph style
|
|
334
|
+
line = " "
|
|
335
|
+
for i, n in enumerate(names):
|
|
336
|
+
chunk = n + (", " if i < len(names) - 1 else "")
|
|
337
|
+
if len(line) + len(chunk) > 78:
|
|
338
|
+
print(line.rstrip(", "))
|
|
339
|
+
line = " " + chunk
|
|
340
|
+
else:
|
|
341
|
+
line += chunk
|
|
342
|
+
if line.strip():
|
|
343
|
+
print(line)
|
|
344
|
+
|
|
345
|
+
print()
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
# ---------------------------------------------------------------------------
|
|
349
|
+
# Argument parser
|
|
350
|
+
# ---------------------------------------------------------------------------
|
|
351
|
+
|
|
352
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
353
|
+
parser = argparse.ArgumentParser(
|
|
354
|
+
prog="numclassify",
|
|
355
|
+
description="Number classification toolkit — 3000+ number types.",
|
|
356
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
357
|
+
)
|
|
358
|
+
sub = parser.add_subparsers(dest="command", metavar="COMMAND")
|
|
359
|
+
sub.required = True
|
|
360
|
+
|
|
361
|
+
# -- check ---------------------------------------------------------------
|
|
362
|
+
p_check = sub.add_parser(
|
|
363
|
+
"check",
|
|
364
|
+
help="Classify a single number.",
|
|
365
|
+
description=(
|
|
366
|
+
"Classify a single integer and print all satisfied properties.\n\n"
|
|
367
|
+
"Examples:\n"
|
|
368
|
+
" numclassify check 153\n"
|
|
369
|
+
" numclassify check 153 --full\n"
|
|
370
|
+
" numclassify check 153 --json"
|
|
371
|
+
),
|
|
372
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
373
|
+
)
|
|
374
|
+
p_check.add_argument("number", help="Integer to classify.")
|
|
375
|
+
p_check.add_argument(
|
|
376
|
+
"--full",
|
|
377
|
+
action="store_true",
|
|
378
|
+
help="Print full formatted report (calls print_properties).",
|
|
379
|
+
)
|
|
380
|
+
p_check.add_argument(
|
|
381
|
+
"--json",
|
|
382
|
+
action="store_true",
|
|
383
|
+
help="Output true and false properties as JSON.",
|
|
384
|
+
)
|
|
385
|
+
p_check.set_defaults(func=cmd_check)
|
|
386
|
+
|
|
387
|
+
# -- range ---------------------------------------------------------------
|
|
388
|
+
p_range = sub.add_parser(
|
|
389
|
+
"range",
|
|
390
|
+
help="Inspect a range of integers.",
|
|
391
|
+
description=(
|
|
392
|
+
"Show property counts or filtered matches for a range.\n\n"
|
|
393
|
+
"Examples:\n"
|
|
394
|
+
" numclassify range 1 20\n"
|
|
395
|
+
" numclassify range 1 100 --filter prime\n"
|
|
396
|
+
" numclassify range 1 50 --filter prime --json"
|
|
397
|
+
),
|
|
398
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
399
|
+
)
|
|
400
|
+
p_range.add_argument("start", help="Start of range (inclusive).")
|
|
401
|
+
p_range.add_argument("end", help="End of range (inclusive).")
|
|
402
|
+
p_range.add_argument(
|
|
403
|
+
"--filter",
|
|
404
|
+
metavar="TYPE",
|
|
405
|
+
default=None,
|
|
406
|
+
help="Show only numbers satisfying this type name.",
|
|
407
|
+
)
|
|
408
|
+
p_range.add_argument(
|
|
409
|
+
"--json",
|
|
410
|
+
action="store_true",
|
|
411
|
+
help="Output results as JSON.",
|
|
412
|
+
)
|
|
413
|
+
p_range.set_defaults(func=cmd_range)
|
|
414
|
+
|
|
415
|
+
# -- find ----------------------------------------------------------------
|
|
416
|
+
p_find = sub.add_parser(
|
|
417
|
+
"find",
|
|
418
|
+
help="Find the first N numbers of a given type.",
|
|
419
|
+
description=(
|
|
420
|
+
"Search up to 100 000 for numbers satisfying a named type.\n\n"
|
|
421
|
+
"Examples:\n"
|
|
422
|
+
" numclassify find armstrong\n"
|
|
423
|
+
" numclassify find 'twin prime' --limit 10\n"
|
|
424
|
+
" numclassify find mersenne_prime --json"
|
|
425
|
+
),
|
|
426
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
427
|
+
)
|
|
428
|
+
p_find.add_argument("type_name", help="Registered type name.")
|
|
429
|
+
p_find.add_argument(
|
|
430
|
+
"--limit",
|
|
431
|
+
type=int,
|
|
432
|
+
default=20,
|
|
433
|
+
metavar="N",
|
|
434
|
+
help="How many numbers to find (default: 20).",
|
|
435
|
+
)
|
|
436
|
+
p_find.add_argument(
|
|
437
|
+
"--json",
|
|
438
|
+
action="store_true",
|
|
439
|
+
help="Output results as JSON array.",
|
|
440
|
+
)
|
|
441
|
+
p_find.set_defaults(func=cmd_find)
|
|
442
|
+
|
|
443
|
+
# -- info ----------------------------------------------------------------
|
|
444
|
+
p_info = sub.add_parser(
|
|
445
|
+
"info",
|
|
446
|
+
help="Show metadata for a registered type.",
|
|
447
|
+
description=(
|
|
448
|
+
"Display name, category, OEIS reference, and description.\n\n"
|
|
449
|
+
"Examples:\n"
|
|
450
|
+
" numclassify info armstrong\n"
|
|
451
|
+
" numclassify info 'twin prime'"
|
|
452
|
+
),
|
|
453
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
454
|
+
)
|
|
455
|
+
p_info.add_argument("type_name", help="Registered type name.")
|
|
456
|
+
p_info.set_defaults(func=cmd_info)
|
|
457
|
+
|
|
458
|
+
# -- list ----------------------------------------------------------------
|
|
459
|
+
p_list = sub.add_parser(
|
|
460
|
+
"list",
|
|
461
|
+
help="List all registered types, grouped by category.",
|
|
462
|
+
description=(
|
|
463
|
+
"Print every registered type name, grouped by category.\n\n"
|
|
464
|
+
"Examples:\n"
|
|
465
|
+
" numclassify list\n"
|
|
466
|
+
" numclassify list --category prime"
|
|
467
|
+
),
|
|
468
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
469
|
+
)
|
|
470
|
+
p_list.add_argument(
|
|
471
|
+
"--category",
|
|
472
|
+
metavar="CAT",
|
|
473
|
+
default=None,
|
|
474
|
+
help="Show only this category.",
|
|
475
|
+
)
|
|
476
|
+
p_list.set_defaults(func=cmd_list)
|
|
477
|
+
|
|
478
|
+
return parser
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
# ---------------------------------------------------------------------------
|
|
482
|
+
# Entry point
|
|
483
|
+
# ---------------------------------------------------------------------------
|
|
484
|
+
|
|
485
|
+
def main() -> None:
|
|
486
|
+
"""Entry point for the ``numclassify`` CLI command."""
|
|
487
|
+
import io
|
|
488
|
+
|
|
489
|
+
global _USE_COLOR, _USE_UNICODE # noqa: PLW0603
|
|
490
|
+
|
|
491
|
+
# ------------------------------------------------------------------
|
|
492
|
+
# 1. Reconfigure stdout to UTF-8 so Unicode chars never crash on
|
|
493
|
+
# Windows cp1252 / latin-1 terminals or when output is piped.
|
|
494
|
+
# ------------------------------------------------------------------
|
|
495
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
496
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr]
|
|
497
|
+
elif hasattr(sys.stdout, "buffer"):
|
|
498
|
+
sys.stdout = io.TextIOWrapper(
|
|
499
|
+
sys.stdout.buffer, encoding="utf-8", errors="replace"
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
# ------------------------------------------------------------------
|
|
503
|
+
# 2. Enable colour + Unicode only when writing to a real terminal.
|
|
504
|
+
# Subprocesses/pipes get plain ASCII — no ✓, no ─, no ANSI codes.
|
|
505
|
+
# ------------------------------------------------------------------
|
|
506
|
+
if sys.stdout.isatty():
|
|
507
|
+
_USE_COLOR = True
|
|
508
|
+
_USE_UNICODE = True
|
|
509
|
+
|
|
510
|
+
parser = build_parser()
|
|
511
|
+
args = parser.parse_args()
|
|
512
|
+
|
|
513
|
+
try:
|
|
514
|
+
args.func(args)
|
|
515
|
+
except SystemExit:
|
|
516
|
+
raise
|
|
517
|
+
except KeyboardInterrupt:
|
|
518
|
+
print() # clean newline
|
|
519
|
+
sys.exit(0)
|
|
520
|
+
except Exception as exc:
|
|
521
|
+
_die(str(exc))
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
if __name__ == "__main__":
|
|
525
|
+
main()
|