batplot 1.8.0__py3-none-any.whl → 1.8.2__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.
Potentially problematic release.
This version of batplot might be problematic. Click here for more details.
- batplot/__init__.py +1 -1
- batplot/args.py +5 -3
- batplot/batplot.py +44 -4
- batplot/cpc_interactive.py +96 -3
- batplot/electrochem_interactive.py +28 -0
- batplot/interactive.py +18 -2
- batplot/modes.py +12 -12
- batplot/operando.py +2 -0
- batplot/operando_ec_interactive.py +112 -11
- batplot/session.py +35 -1
- batplot/utils.py +40 -0
- batplot/version_check.py +85 -6
- {batplot-1.8.0.dist-info → batplot-1.8.2.dist-info}/METADATA +1 -1
- batplot-1.8.2.dist-info/RECORD +75 -0
- {batplot-1.8.0.dist-info → batplot-1.8.2.dist-info}/top_level.txt +1 -0
- batplot_backup_20251221_101150/__init__.py +5 -0
- batplot_backup_20251221_101150/args.py +625 -0
- batplot_backup_20251221_101150/batch.py +1176 -0
- batplot_backup_20251221_101150/batplot.py +3589 -0
- batplot_backup_20251221_101150/cif.py +823 -0
- batplot_backup_20251221_101150/cli.py +149 -0
- batplot_backup_20251221_101150/color_utils.py +547 -0
- batplot_backup_20251221_101150/config.py +198 -0
- batplot_backup_20251221_101150/converters.py +204 -0
- batplot_backup_20251221_101150/cpc_interactive.py +4409 -0
- batplot_backup_20251221_101150/electrochem_interactive.py +4520 -0
- batplot_backup_20251221_101150/interactive.py +3894 -0
- batplot_backup_20251221_101150/manual.py +323 -0
- batplot_backup_20251221_101150/modes.py +799 -0
- batplot_backup_20251221_101150/operando.py +603 -0
- batplot_backup_20251221_101150/operando_ec_interactive.py +5487 -0
- batplot_backup_20251221_101150/plotting.py +228 -0
- batplot_backup_20251221_101150/readers.py +2607 -0
- batplot_backup_20251221_101150/session.py +2951 -0
- batplot_backup_20251221_101150/style.py +1441 -0
- batplot_backup_20251221_101150/ui.py +790 -0
- batplot_backup_20251221_101150/utils.py +1046 -0
- batplot_backup_20251221_101150/version_check.py +253 -0
- batplot-1.8.0.dist-info/RECORD +0 -52
- {batplot-1.8.0.dist-info → batplot-1.8.2.dist-info}/WHEEL +0 -0
- {batplot-1.8.0.dist-info → batplot-1.8.2.dist-info}/entry_points.txt +0 -0
- {batplot-1.8.0.dist-info → batplot-1.8.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"""Manual utilities for batplot.
|
|
2
|
+
|
|
3
|
+
Instead of generating a PDF, this module exposes the packaged Markdown
|
|
4
|
+
manual as a plain text file and opens it in the user's preferred editor.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import List
|
|
11
|
+
|
|
12
|
+
import hashlib
|
|
13
|
+
import os
|
|
14
|
+
import shlex
|
|
15
|
+
import subprocess
|
|
16
|
+
import sys
|
|
17
|
+
import tempfile
|
|
18
|
+
|
|
19
|
+
try: # Python 3.9+ exposes .files API directly
|
|
20
|
+
from importlib import resources as importlib_resources # type: ignore[attr-defined]
|
|
21
|
+
except ImportError: # pragma: no cover
|
|
22
|
+
import importlib_resources # type: ignore
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
MANUAL_RESOURCE = ("batplot.data", "USER_MANUAL.md")
|
|
26
|
+
TEXT_NAME = "batplot_manual.txt"
|
|
27
|
+
HASH_NAME = "batplot_manual.sha256"
|
|
28
|
+
MANUAL_RENDER_VERSION = "4"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _manual_text() -> str:
|
|
32
|
+
"""
|
|
33
|
+
Load the Markdown manual bundled with the package.
|
|
34
|
+
|
|
35
|
+
HOW IT WORKS:
|
|
36
|
+
------------
|
|
37
|
+
This function tries multiple methods to find and load the USER_MANUAL.md file:
|
|
38
|
+
|
|
39
|
+
1. **Modern method (Python 3.9+)**: Uses importlib.resources.files()
|
|
40
|
+
- This is the recommended way to access package data files
|
|
41
|
+
- Works with both regular packages and namespace packages
|
|
42
|
+
- Handles zipped packages (when installed as .egg or .whl)
|
|
43
|
+
|
|
44
|
+
2. **Fallback method (older Python)**: Uses importlib.resources.open_text()
|
|
45
|
+
- Compatible with Python 3.7+
|
|
46
|
+
- Works for most package structures
|
|
47
|
+
|
|
48
|
+
3. **Local file fallback**: Tries to read from batplot/data/ directory
|
|
49
|
+
- Used during development (when package isn't installed)
|
|
50
|
+
- Path(__file__) gets the directory containing this file
|
|
51
|
+
- .parent / "data" goes up one level and into data/ folder
|
|
52
|
+
|
|
53
|
+
WHY MULTIPLE METHODS?
|
|
54
|
+
--------------------
|
|
55
|
+
Different Python versions and installation methods require different approaches.
|
|
56
|
+
We try the most modern method first, then fall back to older methods.
|
|
57
|
+
This ensures the manual can be loaded regardless of Python version or
|
|
58
|
+
how batplot was installed (pip, editable install, or running from source).
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
String containing the full manual text (Markdown format)
|
|
62
|
+
|
|
63
|
+
Raises:
|
|
64
|
+
FileNotFoundError: If manual cannot be found by any method
|
|
65
|
+
"""
|
|
66
|
+
package, resource = MANUAL_RESOURCE # Unpack tuple: ("batplot.data", "USER_MANUAL.md")
|
|
67
|
+
|
|
68
|
+
# METHOD 1: Try modern importlib.resources.files() API (Python 3.9+)
|
|
69
|
+
try:
|
|
70
|
+
# .files() gets a Traversable object representing the package directory
|
|
71
|
+
# .joinpath() navigates to the resource file
|
|
72
|
+
# .read_text() reads the file as a string
|
|
73
|
+
data = importlib_resources.files(package).joinpath(resource).read_text(encoding="utf-8") # type: ignore[attr-defined]
|
|
74
|
+
return data
|
|
75
|
+
except Exception:
|
|
76
|
+
# Method 1 failed, try next method
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
# METHOD 2: Try older importlib.resources.open_text() API (Python 3.7+)
|
|
80
|
+
try:
|
|
81
|
+
# open_text() opens the file as a text file (handles encoding)
|
|
82
|
+
# 'with' statement ensures file is closed automatically
|
|
83
|
+
with importlib_resources.open_text(package, resource, encoding="utf-8") as handle: # type: ignore[attr-defined]
|
|
84
|
+
return handle.read() # Read entire file contents
|
|
85
|
+
except Exception:
|
|
86
|
+
# Method 2 failed, try next method
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
# METHOD 3: Fallback to local file system (for development)
|
|
90
|
+
# Path(__file__) = path to this file (manual.py)
|
|
91
|
+
# .resolve() = convert to absolute path
|
|
92
|
+
# .parent = go up one directory (from batplot/ to batplot_script/)
|
|
93
|
+
# / "data" / resource = navigate to batplot/data/USER_MANUAL.md
|
|
94
|
+
local_path = Path(__file__).resolve().parent / "data" / resource
|
|
95
|
+
if local_path.exists():
|
|
96
|
+
return local_path.read_text(encoding="utf-8")
|
|
97
|
+
|
|
98
|
+
# All methods failed - raise error with helpful message
|
|
99
|
+
raise FileNotFoundError(f"Unable to locate manual resource '{resource}'. Tried package '{package}' and {local_path}")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _manual_hash(text: str) -> str:
|
|
103
|
+
"""
|
|
104
|
+
Calculate SHA256 hash of manual content for cache validation.
|
|
105
|
+
|
|
106
|
+
HOW IT WORKS:
|
|
107
|
+
------------
|
|
108
|
+
This function creates a "fingerprint" of the manual content. If the content
|
|
109
|
+
changes, the hash changes. We use this to detect when the manual has been
|
|
110
|
+
updated and needs to be regenerated.
|
|
111
|
+
|
|
112
|
+
WHY INCLUDE VERSION?
|
|
113
|
+
-------------------
|
|
114
|
+
We include MANUAL_RENDER_VERSION in the hash. This means if we change how
|
|
115
|
+
the manual is formatted (even if content is the same), the hash changes
|
|
116
|
+
and cache is invalidated. This ensures users always get the latest format.
|
|
117
|
+
|
|
118
|
+
HASH ALGORITHM:
|
|
119
|
+
--------------
|
|
120
|
+
SHA256 produces a 256-bit (32-byte) hash. It's:
|
|
121
|
+
- Deterministic: Same input always produces same hash
|
|
122
|
+
- One-way: Can't reverse hash to get original text
|
|
123
|
+
- Collision-resistant: Very unlikely two different texts produce same hash
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
text: Manual content (Markdown text)
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
64-character hexadecimal string (SHA256 hash)
|
|
130
|
+
Example: "a3f5b2c1d4e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2"
|
|
131
|
+
"""
|
|
132
|
+
# Create SHA256 hash object
|
|
133
|
+
digest = hashlib.sha256()
|
|
134
|
+
# Add version to hash (ensures cache invalidates when format changes)
|
|
135
|
+
digest.update(MANUAL_RENDER_VERSION.encode("utf-8"))
|
|
136
|
+
# Add manual text to hash (ensures cache invalidates when content changes)
|
|
137
|
+
digest.update(text.encode("utf-8"))
|
|
138
|
+
# Return hash as hexadecimal string (64 characters)
|
|
139
|
+
return digest.hexdigest()
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _cache_dir() -> Path:
|
|
143
|
+
"""
|
|
144
|
+
Get the directory where manual cache files are stored.
|
|
145
|
+
|
|
146
|
+
HOW IT WORKS:
|
|
147
|
+
------------
|
|
148
|
+
This function determines where to store cached manual files. It tries multiple
|
|
149
|
+
locations in order of preference:
|
|
150
|
+
|
|
151
|
+
1. **Environment variable override**: BATPLOT_CACHE_DIR (user-specified)
|
|
152
|
+
2. **Platform-specific standard locations**:
|
|
153
|
+
- Windows: %LOCALAPPDATA%\\batplot (usually C:\\Users\\username\\AppData\\Local\\batplot)
|
|
154
|
+
- Linux/macOS: ~/.cache/batplot (or $XDG_CACHE_HOME/batplot if set)
|
|
155
|
+
3. **Temporary directory fallback**: If standard locations aren't writable
|
|
156
|
+
|
|
157
|
+
WHY CACHE?
|
|
158
|
+
---------
|
|
159
|
+
The manual is generated from Markdown and cached to avoid regenerating it
|
|
160
|
+
on every run. This speeds up the 'batplot -m' command.
|
|
161
|
+
|
|
162
|
+
WHY PLATFORM-SPECIFIC?
|
|
163
|
+
----------------------
|
|
164
|
+
Different operating systems have different conventions for where application
|
|
165
|
+
data should be stored. We follow platform conventions for better integration.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
Path object pointing to cache directory (always writable)
|
|
169
|
+
"""
|
|
170
|
+
# Check for user override via environment variable
|
|
171
|
+
env_override = os.environ.get("BATPLOT_CACHE_DIR")
|
|
172
|
+
if env_override:
|
|
173
|
+
# User specified custom location - use it
|
|
174
|
+
# expanduser() converts ~ to actual home directory path
|
|
175
|
+
target = Path(env_override).expanduser()
|
|
176
|
+
elif os.name == "nt":
|
|
177
|
+
# Windows: Use LOCALAPPDATA (standard location for app data)
|
|
178
|
+
# Fallback to AppData/Local if environment variable not set
|
|
179
|
+
base = Path(os.environ.get("LOCALAPPDATA", Path.home() / "AppData" / "Local"))
|
|
180
|
+
target = base / "batplot"
|
|
181
|
+
else:
|
|
182
|
+
# Linux/macOS: Use XDG_CACHE_HOME (standard location) or ~/.cache
|
|
183
|
+
# XDG_CACHE_HOME is a Linux standard, ~/.cache is the default
|
|
184
|
+
base = Path(os.environ.get("XDG_CACHE_HOME", Path.home() / ".cache"))
|
|
185
|
+
target = base / "batplot"
|
|
186
|
+
|
|
187
|
+
# Try to create and use the preferred directory
|
|
188
|
+
try:
|
|
189
|
+
# mkdir(parents=True, exist_ok=True) creates directory and all parent directories
|
|
190
|
+
# parents=True: Create parent dirs if they don't exist
|
|
191
|
+
# exist_ok=True: Don't raise error if directory already exists
|
|
192
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
193
|
+
# Check if directory is writable (permissions check)
|
|
194
|
+
if os.access(target, os.W_OK):
|
|
195
|
+
return target
|
|
196
|
+
except OSError:
|
|
197
|
+
# Creation failed or not writable, fall back to temp directory
|
|
198
|
+
pass
|
|
199
|
+
|
|
200
|
+
# Fallback: Use system temporary directory
|
|
201
|
+
# This is always writable, but files may be cleaned up by system
|
|
202
|
+
fallback = Path(tempfile.gettempdir()) / "batplot-manual"
|
|
203
|
+
fallback.mkdir(parents=True, exist_ok=True)
|
|
204
|
+
return fallback
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _resolve_editor_command() -> List[str]:
|
|
208
|
+
"""
|
|
209
|
+
Determine which text editor command to use for opening the manual.
|
|
210
|
+
|
|
211
|
+
HOW IT WORKS:
|
|
212
|
+
------------
|
|
213
|
+
This function checks environment variables and platform defaults to find
|
|
214
|
+
the best text editor to use. It follows this priority order:
|
|
215
|
+
|
|
216
|
+
1. BATPLOT_MANUAL_EDITOR: User's explicit choice for batplot
|
|
217
|
+
2. VISUAL: Standard environment variable (preferred for GUI editors)
|
|
218
|
+
3. EDITOR: Standard environment variable (fallback, often terminal editor)
|
|
219
|
+
4. Platform defaults:
|
|
220
|
+
- Windows: notepad.exe (built-in text editor)
|
|
221
|
+
- macOS: open (opens with default app for .txt files)
|
|
222
|
+
- Linux: xdg-open (opens with default app for .txt files)
|
|
223
|
+
|
|
224
|
+
WHY SHLEX.SPLIT()?
|
|
225
|
+
-----------------
|
|
226
|
+
Environment variables might contain commands with arguments:
|
|
227
|
+
Example: EDITOR="code --wait" or EDITOR="vim -R"
|
|
228
|
+
shlex.split() properly splits the command string into a list:
|
|
229
|
+
"code --wait" → ["code", "--wait"]
|
|
230
|
+
This prevents errors when the command has spaces or arguments.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
List of command and arguments, ready for subprocess.Popen()
|
|
234
|
+
Example: ["code", "--wait"] or ["notepad.exe"]
|
|
235
|
+
"""
|
|
236
|
+
# Check environment variables in priority order
|
|
237
|
+
for var in ("BATPLOT_MANUAL_EDITOR", "VISUAL", "EDITOR"):
|
|
238
|
+
cmd = os.environ.get(var)
|
|
239
|
+
if cmd:
|
|
240
|
+
# Split command string into list (handles arguments properly)
|
|
241
|
+
# Example: "code --wait" → ["code", "--wait"]
|
|
242
|
+
return shlex.split(cmd)
|
|
243
|
+
|
|
244
|
+
# No environment variable set - use platform defaults
|
|
245
|
+
if sys.platform.startswith("win"):
|
|
246
|
+
# Windows: Use built-in Notepad
|
|
247
|
+
return ["notepad.exe"]
|
|
248
|
+
if sys.platform.startswith("darwin"):
|
|
249
|
+
# macOS: Use 'open' command (opens with default app)
|
|
250
|
+
return ["open"]
|
|
251
|
+
# Linux/other: Use xdg-open (opens with default app)
|
|
252
|
+
return ["xdg-open"]
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _open_editor(path: Path) -> None:
|
|
256
|
+
cmd = _resolve_editor_command()
|
|
257
|
+
try:
|
|
258
|
+
process_args = cmd + [str(path)]
|
|
259
|
+
subprocess.Popen(process_args)
|
|
260
|
+
except Exception as exc: # pragma: no cover - best effort
|
|
261
|
+
print(f"Manual saved to {path} (auto-open failed: {exc})")
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def show_manual(open_viewer: bool = True) -> str:
|
|
265
|
+
"""
|
|
266
|
+
Ensure a plain-text manual exists and optionally open it.
|
|
267
|
+
|
|
268
|
+
HOW IT WORKS:
|
|
269
|
+
------------
|
|
270
|
+
This function manages the manual cache and opens the manual for viewing:
|
|
271
|
+
|
|
272
|
+
1. **Load manual text**: Read USER_MANUAL.md from package
|
|
273
|
+
2. **Check cache**: See if we already have a cached .txt file
|
|
274
|
+
3. **Validate cache**: Compare hash of current content vs cached hash
|
|
275
|
+
4. **Regenerate if needed**: If content changed, create new .txt file
|
|
276
|
+
5. **Open editor**: Launch user's preferred text editor (if requested)
|
|
277
|
+
|
|
278
|
+
CACHING STRATEGY:
|
|
279
|
+
---------------
|
|
280
|
+
We cache the manual to avoid regenerating it on every run. The cache is
|
|
281
|
+
invalidated when:
|
|
282
|
+
- Manual content changes (detected by hash comparison)
|
|
283
|
+
- Manual format version changes (MANUAL_RENDER_VERSION)
|
|
284
|
+
|
|
285
|
+
This means users get fresh manual when it's updated, but don't regenerate
|
|
286
|
+
unnecessarily when nothing has changed.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
open_viewer: If True, automatically open manual in text editor.
|
|
290
|
+
If False, just ensure file exists but don't open it.
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
Path to the manual .txt file (as string)
|
|
294
|
+
Example: "/Users/username/.cache/batplot/batplot_manual.txt"
|
|
295
|
+
"""
|
|
296
|
+
text = _manual_text()
|
|
297
|
+
target_dir = _cache_dir()
|
|
298
|
+
txt_path = target_dir / TEXT_NAME
|
|
299
|
+
hash_path = target_dir / HASH_NAME
|
|
300
|
+
content_hash = _manual_hash(text)
|
|
301
|
+
cached_hash = hash_path.read_text(encoding="utf-8").strip() if hash_path.exists() else ""
|
|
302
|
+
|
|
303
|
+
if (not txt_path.exists()) or (cached_hash != content_hash):
|
|
304
|
+
txt_path.write_text(text, encoding="utf-8")
|
|
305
|
+
hash_path.write_text(content_hash, encoding="utf-8")
|
|
306
|
+
|
|
307
|
+
if open_viewer:
|
|
308
|
+
_open_editor(txt_path)
|
|
309
|
+
return str(txt_path)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def main(argv: list[str] | None = None) -> int:
|
|
313
|
+
path = show_manual(open_viewer=True)
|
|
314
|
+
print(f"batplot manual ready at {path}")
|
|
315
|
+
return 0
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
__all__ = ["show_manual", "main"]
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
if __name__ == "__main__": # pragma: no cover
|
|
322
|
+
sys.exit(main())
|
|
323
|
+
|