Coverage for src/jquantstats/_protocol.py: 100%

4 statements  

« prev     ^ index     » next       coverage.py v7.15.3, created at 2026-08-06 04:52 +0000

1"""Shared protocol definitions used across jquantstats subpackages. 

2 

3Design rationale 

4---------------- 

5The analytics subpackages (``_stats``, ``_plots``, ``_reports``, ``_utils``) 

6must not import the concrete `Data` / `Portfolio` classes at runtime — that 

7would create circular imports, since those classes compose the subpackages. 

8 

9**This is enforced, not just documented.** ``[tool.importlinter]`` in 

10``pyproject.toml`` declares it as a contract that ``make arch`` checks and 

11``make test`` runs, so a layering inversion fails CI rather than review. Imports 

12under ``if TYPE_CHECKING:`` are excluded from the contract — annotating against 

13the concrete class costs nothing at runtime and forms no cycle, so it stays 

14allowed. Where a subpackage needs to *construct* a `Data` (rather than annotate 

15one), it goes through ``type(self._data)`` instead of importing the class; see 

16``_stats/_reporting.py::_summary_frame`` and ``_data_reshape.py::_rebuild``. 

17 

18Each consumer annotates against a structural Protocol: 

19 

20- `DataLike` and `StatsLike` (this module) are shared by every subpackage — 

21 there is exactly one definition of each. 

22- ``PortfolioLike`` is deliberately *not* shared: each subpackage declares its 

23 own (``_plots/_protocol.py``, ``_reports/_protocol.py``, 

24 ``_utils/_protocol.py``) listing only the members it actually consumes 

25 (interface segregation). Keep it that way — a merged PortfolioLike would 

26 re-couple the subpackages to the full Portfolio surface. 

27""" 

28 

29from __future__ import annotations 

30 

31from collections.abc import Iterator 

32from typing import Protocol, runtime_checkable 

33 

34import polars as pl 

35 

36 

37class StatsLike(Protocol): # pragma: no cover 

38 """Structural interface for the statistics facade used by reports.""" 

39 

40 def summary(self) -> pl.DataFrame: 

41 """Full summary DataFrame (one row per metric, one column per asset).""" 

42 ... 

43 

44 

45@runtime_checkable 

46class DataLike(Protocol): # pragma: no cover 

47 """Authoritative structural interface for Data consumers. 

48 

49 Union of the members required by the stats mixins, plots, reports, and 

50 utils — annotating against the superset is harmless for consumers that 

51 use only part of it, and keeps a single definition. 

52 """ 

53 

54 @property 

55 def returns(self) -> pl.DataFrame: 

56 """Return DataFrame (asset columns only, no benchmark or date).""" 

57 ... 

58 

59 @property 

60 def index(self) -> pl.DataFrame: 

61 """Date / time index DataFrame.""" 

62 ... 

63 

64 @property 

65 def benchmark(self) -> pl.DataFrame | None: 

66 """Benchmark DataFrame, or None when no benchmark was provided.""" 

67 ... 

68 

69 @property 

70 def all(self) -> pl.DataFrame: 

71 """Combined DataFrame of date index, return, and benchmark columns.""" 

72 ... 

73 

74 @property 

75 def assets(self) -> list[str]: 

76 """Names of the asset return columns.""" 

77 ... 

78 

79 @property 

80 def date_col(self) -> list[str]: 

81 """Column names used as the date/time index.""" 

82 ... 

83 

84 @property 

85 def stats(self) -> StatsLike: 

86 """Statistics facade used by reports.""" 

87 ... 

88 

89 @property 

90 def _periods_per_year(self) -> float: 

91 """Estimated number of return periods per calendar year.""" 

92 ... 

93 

94 def items(self) -> Iterator[tuple[str, pl.Series]]: 

95 """Iterate over (asset_name, returns_series) pairs.""" 

96 ...