v4.0.17 no-backtest

This commit is contained in:
Ichinga Samuel
2026-02-21 07:43:33 +01:00
parent 844e39194b
commit 97aea6952e
124 changed files with 8198 additions and 14349 deletions
+515 -176
View File
@@ -1,225 +1,564 @@
"""Comprehensive tests for the Base and _Base classes.
"""Comprehensive tests for the base module.
Tests cover:
- Base class initialization and attribute handling
- Dictionary conversion with include/exclude filtering
- Annotations and class variables
- _Base class MetaTrader and Config integration
- Pickling/serialization support
- Mode switching (async/sync)
- Base class initialization, set_attributes, repr, annotations, dict, get_dict, class_vars
- BaseMeta metaclass lazy setup behavior
- _Base class with MetaTrader/Config integration and pickling support
- Subclassing and annotation/exclude/include merging
"""
import enum
import pickle
from unittest.mock import patch, MagicMock
import pytest
from aiomql.core.base import Base, _Base
from aiomql.core.base import Base, _Base, BaseMeta
from aiomql.core.config import Config
from aiomql.core.meta_trader import MetaTrader
from aiomql.core.sync.meta_trader import MetaTrader as MetaTraderSync
class ChildClass(Base):
attr: int
attr2: str
cls_attr: int = 10
# ---------------------------------------------------------------------------
# Helper subclasses for testing
# ---------------------------------------------------------------------------
class ChildBaseClass(_Base):
"""Test subclass of _Base for testing MT5/Config integration."""
attr: int
attr2: str
cls_attr: int = 20
class TestEnum(enum.Enum):
"""Test enum for repr testing."""
VALUE_A = 1
VALUE_B = 2
class EnumChild(Base):
"""Test class with enum attribute."""
class SimpleModel(Base):
"""A simple Base subclass with typed annotations."""
name: str
status: TestEnum
value: int
score: float
class TestBaseClass:
"""Tests for the Base class."""
class ExtendedModel(SimpleModel):
"""A child of SimpleModel adding more annotations."""
extra: str
value: float # override parent's int annotation with float
@pytest.fixture
def child(self):
return ChildClass(attr=1, attr2="test")
def test_repr(self, child):
repr_str = repr(child)
assert repr_str.startswith("ChildClass(")
assert "attr=1" in repr_str
assert "attr2=test" in repr_str
class CustomExcludeModel(Base):
"""A model with a custom exclude set."""
exclude: set[str] = {"mt5", "config", "exclude", "include", "annotations", "class_vars", "dict", "_instance",
"mode", "secret"}
name: str
secret: str
visible: int
def test_repr_with_enum(self):
"""Test repr correctly displays enum values."""
obj = EnumChild(name="test", status=TestEnum.VALUE_A)
repr_str = repr(obj)
assert "name=test" in repr_str
assert "VALUE_A" in repr_str
def test_repr_truncates_long_attributes(self):
"""Test repr truncates when there are more than 3 attributes."""
class ManyAttrs(Base):
a: int
b: int
c: int
d: int
e: int
class CustomIncludeModel(Base):
"""A model with a custom include set that overrides exclude."""
include: set[str] = {"config"}
name: str
config: str
obj = ManyAttrs(a=1, b=2, c=3, d=4, e=5)
repr_str = repr(obj)
assert "..." in repr_str
assert "a=1" in repr_str
assert "e=5" in repr_str
def test_set_attributes(self, child):
child.set_attributes(attr3=3.14, attr2="str")
assert child.attr2 == "str"
assert getattr(child, "attr3", None) is None
class ModelWithClassVar(Base):
"""A model with annotated class-level defaults."""
name: str
kind: str = "default_kind"
def test_set_attributes_type_conversion(self):
"""Test set_attributes converts types based on annotations."""
child = ChildClass(attr="42", attr2=123)
assert child.attr == 42
assert child.attr2 == "123"
def test_annotations(self, child):
annotations = child.annotations
assert isinstance(annotations, dict)
assert "attr" in annotations
assert "attr2" in annotations
class EnumColor(enum.Enum):
RED = 1
GREEN = 2
BLUE = 3
def test_annotations_includes_parent_classes(self):
"""Test annotations includes attributes from parent classes."""
class GrandChild(ChildClass):
extra: float
grandchild = GrandChild(attr=1, attr2="test", extra=3.14)
annotations = grandchild.annotations
assert "attr" in annotations
assert "attr2" in annotations
assert "extra" in annotations
class ModelWithEnum(Base):
"""A model containing an enum attribute."""
name: str
color: EnumColor
score: float
def test_get_dict(self, child):
child.set_attributes(attr2="test")
result = child.get_dict()
assert result["attr"] == 1
assert result["attr2"] == "test"
def test_get_dict_with_exclude(self, child):
child.set_attributes(attr2="test")
result = child.get_dict(exclude={"attr"})
assert "attr" not in result
assert result["attr2"] == "test"
class ManyAttrsModel(Base):
"""A model with > 3 simple-typed attributes."""
a: int
b: int
c: int
d: int
e: int
def test_get_dict_with_include(self, child):
child.set_attributes(attr3=3.14)
result = child.get_dict(include={"attr"})
assert result["attr"] == 1
assert "attr2" not in result
def test_get_dict_include_takes_precedence(self, child):
"""Test that include takes precedence over exclude."""
result = child.get_dict(include={"attr"}, exclude={"attr"})
assert "attr" in result
class ModelWithComplexAttr(Base):
"""A model with complex (non-simple) attributes."""
name: str
data: list
meta: dict
def test_class_vars(self, child):
class_vars = child.class_vars
assert isinstance(class_vars, dict)
assert "cls_attr" in class_vars
assert "attr" not in class_vars
def test_dict_property(self, child):
child.set_attributes(attr2="test")
dict_prop = child.dict
assert dict_prop["attr"] == 1
assert dict_prop["attr2"] == "test"
assert dict_prop["cls_attr"] == 10
class SyncBaseModel(_Base):
"""A _Base subclass operating in sync mode."""
mode = "sync"
name: str
class AsyncBaseModel(_Base):
"""A _Base subclass operating in async (default) mode."""
name: str
# ===========================================================================
# TestBaseInit
# ===========================================================================
class TestBaseInit:
"""Tests for Base.__init__ and set_attributes."""
def test_init_sets_annotated_attributes(self):
"""Init with valid annotated kwargs sets attributes."""
obj = SimpleModel(name="hello", value=42, score=3.14)
assert obj.name == "hello"
assert obj.value == 42
assert obj.score == 3.14
def test_init_ignores_non_annotated_kwargs(self):
"""Non-annotated kwargs are silently ignored."""
obj = SimpleModel(name="hello", value=1, score=0.0, unknown="ignored")
assert not hasattr(obj, "unknown")
def test_init_coerces_types(self):
"""Annotation callables are used to coerce values."""
obj = SimpleModel(name="hello", value="99", score="2.5")
assert obj.value == 99
assert isinstance(obj.value, int)
assert obj.score == 2.5
assert isinstance(obj.score, float)
def test_init_fallback_on_conversion_error(self):
"""When coercion raises ValueError/TypeError, raw value is kept."""
obj = SimpleModel(name="hello", value="not_a_number", score=1.0)
# value should be set as the raw string since int("not_a_number") raises ValueError
assert obj.value == "not_a_number"
def test_init_no_args(self):
"""Init with no args creates an instance with no instance attributes."""
obj = SimpleModel()
assert isinstance(obj, SimpleModel)
# No instance attributes should be set
assert "name" not in obj.__dict__
assert "value" not in obj.__dict__
def test_set_attributes_updates_existing(self):
"""set_attributes can update existing attributes."""
obj = SimpleModel(name="original", value=1, score=0.0)
obj.set_attributes(name="updated", value=100)
assert obj.name == "updated"
assert obj.value == 100
def test_set_attributes_ignores_unannotated(self):
"""set_attributes ignores keys not in annotations."""
obj = SimpleModel(name="test", value=1, score=0.0)
obj.set_attributes(phantom="ghost")
assert not hasattr(obj, "phantom")
# ===========================================================================
# TestBaseRepr
# ===========================================================================
class TestBaseRepr:
"""Tests for Base.__repr__."""
def test_repr_with_few_attrs(self):
"""Repr with ≤3 simple-type attrs shows all."""
obj = SimpleModel(name="test", value=42, score=1.5)
r = repr(obj)
assert r.startswith("SimpleModel(")
assert "name=test" in r
assert "value=42" in r
assert "score=1.5" in r
def test_repr_with_many_attrs_truncates(self):
"""Repr with > 3 attrs shows first 3 + ... + last 1."""
obj = ManyAttrsModel(a=1, b=2, c=3, d=4, e=5)
r = repr(obj)
assert "..." in r
assert "a=1" in r
assert "e=5" in r
def test_repr_excludes_private_attrs(self):
"""Repr excludes attributes starting with _."""
obj = SimpleModel(name="test", value=1, score=0.0)
obj._private = "hidden"
r = repr(obj)
assert "_private" not in r
def test_repr_excludes_complex_types(self):
"""Repr excludes list and dict attrs."""
obj = ModelWithComplexAttr(name="test", data=[1, 2, 3], meta={"k": "v"})
r = repr(obj)
assert "data=" not in r
assert "meta=" not in r
assert "name=test" in r
def test_repr_includes_enum_values(self):
"""Repr includes enum attributes."""
obj = ModelWithEnum(name="test", color=EnumColor.RED, score=1.0)
r = repr(obj)
assert "color=" in r
def test_repr_empty_instance(self):
"""Repr of instance with no attributes."""
obj = SimpleModel()
r = repr(obj)
assert r == "SimpleModel()"
# ===========================================================================
# TestBaseAnnotations
# ===========================================================================
class TestBaseAnnotations:
"""Tests for the annotations property."""
def test_annotations_returns_own_annotations(self):
"""annotations includes annotations from the class itself."""
obj = SimpleModel(name="x", value=1, score=0.0)
annots = obj.annotations
assert "name" in annots
assert "value" in annots
assert "score" in annots
def test_annotations_merges_parent(self):
"""annotations includes parent class annotations."""
obj = ExtendedModel(name="x", value=1, score=0.0, extra="e")
annots = obj.annotations
assert "name" in annots # from SimpleModel
assert "score" in annots # from SimpleModel
assert "extra" in annots # from ExtendedModel
def test_annotations_child_overrides_parent(self):
"""Child annotations override parent annotations."""
obj = ExtendedModel(name="x", value=1, score=0.0, extra="e")
annots = obj.annotations
# ExtendedModel annotates value as float, overriding SimpleModel's int
assert annots["value"] is float
def test_annotations_returns_dict(self):
"""annotations property returns a dict."""
obj = SimpleModel(name="x", value=1, score=0.0)
assert isinstance(obj.annotations, dict)
# ===========================================================================
# TestBaseClassVars
# ===========================================================================
class TestBaseClassVars:
"""Tests for the class_vars property."""
def test_class_vars_includes_annotated_defaults(self):
"""class_vars includes annotated class-level variables with defaults."""
obj = ModelWithClassVar(name="test")
cv = obj.class_vars
assert "kind" in cv
assert cv["kind"] == "default_kind"
def test_class_vars_excludes_non_annotated(self):
"""class_vars excludes class variables that are not annotated."""
obj = SimpleModel(name="x", value=1, score=0.0)
cv = obj.class_vars
# 'exclude' and 'include' are defined on Base but not annotated on SimpleModel
# However they ARE annotated on Base itself, so they will appear in class_vars
# The key point is that non-annotated attrs are excluded
for key in cv:
assert key in obj.annotations
# ===========================================================================
# TestBaseDict
# ===========================================================================
class TestBaseDict:
"""Tests for the dict property."""
def test_dict_returns_instance_and_class_attrs(self):
"""dict combines instance attributes and class_vars."""
obj = ModelWithClassVar(name="test")
d = obj.dict
assert "name" in d
assert d["name"] == "test"
assert "kind" in d
assert d["kind"] == "default_kind"
def test_dict_excludes_default_excluded_keys(self):
"""dict excludes keys in the exclude set."""
obj = SimpleModel(name="test", value=1, score=0.0)
d = obj.dict
assert "mt5" not in d
assert "config" not in d
assert "exclude" not in d
assert "include" not in d
assert "annotations" not in d
assert "class_vars" not in d
def test_dict_excludes_none_values(self):
"""Test dict property excludes None values."""
class OptionalAttr(Base):
required: int
optional: str = None
"""dict excludes attributes with None values."""
obj = SimpleModel(name="test", value=1, score=0.0)
obj.name = None # Manually set to None
d = obj.dict
assert "name" not in d
obj = OptionalAttr(required=1)
assert "optional" not in obj.dict
def test_dict_include_overrides_exclude(self):
"""include set can override exclude behavior."""
obj = CustomIncludeModel(name="test", config="my_config")
d = obj.dict
# 'config' is normally excluded, but CustomIncludeModel includes it
assert "config" in d
def test_dict_excludes_internal_attributes(self, child):
"""Test dict excludes internal attributes like mt5, config."""
dict_prop = child.dict
assert "mt5" not in dict_prop
assert "config" not in dict_prop
assert "exclude" not in dict_prop
assert "include" not in dict_prop
def test_dict_custom_exclude(self):
"""Custom exclude set hides specific attrs."""
obj = CustomExcludeModel(name="visible_name", secret="hidden", visible=42)
d = obj.dict
assert "name" in d
assert "visible" in d
assert "secret" not in d
class TestUnderscoreBaseClass:
"""Tests for the _Base class with MT5/Config integration."""
# ===========================================================================
# TestBaseGetDict
# ===========================================================================
@pytest.fixture
def base_child(self):
return ChildBaseClass(attr=1, attr2="test")
def test_has_mt5_attribute(self, base_child):
"""Test _Base provides mt5 attribute."""
assert hasattr(base_child, "mt5")
class TestBaseGetDict:
"""Tests for the get_dict method."""
def test_has_config_attribute(self, base_child):
"""Test _Base provides config attribute."""
assert hasattr(base_child, "config")
assert isinstance(base_child.config, Config)
def test_get_dict_no_args(self):
"""get_dict with no args returns all non-None dict items."""
obj = SimpleModel(name="test", value=1, score=2.5)
d = obj.get_dict()
assert "name" in d
assert "value" in d
assert "score" in d
def test_mt5_is_metatrader_instance(self, base_child):
"""Test mt5 is a MetaTrader instance in async mode."""
# Default mode is async
assert isinstance(base_child.mt5, MetaTrader)
def test_get_dict_include(self):
"""get_dict with include filters to specific keys."""
obj = SimpleModel(name="test", value=1, score=2.5)
d = obj.get_dict(include={"name", "score"})
assert "name" in d
assert "score" in d
assert "value" not in d
def test_config_is_shared(self):
"""Test config is shared across instances."""
child1 = ChildBaseClass(attr=1, attr2="test1")
child2 = ChildBaseClass(attr=2, attr2="test2")
assert child1.config is child2.config
def test_get_dict_exclude(self):
"""get_dict with exclude filters out specific keys."""
obj = SimpleModel(name="test", value=1, score=2.5)
d = obj.get_dict(exclude={"value"})
assert "value" not in d
assert "name" in d
assert "score" in d
def test_mt5_is_shared(self):
"""Test mt5 is shared across instances."""
child1 = ChildBaseClass(attr=1, attr2="test1")
child2 = ChildBaseClass(attr=2, attr2="test2")
assert child1.mt5 is child2.mt5
def test_get_dict_include_overrides_exclude(self):
"""When both include and exclude are set, include takes precedence."""
obj = SimpleModel(name="test", value=1, score=2.5)
d = obj.get_dict(include={"name"}, exclude={"name"})
assert "name" in d
assert "value" not in d
def test_getstate_excludes_mt5(self, base_child):
"""Test __getstate__ excludes mt5 for pickling."""
state = base_child.__getstate__()
def test_get_dict_excludes_none_values(self):
"""get_dict always excludes None values regardless of filters."""
obj = SimpleModel(name="test", value=1, score=2.5)
obj.score = None
d = obj.get_dict(include={"name", "score"})
assert "name" in d
assert "score" not in d
# ===========================================================================
# TestBaseMeta
# ===========================================================================
class TestBaseMeta:
"""Tests for the BaseMeta metaclass behavior."""
def test_instantiation_triggers_setup(self):
"""Instantiating a _Base subclass triggers _setup."""
obj = AsyncBaseModel(name="test")
assert hasattr(AsyncBaseModel, "config")
assert hasattr(AsyncBaseModel, "mt5")
def test_accessing_config_on_class_triggers_setup(self):
"""Accessing 'config' on a _Base subclass class triggers _setup."""
# Create a fresh class to test lazy setup
class FreshModel(_Base):
name: str
_ = FreshModel.config
assert isinstance(FreshModel.__dict__["config"], Config)
def test_accessing_mt5_on_class_triggers_setup(self):
"""Accessing 'mt5' on a _Base subclass class triggers _setup."""
class FreshModel2(_Base):
name: str
_ = FreshModel2.mt5
assert isinstance(FreshModel2.__dict__["mt5"], MetaTrader)
def test_setup_creates_config_instance(self):
"""_setup sets config as a Config instance."""
class TestSetupConfig(_Base):
name: str
TestSetupConfig._setup()
assert isinstance(TestSetupConfig.__dict__["config"], Config)
def test_setup_creates_async_meta_trader_by_default(self):
"""_setup creates MetaTrader (async) when mode is not 'sync'."""
class TestAsyncMT(_Base):
name: str
TestAsyncMT._setup()
assert isinstance(TestAsyncMT.__dict__["mt5"], MetaTrader)
def test_setup_creates_sync_meta_trader_for_sync_mode(self):
"""_setup creates MetaTraderSync when mode is 'sync'."""
class TestSyncMT(_Base):
mode = "sync"
name: str
TestSyncMT._setup()
assert isinstance(TestSyncMT.__dict__["mt5"], MetaTraderSync)
def test_setup_is_idempotent(self):
"""Calling _setup twice doesn't recreate config/mt5."""
class IdempotentModel(_Base):
name: str
IdempotentModel._setup()
config1 = IdempotentModel.__dict__["config"]
mt5_1 = IdempotentModel.__dict__["mt5"]
IdempotentModel._setup()
config2 = IdempotentModel.__dict__["config"]
mt5_2 = IdempotentModel.__dict__["mt5"]
assert config1 is config2
assert mt5_1 is mt5_2
# ===========================================================================
# TestBasePrivateBase
# ===========================================================================
class TestBasePrivateBase:
"""Tests for the _Base class."""
def test_inherits_from_base(self):
"""_Base inherits from Base."""
assert issubclass(_Base, Base)
def test_has_base_meta_metaclass(self):
"""_Base uses BaseMeta as its metaclass."""
assert type(_Base) is BaseMeta
def test_default_mode_is_async(self):
"""Default mode for _Base is 'async'."""
assert _Base.mode == "async"
def test_getstate_removes_mt5(self):
"""__getstate__ removes mt5 from instance state."""
obj = AsyncBaseModel(name="test")
obj.mt5_attr = "should_stay" # custom attr
state = obj.__getstate__()
assert "mt5" not in state
def test_getstate_preserves_other_attributes(self, base_child):
"""Test __getstate__ preserves other instance attributes."""
state = base_child.__getstate__()
assert state["attr"] == 1
assert state["attr2"] == "test"
def test_getstate_preserves_other_attrs(self):
"""__getstate__ keeps all attributes except mt5."""
obj = AsyncBaseModel(name="test_name")
state = obj.__getstate__()
assert state.get("name") == "test_name"
def test_inherits_from_base(self, base_child):
"""Test _Base inherits from Base."""
assert isinstance(base_child, Base)
def test_config_accessible_after_instantiation(self):
"""config is accessible as a class attribute after instantiation."""
obj = AsyncBaseModel(name="test")
assert isinstance(obj.config, Config)
def test_dict_property_works(self, base_child):
"""Test dict property works correctly."""
dict_prop = base_child.dict
assert dict_prop["attr"] == 1
assert dict_prop["attr2"] == "test"
assert dict_prop["cls_attr"] == 20
def test_mt5_accessible_after_instantiation(self):
"""mt5 is accessible as a class attribute after instantiation."""
obj = AsyncBaseModel(name="test")
assert isinstance(obj.mt5, MetaTrader)
def test_mode_attribute(self, base_child):
"""Test default mode is async."""
assert base_child.mode == "async"
def test_sync_mode_creates_sync_meta_trader(self):
"""Sync mode subclass gets MetaTraderSync."""
obj = SyncBaseModel(name="sync_test")
assert isinstance(SyncBaseModel.__dict__["mt5"], MetaTraderSync)
def test_class_setup_called_on_new(self):
"""Test _setup is called during instance creation."""
child = ChildBaseClass(attr=1, attr2="test")
# If _setup was called, mt5 and config should be set
assert hasattr(ChildBaseClass, "mt5")
assert hasattr(ChildBaseClass, "config")
def test_async_mode_creates_async_meta_trader(self):
"""Async mode subclass gets MetaTrader."""
obj = AsyncBaseModel(name="async_test")
assert isinstance(AsyncBaseModel.__dict__["mt5"], MetaTrader)
def test_getstate_does_not_modify_original_dict(self):
"""__getstate__ returns a copy, not modifying __dict__."""
obj = AsyncBaseModel(name="test")
original_dict = obj.__dict__.copy()
_ = obj.__getstate__()
assert obj.__dict__ == original_dict
# ===========================================================================
# TestBaseSubclassing
# ===========================================================================
class TestBaseSubclassing:
"""Tests for subclassing Base with annotation and exclude/include merging."""
def test_subclass_annotations_merge(self):
"""Subclass annotations include parent annotations."""
obj = ExtendedModel(name="x", value=1.5, score=0.0, extra="e")
annots = obj.annotations
assert "name" in annots
assert "score" in annots
assert "extra" in annots
def test_subclass_override_exclude(self):
"""Subclass can define its own exclude set."""
obj = CustomExcludeModel(name="n", secret="s", visible=1)
d = obj.dict
assert "secret" not in d
assert "name" in d
def test_subclass_override_include(self):
"""Subclass include set overrides parent exclude."""
obj = CustomIncludeModel(name="n", config="cfg")
d = obj.dict
assert "config" in d
def test_multiple_levels_of_inheritance(self):
"""Annotations from deeply nested inheritance chain are merged."""
class GrandChild(ExtendedModel):
level: int
obj = GrandChild(name="gc", value=1.0, score=2.0, extra="e", level=3)
annots = obj.annotations
assert "name" in annots
assert "extra" in annots
assert "level" in annots
assert annots["value"] is float # ExtendedModel override
def test_subclass_class_vars_include_parent_defaults(self):
"""Subclass class_vars include annotated defaults from parent."""
class ChildWithDefault(ModelWithClassVar):
extra: str = "extra_default"
obj = ChildWithDefault(name="test")
cv = obj.class_vars
assert "kind" in cv
assert cv["kind"] == "default_kind"
assert "extra" in cv
assert cv["extra"] == "extra_default"
def test_isinstance_checks(self):
"""Subclass instances pass isinstance checks for parent."""
obj = ExtendedModel(name="x", value=1, score=0.0, extra="e")
assert isinstance(obj, Base)
assert isinstance(obj, SimpleModel)
assert isinstance(obj, ExtendedModel)