pymdkit 1.6.3__tar.gz → 1.6.4__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.
- {pymdkit-1.6.3 → pymdkit-1.6.4}/PKG-INFO +24 -2
- {pymdkit-1.6.3 → pymdkit-1.6.4}/README.md +23 -1
- {pymdkit-1.6.3 → pymdkit-1.6.4}/pyproject.toml +1 -1
- {pymdkit-1.6.3 → pymdkit-1.6.4}/src/pymdkit/__init__.py +1 -1
- pymdkit-1.6.4/src/pymdkit/_config.py +89 -0
- pymdkit-1.6.4/src/pymdkit/commands/config.py +45 -0
- {pymdkit-1.6.3 → pymdkit-1.6.4}/src/pymdkit/commands/realloc_occ.py +1 -1
- pymdkit-1.6.4/src/pymdkit/pymdkit_main.py +253 -0
- {pymdkit-1.6.3 → pymdkit-1.6.4}/src/pymdkit.egg-info/PKG-INFO +24 -2
- {pymdkit-1.6.3 → pymdkit-1.6.4}/src/pymdkit.egg-info/SOURCES.txt +3 -0
- pymdkit-1.6.4/tests/test_config_logging.py +97 -0
- {pymdkit-1.6.3 → pymdkit-1.6.4}/tests/test_realloc_occ.py +2 -2
- pymdkit-1.6.3/src/pymdkit/pymdkit_main.py +0 -123
- {pymdkit-1.6.3 → pymdkit-1.6.4}/LICENSE +0 -0
- {pymdkit-1.6.3 → pymdkit-1.6.4}/setup.cfg +0 -0
- {pymdkit-1.6.3 → pymdkit-1.6.4}/src/pymdkit/commands/__init__.py +0 -0
- {pymdkit-1.6.3 → pymdkit-1.6.4}/src/pymdkit/commands/_cifio.py +0 -0
- {pymdkit-1.6.3 → pymdkit-1.6.4}/src/pymdkit/commands/_extxyz.py +0 -0
- {pymdkit-1.6.3 → pymdkit-1.6.4}/src/pymdkit/commands/_fileio.py +0 -0
- {pymdkit-1.6.3 → pymdkit-1.6.4}/src/pymdkit/commands/_geometry.py +0 -0
- {pymdkit-1.6.3 → pymdkit-1.6.4}/src/pymdkit/commands/_gpumd.py +0 -0
- {pymdkit-1.6.3 → pymdkit-1.6.4}/src/pymdkit/commands/_lattice.py +0 -0
- {pymdkit-1.6.3 → pymdkit-1.6.4}/src/pymdkit/commands/_oxidation.py +0 -0
- {pymdkit-1.6.3 → pymdkit-1.6.4}/src/pymdkit/commands/_precision.py +0 -0
- {pymdkit-1.6.3 → pymdkit-1.6.4}/src/pymdkit/commands/_reduction.py +0 -0
- {pymdkit-1.6.3 → pymdkit-1.6.4}/src/pymdkit/commands/_supercell.py +0 -0
- {pymdkit-1.6.3 → pymdkit-1.6.4}/src/pymdkit/commands/_symmetry.py +0 -0
- {pymdkit-1.6.3 → pymdkit-1.6.4}/src/pymdkit/commands/_vasp_output.py +0 -0
- {pymdkit-1.6.3 → pymdkit-1.6.4}/src/pymdkit/commands/_vaspset.py +0 -0
- {pymdkit-1.6.3 → pymdkit-1.6.4}/src/pymdkit/commands/add_config_type.py +0 -0
- {pymdkit-1.6.3 → pymdkit-1.6.4}/src/pymdkit/commands/chemsys_entry.py +0 -0
- {pymdkit-1.6.3 → pymdkit-1.6.4}/src/pymdkit/commands/compute_ehull.py +0 -0
- {pymdkit-1.6.3 → pymdkit-1.6.4}/src/pymdkit/commands/compute_rmsd.py +0 -0
- {pymdkit-1.6.3 → pymdkit-1.6.4}/src/pymdkit/commands/convert.py +0 -0
- {pymdkit-1.6.3 → pymdkit-1.6.4}/src/pymdkit/commands/ewald.py +0 -0
- {pymdkit-1.6.3 → pymdkit-1.6.4}/src/pymdkit/commands/gather_fs.py +0 -0
- {pymdkit-1.6.3 → pymdkit-1.6.4}/src/pymdkit/commands/gpumd_group.py +0 -0
- {pymdkit-1.6.3 → pymdkit-1.6.4}/src/pymdkit/commands/gpumd_msd.py +0 -0
- {pymdkit-1.6.3 → pymdkit-1.6.4}/src/pymdkit/commands/gpumd_relax.py +0 -0
- {pymdkit-1.6.3 → pymdkit-1.6.4}/src/pymdkit/commands/gpumd_thermo.py +0 -0
- {pymdkit-1.6.3 → pymdkit-1.6.4}/src/pymdkit/commands/gpumd_trjcat.py +0 -0
- {pymdkit-1.6.3 → pymdkit-1.6.4}/src/pymdkit/commands/nep_rmse.py +0 -0
- {pymdkit-1.6.3 → pymdkit-1.6.4}/src/pymdkit/commands/pca.py +0 -0
- {pymdkit-1.6.3 → pymdkit-1.6.4}/src/pymdkit/commands/perturb.py +0 -0
- {pymdkit-1.6.3 → pymdkit-1.6.4}/src/pymdkit/commands/submit_vasp.py +0 -0
- {pymdkit-1.6.3 → pymdkit-1.6.4}/src/pymdkit/commands/substitute.py +0 -0
- {pymdkit-1.6.3 → pymdkit-1.6.4}/src/pymdkit/commands/supercell.py +0 -0
- {pymdkit-1.6.3 → pymdkit-1.6.4}/src/pymdkit/commands/trj_extract.py +0 -0
- {pymdkit-1.6.3 → pymdkit-1.6.4}/src/pymdkit/commands/trj_sparse.py +0 -0
- {pymdkit-1.6.3 → pymdkit-1.6.4}/src/pymdkit/commands/umap.py +0 -0
- {pymdkit-1.6.3 → pymdkit-1.6.4}/src/pymdkit/commands/vasp2xyz.py +0 -0
- {pymdkit-1.6.3 → pymdkit-1.6.4}/src/pymdkit/commands/vasp_fe.py +0 -0
- {pymdkit-1.6.3 → pymdkit-1.6.4}/src/pymdkit/commands/vasp_relax.py +0 -0
- {pymdkit-1.6.3 → pymdkit-1.6.4}/src/pymdkit/commands/vasp_static.py +0 -0
- {pymdkit-1.6.3 → pymdkit-1.6.4}/src/pymdkit.egg-info/dependency_links.txt +0 -0
- {pymdkit-1.6.3 → pymdkit-1.6.4}/src/pymdkit.egg-info/entry_points.txt +0 -0
- {pymdkit-1.6.3 → pymdkit-1.6.4}/src/pymdkit.egg-info/requires.txt +0 -0
- {pymdkit-1.6.3 → pymdkit-1.6.4}/src/pymdkit.egg-info/top_level.txt +0 -0
- {pymdkit-1.6.3 → pymdkit-1.6.4}/tests/test_convert.py +0 -0
- {pymdkit-1.6.3 → pymdkit-1.6.4}/tests/test_lattice_symmetry.py +0 -0
- {pymdkit-1.6.3 → pymdkit-1.6.4}/tests/test_vaspset.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pymdkit
|
|
3
|
-
Version: 1.6.
|
|
3
|
+
Version: 1.6.4
|
|
4
4
|
Summary: A unified command-line toolkit for atomistic / MD structure workflows.
|
|
5
5
|
Author-email: Yueda Wang <ydwang0608@ustc.edu.cn>
|
|
6
6
|
License-Expression: GPL-3.0-or-later
|
|
@@ -63,6 +63,27 @@ pmk -help # lists every command
|
|
|
63
63
|
pmk <command> -help # shows that command's flags
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
+
## Global configuration and logs
|
|
67
|
+
|
|
68
|
+
On first use, PMK creates `.pmkrc.yaml` in the active Python environment root.
|
|
69
|
+
For example, an executable at `~/anaconda3/envs/pymdkit/bin/pmk` uses
|
|
70
|
+
`~/anaconda3/envs/pymdkit/.pmkrc.yaml`. View the active file and settings with:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
pmk config
|
|
74
|
+
pmk config silence -on
|
|
75
|
+
pmk config silence -off
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
With silence enabled, successful commands do not print to the terminal. Errors
|
|
79
|
+
that prevent an operation are still shown. Command details are never discarded:
|
|
80
|
+
every `pmk <command> ...` invocation appends its exact command, timestamp, exit
|
|
81
|
+
status, and screen output to `<command>.txt` in the directory where PMK was
|
|
82
|
+
invoked. Repeated runs remain in chronological order in the same file. This log
|
|
83
|
+
always stays in the invocation directory even when `-of` selects an output
|
|
84
|
+
folder. Existing command reports that use the same filename, such as `rmsd.txt`
|
|
85
|
+
and `ehull.txt`, are preserved as the report section of that run.
|
|
86
|
+
|
|
66
87
|
## Commands
|
|
67
88
|
|
|
68
89
|
Commands that transform structures accept either a single file (`-i`/`-o`) or a
|
|
@@ -71,6 +92,7 @@ directory for job sub-folders automatically.
|
|
|
71
92
|
|
|
72
93
|
| Command | What it does |
|
|
73
94
|
|---|---|
|
|
95
|
+
| `config` | View or edit global PMK settings in `.pmkrc.yaml` |
|
|
74
96
|
| `add-config-type` | Add Config_type to one XYZ structure or every trajectory frame |
|
|
75
97
|
| `gpumd-group` | Tag atoms with a GPUMD group index by element order |
|
|
76
98
|
| `gpumd-relax` | Write GPUMD energy-minimization jobs for a file, folder, or trajectory |
|
|
@@ -265,7 +287,7 @@ first, until every multiplicity times occupancy times supercell determinant is
|
|
|
265
287
|
an integer. Equal-volume choices favor the most isotropic resulting cell.
|
|
266
288
|
All selections, site atom counts, oxidation states, charges, failures, the
|
|
267
289
|
allowed occupancy grid, and the final summary are recorded in
|
|
268
|
-
`
|
|
290
|
+
`realloc-occ.txt` in the invocation directory for both single-file and folder modes.
|
|
269
291
|
|
|
270
292
|
`gather-fs` recursively scans all job folders below the current directory. For
|
|
271
293
|
VASP it follows the global `vaspout.h5 > vasprun.xml > OUTCAR` priority and
|
|
@@ -36,6 +36,27 @@ pmk -help # lists every command
|
|
|
36
36
|
pmk <command> -help # shows that command's flags
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
+
## Global configuration and logs
|
|
40
|
+
|
|
41
|
+
On first use, PMK creates `.pmkrc.yaml` in the active Python environment root.
|
|
42
|
+
For example, an executable at `~/anaconda3/envs/pymdkit/bin/pmk` uses
|
|
43
|
+
`~/anaconda3/envs/pymdkit/.pmkrc.yaml`. View the active file and settings with:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pmk config
|
|
47
|
+
pmk config silence -on
|
|
48
|
+
pmk config silence -off
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
With silence enabled, successful commands do not print to the terminal. Errors
|
|
52
|
+
that prevent an operation are still shown. Command details are never discarded:
|
|
53
|
+
every `pmk <command> ...` invocation appends its exact command, timestamp, exit
|
|
54
|
+
status, and screen output to `<command>.txt` in the directory where PMK was
|
|
55
|
+
invoked. Repeated runs remain in chronological order in the same file. This log
|
|
56
|
+
always stays in the invocation directory even when `-of` selects an output
|
|
57
|
+
folder. Existing command reports that use the same filename, such as `rmsd.txt`
|
|
58
|
+
and `ehull.txt`, are preserved as the report section of that run.
|
|
59
|
+
|
|
39
60
|
## Commands
|
|
40
61
|
|
|
41
62
|
Commands that transform structures accept either a single file (`-i`/`-o`) or a
|
|
@@ -44,6 +65,7 @@ directory for job sub-folders automatically.
|
|
|
44
65
|
|
|
45
66
|
| Command | What it does |
|
|
46
67
|
|---|---|
|
|
68
|
+
| `config` | View or edit global PMK settings in `.pmkrc.yaml` |
|
|
47
69
|
| `add-config-type` | Add Config_type to one XYZ structure or every trajectory frame |
|
|
48
70
|
| `gpumd-group` | Tag atoms with a GPUMD group index by element order |
|
|
49
71
|
| `gpumd-relax` | Write GPUMD energy-minimization jobs for a file, folder, or trajectory |
|
|
@@ -238,7 +260,7 @@ first, until every multiplicity times occupancy times supercell determinant is
|
|
|
238
260
|
an integer. Equal-volume choices favor the most isotropic resulting cell.
|
|
239
261
|
All selections, site atom counts, oxidation states, charges, failures, the
|
|
240
262
|
allowed occupancy grid, and the final summary are recorded in
|
|
241
|
-
`
|
|
263
|
+
`realloc-occ.txt` in the invocation directory for both single-file and folder modes.
|
|
242
264
|
|
|
243
265
|
`gather-fs` recursively scans all job folders below the current directory. For
|
|
244
266
|
VASP it follows the global `vaspout.h5 > vasprun.xml > OUTCAR` priority and
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Global PMK configuration stored in the active Python environment."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ast
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Dict, Optional
|
|
11
|
+
|
|
12
|
+
DEFAULT_CONFIG: Dict[str, Any] = {"silence": False}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def config_path() -> Path:
|
|
16
|
+
"""Return the PMK configuration file for the active Python environment."""
|
|
17
|
+
override = os.environ.get("PMK_CONFIG_FILE")
|
|
18
|
+
if override:
|
|
19
|
+
return Path(override).expanduser().resolve()
|
|
20
|
+
return Path(sys.prefix).resolve() / ".pmkrc.yaml"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _parse_scalar(raw: str) -> Any:
|
|
24
|
+
value = raw.strip()
|
|
25
|
+
lowered = value.lower()
|
|
26
|
+
if lowered in {"true", "yes", "on"}:
|
|
27
|
+
return True
|
|
28
|
+
if lowered in {"false", "no", "off"}:
|
|
29
|
+
return False
|
|
30
|
+
if lowered in {"null", "none", "~"}:
|
|
31
|
+
return None
|
|
32
|
+
try:
|
|
33
|
+
return ast.literal_eval(value)
|
|
34
|
+
except (SyntaxError, ValueError):
|
|
35
|
+
return value
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _read_flat_yaml(path: Path) -> Dict[str, Any]:
|
|
39
|
+
settings: Dict[str, Any] = {}
|
|
40
|
+
for line_number, line in enumerate(path.read_text(encoding="utf-8-sig").splitlines(), 1):
|
|
41
|
+
stripped = line.strip()
|
|
42
|
+
if not stripped or stripped.startswith("#"):
|
|
43
|
+
continue
|
|
44
|
+
if ":" not in line:
|
|
45
|
+
raise ValueError(f"invalid YAML at {path}:{line_number}: expected 'name: value'")
|
|
46
|
+
key, raw = line.split(":", 1)
|
|
47
|
+
key = key.strip()
|
|
48
|
+
if not key or any(char.isspace() for char in key):
|
|
49
|
+
raise ValueError(f"invalid setting name at {path}:{line_number}")
|
|
50
|
+
settings[key] = _parse_scalar(raw)
|
|
51
|
+
return settings
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _format_scalar(value: Any) -> str:
|
|
55
|
+
if isinstance(value, bool):
|
|
56
|
+
return "true" if value else "false"
|
|
57
|
+
if value is None:
|
|
58
|
+
return "null"
|
|
59
|
+
if isinstance(value, (int, float)):
|
|
60
|
+
return str(value)
|
|
61
|
+
return json.dumps(str(value), ensure_ascii=True)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def save_config(settings: Dict[str, Any], path: Optional[Path] = None) -> Path:
|
|
65
|
+
destination = config_path() if path is None else Path(path)
|
|
66
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
67
|
+
ordered_keys = [key for key in DEFAULT_CONFIG if key in settings]
|
|
68
|
+
ordered_keys.extend(sorted(key for key in settings if key not in DEFAULT_CONFIG))
|
|
69
|
+
text = "# Global PMK settings. Edit with `pmk config` or a text editor.\n"
|
|
70
|
+
text += "".join(f"{key}: {_format_scalar(settings[key])}\n" for key in ordered_keys)
|
|
71
|
+
temporary = destination.with_name(f".{destination.name}.{os.getpid()}.tmp")
|
|
72
|
+
temporary.write_text(text, encoding="utf-8")
|
|
73
|
+
temporary.replace(destination)
|
|
74
|
+
return destination
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def load_config(*, create: bool = True) -> Dict[str, Any]:
|
|
78
|
+
path = config_path()
|
|
79
|
+
if path.exists():
|
|
80
|
+
settings = _read_flat_yaml(path)
|
|
81
|
+
else:
|
|
82
|
+
settings = {}
|
|
83
|
+
merged = dict(DEFAULT_CONFIG)
|
|
84
|
+
merged.update(settings)
|
|
85
|
+
if not isinstance(merged["silence"], bool):
|
|
86
|
+
raise ValueError(f"setting 'silence' in {path} must be true or false")
|
|
87
|
+
if create and not path.exists():
|
|
88
|
+
save_config(merged, path)
|
|
89
|
+
return merged
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""View or edit global PMK settings."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pymdkit._config import config_path, load_config, save_config
|
|
6
|
+
|
|
7
|
+
COMMAND = "config"
|
|
8
|
+
HELP = "View or edit global PMK settings."
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def add_arguments(parser):
|
|
12
|
+
parser.add_argument("setting", nargs="?", choices=("silence",),
|
|
13
|
+
help="Setting to update; omit it to show all settings.")
|
|
14
|
+
switch = parser.add_mutually_exclusive_group()
|
|
15
|
+
switch.add_argument("-on", dest="value", action="store_const", const=True,
|
|
16
|
+
help="Enable the selected setting.")
|
|
17
|
+
switch.add_argument("-off", dest="value", action="store_const", const=False,
|
|
18
|
+
help="Disable the selected setting.")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def run(args):
|
|
22
|
+
settings = load_config()
|
|
23
|
+
value = getattr(args, "value", None)
|
|
24
|
+
if args.setting is None:
|
|
25
|
+
if value is not None:
|
|
26
|
+
print("Error: -on or -off requires a setting name.")
|
|
27
|
+
return 2
|
|
28
|
+
print(f"Configuration file: {config_path()}")
|
|
29
|
+
for name in sorted(settings):
|
|
30
|
+
if settings[name] is True:
|
|
31
|
+
shown = "on"
|
|
32
|
+
elif settings[name] is False:
|
|
33
|
+
shown = "off"
|
|
34
|
+
else:
|
|
35
|
+
shown = settings[name]
|
|
36
|
+
print(f"{name}: {shown}")
|
|
37
|
+
return 0
|
|
38
|
+
if value is None:
|
|
39
|
+
print(f"Error: pmk config {args.setting} requires -on or -off.")
|
|
40
|
+
return 2
|
|
41
|
+
settings[args.setting] = value
|
|
42
|
+
path = save_config(settings)
|
|
43
|
+
print(f"{args.setting}: {'on' if value else 'off'}")
|
|
44
|
+
print(f"Updated {path}")
|
|
45
|
+
return 0
|
|
@@ -728,7 +728,7 @@ def run(args):
|
|
|
728
728
|
f"input structure(s) to {output_root}."
|
|
729
729
|
)
|
|
730
730
|
_emit(report, summary)
|
|
731
|
-
report_path =
|
|
731
|
+
report_path = Path.cwd() / "realloc-occ.txt"
|
|
732
732
|
report_path.write_text("\n".join(report) + "\n", encoding="utf-8")
|
|
733
733
|
print(f"Wrote {report_path}")
|
|
734
734
|
return 0 if inputs_done == len(sources) else 1
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"""pmk dispatcher."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import ast
|
|
7
|
+
import io
|
|
8
|
+
import importlib
|
|
9
|
+
import os
|
|
10
|
+
import pkgutil
|
|
11
|
+
import shlex
|
|
12
|
+
import sys
|
|
13
|
+
import traceback
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Dict, Tuple
|
|
17
|
+
|
|
18
|
+
from . import __version__, commands
|
|
19
|
+
from ._config import load_config
|
|
20
|
+
|
|
21
|
+
DESCRIPTION = "A python data processing toolkit for molecular dynamics and high-throughput calculations"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _read_meta(path: Path, modname: str) -> Tuple[str, str]:
|
|
25
|
+
command, help_text = modname, ""
|
|
26
|
+
try:
|
|
27
|
+
tree = ast.parse(path.read_text(encoding="utf-8-sig"))
|
|
28
|
+
except (OSError, SyntaxError):
|
|
29
|
+
return command, help_text
|
|
30
|
+
for node in tree.body:
|
|
31
|
+
if isinstance(node, ast.Assign):
|
|
32
|
+
for target in node.targets:
|
|
33
|
+
if (isinstance(target, ast.Name)
|
|
34
|
+
and isinstance(node.value, ast.Constant)
|
|
35
|
+
and isinstance(node.value.value, str)):
|
|
36
|
+
if target.id == "COMMAND":
|
|
37
|
+
command = node.value.value
|
|
38
|
+
elif target.id == "HELP":
|
|
39
|
+
help_text = node.value.value
|
|
40
|
+
if not help_text:
|
|
41
|
+
doc = (ast.get_docstring(tree) or "").strip()
|
|
42
|
+
help_text = doc.splitlines()[0] if doc else ""
|
|
43
|
+
return command, help_text
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _discover() -> Dict[str, Tuple[str, str]]:
|
|
47
|
+
pkg_dir = Path(commands.__path__[0])
|
|
48
|
+
found: Dict[str, Tuple[str, str]] = {}
|
|
49
|
+
for info in pkgutil.iter_modules([str(pkg_dir)]):
|
|
50
|
+
if info.name.startswith("_"):
|
|
51
|
+
continue
|
|
52
|
+
command, help_text = _read_meta(pkg_dir / f"{info.name}.py", info.name)
|
|
53
|
+
found[command] = (info.name, help_text)
|
|
54
|
+
return found
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _add_help(parser: argparse.ArgumentParser) -> None:
|
|
58
|
+
parser.add_argument("-h", "-help", action="help",
|
|
59
|
+
help="show this help message and exit")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def print_main_help(cmds) -> None:
|
|
63
|
+
print("pmk")
|
|
64
|
+
print()
|
|
65
|
+
print(DESCRIPTION)
|
|
66
|
+
print()
|
|
67
|
+
print(f"Version {__version__}")
|
|
68
|
+
print()
|
|
69
|
+
print("Usage: pmk <command> -[options]")
|
|
70
|
+
print()
|
|
71
|
+
print("Available commands:")
|
|
72
|
+
print()
|
|
73
|
+
for name in sorted(cmds):
|
|
74
|
+
print(f" {name:<20} {cmds[name][1]}")
|
|
75
|
+
print()
|
|
76
|
+
print("Command details: pmk <command> -h")
|
|
77
|
+
print()
|
|
78
|
+
print("Options:")
|
|
79
|
+
print(" -h, -help show this help message and exit")
|
|
80
|
+
print(" -v, -version show program's version number and exit")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _build_top_parser(cmds: Dict[str, Tuple[str, str]]) -> argparse.ArgumentParser:
|
|
84
|
+
parser = argparse.ArgumentParser(add_help=False)
|
|
85
|
+
_add_help(parser)
|
|
86
|
+
parser.add_argument("-v", "-version", action="version", version=f"pmk {__version__}")
|
|
87
|
+
sub = parser.add_subparsers(dest="command", metavar="<command>")
|
|
88
|
+
for name in sorted(cmds):
|
|
89
|
+
sub.add_parser(name, help=cmds[name][1], add_help=False)
|
|
90
|
+
return parser
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class _OutputRecorder(io.TextIOBase):
|
|
94
|
+
"""Record output while preserving normal live terminal behavior."""
|
|
95
|
+
|
|
96
|
+
def __init__(self, terminal, *, echo: bool):
|
|
97
|
+
self.terminal = terminal
|
|
98
|
+
self.echo = echo
|
|
99
|
+
self.buffer = io.StringIO()
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def encoding(self):
|
|
103
|
+
return getattr(self.terminal, "encoding", "utf-8")
|
|
104
|
+
|
|
105
|
+
def isatty(self):
|
|
106
|
+
return bool(getattr(self.terminal, "isatty", lambda: False)())
|
|
107
|
+
|
|
108
|
+
def write(self, text):
|
|
109
|
+
self.buffer.write(text)
|
|
110
|
+
if self.echo:
|
|
111
|
+
self.terminal.write(text)
|
|
112
|
+
self.terminal.flush()
|
|
113
|
+
return len(text)
|
|
114
|
+
|
|
115
|
+
def flush(self):
|
|
116
|
+
if self.echo:
|
|
117
|
+
self.terminal.flush()
|
|
118
|
+
|
|
119
|
+
def getvalue(self):
|
|
120
|
+
return self.buffer.getvalue()
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _command_text(argv) -> str:
|
|
124
|
+
return shlex.join(["pmk", *argv])
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _write_command_log(command: str, argv, output: str, status: int,
|
|
128
|
+
previous: str, native_report: str) -> Path:
|
|
129
|
+
path = Path.cwd() / f"{command}.txt"
|
|
130
|
+
separator = "=" * 80
|
|
131
|
+
lines = [
|
|
132
|
+
separator,
|
|
133
|
+
f"Run: {datetime.now().astimezone().isoformat(timespec='seconds')}",
|
|
134
|
+
f"Command: {_command_text(argv)}",
|
|
135
|
+
f"Status: {'success' if status == 0 else 'failed'} (exit code {status})",
|
|
136
|
+
"-" * 80,
|
|
137
|
+
]
|
|
138
|
+
details = output.rstrip()
|
|
139
|
+
if details:
|
|
140
|
+
lines.extend(("Output:", details))
|
|
141
|
+
if native_report.strip():
|
|
142
|
+
lines.extend(("Report:", native_report.rstrip()))
|
|
143
|
+
if not details and not native_report.strip():
|
|
144
|
+
lines.append("No screen output.")
|
|
145
|
+
block = "\n".join(lines) + "\n"
|
|
146
|
+
combined = previous.rstrip()
|
|
147
|
+
if combined:
|
|
148
|
+
combined += "\n\n"
|
|
149
|
+
combined += block
|
|
150
|
+
temporary = path.with_name(f".{path.name}.{os.getpid()}.tmp")
|
|
151
|
+
temporary.write_text(combined, encoding="utf-8")
|
|
152
|
+
temporary.replace(path)
|
|
153
|
+
return path
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _native_report(path: Path, previous: str) -> str:
|
|
157
|
+
if not path.exists():
|
|
158
|
+
return ""
|
|
159
|
+
current = path.read_text(encoding="utf-8", errors="replace")
|
|
160
|
+
if current == previous:
|
|
161
|
+
return ""
|
|
162
|
+
if previous and current.startswith(previous):
|
|
163
|
+
return current[len(previous):].lstrip("\r\n")
|
|
164
|
+
return current
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _run_command(command: str, argv, module, parser, *, silence: bool) -> int:
|
|
168
|
+
log_path = Path.cwd() / f"{command}.txt"
|
|
169
|
+
previous = log_path.read_text(encoding="utf-8", errors="replace") if log_path.exists() else ""
|
|
170
|
+
terminal_out, terminal_err = sys.stdout, sys.stderr
|
|
171
|
+
recorder = _OutputRecorder(terminal_out, echo=not silence)
|
|
172
|
+
status = 0
|
|
173
|
+
pending_exception = None
|
|
174
|
+
exception_text = ""
|
|
175
|
+
try:
|
|
176
|
+
sys.stdout = recorder
|
|
177
|
+
sys.stderr = recorder
|
|
178
|
+
try:
|
|
179
|
+
args = parser.parse_args(argv[1:])
|
|
180
|
+
result = module.run(args)
|
|
181
|
+
status = 0 if result is None else int(result)
|
|
182
|
+
except SystemExit as exc:
|
|
183
|
+
status = exc.code if isinstance(exc.code, int) else 1
|
|
184
|
+
if exc.code and not isinstance(exc.code, int):
|
|
185
|
+
print(exc.code)
|
|
186
|
+
except Exception:
|
|
187
|
+
status = 1
|
|
188
|
+
pending_exception = sys.exc_info()
|
|
189
|
+
exception_text = traceback.format_exc()
|
|
190
|
+
recorder.buffer.write(exception_text)
|
|
191
|
+
finally:
|
|
192
|
+
sys.stdout = terminal_out
|
|
193
|
+
sys.stderr = terminal_err
|
|
194
|
+
|
|
195
|
+
output = recorder.getvalue()
|
|
196
|
+
native_report = _native_report(log_path, previous)
|
|
197
|
+
try:
|
|
198
|
+
_write_command_log(command, argv, output, status, previous, native_report)
|
|
199
|
+
except Exception as exc:
|
|
200
|
+
print(f"Error: could not write {log_path}: {exc}", file=terminal_err)
|
|
201
|
+
if status == 0:
|
|
202
|
+
status = 1
|
|
203
|
+
if silence and status != 0 and pending_exception is None:
|
|
204
|
+
message = output or f"Error: {_command_text(argv)} failed with exit code {status}.\n"
|
|
205
|
+
terminal_err.write(message)
|
|
206
|
+
terminal_err.flush()
|
|
207
|
+
if pending_exception is not None:
|
|
208
|
+
_, error, tb = pending_exception
|
|
209
|
+
raise error.with_traceback(tb)
|
|
210
|
+
return status
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def main(argv=None) -> int:
|
|
214
|
+
argv = list(sys.argv[1:] if argv is None else argv)
|
|
215
|
+
cmds = _discover()
|
|
216
|
+
top = _build_top_parser(cmds)
|
|
217
|
+
|
|
218
|
+
if not argv:
|
|
219
|
+
print_main_help(cmds)
|
|
220
|
+
return 1
|
|
221
|
+
if argv[0] in ("-h", "-help"):
|
|
222
|
+
print_main_help(cmds)
|
|
223
|
+
return 0
|
|
224
|
+
if argv[0] in ("-v", "-version"):
|
|
225
|
+
top.parse_args(argv)
|
|
226
|
+
return 0
|
|
227
|
+
|
|
228
|
+
command = argv[0]
|
|
229
|
+
if command not in cmds:
|
|
230
|
+
print_main_help(cmds)
|
|
231
|
+
raise SystemExit(f"\nError: invalid command {command!r}")
|
|
232
|
+
|
|
233
|
+
modname = cmds[command][0]
|
|
234
|
+
module = importlib.import_module(f"{commands.__name__}.{modname}")
|
|
235
|
+
cmd_parser = argparse.ArgumentParser(
|
|
236
|
+
prog=f"pmk {command}",
|
|
237
|
+
description=getattr(module, "HELP", None) or (module.__doc__ or "").strip(),
|
|
238
|
+
add_help=False,
|
|
239
|
+
)
|
|
240
|
+
_add_help(cmd_parser)
|
|
241
|
+
if hasattr(module, "add_arguments"):
|
|
242
|
+
module.add_arguments(cmd_parser)
|
|
243
|
+
try:
|
|
244
|
+
settings = load_config()
|
|
245
|
+
except (OSError, ValueError) as exc:
|
|
246
|
+
print(f"Error: could not load PMK configuration: {exc}", file=sys.stderr)
|
|
247
|
+
return 2
|
|
248
|
+
return _run_command(command, argv, module, cmd_parser,
|
|
249
|
+
silence=settings["silence"])
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
if __name__ == "__main__":
|
|
253
|
+
raise SystemExit(main())
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pymdkit
|
|
3
|
-
Version: 1.6.
|
|
3
|
+
Version: 1.6.4
|
|
4
4
|
Summary: A unified command-line toolkit for atomistic / MD structure workflows.
|
|
5
5
|
Author-email: Yueda Wang <ydwang0608@ustc.edu.cn>
|
|
6
6
|
License-Expression: GPL-3.0-or-later
|
|
@@ -63,6 +63,27 @@ pmk -help # lists every command
|
|
|
63
63
|
pmk <command> -help # shows that command's flags
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
+
## Global configuration and logs
|
|
67
|
+
|
|
68
|
+
On first use, PMK creates `.pmkrc.yaml` in the active Python environment root.
|
|
69
|
+
For example, an executable at `~/anaconda3/envs/pymdkit/bin/pmk` uses
|
|
70
|
+
`~/anaconda3/envs/pymdkit/.pmkrc.yaml`. View the active file and settings with:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
pmk config
|
|
74
|
+
pmk config silence -on
|
|
75
|
+
pmk config silence -off
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
With silence enabled, successful commands do not print to the terminal. Errors
|
|
79
|
+
that prevent an operation are still shown. Command details are never discarded:
|
|
80
|
+
every `pmk <command> ...` invocation appends its exact command, timestamp, exit
|
|
81
|
+
status, and screen output to `<command>.txt` in the directory where PMK was
|
|
82
|
+
invoked. Repeated runs remain in chronological order in the same file. This log
|
|
83
|
+
always stays in the invocation directory even when `-of` selects an output
|
|
84
|
+
folder. Existing command reports that use the same filename, such as `rmsd.txt`
|
|
85
|
+
and `ehull.txt`, are preserved as the report section of that run.
|
|
86
|
+
|
|
66
87
|
## Commands
|
|
67
88
|
|
|
68
89
|
Commands that transform structures accept either a single file (`-i`/`-o`) or a
|
|
@@ -71,6 +92,7 @@ directory for job sub-folders automatically.
|
|
|
71
92
|
|
|
72
93
|
| Command | What it does |
|
|
73
94
|
|---|---|
|
|
95
|
+
| `config` | View or edit global PMK settings in `.pmkrc.yaml` |
|
|
74
96
|
| `add-config-type` | Add Config_type to one XYZ structure or every trajectory frame |
|
|
75
97
|
| `gpumd-group` | Tag atoms with a GPUMD group index by element order |
|
|
76
98
|
| `gpumd-relax` | Write GPUMD energy-minimization jobs for a file, folder, or trajectory |
|
|
@@ -265,7 +287,7 @@ first, until every multiplicity times occupancy times supercell determinant is
|
|
|
265
287
|
an integer. Equal-volume choices favor the most isotropic resulting cell.
|
|
266
288
|
All selections, site atom counts, oxidation states, charges, failures, the
|
|
267
289
|
allowed occupancy grid, and the final summary are recorded in
|
|
268
|
-
`
|
|
290
|
+
`realloc-occ.txt` in the invocation directory for both single-file and folder modes.
|
|
269
291
|
|
|
270
292
|
`gather-fs` recursively scans all job folders below the current directory. For
|
|
271
293
|
VASP it follows the global `vaspout.h5 > vasprun.xml > OUTCAR` priority and
|
|
@@ -2,6 +2,7 @@ LICENSE
|
|
|
2
2
|
README.md
|
|
3
3
|
pyproject.toml
|
|
4
4
|
src/pymdkit/__init__.py
|
|
5
|
+
src/pymdkit/_config.py
|
|
5
6
|
src/pymdkit/pymdkit_main.py
|
|
6
7
|
src/pymdkit.egg-info/PKG-INFO
|
|
7
8
|
src/pymdkit.egg-info/SOURCES.txt
|
|
@@ -27,6 +28,7 @@ src/pymdkit/commands/add_config_type.py
|
|
|
27
28
|
src/pymdkit/commands/chemsys_entry.py
|
|
28
29
|
src/pymdkit/commands/compute_ehull.py
|
|
29
30
|
src/pymdkit/commands/compute_rmsd.py
|
|
31
|
+
src/pymdkit/commands/config.py
|
|
30
32
|
src/pymdkit/commands/convert.py
|
|
31
33
|
src/pymdkit/commands/ewald.py
|
|
32
34
|
src/pymdkit/commands/gather_fs.py
|
|
@@ -49,6 +51,7 @@ src/pymdkit/commands/vasp2xyz.py
|
|
|
49
51
|
src/pymdkit/commands/vasp_fe.py
|
|
50
52
|
src/pymdkit/commands/vasp_relax.py
|
|
51
53
|
src/pymdkit/commands/vasp_static.py
|
|
54
|
+
tests/test_config_logging.py
|
|
52
55
|
tests/test_convert.py
|
|
53
56
|
tests/test_lattice_symmetry.py
|
|
54
57
|
tests/test_realloc_occ.py
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Tests for PMK global configuration and command logging."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import io
|
|
6
|
+
import os
|
|
7
|
+
import tempfile
|
|
8
|
+
import unittest
|
|
9
|
+
from contextlib import redirect_stderr, redirect_stdout
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from unittest.mock import patch
|
|
12
|
+
|
|
13
|
+
from pymdkit import _config
|
|
14
|
+
from pymdkit import pymdkit_main
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class WorkingDirectory:
|
|
18
|
+
def __init__(self, path):
|
|
19
|
+
self.path = Path(path)
|
|
20
|
+
self.previous = None
|
|
21
|
+
|
|
22
|
+
def __enter__(self):
|
|
23
|
+
self.previous = Path.cwd()
|
|
24
|
+
os.chdir(self.path)
|
|
25
|
+
return self.path
|
|
26
|
+
|
|
27
|
+
def __exit__(self, *exc_info):
|
|
28
|
+
os.chdir(self.previous)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ConfigTests(unittest.TestCase):
|
|
32
|
+
def test_default_path_uses_active_environment_root(self):
|
|
33
|
+
with patch.object(_config.sys, "prefix", r"C:\envs\pymdkit"), \
|
|
34
|
+
patch.dict(os.environ, {}, clear=True):
|
|
35
|
+
self.assertEqual(
|
|
36
|
+
_config.config_path(),
|
|
37
|
+
Path(r"C:\envs\pymdkit").resolve() / ".pmkrc.yaml",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def test_config_command_silence_and_append_only_log(self):
|
|
41
|
+
with tempfile.TemporaryDirectory() as tmp, WorkingDirectory(tmp):
|
|
42
|
+
config_file = Path(tmp) / "environment" / ".pmkrc.yaml"
|
|
43
|
+
environment = {"PMK_CONFIG_FILE": str(config_file)}
|
|
44
|
+
with patch.dict(os.environ, environment, clear=False):
|
|
45
|
+
stdout, stderr = io.StringIO(), io.StringIO()
|
|
46
|
+
with redirect_stdout(stdout), redirect_stderr(stderr):
|
|
47
|
+
self.assertEqual(
|
|
48
|
+
pymdkit_main.main(["config", "silence", "-on"]), 0
|
|
49
|
+
)
|
|
50
|
+
self.assertIn("silence: on", stdout.getvalue())
|
|
51
|
+
self.assertIn("silence: true", config_file.read_text(encoding="utf-8"))
|
|
52
|
+
|
|
53
|
+
stdout, stderr = io.StringIO(), io.StringIO()
|
|
54
|
+
with redirect_stdout(stdout), redirect_stderr(stderr):
|
|
55
|
+
self.assertEqual(pymdkit_main.main(["config"]), 0)
|
|
56
|
+
self.assertEqual(stdout.getvalue(), "")
|
|
57
|
+
self.assertEqual(stderr.getvalue(), "")
|
|
58
|
+
|
|
59
|
+
stdout, stderr = io.StringIO(), io.StringIO()
|
|
60
|
+
with redirect_stdout(stdout), redirect_stderr(stderr):
|
|
61
|
+
self.assertEqual(pymdkit_main.main(["config", "silence"]), 2)
|
|
62
|
+
self.assertEqual(stdout.getvalue(), "")
|
|
63
|
+
self.assertIn("requires -on or -off", stderr.getvalue())
|
|
64
|
+
|
|
65
|
+
stdout, stderr = io.StringIO(), io.StringIO()
|
|
66
|
+
with redirect_stdout(stdout), redirect_stderr(stderr):
|
|
67
|
+
self.assertEqual(
|
|
68
|
+
pymdkit_main.main(["config", "silence", "-off"]), 0
|
|
69
|
+
)
|
|
70
|
+
self.assertEqual(stdout.getvalue(), "")
|
|
71
|
+
self.assertIn("silence: false", config_file.read_text(encoding="utf-8"))
|
|
72
|
+
|
|
73
|
+
report = (Path(tmp) / "config.txt").read_text(encoding="utf-8")
|
|
74
|
+
self.assertIn("Command: pmk config silence -on", report)
|
|
75
|
+
self.assertIn("Command: pmk config silence -off", report)
|
|
76
|
+
self.assertEqual(report.count("Command:"), 4)
|
|
77
|
+
self.assertIn("Status: failed (exit code 2)", report)
|
|
78
|
+
|
|
79
|
+
def test_native_report_is_preserved_in_command_log(self):
|
|
80
|
+
with tempfile.TemporaryDirectory() as tmp, WorkingDirectory(tmp):
|
|
81
|
+
path = Path(tmp) / "rmsd.txt"
|
|
82
|
+
previous = "older run\n"
|
|
83
|
+
path.write_text(previous, encoding="utf-8")
|
|
84
|
+
path.write_text("new RMSD details\n", encoding="utf-8")
|
|
85
|
+
native = pymdkit_main._native_report(path, previous)
|
|
86
|
+
pymdkit_main._write_command_log(
|
|
87
|
+
"rmsd", ["rmsd", "-if", "structures"], "Wrote rmsd.txt\n",
|
|
88
|
+
0, previous, native,
|
|
89
|
+
)
|
|
90
|
+
report = path.read_text(encoding="utf-8")
|
|
91
|
+
self.assertIn("older run", report)
|
|
92
|
+
self.assertIn("Command: pmk rmsd -if structures", report)
|
|
93
|
+
self.assertIn("Report:\nnew RMSD details", report)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
if __name__ == "__main__":
|
|
97
|
+
unittest.main()
|
|
@@ -169,13 +169,13 @@ class ReallocOccTests(unittest.TestCase):
|
|
|
169
169
|
occ=[0.25, 0.5, 0.75, 1.0],
|
|
170
170
|
max_supercell="2x2x2",
|
|
171
171
|
)
|
|
172
|
-
with patch.object(
|
|
172
|
+
with patch.object(realloc_occ.Path, "cwd", return_value=root), patch.object(
|
|
173
173
|
realloc_occ._oxidation,
|
|
174
174
|
"infer_oxidation_states",
|
|
175
175
|
return_value=OXIDATION,
|
|
176
176
|
):
|
|
177
177
|
self.assertEqual(realloc_occ.run(args), 0)
|
|
178
|
-
report = (
|
|
178
|
+
report = (root / "realloc-occ.txt").read_text(encoding="utf-8")
|
|
179
179
|
self.assertIn(
|
|
180
180
|
"Allowed occupancies: 0.250, 0.500, 0.750, 1.000",
|
|
181
181
|
report,
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
"""pmk dispatcher."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import argparse
|
|
6
|
-
import ast
|
|
7
|
-
import importlib
|
|
8
|
-
import pkgutil
|
|
9
|
-
import sys
|
|
10
|
-
from pathlib import Path
|
|
11
|
-
from typing import Dict, Tuple
|
|
12
|
-
|
|
13
|
-
from . import __version__, commands
|
|
14
|
-
|
|
15
|
-
DESCRIPTION = "A python data processing toolkit for molecular dynamics and high-throughput calculations"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def _read_meta(path: Path, modname: str) -> Tuple[str, str]:
|
|
19
|
-
command, help_text = modname, ""
|
|
20
|
-
try:
|
|
21
|
-
tree = ast.parse(path.read_text(encoding="utf-8-sig"))
|
|
22
|
-
except (OSError, SyntaxError):
|
|
23
|
-
return command, help_text
|
|
24
|
-
for node in tree.body:
|
|
25
|
-
if isinstance(node, ast.Assign):
|
|
26
|
-
for target in node.targets:
|
|
27
|
-
if (isinstance(target, ast.Name)
|
|
28
|
-
and isinstance(node.value, ast.Constant)
|
|
29
|
-
and isinstance(node.value.value, str)):
|
|
30
|
-
if target.id == "COMMAND":
|
|
31
|
-
command = node.value.value
|
|
32
|
-
elif target.id == "HELP":
|
|
33
|
-
help_text = node.value.value
|
|
34
|
-
if not help_text:
|
|
35
|
-
doc = (ast.get_docstring(tree) or "").strip()
|
|
36
|
-
help_text = doc.splitlines()[0] if doc else ""
|
|
37
|
-
return command, help_text
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def _discover() -> Dict[str, Tuple[str, str]]:
|
|
41
|
-
pkg_dir = Path(commands.__path__[0])
|
|
42
|
-
found: Dict[str, Tuple[str, str]] = {}
|
|
43
|
-
for info in pkgutil.iter_modules([str(pkg_dir)]):
|
|
44
|
-
if info.name.startswith("_"):
|
|
45
|
-
continue
|
|
46
|
-
command, help_text = _read_meta(pkg_dir / f"{info.name}.py", info.name)
|
|
47
|
-
found[command] = (info.name, help_text)
|
|
48
|
-
return found
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def _add_help(parser: argparse.ArgumentParser) -> None:
|
|
52
|
-
parser.add_argument("-h", "-help", action="help",
|
|
53
|
-
help="show this help message and exit")
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def print_main_help(cmds) -> None:
|
|
57
|
-
print("pmk")
|
|
58
|
-
print()
|
|
59
|
-
print(DESCRIPTION)
|
|
60
|
-
print()
|
|
61
|
-
print(f"Version {__version__}")
|
|
62
|
-
print()
|
|
63
|
-
print("Usage: pmk <command> -[options]")
|
|
64
|
-
print()
|
|
65
|
-
print("Available commands:")
|
|
66
|
-
print()
|
|
67
|
-
for name in sorted(cmds):
|
|
68
|
-
print(f" {name:<20} {cmds[name][1]}")
|
|
69
|
-
print()
|
|
70
|
-
print("Command details: pmk <command> -h")
|
|
71
|
-
print()
|
|
72
|
-
print("Options:")
|
|
73
|
-
print(" -h, -help show this help message and exit")
|
|
74
|
-
print(" -v, -version show program's version number and exit")
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def _build_top_parser(cmds: Dict[str, Tuple[str, str]]) -> argparse.ArgumentParser:
|
|
78
|
-
parser = argparse.ArgumentParser(add_help=False)
|
|
79
|
-
_add_help(parser)
|
|
80
|
-
parser.add_argument("-v", "-version", action="version", version=f"pmk {__version__}")
|
|
81
|
-
sub = parser.add_subparsers(dest="command", metavar="<command>")
|
|
82
|
-
for name in sorted(cmds):
|
|
83
|
-
sub.add_parser(name, help=cmds[name][1], add_help=False)
|
|
84
|
-
return parser
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
def main(argv=None) -> int:
|
|
88
|
-
argv = list(sys.argv[1:] if argv is None else argv)
|
|
89
|
-
cmds = _discover()
|
|
90
|
-
top = _build_top_parser(cmds)
|
|
91
|
-
|
|
92
|
-
if not argv:
|
|
93
|
-
print_main_help(cmds)
|
|
94
|
-
return 1
|
|
95
|
-
if argv[0] in ("-h", "-help"):
|
|
96
|
-
print_main_help(cmds)
|
|
97
|
-
return 0
|
|
98
|
-
if argv[0] in ("-v", "-version"):
|
|
99
|
-
top.parse_args(argv)
|
|
100
|
-
return 0
|
|
101
|
-
|
|
102
|
-
command = argv[0]
|
|
103
|
-
if command not in cmds:
|
|
104
|
-
print_main_help(cmds)
|
|
105
|
-
raise SystemExit(f"\nError: invalid command {command!r}")
|
|
106
|
-
|
|
107
|
-
modname = cmds[command][0]
|
|
108
|
-
module = importlib.import_module(f"{commands.__name__}.{modname}")
|
|
109
|
-
cmd_parser = argparse.ArgumentParser(
|
|
110
|
-
prog=f"pmk {command}",
|
|
111
|
-
description=getattr(module, "HELP", None) or (module.__doc__ or "").strip(),
|
|
112
|
-
add_help=False,
|
|
113
|
-
)
|
|
114
|
-
_add_help(cmd_parser)
|
|
115
|
-
if hasattr(module, "add_arguments"):
|
|
116
|
-
module.add_arguments(cmd_parser)
|
|
117
|
-
args = cmd_parser.parse_args(argv[1:])
|
|
118
|
-
result = module.run(args)
|
|
119
|
-
return 0 if result is None else int(result)
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
if __name__ == "__main__":
|
|
123
|
-
raise SystemExit(main())
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|