diff --git a/chanlun-py/Cargo.toml b/chanlun-py/Cargo.toml index 4b8944d..8b97ef7 100644 --- a/chanlun-py/Cargo.toml +++ b/chanlun-py/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "chanlun-py" -version = "26.6.42" +version = "26.6.44" edition = "2024" description = "缠论技术分析库 — Rust 高性能 Python 绑定" authors = ["YuYuKunKun"] @@ -12,7 +12,7 @@ crate-type = ["cdylib"] name = "chanlun" [dependencies] -chanlun = { path = "../chanlun" } +chanlun = "26.6.2" #{ path = "../chanlun" } lru = "0.18" pyo3 = { version = "0.28", features = ["experimental-inspect"] } serde_json = "1" diff --git a/chanlun-py/pyproject.toml b/chanlun-py/pyproject.toml index 243c313..6e44be9 100644 --- a/chanlun-py/pyproject.toml +++ b/chanlun-py/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "chanlun" -version = "2606.42" +version = "2606.44" description = "缠论技术分析库 — Rust 高性能实现" readme = { file = "README.md", content-type = "text/markdown" } license = { file = "LICENSE", content-type = "text/plain" } diff --git a/chanlun-py/src/algorithm_py.rs b/chanlun-py/src/algorithm_py.rs index b298a2b..a55e5fd 100644 --- a/chanlun-py/src/algorithm_py.rs +++ b/chanlun-py/src/algorithm_py.rs @@ -25,7 +25,7 @@ use crate::kline_py::chan_kline_to_py; use crate::structure_py::{dashed_to_py, fractal_to_py}; use pyo3::prelude::*; -use pyo3::types::{PyDict, PyType}; +use pyo3::types::{PyDict, PyList, PyType}; use std::collections::HashMap; use std::sync::Arc; use std::sync::RwLock; @@ -308,11 +308,13 @@ impl 笔Py { #[classmethod] #[pyo3(signature = (当前分型, 分型序列, 笔序列, 缠K序列, 普K序列, 递归层次, 配置))] /// 笔划分核心递归算法 + /// 分型序列/笔序列 原地修改(与 chan.py 行为一致) + /// :return: 递归层次 fn 分析( _cls: &Bound<'_, PyType>, 当前分型: Option<&Bound<'_, 分型Py>>, - 分型序列: Vec>, - 笔序列: Vec>, + 分型序列: &Bound<'_, PyList>, + 笔序列: &Bound<'_, PyList>, 缠K序列: Vec>, 普K序列: Vec>, 递归层次: i64, @@ -321,14 +323,19 @@ impl 笔Py { ) -> PyResult { let _ = 递归层次; // Python API 兼容参数,核心从0开始计数 let 当前分型_rc = 当前分型.map(|f| Arc::clone(&f.borrow().inner)); - let mut fr_seq: Vec> = 分型序列 - .iter() - .map(|f| Arc::clone(&f.bind(py).borrow().inner)) - .collect(); - let mut bi_seq: Vec> = 笔序列 - .iter() - .map(|d| Arc::clone(&d.bind(py).borrow().inner)) - .collect(); + + // 从 Python 列表提取 + let mut fr_seq = Vec::with_capacity(分型序列.len()); + for item in 分型序列.iter() { + let f: PyRef<'_, 分型Py> = item.extract()?; + fr_seq.push(Arc::clone(&f.inner)); + } + let mut bi_seq = Vec::with_capacity(笔序列.len()); + for item in 笔序列.iter() { + let d: PyRef<'_, 虚线Py> = item.extract()?; + bi_seq.push(Arc::clone(&d.inner)); + } + let ck_list: Vec> = 缠K序列 .iter() .map(|k| Arc::clone(&k.bind(py).borrow().inner)) @@ -338,8 +345,8 @@ impl 笔Py { .map(|k| k.bind(py).borrow().inner.clone()) .collect(); let config = 配置.borrow().to_rust_config(py)?; - match 当前分型_rc { - Some(fr) => Ok(chanlun::algorithm::bi::笔::分析( + let depth = match 当前分型_rc { + Some(fr) => chanlun::algorithm::bi::笔::分析( fr, &mut fr_seq, &mut bi_seq, @@ -347,9 +354,21 @@ impl 笔Py { &bar_list, 递归层次, &config, - )), - None => Ok(递归层次), + ), + None => 递归层次, + }; + + // 写回 Python 列表 + 分型序列.call_method0("clear")?; + for f in fr_seq { + 分型序列.call_method1("append", (fractal_to_py(py, f),))?; } + 笔序列.call_method0("clear")?; + for d in bi_seq { + 笔序列.call_method1("append", (dashed_to_py(py, d),))?; + } + + Ok(depth) } #[classmethod] @@ -502,11 +521,13 @@ impl 线段Py { } #[classmethod] + #[pyo3(signature = (笔序列, 线段序列, 配置, 层级 = 0, 关系序列 = None))] /// 线段划分核心递归算法 + /// 线段序列 原地修改(与 chan.py 行为一致) fn 分析( _cls: &Bound<'_, PyType>, 笔序列: Vec>, - 线段序列: Vec>, + 线段序列: &Bound<'_, PyList>, 配置: &Bound<'_, 缠论配置Py>, 层级: i64, 关系序列: Option>, @@ -516,10 +537,13 @@ impl 线段Py { .iter() .map(|d| Arc::clone(&d.bind(py).borrow().inner)) .collect(); - let mut seg_seq: Vec> = 线段序列 - .iter() - .map(|d| Arc::clone(&d.bind(py).borrow().inner)) - .collect(); + + let mut seg_seq = Vec::with_capacity(线段序列.len()); + for item in 线段序列.iter() { + let d: PyRef<'_, 虚线Py> = item.extract()?; + seg_seq.push(Arc::clone(&d.inner)); + } + let config = 配置.borrow().to_rust_config(py)?; let default_rel = vec![ chanlun::types::相对方向::向上, @@ -535,15 +559,22 @@ impl 线段Py { 层级, &rel_list, ); + + // 写回 Python 列表 + 线段序列.call_method0("clear")?; + for d in seg_seq { + 线段序列.call_method1("append", (dashed_to_py(py, d),))?; + } Ok(()) } #[classmethod] /// 即同级别分析 + /// 线段序列 原地修改(与 chan.py 行为一致) fn 扩展分析( _cls: &Bound<'_, PyType>, 虚线序列: Vec>, - 线段序列: Vec>, + 线段序列: &Bound<'_, PyList>, 配置: &Bound<'_, 缠论配置Py>, py: Python<'_>, ) -> PyResult<()> { @@ -551,12 +582,21 @@ impl 线段Py { .iter() .map(|d| Arc::clone(&d.bind(py).borrow().inner)) .collect(); - let mut seg_seq: Vec> = 线段序列 - .iter() - .map(|d| Arc::clone(&d.bind(py).borrow().inner)) - .collect(); + + let mut seg_seq = Vec::with_capacity(线段序列.len()); + for item in 线段序列.iter() { + let d: PyRef<'_, 虚线Py> = item.extract()?; + seg_seq.push(Arc::clone(&d.inner)); + } + let config = 配置.borrow().to_rust_config(py)?; chanlun::algorithm::segment::线段::扩展分析(&dash_list, &mut seg_seq, &config); + + // 写回 Python 列表 + 线段序列.call_method0("clear")?; + for d in seg_seq { + 线段序列.call_method1("append", (dashed_to_py(py, d),))?; + } Ok(()) } @@ -792,18 +832,26 @@ impl 中枢Py { } /// 当基础序列>=9时,从中枢中提取扩展线段中枢 + /// 扩展中枢 原地修改(与 chan.py 行为一致) fn 获取扩展中枢( &self, - 扩展中枢: Vec>, + 扩展中枢: &Bound<'_, PyList>, 配置: &Bound<'_, crate::config_py::缠论配置Py>, py: Python<'_>, ) -> PyResult<()> { - let mut hub_seq: Vec> = 扩展中枢 - .iter() - .map(|h| Arc::clone(&h.bind(py).borrow().inner)) - .collect(); + let mut hub_seq = Vec::with_capacity(扩展中枢.len()); + for item in 扩展中枢.iter() { + let h: PyRef<'_, 中枢Py> = item.extract()?; + hub_seq.push(Arc::clone(&h.inner)); + } let config = 配置.borrow().to_rust_config(配置.py())?; self.inner.获取扩展中枢(&mut hub_seq, &config); + + // 写回 Python 列表 + 扩展中枢.call_method0("clear")?; + for h in hub_seq { + 扩展中枢.call_method1("append", (hub_to_py(py, h),))?; + } Ok(()) } @@ -889,10 +937,11 @@ impl 中枢Py { #[classmethod] #[pyo3(signature = (虚线序列, 中枢序列, 跳过首部 = true, 标识 = "", 层级 = 0))] /// 中枢识别核心递归算法 + /// 中枢序列 原地修改(与 chan.py 行为一致) fn 分析( _cls: &Bound<'_, PyType>, 虚线序列: Vec>, - 中枢序列: Vec>, + 中枢序列: &Bound<'_, PyList>, 跳过首部: bool, 标识: &str, 层级: i64, @@ -902,11 +951,20 @@ impl 中枢Py { .iter() .map(|d| Arc::clone(&d.bind(py).borrow().inner)) .collect(); - let mut hub_seq: Vec> = 中枢序列 - .iter() - .map(|h| Arc::clone(&h.bind(py).borrow().inner)) - .collect(); + + let mut hub_seq = Vec::with_capacity(中枢序列.len()); + for item in 中枢序列.iter() { + let h: PyRef<'_, 中枢Py> = item.extract()?; + hub_seq.push(Arc::clone(&h.inner)); + } + chanlun::algorithm::hub::中枢::分析(&rc_list, &mut hub_seq, 跳过首部, 标识, 层级); + + // 写回 Python 列表 + 中枢序列.call_method0("clear")?; + for h in hub_seq { + 中枢序列.call_method1("append", (hub_to_py(py, h),))?; + } Ok(()) } diff --git a/chanlun-py/src/kline_py.rs b/chanlun-py/src/kline_py.rs index 490bc5e..24f757f 100644 --- a/chanlun-py/src/kline_py.rs +++ b/chanlun-py/src/kline_py.rs @@ -23,7 +23,7 @@ */ use pyo3::prelude::*; -use pyo3::types::{PyBytes, PyDict, PyType}; +use pyo3::types::{PyBytes, PyDict, PyList, PyType}; use std::collections::HashMap; use std::sync::Arc; use std::sync::RwLock; @@ -663,25 +663,30 @@ impl 缠论K线Py { #[classmethod] /// 分析K线,执行指标计算+包含处理+分型判定 + /// 缠K序列/普K序列 原地修改(与 chan.py 行为一致) + /// :return: (状态, 分型|None) fn 分析( _cls: &Bound<'_, PyType>, 当前K线: &Bound<'_, K线Py>, - 缠K序列: Vec>, - 普K序列: Vec>, + 缠K序列: &Bound<'_, PyList>, + 普K序列: &Bound<'_, PyList>, 配置: &Bound<'_, 缠论配置Py>, py: Python<'_>, ) -> PyResult<(String, Option>)> { let ck_inner = (*当前K线.borrow().inner).clone(); let config = 配置.borrow().to_rust_config(py)?; - let mut ck_seq: Vec<_> = 缠K序列 - .iter() - .map(|k| std::sync::Arc::clone(&k.bind(py).borrow().inner)) - .collect(); - let mut bar_seq: Vec<_> = 普K序列 - .iter() - .map(|k| k.bind(py).borrow().inner.clone()) - .collect(); + // 从 Python 列表提取 + let mut ck_seq = Vec::with_capacity(缠K序列.len()); + for item in 缠K序列.iter() { + let ck: PyRef<'_, Self> = item.extract()?; + ck_seq.push(std::sync::Arc::clone(&ck.inner)); + } + let mut bar_seq = Vec::with_capacity(普K序列.len()); + for item in 普K序列.iter() { + let bar: PyRef<'_, K线Py> = item.extract()?; + bar_seq.push(bar.inner.clone()); + } let (status, fractal) = chanlun::kline::chan_kline::缠论K线::分析( ck_inner, @@ -690,6 +695,16 @@ impl 缠论K线Py { &config, ); + // 写回 Python 列表(clear + extend) + 缠K序列.call_method0("clear")?; + for k in ck_seq { + 缠K序列.call_method1("append", (chan_kline_to_py(py, k),))?; + } + 普K序列.call_method0("clear")?; + for k in bar_seq { + 普K序列.call_method1("append", (bar_to_py(py, k),))?; + } + Ok((status, fractal.map(|f| fractal_to_py(py, f).into_any()))) } diff --git a/chanlun-py/tests/test_all.py b/chanlun-py/tests/test_all.py index 6ca7623..7af8142 100644 --- a/chanlun-py/tests/test_all.py +++ b/chanlun-py/tests/test_all.py @@ -508,13 +508,10 @@ class Test观察者子类化(PyO3SubclassMixin, unittest.TestCase): return obs # 4. 读取数据文件(classmethod 重写 - a = datetime.now() obs = Sub.读取数据文件(NB_PATH) self.assertIsInstance(obs, Sub) self.assertTrue(obs._custom_classmethod_flag) self.assertGreater(len(obs.普通K线序列), 0) - b = datetime.now() - print("读取数据文件 用时:", b - a) # 1. 加载本地数据 obs.重置基础序列() @@ -522,8 +519,6 @@ class Test观察者子类化(PyO3SubclassMixin, unittest.TestCase): self.assertTrue(obs._loaded) self.assertEqual(obs._load_count, 2) self.assertGreater(len(obs.普通K线序列), 0) - c = datetime.now() - print("加载本地数据 用时:", c - b) # 2. 静态重新分析(复用已加载数据的 obs) obs.重置基础序列() @@ -531,8 +526,6 @@ class Test观察者子类化(PyO3SubclassMixin, unittest.TestCase): obs.静态重新分析() self.assertEqual(obs._reanalyzed, 1) self.assertGreaterEqual(len(obs.笔序列), 0) - d = datetime.now() - print("静态重新分析 用时:", d - c) # 3. 保存数据 obs.重置基础序列() @@ -544,8 +537,6 @@ class Test观察者子类化(PyO3SubclassMixin, unittest.TestCase): # 5. 重置次数 self.assertEqual(obs._reload, 6) - e = datetime.now() - print("保存数据 用时:", e - d) def test_override_completely_no_super(self): k = self.make_data_item() @@ -1954,5 +1945,211 @@ class TestApi一致性(ApiConsistencyMixin, unittest.TestCase): # ============================================================ +class Test导出函数双端等效(unittest.TestCase): + """验证: 序列修改类导出函数与 chan.py 行为一致 (就地修改 + 返回值等效)""" + + _TEST_COUNT = 300 # 投喂 K 线数 + + @classmethod + def setUpClass(cls): + if not _has_nb(): + raise unittest.SkipTest("需要 .nb 数据文件") + cls.bars = read_nb_bars(NB_PATH) + + # ---- 辅助 ---- + + def _make_observers(self): + import chanlun + from chanlun import chan + + cfg_rs = chanlun.缠论配置() + cfg_rs.计算指标 = True + cfg_py = chan.缠论配置() + cfg_py.计算指标 = True + + obs_rs = chanlun.观察者("btcusd", 300, cfg_rs) + obs_py = chan.观察者("btcusd", 300, cfg_py) + + for i, (ts, o, h, l, c, v) in enumerate(self.bars[: self._TEST_COUNT]): + obs_rs.投喂原始数据(ts, o, h, l, c, v) + obs_py.投喂原始数据(ts, o, h, l, c, v) + + return obs_rs, obs_py + + # ================================================================ + # 缠论K线.分析 + # ================================================================ + + def test_缠论K线分析_等效(self): + """缠论K线.分析 双端行为一致 (返回值 + 序列长度).""" + import chanlun + from chanlun import chan + + obs_rs, obs_py = self._make_observers() + + ck_rs: list = [] + bar_rs: list = [] + ck_py: list = [] + bar_py: list = [] + mismatches = [] + + for k_rs, k_py in zip(obs_rs.普通K线序列[100:200], obs_py.普通K线序列[100:200]): + st_rs, fx_rs = chanlun.缠论K线.分析(k_rs, ck_rs, bar_rs, obs_rs.配置) + st_py, fx_py = chan.缠论K线.分析(k_py, ck_py, bar_py, obs_py.配置) + + if st_rs != st_py: + mismatches.append(f"状态: R={st_rs} P={st_py}") + if (fx_rs is None) != (fx_py is None): + mismatches.append(f"分型None: R={fx_rs is None} P={fx_py is None}") + + self.assertEqual(len(mismatches), 0, f"缠论K线.分析 不一致 ({len(mismatches)}):\n" + "\n".join(mismatches[:5])) + self.assertEqual(len(ck_rs), len(ck_py), f"缠K序列长度: R={len(ck_rs)} P={len(ck_py)}") + self.assertEqual(len(bar_rs), len(bar_py), f"普K序列长度: R={len(bar_rs)} P={len(bar_py)}") + + # ================================================================ + # 笔.分析 + # ================================================================ + + def test_笔分析_等效(self): + """笔.分析 双端行为一致 (返回值 + 序列修改).""" + import chanlun + from chanlun import chan + + obs_rs, obs_py = self._make_observers() + + ck_rs = list(obs_rs.缠论K线序列) + ck_py = list(obs_py.缠论K线序列) + bar_rs = list(obs_rs.普通K线序列) + bar_py = list(obs_py.普通K线序列) + + fr_rs: list = [] + bi_rs: list = [] + fr_py: list = [] + bi_py: list = [] + mismatches = [] + + for fx_rs, fx_py in zip(obs_rs.分型序列[2:], obs_py.分型序列[2:]): + d_rs = chanlun.笔.分析(fx_rs, fr_rs, bi_rs, ck_rs, bar_rs, 0, obs_rs.配置) + d_py = chan.笔.分析(fx_py, fr_py, bi_py, ck_py, bar_py, 0, obs_py.配置) + + if d_rs != d_py: + mismatches.append(f"递归层次: R={d_rs} P={d_py}") + + self.assertEqual(len(mismatches), 0, f"笔.分析 不一致 ({len(mismatches)}):\n" + "\n".join(mismatches[:3])) + self.assertEqual(len(fr_rs), len(fr_py), f"分型序列长度: R={len(fr_rs)} P={len(fr_py)}") + self.assertGreater(len(bi_rs), 0, "笔序列为空 (Rust)") + self.assertEqual(len(bi_rs), len(bi_py), f"笔序列长度: R={len(bi_rs)} P={len(bi_py)}") + + # ================================================================ + # 线段.分析 + # ================================================================ + + def test_线段分析_等效(self): + """线段.分析 双端行为一致 (序列修改).""" + import chanlun + from chanlun import chan + + obs_rs, obs_py = self._make_observers() + + seg_rs = list(obs_rs.线段序列) + seg_py = list(obs_py.线段序列) + + # 先用 chanlun.线段.分析 重新分析 + bi_list_rs = list(obs_rs.笔序列) + bi_list_py = list(obs_py.笔序列) + + # 重置线段序列 + seg_rs.clear() + seg_py.clear() + + chanlun.线段.分析(bi_list_rs, seg_rs, obs_rs.配置) + chan.线段.分析(bi_list_py, seg_py, obs_py.配置) + + self.assertGreater(len(seg_rs), 0, "线段序列为空 (Rust)") + self.assertEqual(len(seg_rs), len(seg_py), f"线段序列长度: R={len(seg_rs)} P={len(seg_py)}") + + # ================================================================ + # 线段.扩展分析 + # ================================================================ + + def test_线段扩展分析_等效(self): + """线段.扩展分析 双端行为一致.""" + import chanlun + from chanlun import chan + + obs_rs, obs_py = self._make_observers() + + ext_rs = list(obs_rs.扩展线段序列) + ext_py = list(obs_py.扩展线段序列) + + ext_rs.clear() + ext_py.clear() + + bi_list_rs = list(obs_rs.笔序列) + bi_list_py = list(obs_py.笔序列) + + chanlun.线段.扩展分析(bi_list_rs, ext_rs, obs_rs.配置) + chan.线段.扩展分析(bi_list_py, ext_py, obs_py.配置) + + self.assertGreater(len(ext_rs), 0, "扩展线段序列为空 (Rust)") + self.assertEqual(len(ext_rs), len(ext_py), f"扩展线段: R={len(ext_rs)} P={len(ext_py)}") + + # ================================================================ + # 中枢.分析 + # ================================================================ + + def test_中枢分析_等效(self): + """中枢.分析 双端行为一致.""" + import chanlun + from chanlun import chan + + obs_rs, obs_py = self._make_observers() + + hub_rs: list = [] + hub_py: list = [] + bi_list_rs = list(obs_rs.笔序列) + bi_list_py = list(obs_py.笔序列) + + chanlun.中枢.分析(bi_list_rs, hub_rs, True, "", 0) + chan.中枢.分析(bi_list_py, hub_py, True, "", 0) + + self.assertGreater(len(hub_rs), 0, "笔中枢序列为空 (Rust)") + self.assertEqual(len(hub_rs), len(hub_py), f"笔中枢: R={len(hub_rs)} P={len(hub_py)}") + + # ================================================================ + # 中枢.获取扩展中枢 + # ================================================================ + + def test_获取扩展中枢_等效(self): + """中枢.获取扩展中枢 双端行为一致.""" + import chanlun + from chanlun import chan + + obs_rs, obs_py = self._make_observers() + + ext_hub_rs = list(obs_rs.扩展中枢序列) + ext_hub_py = list(obs_py.扩展中枢序列) + + ext_hub_rs.clear() + ext_hub_py.clear() + + # init with 笔中枢 + bi_list_rs = list(obs_rs.笔序列) + bi_list_py = list(obs_py.笔序列) + chanlun.中枢.分析(bi_list_rs, ext_hub_rs, True, "", 0) + chan.中枢.分析(bi_list_py, ext_hub_py, True, "", 0) + + # 若基础序列≥9,调用获取扩展中枢 + for hub_rs, hub_py in zip(ext_hub_rs, ext_hub_py): + sub_rs: list = [] + sub_py: list = [] + hub_rs.获取扩展中枢(sub_rs, obs_rs.配置) + hub_py.获取扩展中枢(sub_py, obs_py.配置) + if len(sub_rs) != len(sub_py): + self.fail(f"获取扩展中枢 长度不一致: R={len(sub_rs)} P={len(sub_py)}") + + # all passed (or vacuously true if no hubs with >=9 segments) + + if __name__ == "__main__": unittest.main() diff --git a/chanlun/Cargo.toml b/chanlun/Cargo.toml index 6342280..c27ec4a 100644 --- a/chanlun/Cargo.toml +++ b/chanlun/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "chanlun" -version = "26.6.1" +version = "26.6.2" edition = "2024" license = "MIT" description = "基于缠论(缠中说禅)理论的量化技术分析核心库,支持流式数据处理和多周期联立分析。" diff --git a/check_license.py b/check_license.py new file mode 100644 index 0000000..16d5d17 --- /dev/null +++ b/check_license.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python3 +"""检测 Rust 源码文件头部是否有 MIT 协议,若无则自动注入。 + +用法: + python3 check_license.py # 检测 chanlun/ 和 chanlun-py/ 下所有 .rs + python3 check_license.py --check-only # 仅检测,不修改 + python3 check_license.py --fix # 检测并修复 + python3 check_license.py path/to/dir # 指定目录 +""" + +import argparse +import os +import sys +from pathlib import Path + + +def find_repo_root() -> Path: + """从脚本位置向上查找仓库根目录(含 LICENSE 文件的目录)。""" + current = Path(__file__).resolve().parent + while current != current.parent: + if (current / "LICENSE").exists(): + return current + current = current.parent + # Fallback: 脚本所在目录的父目录 + return Path(__file__).resolve().parent.parent + + +def build_license_header(license_path: Path) -> str: + """读取 LICENSE 文件并格式化为 Rust 块注释头。""" + lines = license_path.read_text(encoding="utf-8").rstrip("\n").split("\n") + header_lines = ["/*"] + for line in lines: + if line.strip(): + header_lines.append(f" * {line}") + else: + header_lines.append(" *") + header_lines.append(" */") + header_lines.append("") # 末尾空行分隔 + return "\n".join(header_lines) + "\n" + + +def has_license_header(file_path: Path) -> bool: + """检测文件头部是否已包含块注释风格的 MIT License。""" + try: + with open(file_path, "r", encoding="utf-8") as f: + head = f.read(512) + except (OSError, UnicodeDecodeError): + return True + return head.lstrip().startswith("/*") and "MIT License" in head + + +def strip_old_license_header(text: str) -> str: + """去除文件中已有的 // 风格 license header(重新注入前调用)。""" + stripped = text.lstrip("\n") + if stripped.startswith("// MIT License"): + # 找到 // 注释块结束位置(第一个非 // 非空行) + lines = stripped.split("\n") + end_idx = 0 + for i, line in enumerate(lines): + if line.startswith("//") or line.strip() == "": + end_idx = i + 1 + else: + break + return "\n".join(lines[end_idx:]) + return text + + +def inject_license(file_path: Path, header: str) -> bool: + """将 license header 注入文件头部。返回 True 表示已修改。""" + original = file_path.read_text(encoding="utf-8") + # 已有块注释风格则跳过 + if original.lstrip().startswith("/*") and "MIT License" in original[:512]: + return False + # 去除旧的 // 风格 header(如果存在) + cleaned = strip_old_license_header(original) + file_path.write_text(header + cleaned, encoding="utf-8") + return True + + +def collect_rs_files(roots: list[Path]) -> list[Path]: + """递归收集所有 .rs 文件,排除 target/ 等构建产物目录。""" + exclude_dirs = {"target", ".git", "__pycache__", "dist", "build", ".venv", "venv"} + files = [] + for root in roots: + if not root.is_dir(): + continue + for path in root.rglob("*.rs"): + if any(excl in path.parts for excl in exclude_dirs): + continue + files.append(path) + return sorted(files) + + +def main() -> int: + parser = argparse.ArgumentParser(description="检测 Rust 源码 MIT 协议头") + parser.add_argument( + "paths", + nargs="*", + help="要检测的目录(默认: chanlun 和 chanlun-py 源码目录)", + ) + parser.add_argument( + "--check-only", + action="store_true", + help="仅检测,不修改文件", + ) + parser.add_argument( + "--fix", + action="store_true", + help="检测并自动注入缺失的协议头(默认行为)", + ) + args = parser.parse_args() + + repo_root = find_repo_root() + license_path = repo_root / "LICENSE" + + if not license_path.exists(): + print(f"错误: 未找到 LICENSE 文件 ({license_path})", file=sys.stderr) + return 1 + + header = build_license_header(license_path) + + # 确定扫描目录 + if args.paths: + roots = [Path(p).resolve() for p in args.paths] + else: + roots = [ + repo_root / "chanlun" / "src", + repo_root / "chanlun-py" / "src", + repo_root / "chanlun" / "tests", + repo_root / "chanlun-py" / "tests", + ] + roots = [r for r in roots if r.is_dir()] + + if not roots: + print("错误: 未找到任何源码目录", file=sys.stderr) + return 1 + + rs_files = collect_rs_files(roots) + + if not rs_files: + print("未找到 .rs 文件") + return 0 + + missing = [] + injected = [] + + for f in rs_files: + if has_license_header(f): + continue + missing.append(f) + if not args.check_only: + if inject_license(f, header): + injected.append(f) + + if args.check_only: + if missing: + print(f"缺失 MIT 协议头: {len(missing)} 个文件") + for f in missing: + print(f" {f}") + return 1 + else: + print(f"全部 {len(rs_files)} 个 .rs 文件均已包含 MIT 协议头") + return 0 + else: + if injected: + print(f"已注入 MIT 协议头: {len(injected)} 个文件") + for f in injected: + print(f" {f}") + if missing: + already = len(missing) - len(injected) + if already > 0: + print(f"已有协议头: {already} 个文件(无需修改)") + total = len(rs_files) - len(missing) + print(f"总计: {len(rs_files)} 个 .rs 文件, {total} 个已含协议头") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/templates/index.html b/templates/index.html index 37dd964..c88d487 100644 --- a/templates/index.html +++ b/templates/index.html @@ -848,6 +848,7 @@ print("=== 趋势分析 ===") this.websocketManager = websocketManager; this.widget = null; this.shapeIds = new Map(); + this._pendingShapeCreate = new Map(); // id → Promise, 追踪正在创建的图形 this.onRealtimeCallback = null; this.initDataFeed(); this.websocketManager.on('chart', (message) => { @@ -1052,7 +1053,7 @@ print("=== 趋势分析 ===") } } - handleChartMessage(message) { + async handleChartMessage(message) { Utils.log("处理图表消息:", message.type); switch (message.type) { case 'realtime': @@ -1070,11 +1071,11 @@ print("=== 趋势分析 ===") break; case 'shape': if (message.cmd === "APPEND") { - this.addShape(message.id, message.points, message.name, message.overrides); + await this.addShape(message.id, message.points, message.name, message.overrides); } else if (message.cmd === "REMOVE") { - this.removeShape(message.id); + await this.removeShape(message.id); } else if (message.cmd === "MODIFY") { - this.updateShape(message.id, message.points, message.overrides); + await this.updateShape(message.id, message.points, message.overrides); } break; case 'query_result': @@ -1104,6 +1105,12 @@ print("=== 趋势分析 ===") return; } + // 等待之前相同 ID 的创建完成(避免竞态) + const prevPending = this._pendingShapeCreate.get(id); + if (prevPending) { + await prevPending; + } + // 重复判断 const existShapeId = this.shapeIds.get(id); if (existShapeId) { @@ -1112,26 +1119,40 @@ print("=== 趋势分析 ===") return; } - // 创建图形 - const shapeId = await this.widget.chart().createMultipointShape(points, options); - const shape = this.widget.chart().getShapeById(shapeId); + // 记录 pending 状态,供 removeShape/updateShape 等待 + const createPromise = (async () => { + // 创建图形 + const shapeId = await this.widget.chart().createMultipointShape(points, options); + const shape = this.widget.chart().getShapeById(shapeId); - if (!shape) { - Utils.error("添加形状失败:shape 为 null", {id, shapeId}); - return; + if (!shape) { + Utils.error("添加形状失败:shape 为 null", {id, shapeId}); + return null; + } + + // 设置属性 + //Utils.log("shapeId:", shape.getProperties()); + //shape.setProperties(properties); + shape.bringToFront(); + + // 保存映射 + this.shapeIds.set(id, shapeId); + Utils.log("添加形状", {id, shapeId}); + + return shapeId; + })(); + + this._pendingShapeCreate.set(id, createPromise); + + const shapeId = await createPromise; + if (shapeId === null) { + // shape creation failed } - // 设置属性 - //Utils.log("shapeId:", shape.getProperties()); - //shape.setProperties(properties); - shape.bringToFront(); - - // 保存映射 - this.shapeIds.set(id, shapeId); - Utils.log("添加形状", {id, shapeId}); - } catch (error) { Utils.error("添加形状失败", error); + } finally { + this._pendingShapeCreate.delete(id); } } @@ -1139,6 +1160,12 @@ print("=== 趋势分析 ===") try { if (!this.widget || !this.widget.chart) return; + // 等待该 ID 的 addShape 完成(避免竞态) + const pending = this._pendingShapeCreate.get(id); + if (pending) { + await pending; + } + const shapeId = this.shapeIds.get(id); if (!shapeId) { Utils.log("图形不存在,无需删除", {id}); @@ -1162,6 +1189,12 @@ print("=== 趋势分析 ===") try { if (!this.widget || !this.widget.chart) return; + // 等待该 ID 的 addShape 完成(避免竞态) + const pending = this._pendingShapeCreate.get(id); + if (pending) { + await pending; + } + const shapeId = this.shapeIds.get(id); if (!shapeId) { Utils.error("更新失败:图形不存在", {id});