treeing 1.0.1__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.
treeing/config.py ADDED
@@ -0,0 +1,205 @@
1
+ """treeing/config.py
2
+
3
+ Defines string-resource loading and configuration management.
4
+ Loads, formats and falls back for strings.json / strings.bootstrap.json,
5
+ providing a unified multi-language text interface for the CLI and GUI.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import sys
12
+ from pathlib import Path
13
+
14
+ _CONFIG = None
15
+ _BOOTSTRAP: dict | None = None
16
+ _BOOTSTRAP_FAILED_PATH: Path | None = None
17
+
18
+ _BOOTSTRAP_FILENAME = "strings.bootstrap.json"
19
+
20
+
21
+ class ConfigError(Exception):
22
+ """
23
+ Raised when strings.json cannot be loaded or is malformed.
24
+
25
+ MING keeps error messages in strings.json too, but if strings.json itself is
26
+ broken, the code falls back to the minimal text in strings.bootstrap.json so
27
+ the user still sees a sensible error message.
28
+ """
29
+
30
+
31
+ def _resource_dir() -> Path:
32
+ """
33
+ Locate the directory that holds strings.json.
34
+
35
+ In development this is the source directory; under a PyInstaller bundle it
36
+ is the _MEIPASS temporary directory.
37
+ """
38
+ if getattr(sys, 'frozen', False):
39
+ meipass = getattr(sys, '_MEIPASS', None)
40
+ if meipass is not None:
41
+ return Path(meipass) / "treeing"
42
+ return Path(__file__).parent
43
+
44
+
45
+ def _format_string(text: str, **kwargs) -> str:
46
+ """
47
+ Interpolate text via str.format, returning the original on failure.
48
+
49
+ MING wraps this in try/except because user-supplied placeholders may not
50
+ match the text; showing the raw string is easier to debug than raising.
51
+ """
52
+ if not kwargs:
53
+ return text
54
+ try:
55
+ return text.format(**kwargs)
56
+ except (KeyError, ValueError, IndexError):
57
+ return text
58
+
59
+
60
+ def _bootstrap_unavailable_fallback(key: str, path: Path, **kwargs) -> str:
61
+ """
62
+ Last-resort fallback when the bootstrap JSON is unreadable.
63
+
64
+ Returns only technical information such as the key name and path (no
65
+ user-visible text is hard-coded), so the user never sees garbled output
66
+ when both strings.json and the bootstrap file are corrupt.
67
+ """
68
+ parts = [key, f"path={path}"]
69
+ for name, value in kwargs.items():
70
+ if name != 'path':
71
+ parts.append(f"{name}={value}")
72
+ return " ".join(parts)
73
+
74
+
75
+ def _load_bootstrap() -> dict:
76
+ """
77
+ Load the bootstrap text (the minimal configurable set used when the main strings.json is unavailable).
78
+
79
+ The result is cached in _BOOTSTRAP; on failure _BOOTSTRAP_FAILED_PATH records the path.
80
+ """
81
+ global _BOOTSTRAP, _BOOTSTRAP_FAILED_PATH
82
+ if _BOOTSTRAP is not None:
83
+ return _BOOTSTRAP
84
+
85
+ bootstrap_path = _resource_dir() / _BOOTSTRAP_FILENAME
86
+ try:
87
+ with open(bootstrap_path, encoding='utf-8') as f:
88
+ data = json.load(f)
89
+ if not isinstance(data, dict):
90
+ _BOOTSTRAP = {}
91
+ _BOOTSTRAP_FAILED_PATH = bootstrap_path
92
+ else:
93
+ _BOOTSTRAP = data
94
+ _BOOTSTRAP_FAILED_PATH = None
95
+ except (OSError, json.JSONDecodeError):
96
+ _BOOTSTRAP = {}
97
+ _BOOTSTRAP_FAILED_PATH = bootstrap_path
98
+ return _BOOTSTRAP
99
+
100
+
101
+ def bootstrap_string(key: str, **kwargs) -> str:
102
+ """
103
+ Fetch text from strings.bootstrap.json.
104
+
105
+ When the bootstrap file is unavailable, falls back to the key name plus
106
+ path= and other technical fields, so the program never crashes.
107
+ """
108
+ data = _load_bootstrap()
109
+ failed_path = _BOOTSTRAP_FAILED_PATH
110
+
111
+ if failed_path is not None:
112
+ text = data.get(key)
113
+ if text is None:
114
+ if key == 'config_err_bootstrap_missing':
115
+ report_path = kwargs.get('path', failed_path)
116
+ extra = {k: v for k, v in kwargs.items() if k != 'path'}
117
+ return _bootstrap_unavailable_fallback(key, report_path, **extra)
118
+ return bootstrap_string('config_err_bootstrap_missing', path=failed_path)
119
+ else:
120
+ text = data.get(key, key)
121
+
122
+ return _format_string(text, **kwargs)
123
+
124
+
125
+ def load_config():
126
+ """
127
+ Load and cache strings.json.
128
+
129
+ Raises ConfigError when the file is missing, the JSON is malformed, the
130
+ root is not an object, or reading fails; the error message is taken from
131
+ bootstrap_string where possible.
132
+ """
133
+ global _CONFIG
134
+ if _CONFIG is None:
135
+ json_path = _resource_dir() / "strings.json"
136
+ try:
137
+ with open(json_path, encoding='utf-8') as f:
138
+ data = json.load(f)
139
+ except FileNotFoundError as e:
140
+ raise ConfigError(
141
+ bootstrap_string("config_err_missing", path=json_path),
142
+ ) from e
143
+ except json.JSONDecodeError as e:
144
+ raise ConfigError(
145
+ bootstrap_string("config_err_invalid_json", path=json_path, error=e),
146
+ ) from e
147
+ except OSError as e:
148
+ raise ConfigError(
149
+ bootstrap_string("config_err_read", path=json_path, error=e),
150
+ ) from e
151
+ if not isinstance(data, dict):
152
+ raise ConfigError(
153
+ bootstrap_string("config_err_not_object", path=json_path),
154
+ )
155
+ _CONFIG = data
156
+ return _CONFIG
157
+
158
+
159
+ def get_string(key, **kwargs):
160
+ """
161
+ Fetch and interpolate text from strings.json.
162
+
163
+ If the key is missing, the key name itself is returned, so a missing entry
164
+ is immediately obvious during development and testing.
165
+ """
166
+ cfg = load_config()
167
+ text = cfg.get(key, key)
168
+ return _format_string(text, **kwargs)
169
+
170
+
171
+ # Invocation placeholders: help / about text refers to these uniformly, so we
172
+ # do not hard-code only the `python -m` form.
173
+ CLI_PYTHON = "python -m treeing.main"
174
+ CLI_EXE_UNIX = "treeing-cli"
175
+ CLI_EXE_WIN = "treeing-cli.exe"
176
+ GUI_PYTHON = "python -m treeing.main"
177
+ GUI_EXE_UNIX = "treeing-gui"
178
+ GUI_EXE_WIN = "treeing-gui.exe"
179
+
180
+
181
+ def invocation_vars() -> dict[str, str]:
182
+ """
183
+ Return the invocation placeholders used in help / about text.
184
+
185
+ Covers both source (`python -m`) and PyInstaller artefacts
186
+ (treeing-cli / treeing-gui); the Windows executable carries the .exe suffix.
187
+ """
188
+ return {
189
+ "cli_python": CLI_PYTHON,
190
+ "cli_exe_unix": CLI_EXE_UNIX,
191
+ "cli_exe_win": CLI_EXE_WIN,
192
+ "gui_python": GUI_PYTHON,
193
+ "gui_exe_unix": GUI_EXE_UNIX,
194
+ "gui_exe_win": GUI_EXE_WIN,
195
+ }
196
+
197
+
198
+ def get_ui_string(key: str, **kwargs) -> str:
199
+ """
200
+ Fetch user-visible text that includes invocation placeholders.
201
+
202
+ Used by help, about and GUI error prompts that need to show both the
203
+ Python and the executable forms.
204
+ """
205
+ return get_string(key, **invocation_vars(), **kwargs)
@@ -0,0 +1,15 @@
1
+ """
2
+ treeing/core
3
+
4
+ Core parsing and generation engine.
5
+ Provides ASCII/Unicode tree-text parsing (parser), filesystem generation (generator), preview rendering (preview) and constants (constants), shared by the CLI and GUI.
6
+ """
7
+
8
+ from .constants import DOT_ROOT # noqa: F401
9
+ from .generator import ( # noqa: F401
10
+ DuplicateNameError,
11
+ PathConflictError,
12
+ create_from_tree,
13
+ iter_nodes,
14
+ )
15
+ from .parser import build_tree # noqa: F401
@@ -0,0 +1,20 @@
1
+ """
2
+ treeing/core/constants.py
3
+
4
+ Defines the special node-name constants used during parsing and generation.
5
+ These names mark virtual nodes (<auto>, <virtual>) and the dot root (.),
6
+ and need special handling in parsing, preview and generation to avoid
7
+ creating or displaying them by mistake.
8
+ """
9
+
10
+ DOT_ROOT = '.'
11
+ VIRTUAL_AUTO = '<auto>'
12
+ VIRTUAL_ROOT = '<virtual>'
13
+
14
+ # Names rendered with the "virtual node" style in the preview and GUI.
15
+ VIRTUAL_NODE_NAMES = frozenset({VIRTUAL_AUTO, VIRTUAL_ROOT})
16
+
17
+ # Names skipped (not actually created) during generation.
18
+ # MING includes DOT_ROOT here as well, because '.' only means "the current
19
+ # directory" and is not a real folder to create.
20
+ TRANSPARENT_NODE_NAMES = frozenset({VIRTUAL_AUTO, VIRTUAL_ROOT, DOT_ROOT})