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/__init__.py +7 -0
- treeing/_release_defaults.py +3 -0
- treeing/assets/icon.icns +0 -0
- treeing/assets/icon.ico +0 -0
- treeing/assets/icon.png +0 -0
- treeing/cli/__init__.py +5 -0
- treeing/cli/confirm.py +197 -0
- treeing/cli/help_text.py +311 -0
- treeing/cli/io.py +72 -0
- treeing/cli/main.py +427 -0
- treeing/cli/report.py +104 -0
- treeing/cli_entry.py +16 -0
- treeing/config.py +205 -0
- treeing/core/__init__.py +15 -0
- treeing/core/constants.py +20 -0
- treeing/core/generator.py +567 -0
- treeing/core/parser.py +407 -0
- treeing/core/preview.py +98 -0
- treeing/gui/__init__.py +5 -0
- treeing/gui/app.py +886 -0
- treeing/gui/dnd.py +124 -0
- treeing/gui/icon.py +69 -0
- treeing/gui/preview.py +9 -0
- treeing/gui/settings.py +80 -0
- treeing/gui/tooltip.py +234 -0
- treeing/gui_entry.py +39 -0
- treeing/main.py +21 -0
- treeing/path_checks.py +87 -0
- treeing/strings.bootstrap.json +7 -0
- treeing/strings.json +222 -0
- treeing-1.0.1.dist-info/METADATA +112 -0
- treeing-1.0.1.dist-info/RECORD +36 -0
- treeing-1.0.1.dist-info/WHEEL +5 -0
- treeing-1.0.1.dist-info/entry_points.txt +3 -0
- treeing-1.0.1.dist-info/licenses/LICENSE +21 -0
- treeing-1.0.1.dist-info/top_level.txt +1 -0
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)
|
treeing/core/__init__.py
ADDED
|
@@ -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})
|