PyREUser3 0.1.0__tar.gz → 0.2.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 (57) hide show
  1. {pyreuser3-0.1.0 → pyreuser3-0.2.0}/PKG-INFO +1 -1
  2. {pyreuser3-0.1.0 → pyreuser3-0.2.0}/PyREUser3.egg-info/PKG-INFO +1 -1
  3. {pyreuser3-0.1.0 → pyreuser3-0.2.0}/pyproject.toml +1 -1
  4. {pyreuser3-0.1.0 → pyreuser3-0.2.0}/pyreuser3/__init__.py +9 -16
  5. {pyreuser3-0.1.0 → pyreuser3-0.2.0}/pyreuser3/__main__.py +2 -1
  6. pyreuser3-0.2.0/pyreuser3/api.py +411 -0
  7. {pyreuser3-0.1.0 → pyreuser3-0.2.0}/pyreuser3/cli.py +71 -12
  8. pyreuser3-0.2.0/pyreuser3/core.py +410 -0
  9. pyreuser3-0.2.0/pyreuser3/export/__init__.py +9 -0
  10. {pyreuser3-0.1.0 → pyreuser3-0.2.0}/pyreuser3/export/base.py +82 -74
  11. {pyreuser3-0.1.0 → pyreuser3-0.2.0}/pyreuser3/export/enums.py +44 -31
  12. {pyreuser3-0.1.0 → pyreuser3-0.2.0}/pyreuser3/export/fields.py +65 -44
  13. {pyreuser3-0.1.0 → pyreuser3-0.2.0}/pyreuser3/export/metadata.py +107 -74
  14. {pyreuser3-0.1.0 → pyreuser3-0.2.0}/pyreuser3/export/postprocess.py +127 -83
  15. {pyreuser3-0.1.0 → pyreuser3-0.2.0}/pyreuser3/export/tree.py +102 -74
  16. {pyreuser3-0.1.0 → pyreuser3-0.2.0}/pyreuser3/export/user3.py +89 -60
  17. pyreuser3-0.2.0/pyreuser3/pack/__init__.py +9 -0
  18. {pyreuser3-0.1.0 → pyreuser3-0.2.0}/pyreuser3/pack/base.py +115 -82
  19. pyreuser3-0.2.0/pyreuser3/pack/models.py +154 -0
  20. {pyreuser3-0.1.0 → pyreuser3-0.2.0}/pyreuser3/pack/plan.py +220 -158
  21. {pyreuser3-0.1.0 → pyreuser3-0.2.0}/pyreuser3/pack/writer.py +104 -67
  22. pyreuser3-0.2.0/pyreuser3/rich_ui.py +127 -0
  23. pyreuser3-0.2.0/pyreuser3/schema.py +199 -0
  24. pyreuser3-0.2.0/pyreuser3/web/__init__.py +10 -0
  25. pyreuser3-0.2.0/pyreuser3/web/__main__.py +9 -0
  26. pyreuser3-0.2.0/pyreuser3/web/handler.py +208 -0
  27. pyreuser3-0.2.0/pyreuser3/web/jobs.py +271 -0
  28. {pyreuser3-0.1.0 → pyreuser3-0.2.0}/pyreuser3/web/page.py +155 -31
  29. pyreuser3-0.2.0/pyreuser3/web/picker.py +94 -0
  30. pyreuser3-0.2.0/pyreuser3/web/runners.py +267 -0
  31. pyreuser3-0.2.0/pyreuser3/web/server.py +102 -0
  32. pyreuser3-0.2.0/pyreuser3/web/settings.py +45 -0
  33. pyreuser3-0.1.0/pyreuser3/api.py +0 -410
  34. pyreuser3-0.1.0/pyreuser3/core.py +0 -358
  35. pyreuser3-0.1.0/pyreuser3/export/__init__.py +0 -11
  36. pyreuser3-0.1.0/pyreuser3/pack/__init__.py +0 -10
  37. pyreuser3-0.1.0/pyreuser3/pack/models.py +0 -140
  38. pyreuser3-0.1.0/pyreuser3/rich_ui.py +0 -126
  39. pyreuser3-0.1.0/pyreuser3/schema.py +0 -193
  40. pyreuser3-0.1.0/pyreuser3/web/__init__.py +0 -6
  41. pyreuser3-0.1.0/pyreuser3/web/__main__.py +0 -6
  42. pyreuser3-0.1.0/pyreuser3/web/handler.py +0 -178
  43. pyreuser3-0.1.0/pyreuser3/web/jobs.py +0 -243
  44. pyreuser3-0.1.0/pyreuser3/web/picker.py +0 -92
  45. pyreuser3-0.1.0/pyreuser3/web/runners.py +0 -238
  46. pyreuser3-0.1.0/pyreuser3/web/server.py +0 -104
  47. pyreuser3-0.1.0/pyreuser3/web/settings.py +0 -42
  48. {pyreuser3-0.1.0 → pyreuser3-0.2.0}/LICENSE +0 -0
  49. {pyreuser3-0.1.0 → pyreuser3-0.2.0}/MANIFEST.in +0 -0
  50. {pyreuser3-0.1.0 → pyreuser3-0.2.0}/PyREUser3.egg-info/SOURCES.txt +0 -0
  51. {pyreuser3-0.1.0 → pyreuser3-0.2.0}/PyREUser3.egg-info/dependency_links.txt +0 -0
  52. {pyreuser3-0.1.0 → pyreuser3-0.2.0}/PyREUser3.egg-info/entry_points.txt +0 -0
  53. {pyreuser3-0.1.0 → pyreuser3-0.2.0}/PyREUser3.egg-info/requires.txt +0 -0
  54. {pyreuser3-0.1.0 → pyreuser3-0.2.0}/PyREUser3.egg-info/top_level.txt +0 -0
  55. {pyreuser3-0.1.0 → pyreuser3-0.2.0}/README.md +0 -0
  56. {pyreuser3-0.1.0 → pyreuser3-0.2.0}/docs/README.zh-CN.md +0 -0
  57. {pyreuser3-0.1.0 → pyreuser3-0.2.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyREUser3
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Pure Python tools for converting RE Engine .user.3 files to and from JSON.
5
5
  Author: Egg Targaryen
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyREUser3
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Pure Python tools for converting RE Engine .user.3 files to and from JSON.
5
5
  Author: Egg Targaryen
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "PyREUser3"
7
- version = "0.1.0"
7
+ version = "0.2.0"
8
8
  description = "Pure Python tools for converting RE Engine .user.3 files to and from JSON."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -1,8 +1,7 @@
1
- """RE User3 JSON 工具包的公开导出入口。
1
+ """Expose the stable public API while keeping expensive converter modules lazy.
2
2
 
3
- 较重的转换模块会依赖 Rich 等命令行显示库。这里使用惰性导出,
4
- `pyreuser3-web --help` `python -m pyreuser3.web --help` 这类轻量入口
5
- 不需要提前加载完整导出器或封包器。
3
+ The package-level __getattr__ hook avoids importing Rich and binary conversion code for
4
+ lightweight operations such as version checks and Web UI help output.
6
5
  """
7
6
 
8
7
  from __future__ import annotations
@@ -40,25 +39,19 @@ _EXPORT_MODULES = {
40
39
 
41
40
 
42
41
  def __getattr__(name: str) -> Any:
43
- """首次访问公开名称时再惰性导入对应模块。
42
+ """Resolve a lazily exported package attribute on first access.
44
43
 
45
- PEP 562 的模块级 ``__getattr__`` 钩子:仅当访问 ``__all__`` 中声明的名称
46
- 且该名称尚未缓存到模块全局时才会触发,从而实现按需导入的惰性导出。
44
+ Args:
45
+ name (str): Symbolic schema, class, field, or enum name being stored or looked up.
47
46
 
48
- 参数:
49
- name (str): 正在访问的属性名(通常是 ``__all__`` 中的某个公开名称)。
50
-
51
- 返回:
52
- Any: 解析并缓存后的目标对象(类、函数或常量)。
53
-
54
- 异常:
55
- AttributeError: 当 ``name`` 不在惰性导出表 ``_EXPORT_MODULES`` 中时抛出。
47
+ Returns:
48
+ Any: Normalized value ready for the next parse, export, post-processing, or pack step.
56
49
  """
57
50
  module_name = _EXPORT_MODULES.get(name)
58
51
  if module_name is None:
59
52
  raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
60
53
 
61
- # 导入后把结果缓存到 globals,后续访问不再触发 __getattr__
54
+ # Cache the resolved object in globals so later attribute access bypasses __getattr__ and avoids another import.
62
55
  module = import_module(module_name, __name__)
63
56
  value = getattr(module, name)
64
57
  globals()[name] = value
@@ -1,4 +1,5 @@
1
- """Run the PyREUser3 CLI with ``python -m pyreuser3``."""
1
+ """Provide helpers for __main__.
2
+ """
2
3
 
3
4
  from __future__ import annotations
4
5
 
@@ -0,0 +1,411 @@
1
+ """Provide the REUser3Converter facade used by downstream Python code.
2
+
3
+ The facade hides exporter and packer construction details, keeps schema and il2cpp dump
4
+ configuration in one place, and offers convenient single-file, batch, and patch-and-
5
+ repack workflows.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import inspect
11
+ import json
12
+ import re
13
+ from pathlib import Path
14
+ from typing import Any, Callable, Optional
15
+
16
+ from .core import RSZ_MAGIC, USR_MAGIC
17
+ from .export import User3Exporter
18
+ from .pack import User3Packer
19
+
20
+ # Preserve the exported JSON structure so external scripts and hand-edited files remain
21
+ # compatible across workflows.
22
+ JsonTree = Any
23
+ # Patch callbacks may accept only the parsed data or both the data and source
24
+ # path; returning None means the callback mutated in place.
25
+ PatchCallback = Callable[..., Optional[JsonTree]]
26
+
27
+
28
+ class REUser3Converter:
29
+ """Store shared conversion configuration and expose high-level export, parse, patch, and
30
+ pack workflows.
31
+ """
32
+
33
+ def __init__(
34
+ self,
35
+ schema_path: str | Path | None = None,
36
+ il2cpp_dump_path: str | Path | None = None,
37
+ tree_depth: int | str = "auto",
38
+ schema_dir: str | Path | None = None,
39
+ user_magic: int = USR_MAGIC,
40
+ rsz_magic: int = RSZ_MAGIC,
41
+ ) -> None:
42
+ """Initialize REUser3Converter with validated configuration and state.
43
+
44
+ Args:
45
+ schema_path (str | Path | None): Explicit RE_RSZ schema JSON file path.
46
+ il2cpp_dump_path (str | Path | None): Path to il2cpp_dump.json for enum metadata.
47
+ tree_depth (int | str): Requested reference-tree expansion depth or auto mode.
48
+ schema_dir (str | Path | None): Compatibility schema argument that must resolve to a
49
+ schema JSON file.
50
+ user_magic (int): Expected magic value for the outer .user.3 container header.
51
+ rsz_magic (int): Expected magic value for embedded RSZ blocks.
52
+
53
+ Returns:
54
+ None. The method performs its documented side effect in place and raises on invalid input.
55
+
56
+ Raises:
57
+ TypeError: The caller supplied a value of an unsupported type.
58
+ """
59
+ # Accept the legacy schema_dir alias from older callers, but normalize all internal state to schema_path.
60
+ if schema_path is None:
61
+ schema_path = schema_dir
62
+ if schema_path is None:
63
+ raise TypeError("schema_path is required")
64
+ self.schema_path = Path(schema_path)
65
+ self.il2cpp_dump_path = Path(il2cpp_dump_path) if il2cpp_dump_path else None
66
+ self.tree_depth = tree_depth
67
+ self.user_magic = int(user_magic)
68
+ self.rsz_magic = int(rsz_magic)
69
+
70
+ def export_directory(
71
+ self,
72
+ user3_root: str | Path,
73
+ output_root: str | Path,
74
+ exclude_regexes: list[str] | None = None,
75
+ ) -> dict[str, int]:
76
+ """Export every selected .user.3 file under a directory or single-file root.
77
+
78
+ Args:
79
+ user3_root (str | Path): Source .user.3 file or directory root.
80
+ output_root (str | Path): Directory where generated output is written.
81
+ exclude_regexes (list[str] | None): Regular expressions used to skip matching
82
+ relative paths.
83
+
84
+ Returns:
85
+ dict[str, int]: Counters describing total, successful, and failed items.
86
+ """
87
+ exporter = self._new_exporter(user3_root, output_root, exclude_regexes)
88
+ return exporter.run()
89
+
90
+ def export_file(
91
+ self,
92
+ user3_path: str | Path,
93
+ json_path: str | Path,
94
+ ) -> Path:
95
+ """Export one .user.3 file to the requested JSON path.
96
+
97
+ Args:
98
+ user3_path (str | Path): Path to the .user.3 file being parsed, exported, patched, or packed.
99
+ json_path (str | Path): Path to the JSON document read from or written by this workflow.
100
+
101
+ Returns:
102
+ Path: Concrete filesystem path returned after the read, write, or resolution step finishes.
103
+ """
104
+ # Reuse parse_file so single-file and batch exports keep the same parsed JSON shape and metadata handling.
105
+ # Preserve the exported JSON structure so external scripts and hand-edited files
106
+ # remain compatible across workflows.
107
+ tree = self.parse_file(user3_path, round_floats=True)
108
+ target = Path(json_path)
109
+ target.parent.mkdir(parents=True, exist_ok=True)
110
+
111
+ with target.open("w", encoding="utf-8") as f:
112
+ json.dump(tree, f, ensure_ascii=False, indent=2)
113
+ return target
114
+
115
+ def parse_file(self, user3_path: str | Path, round_floats: bool = True) -> JsonTree:
116
+ """Parse one .user.3 file into the compact exported JSON tree.
117
+
118
+ Args:
119
+ user3_path (str | Path): Path to the .user.3 file being parsed, exported, patched, or packed.
120
+ round_floats (bool): Whether exported floats should be rounded to four decimal places for readability.
121
+
122
+ Returns:
123
+ JsonTree: JSON-compatible tree used by export, editing, or packing workflows.
124
+ """
125
+ exporter = self._new_exporter(user3_path, Path.cwd(), [])
126
+ # Build enum lookup and context metadata in memory; single-file parsing should not create Enums_Internal.json.
127
+ # Register enum values through the shared lookup tables so readable labels and
128
+ # numeric packing stay reversible.
129
+ self._prepare_exporter_metadata(exporter)
130
+ tree = exporter._parse_user3(Path(user3_path))
131
+ tree = exporter._postprocess_enum_nodes(tree)
132
+ tree = exporter._finalize_export_tree(tree)
133
+ if round_floats:
134
+ return exporter._round_export_floats(tree)
135
+ return tree
136
+
137
+ def parse_pack_file(self, user3_path: str | Path) -> JsonTree:
138
+ """Parse one .user.3 file into the full instance-table JSON used for stable repacking.
139
+
140
+ Args:
141
+ user3_path (str | Path): Path to the .user.3 file being parsed, exported, patched, or packed.
142
+
143
+ Returns:
144
+ JsonTree: JSON-compatible tree used by export, editing, or packing workflows.
145
+ """
146
+ exporter = self._new_exporter(user3_path, Path.cwd(), [])
147
+ self._prepare_exporter_metadata(exporter)
148
+ return exporter._parse_user3_pack(Path(user3_path))
149
+
150
+ def pack_directory(
151
+ self,
152
+ json_root: str | Path,
153
+ output_root: str | Path,
154
+ exclude_regexes: list[str] | None = None,
155
+ ) -> dict[str, int]:
156
+ """Pack every selected JSON file under a directory or single-file root.
157
+
158
+ Args:
159
+ json_root (str | Path): JSON file or directory root to process.
160
+ output_root (str | Path): Directory where generated output is written.
161
+ exclude_regexes (list[str] | None): Regular expressions used to skip matching
162
+ relative paths.
163
+
164
+ Returns:
165
+ dict[str, int]: Counters describing total, successful, and failed items.
166
+ """
167
+ packer = self._new_packer(output_root)
168
+ return packer.pack_directory(json_root, output_root, exclude_regexes)
169
+
170
+ def pack_file(self, json_path: str | Path, user3_path: str | Path) -> Path:
171
+ """Pack one JSON document to the requested .user.3 path.
172
+
173
+ Args:
174
+ json_path (str | Path): Path to the JSON document read from or written by this workflow.
175
+ user3_path (str | Path): Path to the .user.3 file being parsed, exported, patched, or packed.
176
+
177
+ Returns:
178
+ Path: Concrete filesystem path returned after the read, write, or resolution step finishes.
179
+ """
180
+ packer = self._new_packer(Path(user3_path).parent)
181
+ return packer.pack_json_file(json_path, user3_path)
182
+
183
+ def pack(self, data: Any) -> bytes:
184
+ """Encode an in-memory JSON tree as .user.3 bytes.
185
+
186
+ Args:
187
+ data (Any): JSON tree or binary payload consumed by this conversion step.
188
+
189
+ Returns:
190
+ bytes: Encoded binary data ready to write to disk.
191
+ """
192
+ return self._new_packer(None).pack(data)
193
+
194
+ def patch_file(
195
+ self,
196
+ user3_path: str | Path,
197
+ output_path: str | Path,
198
+ callback: PatchCallback,
199
+ ) -> Path:
200
+ """Patch one .user.3 file through a callback and write the packed result.
201
+
202
+ Args:
203
+ user3_path (str | Path): Path to the .user.3 file being parsed, exported, patched, or packed.
204
+ output_path (str | Path): Destination path where the generated file is written.
205
+ callback (PatchCallback): User callback that may inspect or modify parsed JSON.
206
+
207
+ Returns:
208
+ Path: Concrete filesystem path returned after the read, write, or resolution step finishes.
209
+ """
210
+ source = Path(user3_path)
211
+ # Preserve instance numbering and reference identity; RSZ object links depend on
212
+ # these indexes remaining stable.
213
+ data = self.parse_pack_file(source)
214
+ modified = self._run_callback(callback, data, source)
215
+ if modified is None:
216
+ modified = data
217
+ target = Path(output_path)
218
+ target.parent.mkdir(parents=True, exist_ok=True)
219
+ target.write_bytes(self.pack(modified))
220
+ return target
221
+
222
+ def patch_directory(
223
+ self,
224
+ user3_root: str | Path,
225
+ output_root: str | Path,
226
+ callback: PatchCallback,
227
+ include_regexes: list[str] | None = None,
228
+ exclude_regexes: list[str] | None = None,
229
+ ) -> dict[str, int]:
230
+ """Patch every selected .user.3 file under a root directory.
231
+
232
+ Args:
233
+ user3_root (str | Path): Source .user.3 file or directory root.
234
+ output_root (str | Path): Directory where generated output is written.
235
+ callback (PatchCallback): User callback that may inspect or modify parsed JSON.
236
+ include_regexes (list[str] | None): Regular expressions used to include matching
237
+ relative paths.
238
+ exclude_regexes (list[str] | None): Regular expressions used to skip matching
239
+ relative paths.
240
+
241
+ Returns:
242
+ dict[str, int]: Counters describing total, successful, and failed items.
243
+ """
244
+ source_root = Path(user3_root)
245
+ target_root = Path(output_root)
246
+ files = self._discover_user3_files(source_root)
247
+ include_patterns = [re.compile(p) for p in (include_regexes or [])]
248
+ exclude_patterns = [re.compile(p) for p in (exclude_regexes or [])]
249
+
250
+ total = success = failed = skipped = 0
251
+ for file_path in files:
252
+ # Resolve and validate paths at the boundary so later code never guesses
253
+ # relative to a surprising working directory.
254
+ rel = (
255
+ file_path.name
256
+ if source_root.is_file()
257
+ else file_path.relative_to(source_root).as_posix()
258
+ )
259
+ if include_patterns and not any(
260
+ pattern.search(rel) for pattern in include_patterns
261
+ ):
262
+ skipped += 1
263
+ continue
264
+ if any(pattern.search(rel) for pattern in exclude_patterns):
265
+ skipped += 1
266
+ continue
267
+
268
+ total += 1
269
+ output_path = target_root / (
270
+ file_path.name
271
+ if source_root.is_file()
272
+ else file_path.relative_to(source_root)
273
+ )
274
+ try:
275
+ # Treat each file independently so one malformed resource is reported
276
+ # but does not stop the rest of the batch.
277
+ self.patch_file(file_path, output_path, callback)
278
+ success += 1
279
+ except Exception:
280
+ failed += 1
281
+ return {
282
+ "total": total,
283
+ "success": success,
284
+ "failed": failed,
285
+ "skipped": skipped,
286
+ }
287
+
288
+ def _new_exporter(
289
+ self,
290
+ user3_root: str | Path,
291
+ output_root: str | Path,
292
+ exclude_regexes: list[str] | None,
293
+ ) -> User3Exporter:
294
+ """Create an exporter with this facade's schema, enum metadata, and magic values.
295
+
296
+ Args:
297
+ user3_root (str | Path): Source .user.3 file or directory root.
298
+ output_root (str | Path): Directory where generated output is written.
299
+ exclude_regexes (list[str] | None): Regular expressions used to skip matching
300
+ relative paths.
301
+
302
+ Returns:
303
+ User3Exporter: Configured object or normalized value returned for the caller to use directly.
304
+
305
+ Raises:
306
+ FileNotFoundError: A required file or directory was missing.
307
+ """
308
+ if self.il2cpp_dump_path is None:
309
+ raise FileNotFoundError("il2cpp_dump_path is required for exporting JSON")
310
+ return User3Exporter(
311
+ user3_root=user3_root,
312
+ schema_dir=self.schema_path,
313
+ output_root=output_root,
314
+ tree_depth=self.tree_depth,
315
+ exclude_regexes=exclude_regexes or [],
316
+ il2cpp_dump_path=self.il2cpp_dump_path,
317
+ user_magic=self.user_magic,
318
+ rsz_magic=self.rsz_magic,
319
+ )
320
+
321
+ def _new_packer(self, output_root: str | Path | None) -> User3Packer:
322
+ """Create a packer with this facade's schema, enum metadata, and magic values.
323
+
324
+ Args:
325
+ output_root (str | Path | None): Directory where generated output is written.
326
+
327
+ Returns:
328
+ User3Packer: Configured object or normalized value returned for the caller to use directly.
329
+ """
330
+ return User3Packer(
331
+ schema_dir=self.schema_path,
332
+ il2cpp_dump_path=self.il2cpp_dump_path,
333
+ output_root=output_root,
334
+ user_magic=self.user_magic,
335
+ rsz_magic=self.rsz_magic,
336
+ )
337
+
338
+ def _prepare_exporter_metadata(self, exporter: User3Exporter) -> None:
339
+ """Prepare exporter metadata.
340
+
341
+ Args:
342
+ exporter (User3Exporter): Exporter instance whose metadata caches are being prepared.
343
+
344
+ Returns:
345
+ None. The method performs its documented side effect in place and raises on invalid input.
346
+
347
+ Raises:
348
+ FileNotFoundError: A required file or directory was missing.
349
+ """
350
+ if self.il2cpp_dump_path is None or not self.il2cpp_dump_path.is_file():
351
+ raise FileNotFoundError("il2cpp_dump_path is required for parsing JSON")
352
+ with self.il2cpp_dump_path.open("r", encoding="utf-8") as f:
353
+ il2cpp_dump = json.load(f)
354
+ # Batch export writes Enums_Internal.json for downstream tools; direct
355
+ # parsing keeps the same lookup only in memory.
356
+ # Keep the generated metadata attached to the exporter so field parsing can
357
+ # format enum names consistently.
358
+ enums_internal = exporter.export_enums_internal(il2cpp_dump)
359
+ exporter.enum_lookup = exporter._build_enum_lookup_from_enums_internal(
360
+ enums_internal
361
+ )
362
+ enum_context = exporter.export_enum_context_internal(il2cpp_dump)
363
+ exporter._apply_enum_context(enum_context)
364
+ exporter._ensure_enum_lookup()
365
+
366
+ @staticmethod
367
+ def _discover_user3_files(user3_root: Path) -> list[Path]:
368
+ """Discover user3 files.
369
+
370
+ Args:
371
+ user3_root (Path): Source .user.3 file or directory root.
372
+
373
+ Returns:
374
+ list[Path]: Filesystem paths selected for batch processing after suffix and exclusion checks.
375
+
376
+ Raises:
377
+ FileNotFoundError: A required file or directory was missing.
378
+ """
379
+ if user3_root.is_file():
380
+ return [user3_root]
381
+ if not user3_root.is_dir():
382
+ raise FileNotFoundError(f"user3 root not found: {user3_root}")
383
+ files = sorted(user3_root.rglob("*.user.3"))
384
+ if not files:
385
+ raise FileNotFoundError(f"no *.user.3 found under: {user3_root}")
386
+ return files
387
+
388
+ @staticmethod
389
+ def _run_callback(
390
+ callback: PatchCallback, data: JsonTree, source_path: Path
391
+ ) -> JsonTree | None:
392
+ """Run callback.
393
+
394
+ Args:
395
+ callback (PatchCallback): User callback that may inspect or modify parsed JSON.
396
+ data (JsonTree): JSON tree or binary payload consumed by this conversion step.
397
+ source_path (Path): Original source path associated with a patch callback or output
398
+ path.
399
+
400
+ Returns:
401
+ JsonTree | None: Configured object or normalized value returned for the caller to use directly.
402
+ """
403
+ try:
404
+ param_count = len(inspect.signature(callback).parameters)
405
+ except (TypeError, ValueError):
406
+ # Some callables do not expose inspectable signatures, so fall back to
407
+ # the full callback signature when introspection fails.
408
+ param_count = 2
409
+ if param_count <= 1:
410
+ return callback(data)
411
+ return callback(data, source_path)
@@ -1,4 +1,8 @@
1
- """Command line interface for the PyREUser3 package."""
1
+ """Define the pyreuser3 command line interface for exporting .user.3 files to JSON and packing JSON back to .user.3.
2
+
3
+ Heavy converter imports stay inside subcommand handlers so help and version output
4
+ remain lightweight.
5
+ """
2
6
 
3
7
  from __future__ import annotations
4
8
 
@@ -8,18 +12,27 @@ from importlib.metadata import PackageNotFoundError, version
8
12
  from typing import Sequence
9
13
 
10
14
  from .core import RSZ_MAGIC, USR_MAGIC
11
- from .export import User3Exporter
12
- from .pack import User3Packer
13
- from .rich_ui import get_console
14
15
 
15
16
 
16
17
  def parse_int_arg(value: str) -> int:
17
- """Parse decimal or 0x-prefixed integer command line values."""
18
+ """Parse a decimal or hexadecimal integer command line value.
19
+
20
+ Args:
21
+ value (str): Value to parse, normalize, compare, or serialize.
22
+
23
+ Returns:
24
+ int: Integer decoded from input data, metadata, or the command-line option being parsed.
25
+ """
18
26
  return int(value, 0)
19
27
 
20
28
 
21
29
  def package_version() -> str:
22
- """Return installed package version, with a source-tree fallback."""
30
+ """Return the installed package version with a source-tree fallback.
31
+
32
+
33
+ Returns:
34
+ str: Normalized or formatted text.
35
+ """
23
36
  try:
24
37
  return version("PyREUser3")
25
38
  except PackageNotFoundError:
@@ -27,7 +40,14 @@ def package_version() -> str:
27
40
 
28
41
 
29
42
  def normalize_tree_depth(value: str) -> int | str:
30
- """Normalize tree depth argument to an integer or the string 'auto'."""
43
+ """Normalize a tree-depth CLI value to auto or a non-negative integer.
44
+
45
+ Args:
46
+ value (str): Value to parse, normalize, compare, or serialize.
47
+
48
+ Returns:
49
+ int | str: Parsed integer value or the literal auto mode.
50
+ """
31
51
  text = value.strip().lower()
32
52
  if text == "auto":
33
53
  return "auto"
@@ -38,7 +58,14 @@ def normalize_tree_depth(value: str) -> int | str:
38
58
 
39
59
 
40
60
  def add_magic_args(parser: argparse.ArgumentParser) -> None:
41
- """Add common .user.3 and RSZ magic options to a subparser."""
61
+ """Add shared USR and RSZ magic number options to a parser.
62
+
63
+ Args:
64
+ parser (argparse.ArgumentParser): Argument parser being populated with CLI subcommands and shared options.
65
+
66
+ Returns:
67
+ None. The method performs its documented side effect in place and raises on invalid input.
68
+ """
42
69
  parser.add_argument(
43
70
  "--user-magic",
44
71
  type=parse_int_arg,
@@ -54,7 +81,12 @@ def add_magic_args(parser: argparse.ArgumentParser) -> None:
54
81
 
55
82
 
56
83
  def build_parser() -> argparse.ArgumentParser:
57
- """Build the top-level CLI parser."""
84
+ """Build the CLI parser and subcommands.
85
+
86
+
87
+ Returns:
88
+ argparse.ArgumentParser: Configured argument parser for the command-line interface.
89
+ """
58
90
  parser = argparse.ArgumentParser(
59
91
  prog="pyreuser3",
60
92
  description="Convert RE Engine .user.3 files to JSON and pack them back.",
@@ -158,7 +190,17 @@ def build_parser() -> argparse.ArgumentParser:
158
190
 
159
191
 
160
192
  def run_export(args: argparse.Namespace) -> int:
161
- """Run the export subcommand."""
193
+ """Run the CLI export subcommand.
194
+
195
+ Args:
196
+ args (argparse.Namespace): Parsed command-line namespace for the selected CLI command.
197
+
198
+ Returns:
199
+ int: Integer decoded from input data, metadata, or the command-line option being parsed.
200
+ """
201
+ from .export import User3Exporter
202
+ from .rich_ui import get_console
203
+
162
204
  console = get_console()
163
205
  console.log("Exporting .user.3 files to JSON...")
164
206
  exporter = User3Exporter(
@@ -177,7 +219,17 @@ def run_export(args: argparse.Namespace) -> int:
177
219
 
178
220
 
179
221
  def run_pack(args: argparse.Namespace) -> int:
180
- """Run the pack subcommand."""
222
+ """Run the CLI pack subcommand.
223
+
224
+ Args:
225
+ args (argparse.Namespace): Parsed command-line namespace for the selected CLI command.
226
+
227
+ Returns:
228
+ int: Integer decoded from input data, metadata, or the command-line option being parsed.
229
+ """
230
+ from .pack import User3Packer
231
+ from .rich_ui import get_console
232
+
181
233
  console = get_console()
182
234
  console.log("Packing JSON files to .user.3...")
183
235
  packer = User3Packer(
@@ -197,7 +249,14 @@ def run_pack(args: argparse.Namespace) -> int:
197
249
 
198
250
 
199
251
  def main(argv: Sequence[str] | None = None) -> int:
200
- """CLI entry point used by the pyreuser3 console script."""
252
+ """Run the command entry point.
253
+
254
+ Args:
255
+ argv (Sequence[str] | None): Optional argument list; None means use the process command line.
256
+
257
+ Returns:
258
+ int: Integer decoded from input data, metadata, or the command-line option being parsed.
259
+ """
201
260
  parser = build_parser()
202
261
  args = parser.parse_args(argv)
203
262
  return int(args.func(args))