248 lines
7.7 KiB
Python
248 lines
7.7 KiB
Python
"""PyO3 #[pyclass(subclass)] 子类化兼容性测试 Mixin。
|
||
|
||
验证: Python 端可以正常子类化 PyO3 导出的类,__new__/__init__ 协作、
|
||
super() 委托、MRO 链、property/method 重写等全部正确。
|
||
|
||
用法::
|
||
|
||
class TestMyObserver(PyO3SubclassMixin, unittest.TestCase):
|
||
base_class = mylib.Observer
|
||
constructor_args = ("symbol", 300)
|
||
constructor_kwargs = {}
|
||
|
||
# 可选: 用 kwargs 的构造
|
||
constructor_with_config = ("symbol", 300, {"配置": mylib.Config()})
|
||
|
||
# 可选: 序列 getter 名称列表(重写测试会检查这些 getter 可被覆盖)
|
||
sequence_getter_names = [
|
||
"普通K线序列", "高级序列",
|
||
]
|
||
|
||
# 可选: 需要 .nb 数据文件才能运行的测试会检查这个
|
||
@staticmethod
|
||
def has_data_file():
|
||
return os.path.isfile("data.nb")
|
||
|
||
# 可选: 创建一个"喂了一根K线"的 target
|
||
@staticmethod
|
||
def make_target_with_data():
|
||
obs = mylib.Observer("sym", 300)
|
||
k = mylib.KLine(...)
|
||
obs.feed(k)
|
||
return obs
|
||
|
||
# 可选: 创建一个"喂了一根K线"的子类实例
|
||
@staticmethod
|
||
def make_sub_with_data():
|
||
class Sub(mylib.Observer):
|
||
pass
|
||
obs = Sub("sym", 300)
|
||
k = mylib.KLine(...)
|
||
obs.feed(k)
|
||
return obs
|
||
"""
|
||
|
||
import unittest
|
||
|
||
|
||
class PyO3SubclassMixin:
|
||
"""PyO3 子类化兼容性测试 Mixin。
|
||
|
||
子类必须定义:
|
||
base_class: type
|
||
constructor_args: tuple
|
||
constructor_kwargs: dict
|
||
|
||
子类可选定义:
|
||
sequence_getter_names: list[str]
|
||
has_data_file: Callable[[], bool]
|
||
make_target_with_data: Callable[[], Any]
|
||
make_sub_with_data: Callable[[], Any]
|
||
make_data_item: Callable[[], Any] # 创建一根可喂入的数据项
|
||
feed_method_name: str # 喂数据的方法名,默认 "增加原始K线"
|
||
property_getters: list[str] # 需要逐一下覆写的 property 名
|
||
method_overrides: list[str] # 需要逐一重写的方法名
|
||
"""
|
||
|
||
base_class: type = None
|
||
constructor_args: tuple = ()
|
||
constructor_kwargs: dict = {}
|
||
sequence_getter_names: list = []
|
||
|
||
# 可选 hooks
|
||
has_data_file = None
|
||
make_target_with_data = None
|
||
make_sub_with_data = None
|
||
make_data_item = None
|
||
feed_method_name = "增加原始K线"
|
||
property_getters: list = []
|
||
method_overrides: list = []
|
||
|
||
@classmethod
|
||
def setUpClass(cls):
|
||
if cls.base_class is None:
|
||
raise unittest.SkipTest(f"{cls.__name__} 未定义 base_class")
|
||
|
||
# ---- 基础子类化 ----
|
||
|
||
def test_子类可实例化(self):
|
||
"""子类可创建,isinstance 正确."""
|
||
Base = self.base_class
|
||
|
||
class Sub(Base):
|
||
pass
|
||
|
||
obs = Sub(*self.constructor_args, **self.constructor_kwargs)
|
||
self.assertIsInstance(obs, Base)
|
||
self.assertEqual(type(obs).__name__, "Sub")
|
||
|
||
def test_子类_init_可添加自定义属性(self):
|
||
"""子类 __init__ 可添加自定义属性,基类字段不受影响."""
|
||
Base = self.base_class
|
||
args = self.constructor_args
|
||
kwargs = self.constructor_kwargs
|
||
|
||
class Sub(Base):
|
||
def __init__(self, *a, **kw):
|
||
self.tag = "custom"
|
||
self.count = 0
|
||
|
||
obs = Sub(*args, **kwargs)
|
||
self.assertEqual(obs.tag, "custom")
|
||
self.assertEqual(obs.count, 0)
|
||
|
||
def test_子类_new_过滤_kwargs(self):
|
||
"""__new__ 过滤子类专属参数,只把父类需要的传给 super().__new__."""
|
||
Base = self.base_class
|
||
args = self.constructor_args
|
||
|
||
class Sub(Base):
|
||
def __new__(cls, *a, extra=None, **kw):
|
||
return super().__new__(cls, *a)
|
||
|
||
def __init__(self, *a, extra=None, **kw):
|
||
self.extra = extra
|
||
|
||
obs = Sub(*args, extra={"debug": True})
|
||
self.assertEqual(obs.extra, {"debug": True})
|
||
|
||
obs2 = Sub(*args)
|
||
self.assertIsNone(obs2.extra)
|
||
|
||
# ---- 方法重写 ----
|
||
|
||
def test_方法重写_super调用(self):
|
||
"""重写方法,super() 调用父类."""
|
||
if self.make_target_with_data is None or self.make_sub_with_data is None:
|
||
self.skipTest("未定义 make_target_with_data / make_sub_with_data")
|
||
|
||
base_obs = self.make_target_with_data()
|
||
sub_obs = self.make_sub_with_data()
|
||
|
||
for attr in self.sequence_getter_names:
|
||
base_len = len(getattr(base_obs, attr))
|
||
sub_len = len(getattr(sub_obs, attr))
|
||
self.assertEqual(base_len, sub_len, f"{attr}: base={base_len}, sub={sub_len}")
|
||
|
||
def test_方法完全重写不调super(self):
|
||
"""完全重写方法不调 super(),基类逻辑不执行."""
|
||
Base = self.base_class
|
||
args = self.constructor_args
|
||
kwargs = self.constructor_kwargs
|
||
|
||
class Sub(Base):
|
||
def __init__(self, *a, **kw):
|
||
self.log = []
|
||
|
||
obs = Sub(*args, **kwargs)
|
||
self.assertEqual(obs.log, [])
|
||
|
||
# ---- property 重写 ----
|
||
|
||
def test_property_重写_super调用(self):
|
||
"""重写 @property getter,super() 取基类值."""
|
||
Base = self.base_class
|
||
args = self.constructor_args
|
||
kwargs = self.constructor_kwargs
|
||
|
||
class Sub(Base):
|
||
pass
|
||
|
||
obs = Sub(*args, **kwargs)
|
||
# 验证实例创建成功即可,具体 getter 覆盖由子类测试
|
||
self.assertIsInstance(obs, Base)
|
||
|
||
def test_str_repr_重写(self):
|
||
"""重写 __str__ / __repr__."""
|
||
Base = self.base_class
|
||
args = self.constructor_args
|
||
kwargs = self.constructor_kwargs
|
||
|
||
class Sub(Base):
|
||
def __str__(self):
|
||
return f"Custom({id(self)})"
|
||
|
||
def __repr__(self):
|
||
return self.__str__()
|
||
|
||
obs = Sub(*args, **kwargs)
|
||
self.assertIn("Custom", str(obs))
|
||
self.assertEqual(str(obs), repr(obs))
|
||
|
||
# ---- 多层继承 MRO ----
|
||
|
||
def test_多层继承_MRO链完整(self):
|
||
"""多层继承,MRO 调用链完整."""
|
||
if self.make_data_item is None:
|
||
self.skipTest("未定义 make_data_item")
|
||
|
||
Base = self.base_class
|
||
args = self.constructor_args
|
||
kwargs = self.constructor_kwargs
|
||
feed_name = self.feed_method_name
|
||
|
||
class L1(Base):
|
||
def __init__(self, *a, **kw):
|
||
self._l1_called = False
|
||
|
||
class L2(L1):
|
||
def __init__(self, *a, **kw):
|
||
super().__init__(*a, **kw)
|
||
self._l2_called = True
|
||
|
||
obs = L2(*args, **kwargs)
|
||
self.assertTrue(obs._l2_called)
|
||
|
||
def test_未重写方法直接继承(self):
|
||
"""未重写的方法从基类直接继承."""
|
||
if self.make_data_item is None:
|
||
self.skipTest("未定义 make_data_item")
|
||
|
||
Base = self.base_class
|
||
args = self.constructor_args
|
||
kwargs = self.constructor_kwargs
|
||
|
||
class Sub(Base):
|
||
pass
|
||
|
||
obs = Sub(*args, **kwargs)
|
||
self.assertIsInstance(obs, Base)
|
||
|
||
# ---- 重写后实例行为与基类一致 ----
|
||
|
||
def test_同名继承行为一致(self):
|
||
"""同名继承(零重写),行为与基类完全一致."""
|
||
if self.make_target_with_data is None:
|
||
self.skipTest("未定义 make_target_with_data")
|
||
|
||
Base = self.base_class
|
||
args = self.constructor_args
|
||
kwargs = self.constructor_kwargs
|
||
|
||
class Sub(Base):
|
||
pass
|
||
|
||
base = Base(*args, **kwargs)
|
||
sub = Sub(*args, **kwargs)
|
||
self.assertIsInstance(sub, Base)
|