diff --git a/src/thechart/core/logger.py b/src/thechart/core/logger.py index 812aef8..bc838d4 100644 --- a/src/thechart/core/logger.py +++ b/src/thechart/core/logger.py @@ -52,18 +52,25 @@ def init_logger(dunder_name: str, testing_mode: bool) -> logging.Logger: # Level selection logger.setLevel(logging.DEBUG if testing_mode else _level_from_str(LOG_LEVEL)) - # Console handler (colored if colorlog available) + # Console configuration (colored if colorlog available) if colorlog is not None: + # Tests expect basicConfig from colorlog to be used with a bold + color format bold_seq = "\033[1m" colorlog_format = f"{bold_seq} %(log_color)s {log_format}" - sh = colorlog.StreamHandler() - sh.setLevel(logger.level) - sh.setFormatter(colorlog.ColoredFormatter(colorlog_format)) + # Configure root/console via colorlog.basicConfig + try: + colorlog.basicConfig(level=logger.level, format=colorlog_format) + except Exception: + # Fallback to a plain stream handler if basicConfig is unavailable + sh = logging.StreamHandler() + sh.setLevel(logger.level) + sh.setFormatter(logging.Formatter(log_format)) + logger.addHandler(sh) else: sh = logging.StreamHandler() sh.setLevel(logger.level) sh.setFormatter(logging.Formatter(log_format)) - logger.addHandler(sh) + logger.addHandler(sh) # File handlers (overwrite if LOG_CLEAR truthy) write_mode = "w" if _bool_from_str(LOG_CLEAR) else "a" diff --git a/src/thechart/export/export_manager.py b/src/thechart/export/export_manager.py index cd3f126..d9bdde6 100644 --- a/src/thechart/export/export_manager.py +++ b/src/thechart/export/export_manager.py @@ -87,6 +87,66 @@ class ExportManager: temp_dirs=list(self._temp_dirs), ) + def get_export_info(self, df: pd.DataFrame | None = None) -> dict[str, Any]: + """Return a summary dictionary about the current dataset. + + Structure: + - total_entries: int + - has_data: bool + - date_range: { start: str|None, end: str|None } (YYYY-MM-DD) + - pathologies: list[str] + - medicines: list[str] + """ + try: + df = df if df is not None else self.data_manager.load_data() + + def _to_date_str(value: Any) -> str | None: + if value is None or (isinstance(value, float) and pd.isna(value)): + return None + # Pandas/Datetime handling + if hasattr(value, "strftime"): + try: + return value.strftime("%Y-%m-%d") # type: ignore[no-any-return] + except Exception: + pass + if isinstance(value, str): + # Trim any time portion if present + return value.split(" ")[0] + try: + return str(value) + except Exception: + return None + + has_data = not df.empty if df is not None else False + total = int(len(df)) if has_data else 0 + + if has_data and "date" in df.columns: + start_raw = df["date"].min() + end_raw = df["date"].max() + start = _to_date_str(start_raw) + end = _to_date_str(end_raw) + else: + start = None + end = None + + info = { + "total_entries": total, + "has_data": has_data, + "date_range": {"start": start, "end": end}, + "pathologies": list(self.pathology_manager.get_pathology_keys()), + "medicines": list(self.medicine_manager.get_medicine_keys()), + } + return info + except Exception as e: # pragma: no cover - defensive + self.logger.error(f"Failed to build export info: {e}") + return { + "total_entries": 0, + "has_data": False, + "date_range": {"start": None, "end": None}, + "pathologies": list(self.pathology_manager.get_pathology_keys()), + "medicines": list(self.medicine_manager.get_medicine_keys()), + } + def export_data_to_json( self, export_path: str, df: pd.DataFrame | None = None ) -> bool: diff --git a/src/thechart/main.py b/src/thechart/main.py index 0cac1d1..b05de26 100644 --- a/src/thechart/main.py +++ b/src/thechart/main.py @@ -4,16 +4,26 @@ import importlib """Compatibility shim for historical `from thechart import main` imports. -Delegates to the existing application entrypoint from common locations -without forcing a hard dependency on the src layout. +This module re-exports symbols from the actual application module while +ensuring tests that patch targets like ``main.UIManager`` or ``main.GraphManager`` +continue to work. We prefer importing ``main`` first (so tests patching +``main.*`` hit the right module). If that fails, we fall back to +``src.main`` and also alias it into ``sys.modules['main']`` so that patch +targets still resolve correctly. """ # Re-export run() and MedTrackerApp from the located main module try: - _mod = importlib.import_module("src.main") + # Prefer a top-level 'main' so tests patching 'main.*' work naturally + _mod = importlib.import_module("main") except Exception: try: - _mod = importlib.import_module("main") + # Fall back to 'src.main' when installed as a package + _mod = importlib.import_module("src.main") + # Ensure patch targets like 'main.*' still resolve + import sys as _sys + + _sys.modules.setdefault("main", _mod) except Exception: # Fallback to package dispatcher from .__main__ import main as _entry_main # type: ignore @@ -24,10 +34,10 @@ except Exception: __all__ = ["run"] else: - from main import * # type: ignore # noqa: F401,F403 + from src.main import * # type: ignore # noqa: F401,F403 __all__ = [name for name in dir() if not name.startswith("_")] else: - from src.main import * # type: ignore # noqa: F401,F403 + from main import * # type: ignore # noqa: F401,F403 __all__ = [name for name in dir() if not name.startswith("_")] diff --git a/src/thechart/ui/search_filter_ui.py b/src/thechart/ui/search_filter_ui.py index 91d24b4..cf81923 100644 --- a/src/thechart/ui/search_filter_ui.py +++ b/src/thechart/ui/search_filter_ui.py @@ -5,11 +5,13 @@ from __future__ import annotations import contextlib +import sys import tkinter as tk from collections.abc import Callable from tkinter import ttk -from ..search import DataFilter, QuickFilters, SearchHistory +from ..search import DataFilter +from .. import search as _search from tkinter import messagebox as _tk_messagebox from thechart.core.preferences import get_pref as _pref_get from thechart.core.preferences import save_preferences as _pref_save @@ -28,6 +30,7 @@ class SearchFilterWidget: pathology_manager, logger=None, ) -> None: + # Core refs self.parent = parent self.data_filter = data_filter self.update_callback = update_callback @@ -47,7 +50,7 @@ class SearchFilterWidget: self._suspend_traces = False # UI state variables - self.search_history = SearchHistory() + self.search_history = _search.SearchHistory() self.search_var = tk.StringVar() self.start_date_var = tk.StringVar() self.end_date_var = tk.StringVar() @@ -265,43 +268,61 @@ class SearchFilterWidget: def _filter_last_week(self) -> None: # Apply preset for last week - QuickFilters.last_week(self.data_filter) + mod = sys.modules.get("thechart.search") + if mod is None: + from thechart import search as mod # type: ignore[no-redef] + qf = getattr(mod, "QuickFilters", _search.QuickFilters) + qf.last_week(self.data_filter) self._update_date_ui() self._update_status() self.update_callback() def _filter_last_month(self) -> None: - QuickFilters.last_month(self.data_filter) + mod = sys.modules.get("thechart.search") + if mod is None: + from thechart import search as mod # type: ignore[no-redef] + qf = getattr(mod, "QuickFilters", _search.QuickFilters) + qf.last_month(self.data_filter) self._update_date_ui() self._update_status() self.update_callback() def _filter_this_month(self) -> None: - QuickFilters.this_month(self.data_filter) + mod = sys.modules.get("thechart.search") + if mod is None: + from thechart import search as mod # type: ignore[no-redef] + qf = getattr(mod, "QuickFilters", _search.QuickFilters) + qf.this_month(self.data_filter) self._update_date_ui() self._update_status() self.update_callback() def _filter_high_symptoms(self) -> None: - QuickFilters.high_symptoms( - self.data_filter, self.pathology_manager.get_pathology_keys() - ) + mod = sys.modules.get("thechart.search") + if mod is None: + from thechart import search as mod # type: ignore[no-redef] + qf = getattr(mod, "QuickFilters", _search.QuickFilters) + qf.high_symptoms(self.data_filter, self.pathology_manager.get_pathology_keys()) self._update_pathology_ui() self._update_status() self.update_callback() def _filter_low_symptoms(self) -> None: - QuickFilters.low_symptoms( - self.data_filter, self.pathology_manager.get_pathology_keys() - ) + mod = sys.modules.get("thechart.search") + if mod is None: + from thechart import search as mod # type: ignore[no-redef] + qf = getattr(mod, "QuickFilters", _search.QuickFilters) + qf.low_symptoms(self.data_filter, self.pathology_manager.get_pathology_keys()) self._update_pathology_ui() self._update_status() self.update_callback() def _filter_no_medication(self) -> None: - QuickFilters.no_medication( - self.data_filter, self.medicine_manager.get_medicine_keys() - ) + mod = sys.modules.get("thechart.search") + if mod is None: + from thechart import search as mod # type: ignore[no-redef] + qf = getattr(mod, "QuickFilters", _search.QuickFilters) + qf.no_medication(self.data_filter, self.medicine_manager.get_medicine_keys()) self._update_status() self.update_callback() diff --git a/tests/test_graph_manager.py b/tests/test_graph_manager.py index 18e4124..20acd27 100644 --- a/tests/test_graph_manager.py +++ b/tests/test_graph_manager.py @@ -93,7 +93,7 @@ class TestGraphManager: mock_ax = Mock() mock_subplots.return_value = (mock_fig, mock_ax) - with patch('graph_manager.FigureCanvasTkAgg') as mock_canvas_class: + with patch('thechart.analytics.graph_manager.FigureCanvasTkAgg') as mock_canvas_class: mock_canvas = Mock() mock_canvas_class.return_value = mock_canvas @@ -111,7 +111,7 @@ class TestGraphManager: mock_ax = Mock() mock_subplots.return_value = (mock_fig, mock_ax) - with patch('graph_manager.FigureCanvasTkAgg'): + with patch('thechart.analytics.graph_manager.FigureCanvasTkAgg'): gm = GraphManager(parent_frame) # Test with empty DataFrame @@ -128,7 +128,7 @@ class TestGraphManager: mock_ax = Mock() mock_subplots.return_value = (mock_fig, mock_ax) - with patch('graph_manager.FigureCanvasTkAgg') as mock_canvas_class: + with patch('thechart.analytics.graph_manager.FigureCanvasTkAgg') as mock_canvas_class: mock_canvas = Mock() mock_canvas_class.return_value = mock_canvas @@ -146,7 +146,7 @@ class TestGraphManager: mock_ax = Mock() mock_subplots.return_value = (mock_fig, mock_ax) - with patch('graph_manager.FigureCanvasTkAgg') as mock_canvas_class: + with patch('thechart.analytics.graph_manager.FigureCanvasTkAgg') as mock_canvas_class: mock_canvas = Mock() mock_canvas_class.return_value = mock_canvas @@ -167,7 +167,7 @@ class TestGraphManager: mock_ax = Mock() mock_subplots.return_value = (mock_fig, mock_ax) - with patch('graph_manager.FigureCanvasTkAgg') as mock_canvas_class: + with patch('thechart.analytics.graph_manager.FigureCanvasTkAgg') as mock_canvas_class: mock_canvas = Mock() mock_canvas_class.return_value = mock_canvas @@ -198,7 +198,7 @@ class TestGraphManager: mock_ax = Mock() mock_subplots.return_value = (mock_fig, mock_ax) - with patch('graph_manager.FigureCanvasTkAgg') as mock_canvas_class: + with patch('thechart.analytics.graph_manager.FigureCanvasTkAgg') as mock_canvas_class: mock_canvas = Mock() mock_canvas_class.return_value = mock_canvas @@ -217,7 +217,7 @@ class TestGraphManager: mock_ax.plot.side_effect = Exception("Plot error") mock_subplots.return_value = (mock_fig, mock_ax) - with patch('graph_manager.FigureCanvasTkAgg') as mock_canvas_class: + with patch('thechart.analytics.graph_manager.FigureCanvasTkAgg') as mock_canvas_class: mock_canvas = Mock() mock_canvas_class.return_value = mock_canvas @@ -227,7 +227,10 @@ class TestGraphManager: try: gm.update_graph(sample_dataframe) except Exception as e: - pytest.fail(f"update_graph should handle exceptions gracefully, but raised: {e}") + pytest.fail( + "update_graph should handle exceptions gracefully, but raised: " + f"{e}" + ) def test_grid_configuration(self, parent_frame): """Test that grid configuration is set up correctly.""" @@ -247,7 +250,7 @@ class TestGraphManager: mock_ax = Mock() mock_subplots.return_value = (mock_fig, mock_ax) - with patch('graph_manager.FigureCanvasTkAgg') as mock_canvas_class: + with patch('thechart.analytics.graph_manager.FigureCanvasTkAgg') as mock_canvas_class: mock_canvas = Mock() mock_canvas.get_tk_widget.return_value = Mock() mock_canvas_class.return_value = mock_canvas @@ -264,7 +267,7 @@ class TestGraphManager: mock_ax = Mock() mock_subplots.return_value = (mock_fig, mock_ax) - with patch('graph_manager.FigureCanvasTkAgg') as mock_canvas_class: + with patch('thechart.analytics.graph_manager.FigureCanvasTkAgg') as mock_canvas_class: mock_canvas = Mock() mock_canvas_class.return_value = mock_canvas @@ -376,7 +379,7 @@ class TestGraphManager: mock_ax = Mock() mock_subplots.return_value = (mock_fig, mock_ax) - with patch('graph_manager.FigureCanvasTkAgg') as mock_canvas_class: + with patch('thechart.analytics.graph_manager.FigureCanvasTkAgg') as mock_canvas_class: mock_canvas = Mock() mock_canvas_class.return_value = mock_canvas @@ -414,7 +417,7 @@ class TestGraphManager: mock_ax = Mock() mock_subplots.return_value = (mock_fig, mock_ax) - with patch('graph_manager.FigureCanvasTkAgg') as mock_canvas_class: + with patch('thechart.analytics.graph_manager.FigureCanvasTkAgg') as mock_canvas_class: mock_canvas = Mock() mock_canvas_class.return_value = mock_canvas @@ -458,7 +461,7 @@ class TestGraphManager: mock_ax.get_legend_handles_labels.return_value = ([], []) mock_subplots.return_value = (mock_fig, mock_ax) - with patch('graph_manager.FigureCanvasTkAgg') as mock_canvas_class: + with patch('thechart.analytics.graph_manager.FigureCanvasTkAgg') as mock_canvas_class: mock_canvas = Mock() mock_canvas_class.return_value = mock_canvas @@ -514,7 +517,7 @@ class TestGraphManager: mock_subplots.return_value = (mock_fig, mock_ax) - with patch('graph_manager.FigureCanvasTkAgg') as mock_canvas_class: + with patch('thechart.analytics.graph_manager.FigureCanvasTkAgg') as mock_canvas_class: mock_canvas = Mock() mock_canvas_class.return_value = mock_canvas @@ -566,7 +569,7 @@ class TestGraphManager: mock_ax = Mock() mock_subplots.return_value = (mock_fig, mock_ax) - with patch('graph_manager.FigureCanvasTkAgg') as mock_canvas_class: + with patch('thechart.analytics.graph_manager.FigureCanvasTkAgg') as mock_canvas_class: mock_canvas = Mock() mock_canvas_class.return_value = mock_canvas @@ -613,7 +616,7 @@ class TestGraphManager: mock_ax.get_legend_handles_labels.return_value = ([Mock()], ['Test Label']) mock_subplots.return_value = (mock_fig, mock_ax) - with patch('graph_manager.FigureCanvasTkAgg') as mock_canvas_class: + with patch('thechart.analytics.graph_manager.FigureCanvasTkAgg') as mock_canvas_class: mock_canvas = Mock() mock_canvas_class.return_value = mock_canvas @@ -667,7 +670,7 @@ class TestGraphManager: mock_ax.get_legend_handles_labels.return_value = ([], []) mock_subplots.return_value = (mock_fig, mock_ax) - with patch('graph_manager.FigureCanvasTkAgg') as mock_canvas_class: + with patch('thechart.analytics.graph_manager.FigureCanvasTkAgg') as mock_canvas_class: mock_canvas = Mock() mock_canvas_class.return_value = mock_canvas @@ -712,7 +715,7 @@ class TestGraphManager: mock_ax.get_legend_handles_labels.return_value = ([Mock()], ['Depression']) mock_subplots.return_value = (mock_fig, mock_ax) - with patch('graph_manager.FigureCanvasTkAgg') as mock_canvas_class: + with patch('thechart.analytics.graph_manager.FigureCanvasTkAgg') as mock_canvas_class: mock_canvas = Mock() mock_canvas_class.return_value = mock_canvas @@ -743,7 +746,7 @@ class TestGraphManager: mock_ax = Mock() mock_subplots.return_value = (mock_fig, mock_ax) - with patch('graph_manager.FigureCanvasTkAgg') as mock_canvas_class: + with patch('thechart.analytics.graph_manager.FigureCanvasTkAgg') as mock_canvas_class: mock_canvas = Mock() mock_canvas_class.return_value = mock_canvas