tbtools-cli 1.2.0__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.
- tbtools_cli/__init__.py +18 -0
- tbtools_cli/auto_commands.py +1149 -0
- tbtools_cli/cli.py +528 -0
- tbtools_cli/cli_load.py +352 -0
- tbtools_cli/cli_rpc.py +303 -0
- tbtools_cli/cli_tools_registry.py +93 -0
- tbtools_cli/cli_top.py +1160 -0
- tbtools_cli/command_metadata.json +4606 -0
- tbtools_cli/command_spec.py +382 -0
- tbtools_cli/config.example.toml +19 -0
- tbtools_cli/config.py +43 -0
- tbtools_cli/core.py +508 -0
- tbtools_cli/errors.py +38 -0
- tbtools_cli/presets.py +100 -0
- tbtools_cli/scenarios.py +92 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/AdmixtureCli.java +91 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/AmazingMetaCli.java +78 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/AssemblyRecommandCli.java +47 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/BamIndexCli.java +31 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/BamSortCli.java +43 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/BamStateCli.java +49 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/BarPlotterCli.java +18 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/BarplotCli.java +168 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/BatchVizMotifsCli.java +47 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/BlastXmlConvertCli.java +45 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/BlatExecutorCli.java +66 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/CalcRepeatCli.java +62 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/CalculateSimilarityCli.java +34 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/CddMotifCli.java +46 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/CircleGeneViewerCli.java +81 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/CircosCli.java +76 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/ColorSchemeCli.java +29 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/CtgGroupCli.java +29 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/CubeHeatmapCli.java +70 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/DeHistCli.java +46 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/DegramdomCli.java +33 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/DiffExpCli.java +44 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/DistanceCli.java +53 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/DualSynCli.java +147 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/EggnogCli.java +39 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/ExprCorrCli.java +27 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/FastaMergerCli.java +36 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/FastaTableConvertCli.java +38 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/FileCleanerCli.java +31 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/FileSplitCli.java +15 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/FindBlockDualCli.java +71 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/FindBlockMultipleCli.java +67 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/FindPathCli.java +15 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/GSEAWrapperCli.java +33 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/GenBank2FastaCli.java +32 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/GeneDensityCli.java +38 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/GeneLocGffCli.java +140 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/GeneStructureCli.java +90 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/GenericCli.java +177 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/GetENALinksCli.java +36 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/GetSubNewickTreeCli.java +50 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/GoEnrichCli.java +42 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/GoLevelCli.java +59 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/GroupedBarCli.java +95 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/GsaDiagCli.java +45 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/GxfFilterCli.java +39 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/GxfSortCli.java +23 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/HclustCli.java +43 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/HeatmapCli.java +89 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/HmmerSuiteCli.java +47 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/JgblocksCli.java +65 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/KeggEnrichCli.java +31 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/LayoutHeatmapCli.java +90 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/MCScanXCli.java +80 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/MCScanXFastCli.java +41 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/MSACli.java +46 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/MakeMotifCli.java +39 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/MarkerDesignCli.java +62 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/MarkerToolsCli.java +57 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/Mast2TabCli.java +27 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/MastCli.java +54 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/MastRunCli.java +39 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/Meme2TabCli.java +26 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/MemeCli.java +74 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/MemeRunCli.java +40 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/MgGxfCli.java +35 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/MicroSynCli.java +118 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/MirIdentifyCli.java +55 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/MotifCli.java +75 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/MotifPatternCli.java +67 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/MotifShiftCli.java +30 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/MountainPlotCli.java +44 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/MultiSuperHeatCli.java +64 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/NeedlemanWunschCli.java +76 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/PafGC.java +15 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/PafVizCli.java +67 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/PeakDistCli.java +58 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/Pep2CodonCli.java +26 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/PfamMotifCli.java +43 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/PhyloTreeCli.java +92 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/PileUpCli.java +55 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/PlantCAREResultClassifyCli.java +36 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/PubmedSearchCli.java +25 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/QpcrCli.java +69 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/QpcrDdctCli.java +28 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/QpcrProcCli.java +28 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/QuickGenomeDotCli.java +66 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/QuickProteinAnnoCli.java +34 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/QuickRunIQtreeCli.java +69 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/QuickTrimALCli.java +67 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/RNAplotCli.java +129 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/RegionBedToGFF3Cli.java +43 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/RegionDepthCli.java +29 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/SamBamCovCli.java +33 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/SeqConverterCli.java +17 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/SeqLenTrackCli.java +41 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/SeveralSpeciesCli.java +132 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/SimpleEnricherCli.java +44 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/SimpleHmmscanCli.java +32 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/SixFrameTranlaterCli.java +35 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/StructAnnoCompareCli.java +66 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/SubmitSMARTCli.java +30 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/SuperCircosCli.java +330 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/TableColManipCli.java +56 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/TableCollapseCli.java +33 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/TargetScoreCli.java +47 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/TauCalcCli.java +28 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/TaxonomyBatchCli.java +72 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/TreeCli.java +75 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/TreeRootingCli.java +30 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/TrimMSACli.java +33 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/UnrootedTreeCli.java +44 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/UpSetCli.java +90 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/Venn5Cli.java +60 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/Venn6Cli.java +65 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/ViolinCli.java +66 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/VisualizeCli.java +57 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/VizGFACli.java +39 -0
- tbtools_cli-1.2.0.dist-info/METADATA +504 -0
- tbtools_cli-1.2.0.dist-info/RECORD +139 -0
- tbtools_cli-1.2.0.dist-info/WHEEL +5 -0
- tbtools_cli-1.2.0.dist-info/entry_points.txt +2 -0
- tbtools_cli-1.2.0.dist-info/licenses/LICENSE +21 -0
- tbtools_cli-1.2.0.dist-info/top_level.txt +1 -0
tbtools_cli/core.py
ADDED
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
"""tbtools-cli 核心引擎:通用选项 + _run_java wrapper + 统一输出格式 + 输入校验"""
|
|
2
|
+
import os
|
|
3
|
+
import shutil
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
import tempfile
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
from tbtools_cli.config import get_default # heap 可配置(第六轮评审)
|
|
10
|
+
|
|
11
|
+
# ── 轻量 i18n(--lang en / LC_ALL / config [defaults] lang)──
|
|
12
|
+
_LANG_EN = None
|
|
13
|
+
|
|
14
|
+
def _use_en() -> bool:
|
|
15
|
+
"""是否英文输出: config [defaults] lang=en 或 LC_ALL/LANG 含 en|c。中文默认。"""
|
|
16
|
+
global _LANG_EN
|
|
17
|
+
if _LANG_EN is None:
|
|
18
|
+
cfg = get_default("lang", "")
|
|
19
|
+
env = (os.environ.get("LC_ALL", "") + " " + os.environ.get("LANG", "")).lower()
|
|
20
|
+
lang_code = env.split()[0].split(".")[0] if env.split() else ""
|
|
21
|
+
# en 开头 → 英文; C/POSIX locale(如 C.UTF-8)也是英文环境
|
|
22
|
+
_LANG_EN = bool(cfg and str(cfg).lower().startswith("en")) or "en" in env or lang_code in ("c", "posix")
|
|
23
|
+
return _LANG_EN
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _lang_cache_clear():
|
|
27
|
+
global _LANG_EN
|
|
28
|
+
_LANG_EN = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _(zh: str, en: str) -> str:
|
|
32
|
+
"""双语消息选择(中文默认;英文开关)"""
|
|
33
|
+
return en if _use_en() else zh
|
|
34
|
+
|
|
35
|
+
# ---- 平台常量(Windows 主战场:classpath 分隔符;Linux/WSL 用 :)----
|
|
36
|
+
CP_SEP = ";" if os.name == "nt" else ":"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def safe_temp(prefix="tmp.", suffix="", dir=None, text=True):
|
|
40
|
+
"""mkstemp 封装(替代有竞态的 tempfile.mktemp, 第六轮评审): 返回已关 fd 的路径"""
|
|
41
|
+
fd, path = tempfile.mkstemp(prefix=prefix, suffix=suffix, dir=dir, text=text)
|
|
42
|
+
os.close(fd)
|
|
43
|
+
return path
|
|
44
|
+
def cp(*parts):
|
|
45
|
+
"""平台安全的 classpath 拼接(Windows ; / POSIX :)"""
|
|
46
|
+
return CP_SEP.join(p for p in parts if p)
|
|
47
|
+
|
|
48
|
+
def stdout_path():
|
|
49
|
+
"""标准输出占位路径:POSIX /dev/stdout;Windows 用 CON(模式受限时回退临时文件)"""
|
|
50
|
+
return "/dev/stdout" if os.name != "nt" else "CON"
|
|
51
|
+
|
|
52
|
+
# ---- 配置 ----
|
|
53
|
+
def get_jar():
|
|
54
|
+
jar = os.environ.get("TBTOOLS_JAR", "")
|
|
55
|
+
if jar and os.path.isfile(jar):
|
|
56
|
+
return jar
|
|
57
|
+
# 配置文件
|
|
58
|
+
try:
|
|
59
|
+
from tbtools_cli.config import get_jar as cfg_jar
|
|
60
|
+
cj = cfg_jar()
|
|
61
|
+
if cj and os.path.isfile(cj):
|
|
62
|
+
return cj
|
|
63
|
+
except Exception:
|
|
64
|
+
pass
|
|
65
|
+
# 常见路径 + Windows/WSL/macOS 路径
|
|
66
|
+
for cand in [
|
|
67
|
+
os.path.expanduser("~/tbtools-cli/lib/TBtools_JRE1.6.jar"),
|
|
68
|
+
os.path.expanduser("~/TBtools/TBtools_JRE1.6.jar"),
|
|
69
|
+
os.path.expanduser("~/Downloads/TBtools_JRE1.6.jar"),
|
|
70
|
+
os.path.expanduser("~/下载/TBtools_JRE1.6.jar"),
|
|
71
|
+
os.path.expanduser("~/Desktop/TBtools_JRE1.6.jar"),
|
|
72
|
+
os.path.expanduser("~/桌面/TBtools_JRE1.6.jar"),
|
|
73
|
+
"/opt/TBtools/TBtools_JRE1.6.jar",
|
|
74
|
+
"/usr/local/lib/TBtools_JRE1.6.jar",
|
|
75
|
+
# Windows 原生路径(git-bash / cmd 环境)
|
|
76
|
+
"C:/TBtools/TBtools_JRE1.6.jar",
|
|
77
|
+
"C:/Program Files/TBtools/TBtools_JRE1.6.jar",
|
|
78
|
+
"C:/Users/%s/Downloads/TBtools_JRE1.6.jar" % os.environ.get("USERNAME", ""),
|
|
79
|
+
# WSL 挂载 Win 盘
|
|
80
|
+
"/mnt/c/TBtools/TBtools_JRE1.6.jar",
|
|
81
|
+
"/mnt/c/Program Files/TBtools/TBtools_JRE1.6.jar",
|
|
82
|
+
"/mnt/d/TBtools/TBtools_JRE1.6.jar",
|
|
83
|
+
"/mnt/c/Users/*/Downloads/TBtools_JRE1.6.jar",
|
|
84
|
+
"/mnt/c/Users/*/Desktop/TBtools_JRE1.6.jar",
|
|
85
|
+
# macOS
|
|
86
|
+
"/Applications/TBtools/TBtools_JRE1.6.jar",
|
|
87
|
+
os.path.expanduser("~/Applications/TBtools/TBtools_JRE1.6.jar"),
|
|
88
|
+
]:
|
|
89
|
+
if os.path.isfile(cand):
|
|
90
|
+
return cand
|
|
91
|
+
return jar # 返回空或原始值(让下游报错)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def find_jar_deep():
|
|
95
|
+
"""全盘深搜 TBtools jar(限定常见挂载点 + 递归 glob)。
|
|
96
|
+
|
|
97
|
+
外部审查反馈(2026-09-20):原 get_jar 在模块导入期执行递归全盘
|
|
98
|
+
glob(/mnt/*/TBtools*/**/...),无 jar 机器每次起 CLI 都白扫一遍。
|
|
99
|
+
现改为独立函数,仅 doctor / setup --auto 显式调用。
|
|
100
|
+
"""
|
|
101
|
+
import glob
|
|
102
|
+
for pat in [
|
|
103
|
+
"/mnt/*/TBtools*/**/TBtools_JRE1.6.jar",
|
|
104
|
+
"/mnt/*/Users/*/Downloads/TBtools*.jar",
|
|
105
|
+
"/mnt/*/Users/*/Desktop/TBtools*.jar",
|
|
106
|
+
"/mnt/c/Users/*/Downloads/TBtools*.jar",
|
|
107
|
+
"/mnt/c/Users/*/Desktop/TBtools*.jar",
|
|
108
|
+
]:
|
|
109
|
+
try:
|
|
110
|
+
hits = sorted(glob.glob(pat, recursive=True))
|
|
111
|
+
except Exception:
|
|
112
|
+
continue
|
|
113
|
+
if hits and os.path.isfile(hits[0]):
|
|
114
|
+
return hits[0]
|
|
115
|
+
return ""
|
|
116
|
+
|
|
117
|
+
JAR = get_jar()
|
|
118
|
+
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
119
|
+
_SRC_BRIDGES = os.path.join(ROOT, "bridges")
|
|
120
|
+
if os.path.isdir(_SRC_BRIDGES):
|
|
121
|
+
BRIDGES_DIR: str = _SRC_BRIDGES
|
|
122
|
+
elif os.path.isdir(os.path.join(os.path.dirname(__file__), "bridges")):
|
|
123
|
+
BRIDGES_DIR = os.path.join(os.path.dirname(__file__), "bridges")
|
|
124
|
+
elif os.path.isdir(os.path.join(sys.prefix, "tbtools_cli", "bridges")):
|
|
125
|
+
BRIDGES_DIR = os.path.join(sys.prefix, "tbtools_cli", "bridges")
|
|
126
|
+
else:
|
|
127
|
+
BRIDGES_DIR = "" # 空串约定(同 JAR): 桥命令会报未配置
|
|
128
|
+
# bridges 位置: 源码环境 ROOT/bridges;pip data-files 装到 sys.prefix/tbtools_cli/bridges(实测);包内路径兜底
|
|
129
|
+
_SRC_BUILD = os.path.join(ROOT, "build")
|
|
130
|
+
BUILD_DIR = _SRC_BUILD if (os.path.isdir(_SRC_BUILD) or os.access(ROOT, os.W_OK)) else os.path.join(os.path.expanduser("~/.cache/tbtools-cli/build"))
|
|
131
|
+
|
|
132
|
+
# ---- 输入校验 ----
|
|
133
|
+
def validate_file(path: str, desc: str = "输入文件", check_readable: bool = True) -> tuple[bool, str]:
|
|
134
|
+
"""校验文件存在性 + 可读性。返回 (ok, msg)"""
|
|
135
|
+
if not path:
|
|
136
|
+
return False, f"❌ {desc}: 路径为空"
|
|
137
|
+
if path in ("-", "/dev/stdin", "/dev/stdout"):
|
|
138
|
+
return True, "" # 管道跳过校验
|
|
139
|
+
if not os.path.exists(path):
|
|
140
|
+
return False, f"❌ {desc}: 文件不存在 → {path}"
|
|
141
|
+
if os.path.isdir(path):
|
|
142
|
+
return False, f"❌ {desc}: 是目录不是文件 → {path}"
|
|
143
|
+
if check_readable and not os.access(path, os.R_OK):
|
|
144
|
+
return False, f"❌ {desc}: 无读取权限 → {path}"
|
|
145
|
+
size = os.path.getsize(path)
|
|
146
|
+
if size == 0:
|
|
147
|
+
return False, f"❌ {desc}: 文件为空(0 字节)→ {path}"
|
|
148
|
+
return True, ""
|
|
149
|
+
|
|
150
|
+
def detect_format(path: str, max_lines: int = 3) -> tuple[str, int, list[str]]:
|
|
151
|
+
"""探测文件格式(peek 前 N 行)。返回 (format_hint, ncols, sample_lines)"""
|
|
152
|
+
if path in ("-", "/dev/stdin"):
|
|
153
|
+
return ("stdin", 0, [])
|
|
154
|
+
try:
|
|
155
|
+
with open(path, 'r', errors='replace') as f:
|
|
156
|
+
lines = []
|
|
157
|
+
for i, line in enumerate(f):
|
|
158
|
+
if i >= max_lines:
|
|
159
|
+
break
|
|
160
|
+
lines.append(line.rstrip('\n'))
|
|
161
|
+
except Exception as e:
|
|
162
|
+
import logging; logging.getLogger(__name__).debug("detect_format %s: %s", path, e)
|
|
163
|
+
return ("unknown", 0, [])
|
|
164
|
+
if not lines:
|
|
165
|
+
return ("empty", 0, [])
|
|
166
|
+
# FASTA
|
|
167
|
+
if lines[0].startswith('>'):
|
|
168
|
+
return ("fasta", 0, lines)
|
|
169
|
+
# GFF3(N16: 含 ##gff-version 头或特征列 gene/mRNA 的均判 GFF3,此前误判 text)
|
|
170
|
+
if lines[0].startswith('##gff-version') or '\tgene\t' in lines[0] or '\tmRNA\t' in lines[0]:
|
|
171
|
+
return ("gff3", len(lines[0].split('\t')), lines)
|
|
172
|
+
# GFF3(旧判定保留)
|
|
173
|
+
if '\tgff3' in lines[0].lower() or '\tgff' in lines[0].lower():
|
|
174
|
+
return ("gff3", len(lines[0].split('\t')), lines)
|
|
175
|
+
# Newick
|
|
176
|
+
if lines[0].startswith('(') or lines[0].endswith(';'):
|
|
177
|
+
return ("newick", 0, lines)
|
|
178
|
+
# MEME XML
|
|
179
|
+
if lines[0].lstrip().startswith('<?xml'): # 收紧: 仅真 XML 声明(评审: 含<和?的任何文本误判)
|
|
180
|
+
return ("xml", 0, lines)
|
|
181
|
+
# TSV/CSV
|
|
182
|
+
delim = '\t' if '\t' in lines[0] else (',' if ',' in lines[0] else None)
|
|
183
|
+
if delim:
|
|
184
|
+
ncols = len(lines[0].split(delim))
|
|
185
|
+
return ("tsv" if delim == '\t' else "csv", ncols, lines)
|
|
186
|
+
return ("text", 0, lines)
|
|
187
|
+
|
|
188
|
+
def validate_format_cols(path, expected_cols, desc="输入文件"):
|
|
189
|
+
"""校验文件列数是否符合预期"""
|
|
190
|
+
fmt, ncols, _ = detect_format(path)
|
|
191
|
+
if ncols > 0 and expected_cols and ncols < expected_cols:
|
|
192
|
+
return False, f"❌ {desc}: 需要 ≥{expected_cols} 列,实际 {ncols} 列({fmt} 格式)→ {path}"
|
|
193
|
+
return True, ""
|
|
194
|
+
|
|
195
|
+
# ---- C2: 早期格式不匹配警告 ----
|
|
196
|
+
# 命令 → (期望格式, 最少列数, 人类描述)。仅高置信场景,警告不阻断。
|
|
197
|
+
EXPECTED_INPUT_FORMATS = {
|
|
198
|
+
"hclust": ("tsv", 3, "三列距离文件 GeneA\tGeneB\tdist"),
|
|
199
|
+
"volcano": ("tsv", 3, "DEG 表(ID\tlog2FC\tP值...)"),
|
|
200
|
+
"heatmap": ("tsv", 2, "表达矩阵(基因×样本)"),
|
|
201
|
+
"pca": ("tsv", 2, "表达矩阵(基因×样本,行=观测)"),
|
|
202
|
+
"msa": ("fasta", 0, "多序列比对 FASTA"),
|
|
203
|
+
"logo": ("fasta", 0, "比对 FASTA"),
|
|
204
|
+
"motif": ("xml", 0, "MEME XML"),
|
|
205
|
+
"structure": ("gff3", 9, "GFF3 注释"),
|
|
206
|
+
"tree": ("newick", 0, "Newick 树文件"),
|
|
207
|
+
"barplot": ("tsv", 2, "富集表(term\tP值...)"),
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
def check_input_format(cmd_name: str, path: str) -> str | None:
|
|
211
|
+
"""早期格式检测:期望格式与实际不符时返回警告文本(不阻断)"""
|
|
212
|
+
exp = EXPECTED_INPUT_FORMATS.get(cmd_name)
|
|
213
|
+
if not exp:
|
|
214
|
+
return None
|
|
215
|
+
exp_fmt, min_cols, desc = exp
|
|
216
|
+
fmt, ncols, _ = detect_format(path)
|
|
217
|
+
if fmt in ("unknown", "empty", "stdin"):
|
|
218
|
+
return None
|
|
219
|
+
if exp_fmt == "fasta" and fmt != "fasta":
|
|
220
|
+
return f"输入文件看起来是 {fmt},但 {cmd_name} 通常需要 FASTA({desc})"
|
|
221
|
+
if exp_fmt == "newick" and fmt != "newick":
|
|
222
|
+
return f"输入文件看起来是 {fmt},但 {cmd_name} 需要 Newick 树文件({desc})"
|
|
223
|
+
if exp_fmt == "xml" and fmt != "xml":
|
|
224
|
+
return f"输入文件看起来是 {fmt},但 {cmd_name} 需要 XML({desc})"
|
|
225
|
+
if exp_fmt == "gff3" and fmt != "gff3":
|
|
226
|
+
return f"输入文件看起来是 {fmt},但 {cmd_name} 需要 GFF3({desc})"
|
|
227
|
+
if exp_fmt == "tsv" and fmt not in ("tsv", "csv"):
|
|
228
|
+
return f"输入文件看起来是 {fmt},但 {cmd_name} 需要表格({desc})"
|
|
229
|
+
if exp_fmt == "tsv" and min_cols and ncols and ncols < min_cols:
|
|
230
|
+
return f"输入文件只有 {ncols} 列,{cmd_name} 通常需要 ≥{min_cols} 列({desc})"
|
|
231
|
+
return None
|
|
232
|
+
|
|
233
|
+
# ---- 已知坑位提示 ----
|
|
234
|
+
PITFALL_HINTS = {
|
|
235
|
+
"onesteptree": "--bb-time 必须 ≥1000(IQ-TREE UFBoot 下限),小于 1000 会静默不产树;序列须 ≥4 条唯一(太相似会被合并报错);outFilePrefix 若是目录,产物命名为 目录/TBtools.*",
|
|
236
|
+
"draw": "输入必须是 TreeTab 配置([TYPE]:Tree + [NEWICK]: 行),直接喂 .nwk 曾导致引擎从 stdin 读入而挂起(G2 已修复为快速报错);只画树用 tbtools tree phylotree",
|
|
237
|
+
"hmmsearch": "调系统 hmmsearch 二进制(Linux: apt install hmmer;Windows: TBtools-II/bin 需加入 PATH);idList 是 Pfam ID 每行一个(如 GRAS),不是基因 ID",
|
|
238
|
+
"simplehmmscan": "调系统 hmmsearch 二进制(Linux: apt install hmmer;Windows: TBtools-II/bin 需加入 PATH);需 Pfam-A.hmm 数据库文件,idList 每行一个 Pfam",
|
|
239
|
+
"hclust": "输入必须是三列距离文件 GeneA\\tGeneB\\tdist(不是表达矩阵!)",
|
|
240
|
+
"barplot": "termCol/pvalCol 用列名(如 Term/Pvalue),不是列索引数字",
|
|
241
|
+
"cubeheatmap": "group 文件第一行会被当数据——喂前先去表头",
|
|
242
|
+
"admixture": "第一个参数是 qFiles.lst(每行一个 Q 矩阵文件路径),不是 Q 矩阵内容",
|
|
243
|
+
"dotplot": "--chrLayout 传文件路径(内容: Genome: Chr1 Chr2...),不是内联字符串",
|
|
244
|
+
"microsyn": "必须指定 --chr1/--start1/--end1 和 --chr2/--start2/--end2;染色体名须数字;简化 GFF=数字染色体名\tGene\tStart\tEnd;输出父目录须存在",
|
|
245
|
+
"dualsyn": "简化 GFF 染色体名必须数字(parseInt);需显式 --chr1/--chr2",
|
|
246
|
+
"msy": "简化 GFF 染色体名必须数字;基因名在第 2 列;坐标列不连 -",
|
|
247
|
+
"multisyn": "染色体名必须数字;gxf.lst 路径不能硬编码",
|
|
248
|
+
"pafviz": "PAF 文件必须 13 列(不足会 [12] 越界)",
|
|
249
|
+
"pafref": "PAF 必须含 cg:Z: CIGAR 标签(minimap2 -c --cs 输出自带)",
|
|
250
|
+
"peaktss": "签名 <gxf> <macs2_peak.xls> <out>——gxf 是必给第 1 参",
|
|
251
|
+
"peakanno": "peak 用 MACS2 格式;坐标须百万级 bp(小坐标触发 bin 边界 bug)",
|
|
252
|
+
"supercircos": "配置文件 [chrLen] 后跟文件路径,非内联数据;[link]/[gene]/[track] 同理",
|
|
253
|
+
"gel": "LaneLabels 逗号分隔(第一个给 marker);MarkerRange 降序;FragmentRangeArr 分号泳道/逗号片段",
|
|
254
|
+
"motif": "需真 meme.xml + 序列 ID 匹配的 ID 列表(grep -oP 'name=\"[^\"]+\"' meme.xml 提取)",
|
|
255
|
+
"colorscheme": "refColIndex 是 1-based(传 0 会 IndexOutOfBounds)",
|
|
256
|
+
"plotrna": "必须带 --directPDF 否则弹窗;只支持 PDF",
|
|
257
|
+
"pep2codon": "参数顺序 <cds.fa> <pep.aln.fa> <out>(先 CDS 后蛋白比对)",
|
|
258
|
+
"goParse": "产物写到输入文件同目录(<输入名>.TBtools.Parsed.*),无独立输出参数",
|
|
259
|
+
"mcscanx": "gff 简化格式 chr\\tgene\\tstart\\tend;blast 用 tab6;classify 须同时给 collinearityFile+geneTypeFile",
|
|
260
|
+
"efpHeat": "TGA 底图必须 TrueColor(type2);需 fake DatatypeConverter",
|
|
261
|
+
"multiEfp": "TGA 底图必须 TrueColor(type2);需 fake DatatypeConverter",
|
|
262
|
+
"layoutheatmap": "layout.tsv 样本名须与 expr.tsv 表头一致(官方 examples 两文件样本名不匹配会 ArrayIndexOutOfBounds,属数据问题非命令缺陷)",
|
|
263
|
+
"annocompare": "输入两个 GFF3 + 输出目录;生成 change_summary.csv + figures/*;before/after 须有共同 seqid(无共同序列报 IOException)",
|
|
264
|
+
"nwAlign": "输入文件每行一条序列,无 FASTA 头(传 FASTA 会把 >s1 当序列)",
|
|
265
|
+
"treeRooting": "Newick 树必须带枝长(裸 Newick 报 Corrupt NEWICK format)",
|
|
266
|
+
"distance": "方法名小写 euclidean/pearson/pearsonDist;结果输出到 stdout(非文件);col1/col2 是列索引(从 0 起)非列名(喂列名报 NumberFormatException)",
|
|
267
|
+
"markertools": "首参是子命令 filter|dist|sampledist(非文件);结果输出到 stderr(非 stdout!);$(...) 需 2>&1 捕获",
|
|
268
|
+
"barplotter": "选项式引擎: -g <gff> -s <synteny> -c <ctl> -o <out>(非位置参数;宽高须 >0)",
|
|
269
|
+
"qpcrproc": "输入 qpcr 表列格式须规范(列数不足 ArrayIndexOutOfBounds)",
|
|
270
|
+
"qdot": "GFF 用 4 列简化格式 Chr\tGene\tStart\tEnd(全 GFF 带特征列会被引擎当数字解析报 NumberFormatException)",
|
|
271
|
+
"findblockmultiple": "需真实跨物种共线数据(合成数据无共线块→空输出)",
|
|
272
|
+
"cddmotif": "cdd.hitdata 须 CDD 标准 8 列(qstart/qend/…);列数不足越界",
|
|
273
|
+
"seqlentrack": "序列 ID/树 taxon 须与 motif 域信息匹配(不匹配报 IOException)",
|
|
274
|
+
"pfammotif": "输入 newick 树 taxon 须与 motif 信息匹配",
|
|
275
|
+
"calcRepeat": "需要 jellyfish 在 PATH(Windows 默认缺失;apt install jellyfish)",
|
|
276
|
+
"rnaplot": "需要 RNAfold/RNAplot 在 PATH(Windows 默认缺失;Linux apt install rna-folding)",
|
|
277
|
+
"preparespecies": "首参是 ID 前缀字符串(非文件)",
|
|
278
|
+
"marker": "首参是子命令 MarkerDist|MarkerFilter|SampleDist|BigMarkerRandomDesign(非文件)",
|
|
279
|
+
"venn5": "首参是输出文件(非输入);setA..E.txt 才是输入",
|
|
280
|
+
"venn6": "首参是输出文件(非输入);setA..F.txt 才是输入",
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
# PITFALL 英文版(Top10 高频命令;其余 en 模式回退中文——完整翻译见 backlog)
|
|
285
|
+
PITFALL_HINTS_EN = {
|
|
286
|
+
"hclust": "hclust requires a THREE-column distance file: GeneA\tGeneB\tdistance (not an expression matrix!)",
|
|
287
|
+
"msy": "Microsynteny: simplified GFF must use NUMERIC chromosome names; gene name in column 2; coordinates in one column",
|
|
288
|
+
"venn2": "venn2 uses flag style: --List1 a.txt --List2 b.txt --label1 A --label2 B --graph out.svg --prefix <prefix>",
|
|
289
|
+
"venn5": "venn5: first arg is the OUTPUT file (not input); pass setA..E.txt as inputs",
|
|
290
|
+
"venn6": "venn6: first arg is the OUTPUT file (not input); pass setA..F.txt as inputs",
|
|
291
|
+
"peaktss": "peaktss: input GXF + MACS2 peak table; --dist sets the window around TSS",
|
|
292
|
+
"onesteptree": "--bb-time must be >= 1000 (IQ-TREE UFBoot minimum); smaller values are rejected",
|
|
293
|
+
"motif": "motif requires a REAL MEME XML (with motif definitions) + an ID list matching the sequences",
|
|
294
|
+
"tableMerge": "tableMerge: engine takes --inFileArr/--inColIndexArr/--outTable (hidden --config supported); first positional arg treated as output",
|
|
295
|
+
"barplot": "barplot uses COLUMN NAMES (e.g. Term/Pvalue), not column indices",
|
|
296
|
+
"draw": "Input must be a TreeTab config ([TYPE]:Tree + [NEWICK]: lines); feeding a raw .nwk used to crash",
|
|
297
|
+
"hmmsearch": "Calls the system hmmsearch binary (Linux: apt install hmmer; Windows: bundled in TBtools)",
|
|
298
|
+
"simplehmmscan": "Calls the system hmmsearch binary (Linux: apt install hmmer; Windows: bundled in TBtools)",
|
|
299
|
+
"cubeheatmap": "The first row of the group file is treated as data — strip the header before feeding",
|
|
300
|
+
"admixture": "First arg is qFiles.lst (one Q-matrix file path per line), not the Q matrix content",
|
|
301
|
+
"dotplot": "--chrLayout takes a FILE PATH (content: Genome: Chr1 Chr2...), not an inline string",
|
|
302
|
+
"microsyn": "Must specify --chr1/--start1/--end1 and --chr2/--start2/--end2; chromosome names must be numeric",
|
|
303
|
+
"dualsyn": "Simplified GFF chromosome names must be numeric (parseInt); explicit --chr1/--chr2 required",
|
|
304
|
+
"multisyn": "Chromosome names must be numeric; the gxf.lst path cannot be hardcoded",
|
|
305
|
+
"pafviz": "PAF file must have 13 columns (fewer causes index [12] out-of-bounds)",
|
|
306
|
+
"pafref": "PAF must include cg:Z: CIGAR tags (minimap2 -c --cs output has them)",
|
|
307
|
+
"peakanno": "Peaks use MACS2 format; coordinates must be in megabases (small coordinates trigger a bin-boundary bug)",
|
|
308
|
+
"supercircos": "Config [chrLen] takes a file path, not inline data; same for [link]/[gene]/[track]",
|
|
309
|
+
"gel": "LaneLabels comma-separated (the first one is the marker); MarkerRange descending; FragmentRange within bounds",
|
|
310
|
+
"colorscheme": "refColIndex is 1-based (passing 0 causes IndexOutOfBounds)",
|
|
311
|
+
"plotrna": "Must use --directPDF or it pops a dialog; PDF output only",
|
|
312
|
+
"pep2codon": "Argument order is <cds.fa> <pep.aln.fa> <out> (CDS first, then the protein alignment)",
|
|
313
|
+
"goParse": "Outputs are written next to the input file (<input>.TBtools.Parsed.*); no separate output argument",
|
|
314
|
+
"mcscanx": "gff simplified format chr\tgene\tstart\tend; blast uses tab6; classify requires the same prefix",
|
|
315
|
+
"efpHeat": "TGA base image must be TrueColor (type 2); needs the fake DatatypeConverter",
|
|
316
|
+
"multiEfp": "TGA base image must be TrueColor (type 2); needs the fake DatatypeConverter",
|
|
317
|
+
"layoutheatmap": "layout.tsv sample names must match expr.tsv headers (official examples mismatch → ArrayIndexOutOfBounds)",
|
|
318
|
+
"annocompare": "Inputs are two GFF3 + an output dir; generates change_summary.csv + figures/*; annotate before compare",
|
|
319
|
+
"nwAlign": "Each line of input is one sequence without FASTA headers (FASTA input treats >s1 as a sequence)",
|
|
320
|
+
"treeRooting": "Newick tree must have branch lengths (bare Newick → Corrupt NEWICK format)",
|
|
321
|
+
"distance": "Method names lowercase: euclidean/pearson/pearsonDist; results go to stdout (not a file)",
|
|
322
|
+
"markertools": "First arg is a subcommand filter|dist|sampledist (not a file); results go to stderr",
|
|
323
|
+
"barplotter": "Flag-style engine: -g <gff> -s <synteny> -c <ctl> -o <out> (not positional; width/height as flags)",
|
|
324
|
+
"qpcrproc": "Input qpcr table columns must be well-formed (too few columns → ArrayIndexOutOfBounds)",
|
|
325
|
+
"qdot": "GFF uses the 4-column simplified format Chr\tGene\tStart\tEnd (full GFF → NumberFormatException)",
|
|
326
|
+
"findblockmultiple": "Needs real cross-species collinearity data (synthetic data has no blocks → empty output)",
|
|
327
|
+
"cddmotif": "cdd.hitdata must be standard CDD 8 columns (qstart/qend/...); too few columns → out-of-bounds",
|
|
328
|
+
"seqlentrack": "Sequence IDs / tree taxa must match the motif domain info (mismatch → IOException)",
|
|
329
|
+
"pfammotif": "Input newick tree taxa must match the motif info",
|
|
330
|
+
"calcRepeat": "Needs jellyfish in PATH (missing on Windows by default; apt install jellyfish)",
|
|
331
|
+
"rnaplot": "Needs RNAfold/RNAplot in PATH (missing on Windows by default; Linux: apt install ...)",
|
|
332
|
+
"preparespecies": "First arg is an ID prefix string (not a file)",
|
|
333
|
+
"marker": "First arg is a subcommand MarkerDist|MarkerFilter|SampleDist|BigMarkerRandomDesign (not a file)",
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def get_pitfall_hint(command_name: str) -> str | None:
|
|
338
|
+
"""获取已知坑位提示;en 模式优先英文版(未翻译回退中文)"""
|
|
339
|
+
if _use_en():
|
|
340
|
+
return PITFALL_HINTS_EN.get(command_name) or PITFALL_HINTS.get(command_name, "")
|
|
341
|
+
return PITFALL_HINTS.get(command_name, "")
|
|
342
|
+
|
|
343
|
+
# ---- 统一输出格式处理 ----
|
|
344
|
+
def get_java() -> str | None:
|
|
345
|
+
"""定位 java 可执行文件(N1:tool 层 PATH 依赖误导报错)。
|
|
346
|
+
|
|
347
|
+
优先级: TBTOOLS_JAVA 环境变量 > PATH 搜索 > 常见位置。
|
|
348
|
+
Windows 下 Python 运行时注入 PATH 对 CreateProcess 无效(交付包实测),
|
|
349
|
+
所以调用前必须解析出绝对路径而非依赖 PATH。
|
|
350
|
+
"""
|
|
351
|
+
j = os.environ.get("TBTOOLS_JAVA", "")
|
|
352
|
+
if j and os.path.isfile(j):
|
|
353
|
+
return j
|
|
354
|
+
w = shutil.which("java")
|
|
355
|
+
if w:
|
|
356
|
+
return w
|
|
357
|
+
for cand in (
|
|
358
|
+
"/usr/bin/java", "/usr/local/bin/java", "/opt/java/bin/java",
|
|
359
|
+
os.path.expanduser("~/jdk*/bin/java"),
|
|
360
|
+
"C:/Program Files/TBtools/jre/bin/java.exe",
|
|
361
|
+
"C:/Program Files/Java/*/bin/java.exe",
|
|
362
|
+
"/mnt/c/Program Files/TBtools/jre/bin/java.exe",
|
|
363
|
+
"/mnt/c/Program Files/Java/*/bin/java.exe",
|
|
364
|
+
"/Applications/TBtools/jre/bin/java",
|
|
365
|
+
):
|
|
366
|
+
import glob as _glob
|
|
367
|
+
hits = _glob.glob(cand)
|
|
368
|
+
for h in hits:
|
|
369
|
+
if os.path.isfile(h):
|
|
370
|
+
return h
|
|
371
|
+
return ""
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
# ---- 桥编译 ----
|
|
375
|
+
def ensure_bridge(bridge_name: str) -> None:
|
|
376
|
+
"""确保桥 Java 文件已编译到 build/ 目录"""
|
|
377
|
+
src = os.path.join(BRIDGES_DIR, f"{bridge_name}.java")
|
|
378
|
+
dst = os.path.join(BUILD_DIR, f"{bridge_name}.java")
|
|
379
|
+
|
|
380
|
+
# 同步源码到 build/
|
|
381
|
+
if os.path.isfile(src):
|
|
382
|
+
need_copy = (not os.path.isfile(dst)
|
|
383
|
+
or os.path.getmtime(src) > os.path.getmtime(dst))
|
|
384
|
+
if need_copy:
|
|
385
|
+
os.makedirs(os.path.dirname(dst), exist_ok=True)
|
|
386
|
+
shutil.copy2(src, dst)
|
|
387
|
+
|
|
388
|
+
# 编译(如果 .class 不存在或源码更新)
|
|
389
|
+
cls_file = os.path.join(BUILD_DIR, f"{bridge_name}.class")
|
|
390
|
+
if not os.path.isfile(cls_file) or (
|
|
391
|
+
os.path.isfile(dst) and os.path.getmtime(dst) > os.path.getmtime(cls_file)
|
|
392
|
+
):
|
|
393
|
+
_r = subprocess.run(
|
|
394
|
+
["javac", "-cp", JAR, dst],
|
|
395
|
+
capture_output=True, cwd=BUILD_DIR
|
|
396
|
+
)
|
|
397
|
+
# 外部审查反馈: javac 编译错误不再静默吞掉(此前 capture_output 丢 stderr)
|
|
398
|
+
if _r.returncode != 0:
|
|
399
|
+
_err = _r.stderr.decode("utf-8", "replace") if _r.stderr else ""
|
|
400
|
+
print(f"⚠️ 桥编译失败 {bridge_name}:\n{_err[-800:]}", file=sys.stderr)
|
|
401
|
+
|
|
402
|
+
# N27: fake jaxb DatatypeConverter(JDK9+ 无 javax.xml.bind)随仓库分发源码,
|
|
403
|
+
# 有需要即编译到 build/javax/xml/bind/(全新 checkout 也能重建,修复 NoClassDefFoundError)
|
|
404
|
+
fake_src = os.path.join(BRIDGES_DIR, "javax", "xml", "bind", "DatatypeConverter.java")
|
|
405
|
+
if os.path.isfile(fake_src):
|
|
406
|
+
fake_cls = os.path.join(BUILD_DIR, "javax", "xml", "bind", "DatatypeConverter.class")
|
|
407
|
+
if (not os.path.isfile(fake_cls)
|
|
408
|
+
or os.path.getmtime(fake_src) > os.path.getmtime(fake_cls)):
|
|
409
|
+
os.makedirs(os.path.dirname(fake_cls), exist_ok=True)
|
|
410
|
+
subprocess.run(["javac", "-d", BUILD_DIR, fake_src], capture_output=True)
|
|
411
|
+
|
|
412
|
+
# ---- xvfb-run 包装 ----
|
|
413
|
+
def run_plot(java_args: list, verbose: bool = False, quiet: bool = False, use_xvfb: bool = True, command_name: str | None = None) -> int:
|
|
414
|
+
"""执行绘图引擎(需要 xvfb-run)"""
|
|
415
|
+
if use_xvfb and shutil.which("xvfb-run"):
|
|
416
|
+
full_args = ["xvfb-run", "-a"] + java_args
|
|
417
|
+
else:
|
|
418
|
+
full_args = java_args
|
|
419
|
+
return run_java(full_args, verbose=verbose, quiet=quiet, command_name=command_name)
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
# ---- ANSI 彩色(仅 TTY 时启用)----
|
|
423
|
+
def _tty() -> bool:
|
|
424
|
+
try:
|
|
425
|
+
return bool(sys.stdout.isatty())
|
|
426
|
+
except Exception:
|
|
427
|
+
return False
|
|
428
|
+
|
|
429
|
+
def c(text, color=None, bold=False):
|
|
430
|
+
"""条件 ANSI 着色:非 TTY 返回原样"""
|
|
431
|
+
if not _tty():
|
|
432
|
+
return text
|
|
433
|
+
codes = {"red": "31", "green": "32", "yellow": "33", "blue": "34",
|
|
434
|
+
"magenta": "35", "cyan": "36", "dim": "2", "bold": "1"}
|
|
435
|
+
out = []
|
|
436
|
+
if bold:
|
|
437
|
+
out.append("1")
|
|
438
|
+
if color in codes:
|
|
439
|
+
out.append(codes[color])
|
|
440
|
+
if not out:
|
|
441
|
+
return text
|
|
442
|
+
return f"\033[{';'.join(out)}m{text}\033[0m"
|
|
443
|
+
|
|
444
|
+
def pre_flight(cmd_name: str, first_file: str) -> None:
|
|
445
|
+
"""手动注册命令的早期格式检查(打印警告,不阻断)"""
|
|
446
|
+
if not first_file or str(first_file).startswith('-'):
|
|
447
|
+
return
|
|
448
|
+
ok, _ = validate_file(str(first_file))
|
|
449
|
+
if not ok:
|
|
450
|
+
return
|
|
451
|
+
warn = check_input_format(cmd_name, str(first_file))
|
|
452
|
+
if warn:
|
|
453
|
+
print(f"⚠️ 格式提醒: {warn}", file=sys.stderr)
|
|
454
|
+
print(" (继续执行;如确认无误可忽略)", file=sys.stderr)
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
# ---- G5: 注册引擎类完整性探测(不起 JVM,zip 中央目录秒查)----
|
|
458
|
+
def probe_dead_engines():
|
|
459
|
+
"""提取代码中硬编码的 biocjava.* 引擎类,检查 jar 内是否存在对应 .class。
|
|
460
|
+
|
|
461
|
+
覆盖: auto_commands.py / cli.py / cli_tools_registry.py / bridges/*.java
|
|
462
|
+
返回 [(className, 来源文件), ...](缺失项);jar 不可读时返回 []。
|
|
463
|
+
用途: tbtools doctor 死命令预警(外部测试 P1-1:2.475 jar 无
|
|
464
|
+
Phylogenetics.OneStepTree,tbtools tree one-step 直接 ClassNotFound)。
|
|
465
|
+
"""
|
|
466
|
+
import re as _re
|
|
467
|
+
import zipfile
|
|
468
|
+
try:
|
|
469
|
+
with zipfile.ZipFile(JAR) as z:
|
|
470
|
+
names = set(z.namelist())
|
|
471
|
+
except Exception:
|
|
472
|
+
return []
|
|
473
|
+
pat = _re.compile(r'"(biocjava\.[A-Za-z0-9_]+(?:\.[A-Za-z0-9_]+)+)"')
|
|
474
|
+
dead: dict[str, str] = {}
|
|
475
|
+
srcs = [os.path.join(ROOT, "tbtools_cli", "auto_commands.py"),
|
|
476
|
+
os.path.join(ROOT, "tbtools_cli", "cli.py"),
|
|
477
|
+
os.path.join(ROOT, "tbtools_cli", "cli_tools_registry.py")]
|
|
478
|
+
bridges_dir = os.path.join(ROOT, "bridges")
|
|
479
|
+
if os.path.isdir(bridges_dir):
|
|
480
|
+
srcs += [os.path.join(bridges_dir, f) for f in os.listdir(bridges_dir) if f.endswith(".java")]
|
|
481
|
+
for src in srcs:
|
|
482
|
+
if not os.path.isfile(src):
|
|
483
|
+
continue
|
|
484
|
+
try:
|
|
485
|
+
with open(src, encoding="utf-8", errors="replace") as f:
|
|
486
|
+
content = f.read()
|
|
487
|
+
except Exception:
|
|
488
|
+
continue
|
|
489
|
+
for m in pat.finditer(content):
|
|
490
|
+
cls = m.group(1)
|
|
491
|
+
path = cls.replace(".", "/") + ".class"
|
|
492
|
+
if path not in names:
|
|
493
|
+
dead.setdefault(cls, os.path.basename(src))
|
|
494
|
+
return sorted(dead.items())
|
|
495
|
+
|
|
496
|
+
# ── core.py 拆分(GPT 评审 #8): Java 执行/输入保护/provenance 迁至 runtime/java.py, 此处重导出保持兼容 ──
|
|
497
|
+
from tbtools_cli.runtime.java import ( # noqa: E402 # 延迟到模块加载完(避免循环 import)
|
|
498
|
+
_n19_move_result,
|
|
499
|
+
_sha1_file,
|
|
500
|
+
_write_provenance,
|
|
501
|
+
check_missing_outputs,
|
|
502
|
+
cleanup_side_effects,
|
|
503
|
+
find_empty_inputs,
|
|
504
|
+
resolve_output,
|
|
505
|
+
run_java,
|
|
506
|
+
snapshot_inputs,
|
|
507
|
+
verify_and_restore,
|
|
508
|
+
) # noqa: F401 # 重导出(拆分兼容)——配合 pyproject: 见 [tool.ruff.lint.per-file-ignores]
|
tbtools_cli/errors.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""结构化错误契约(GLM 评审): 错误码注册表 + 异常分类。
|
|
2
|
+
|
|
3
|
+
独立模块(零依赖), 供 core/运行时/CLI 复用。
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
# ── Error Code Registry(GLM 评审: 结构化错误契约, AI 可编程处理)──
|
|
8
|
+
ERROR_CODES: dict[str, dict[str, object]] = {
|
|
9
|
+
"TB001_INVALID_ARGUMENT": {"exit": 1, "retryable": False, "action": "check argument names/values, see --help"},
|
|
10
|
+
"TB002_FILE_NOT_FOUND": {"exit": 2, "retryable": False, "action": "check the input file path exists"},
|
|
11
|
+
"TB003_INPUT_FORMAT_ERROR": {"exit": 3, "retryable": False, "action": "check input format/columns/separator"},
|
|
12
|
+
"TB004_INPUT_SCHEMA_ERROR": {"exit": 3, "retryable": False, "action": "input does not match required schema"},
|
|
13
|
+
"TB005_DEPENDENCY_MISSING": {"exit": 1, "retryable": False, "action": "install missing dependency (see doctor)"},
|
|
14
|
+
"TB007_TOOL_TIMEOUT": {"exit": 1, "retryable": True, "action": "retry with more time or smaller input"},
|
|
15
|
+
"TB008_OUT_OF_MEMORY": {"exit": 4, "retryable": True, "action": "increase memory in config.toml [defaults]"},
|
|
16
|
+
"TB009_ENGINE_CRASH": {"exit": 1, "retryable": False, "action": "engine-level defect; see PITFALL/docs"},
|
|
17
|
+
"TB010_OUTPUT_MISSING": {"exit": 1, "retryable": False, "action": "output not produced; check engine"},
|
|
18
|
+
"TB012_INTERNAL_ERROR": {"exit": 1, "retryable": False, "action": "wrapper bug; report with --verbose"},
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def classify_error(err_text: str) -> tuple[str, int, str]:
|
|
23
|
+
"""异常文本 → (错误码, 退出码, hint)。退出码与现分类一致(0/2/3/4... 语义化)。"""
|
|
24
|
+
if "FileNotFoundException" in err_text:
|
|
25
|
+
return "TB002_FILE_NOT_FOUND", 2, "文件不存在或路径错误,检查输入文件路径"
|
|
26
|
+
if "ClassNotFoundException" in err_text:
|
|
27
|
+
return "TB009_ENGINE_CRASH", 1, "引擎类不存在(版本不匹配或死命令)"
|
|
28
|
+
if "NumberFormatException" in err_text:
|
|
29
|
+
return "TB003_INPUT_FORMAT_ERROR", 3, "数据格式不匹配,检查列数/类型/分隔符"
|
|
30
|
+
if "ArrayIndexOutOfBoundsException" in err_text:
|
|
31
|
+
return "TB003_INPUT_FORMAT_ERROR", 3, "行列数不足或参数缺省"
|
|
32
|
+
if "OutOfMemoryError" in err_text:
|
|
33
|
+
return "TB008_OUT_OF_MEMORY", 4, "内存不足: config.toml [defaults] memory 调大"
|
|
34
|
+
if "NullPointerException" in err_text:
|
|
35
|
+
return "TB001_INVALID_ARGUMENT", 1, "可能缺少必需参数或格式不匹配"
|
|
36
|
+
if "NoClassDefFoundError|DatatypeConverter" in err_text:
|
|
37
|
+
return "TB005_DEPENDENCY_MISSING", 1, "缺 javax.xml 类(ensure_bridge 应已编译 fake DatatypeConverter)"
|
|
38
|
+
return "TB001_INVALID_ARGUMENT", 1, "参数缺失/格式不对/路径错误/数据不匹配"
|
tbtools_cli/presets.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""出版级预设模板 — 期刊风格一键应用"""
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
# 预设定义
|
|
6
|
+
PRESETS: dict[str, dict] = {
|
|
7
|
+
# ── 期刊预设 ──
|
|
8
|
+
"nature": {
|
|
9
|
+
"desc": "Nature: 单栏 89mm, 双栏 183mm, 8pt, Arial, 300dpi",
|
|
10
|
+
"width": 89, "height": 89, "dpi": 300,
|
|
11
|
+
"font": "Arial", "font_size": 8,
|
|
12
|
+
"palette": "nature",
|
|
13
|
+
},
|
|
14
|
+
"cell": {
|
|
15
|
+
"desc": "Cell: 单栏 85mm, 双栏 170mm, 7pt, Arial, 300dpi",
|
|
16
|
+
"width": 85, "height": 85, "dpi": 300,
|
|
17
|
+
"font": "Arial", "font_size": 7,
|
|
18
|
+
"palette": "cell",
|
|
19
|
+
},
|
|
20
|
+
"plant_journal": {
|
|
21
|
+
"desc": "Plant Journal: 单栏 80mm, 双栏 170mm, 8pt, Helvetica, 300dpi",
|
|
22
|
+
"width": 80, "height": 80, "dpi": 300,
|
|
23
|
+
"font": "Helvetica", "font_size": 8,
|
|
24
|
+
"palette": "viridis",
|
|
25
|
+
},
|
|
26
|
+
"new_phytologist": {
|
|
27
|
+
"desc": "New Phytologist: 单栏 80mm, 双栏 170mm, 7pt, Arial, 300dpi",
|
|
28
|
+
"width": 80, "height": 80, "dpi": 300,
|
|
29
|
+
"font": "Arial", "font_size": 7,
|
|
30
|
+
"palette": "viridis",
|
|
31
|
+
},
|
|
32
|
+
"wide": {
|
|
33
|
+
"desc": "宽幅: 183mm 双栏, 8pt, Arial, 300dpi",
|
|
34
|
+
"width": 183, "height": 120, "dpi": 300,
|
|
35
|
+
"font": "Arial", "font_size": 8,
|
|
36
|
+
"palette": "viridis",
|
|
37
|
+
},
|
|
38
|
+
"poster": {
|
|
39
|
+
"desc": "海报: 大尺寸 400×300mm, 12pt, Arial, 150dpi",
|
|
40
|
+
"width": 400, "height": 300, "dpi": 150,
|
|
41
|
+
"font": "Arial", "font_size": 12,
|
|
42
|
+
"palette": "set2",
|
|
43
|
+
},
|
|
44
|
+
# ── 色板预设 ──
|
|
45
|
+
"gras": {
|
|
46
|
+
"desc": "GRAS 项目: 17 亚家族定色, viridis 底色",
|
|
47
|
+
"palette": "gras17",
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# 色板定义
|
|
52
|
+
PALETTES = {
|
|
53
|
+
"nature": ["#4E79A7", "#F28E2B", "#E15759", "#76B7B2", "#59A14F",
|
|
54
|
+
"#EDC948", "#B07AA1", "#FF9DA7", "#9C755F", "#BAB0AC"],
|
|
55
|
+
"cell": ["#1F77B4", "#FF7F0E", "#2CA02C", "#D62728", "#9467BD",
|
|
56
|
+
"#8C564B", "#E377C2", "#7F7F7F", "#BCBD22", "#17BECF"],
|
|
57
|
+
"viridis": "viridis", # 引擎内置
|
|
58
|
+
"set2": "set2",
|
|
59
|
+
"gras17": ["#E64B35", "#4DBBD5", "#00A087", "#3C5488", "#F39B7F",
|
|
60
|
+
"#8491B4", "#91D1C2", "#DC0000", "#7E6148", "#B09C85",
|
|
61
|
+
"#FF9DA7", "#9C755F", "#BAB0AC", "#76B7B2", "#59A14F",
|
|
62
|
+
"#EDC948", "#B07AA1"],
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
def get_preset(name):
|
|
66
|
+
"""获取预设配置,返回 dict 或 None"""
|
|
67
|
+
return PRESETS.get(name)
|
|
68
|
+
|
|
69
|
+
def list_presets():
|
|
70
|
+
"""列出所有预设"""
|
|
71
|
+
return [(k, v["desc"]) for k, v in PRESETS.items()]
|
|
72
|
+
|
|
73
|
+
def apply_preset(name, width=None, height=None):
|
|
74
|
+
"""应用预设,返回参数 dict。用户显式指定的 width/height 覆盖预设值"""
|
|
75
|
+
p = get_preset(name)
|
|
76
|
+
if not p:
|
|
77
|
+
return {}
|
|
78
|
+
result = {}
|
|
79
|
+
if width is None and "width" in p:
|
|
80
|
+
result["width"] = p["width"]
|
|
81
|
+
if height is None and "height" in p:
|
|
82
|
+
result["height"] = p["height"]
|
|
83
|
+
if "dpi" in p:
|
|
84
|
+
result["dpi"] = p["dpi"]
|
|
85
|
+
if "font" in p:
|
|
86
|
+
result["font"] = p["font"]
|
|
87
|
+
if "font_size" in p:
|
|
88
|
+
result["font_size"] = p["font_size"]
|
|
89
|
+
if "palette" in p:
|
|
90
|
+
result["palette"] = p["palette"]
|
|
91
|
+
return result
|
|
92
|
+
|
|
93
|
+
def load_custom_preset(path):
|
|
94
|
+
"""从 JSON 文件加载自定义预设"""
|
|
95
|
+
if not os.path.isfile(path):
|
|
96
|
+
return None
|
|
97
|
+
with open(path) as f:
|
|
98
|
+
data = json.load(f)
|
|
99
|
+
PRESETS[data.get("name", "custom")] = data
|
|
100
|
+
return data
|