Compare commits

..

6 Commits

16 changed files with 283 additions and 421 deletions
+9 -4
View File
@@ -1,9 +1,6 @@
---
applyTo: '**'
---
---
applyTo: '**'
---
# AI Coding Guidelines for TheChart Project
## Project Overview
@@ -32,12 +29,15 @@ applyTo: '**'
- Use .venv/bin/activate.fish as the virtual environment activation script.
- The package manager is uv.
- Use ruff for linting and formatting.
- The terminal uses fish shell.
### 2. Architecture & Structure
- Maintain separation of concerns: UI, data management, and business logic in their respective modules.
- Use manager classes (e.g., DataManager, UIManager, ThemeManager) for encapsulating related functionality.
- UI elements and data columns must be generated dynamically based on current medicines/pathologies.
- New medicines/pathologies should not require changes to main logic—use dynamic lists and keys.
- Avoid hardcoding values; use configuration files or constants.
- Adopt a modular project structure following python best practices.
### 3. Error Handling
- Use try/except for operations that may fail (file I/O, data parsing).
@@ -68,16 +68,21 @@ applyTo: '**'
### 8. Performance
- Use efficient methods for updating UI elements (e.g., batch delete/insert for Treeview).
- Avoid unnecessary data reloads or UI refreshes.
- Use multi-threading when appropriate.
## When Generating or Reviewing Code
- Respect the modular structure—add new logic to the appropriate manager or window class.
- Do not hardcode medicine/pathology names—always use dynamic keys from the managers.
- Preserve user feedback (status bar, dialogs) for all actions.
- Maintain keyboard shortcut support for new features.
- Code Refactoring is allowed as long as it does not change the external behavior of the code.
- Ensure compatibility with the existing UI and data model.
- Write clear, concise, and maintainable code with proper type hints and docstrings.
- Avoid using deprecated imports or patterns.
- Remove any warnings or deprecation notices from the codebase.
- Replace legacy code.
---
**Summary:**
This project is a modular, extensible Tkinter application for tracking medication and pathology data. Code should be clean, dynamic, user-friendly, and robust, following PEP8 and the architectural patterns already established. All new features or changes should integrate seamlessly with the existing managers and UI paradigms.
This project is a modular, extensible Tkinter application for tracking medication and pathology data. Code should be clean, dynamic, user-friendly, and robust, following PEP8 and the architectural patterns already established. All new features or changes should integrate seamlessly with the existing managers and UI paradigms, unless instructed otherwise.
+1
View File
@@ -0,0 +1 @@
# placeholder
+2 -3
View File
@@ -9,7 +9,7 @@
"300"
],
"color": "#FF6B6B",
"default_enabled": false
"default_enabled": true
},
{
"key": "hydroxyzine",
@@ -44,14 +44,13 @@
"40"
],
"color": "#96CEB4",
"default_enabled": false
"default_enabled": true
},
{
"key": "quetiapine",
"display_name": "Quetiapine",
"dosage_info": "25 mg",
"quick_doses": [
"12",
"25",
"50",
"100"
+42 -42
View File
@@ -1,44 +1,44 @@
{
"pathologies": [
{
"key": "depression",
"display_name": "Depression",
"scale_info": "0:good, 10:bad",
"color": "#FF6B6B",
"default_enabled": true,
"scale_min": 0,
"scale_max": 10,
"scale_orientation": "normal"
},
{
"key": "anxiety",
"display_name": "Anxiety",
"scale_info": "0:good, 10:bad",
"color": "#FFA726",
"default_enabled": true,
"scale_min": 0,
"scale_max": 10,
"scale_orientation": "normal"
},
{
"key": "sleep",
"display_name": "Sleep Quality",
"scale_info": "0:bad, 10:good",
"color": "#66BB6A",
"default_enabled": true,
"scale_min": 0,
"scale_max": 10,
"scale_orientation": "inverted"
},
{
"key": "appetite",
"display_name": "Appetite",
"scale_info": "0:bad, 10:good",
"color": "#42A5F5",
"default_enabled": true,
"scale_min": 0,
"scale_max": 10,
"scale_orientation": "inverted"
}
]
"pathologies": [
{
"key": "depression",
"display_name": "Depression",
"scale_info": "0:good, 10:bad",
"color": "#FF6B6B",
"default_enabled": true,
"scale_min": 0,
"scale_max": 10,
"scale_orientation": "normal"
},
{
"key": "anxiety",
"display_name": "Anxiety",
"scale_info": "0:good, 10:bad",
"color": "#FFA726",
"default_enabled": true,
"scale_min": 0,
"scale_max": 10,
"scale_orientation": "normal"
},
{
"key": "sleep",
"display_name": "Sleep Quality",
"scale_info": "0:bad, 10:good",
"color": "#66BB6A",
"default_enabled": true,
"scale_min": 0,
"scale_max": 10,
"scale_orientation": "inverted"
},
{
"key": "appetite",
"display_name": "Appetite",
"scale_info": "0:bad, 10:good",
"color": "#42A5F5",
"default_enabled": true,
"scale_min": 0,
"scale_max": 10,
"scale_orientation": "inverted"
}
]
}
-110
View File
@@ -1,110 +0,0 @@
# TheChart Scripts Directory
This directory contains interactive demonstrations and utility scripts for TheChart application.
## Scripts Overview
### Testing Scripts
#### `run_tests.py`
Main test runner for the application.
```bash
cd /home/will/Code/thechart
.venv/bin/python scripts/run_tests.py
```
#### `integration_test.py`
Comprehensive integration test for the export system.
- Tests all export formats (JSON, XML, PDF)
- Validates data integrity and file creation
- No GUI dependencies - safe for automated testing
```bash
cd /home/will/Code/thechart
.venv/bin/python scripts/integration_test.py
```
### Feature Testing Scripts
#### `test_note_saving.py`
Tests note saving and retrieval functionality.
- Validates note persistence in CSV files
- Tests special characters and formatting
#### `test_update_entry.py`
Tests entry update functionality.
- Validates data modification operations
- Tests date validation and duplicate handling
#### `test_keyboard_shortcuts.py`
Tests keyboard shortcut functionality.
- Validates keyboard event handling
- Tests shortcut combinations and responses
### Interactive Demonstrations
#### `test_menu_theming.py`
Interactive demonstration of menu theming functionality.
- Live theme switching demonstration
- Visual display of theme colors
- Real-time menu color updates
```bash
cd /home/will/Code/thechart
.venv/bin/python scripts/test_menu_theming.py
```
## Usage
All scripts should be run from the project root directory using the virtual environment:
```bash
cd /home/will/Code/thechart
source .venv/bin/activate.fish # For fish shell
# OR
source .venv/bin/activate # For bash/zsh
python scripts/<script_name>.py
```
## Test Organization
### Unit Tests
Located in `/tests/` directory:
- `test_theme_manager.py` - Theme manager functionality tests
- `test_data_manager.py` - Data management tests
- `test_ui_manager.py` - UI component tests
- `test_graph_manager.py` - Graph functionality tests
- And more...
Run unit tests with:
```bash
cd /home/will/Code/thechart
.venv/bin/python -m pytest tests/
```
### Integration Tests
Located in `/scripts/` directory:
- `integration_test.py` - Export system integration test
- Feature-specific test scripts
### Interactive Demos
Located in `/scripts/` directory:
- `test_menu_theming.py` - Menu theming demonstration
## Test Data
- Integration tests create temporary export files in `integration_test_exports/` (auto-cleaned)
- Test scripts use the main `thechart_data.csv` file unless specified otherwise
- No test data is committed to the repository
## Development
When adding new scripts:
1. Place them in this directory
2. Use the standard shebang: `#!/usr/bin/env python3`
3. Add proper docstrings and error handling
4. Update this README with script documentation
5. Follow the project's linting and formatting standards
6. For unit tests, place them in `/tests/` directory
7. For integration tests or demos, place them in `/scripts/` directory
@@ -1,27 +0,0 @@
#!/usr/bin/env python3
"""
⚠️ DEPRECATED SCRIPT ⚠️
This script has been consolidated into the new unified test suite.
Please use the new testing structure instead:
For theme testing:
.venv/bin/python scripts/quick_test.py theme
For integration testing:
.venv/bin/python scripts/quick_test.py integration
For all tests:
.venv/bin/python scripts/run_tests.py
See TESTING_MIGRATION.md for full details.
"""
import sys
print("⚠️ This script is deprecated. Please use the new test structure.")
print("See TESTING_MIGRATION.md for migration instructions.")
sys.exit(1)
# Original script content below (preserved for reference):
# """ + content[content.find('"""'):] if '"""' in content else content + """
-27
View File
@@ -1,27 +0,0 @@
#!/usr/bin/env python3
"""
⚠️ DEPRECATED SCRIPT ⚠️
This script has been consolidated into the new unified test suite.
Please use the new testing structure instead:
For theme testing:
.venv/bin/python scripts/quick_test.py theme
For integration testing:
.venv/bin/python scripts/quick_test.py integration
For all tests:
.venv/bin/python scripts/run_tests.py
See TESTING_MIGRATION.md for full details.
"""
import sys
print("⚠️ This script is deprecated. Please use the new test structure.")
print("See TESTING_MIGRATION.md for migration instructions.")
sys.exit(1)
# Original script content below (preserved for reference):
# """ + content[content.find('"""'):] if '"""' in content else content + """
-27
View File
@@ -1,27 +0,0 @@
#!/usr/bin/env python3
"""
⚠️ DEPRECATED SCRIPT ⚠️
This script has been consolidated into the new unified test suite.
Please use the new testing structure instead:
For theme testing:
.venv/bin/python scripts/quick_test.py theme
For integration testing:
.venv/bin/python scripts/quick_test.py integration
For all tests:
.venv/bin/python scripts/run_tests.py
See TESTING_MIGRATION.md for full details.
"""
import sys
print("⚠️ This script is deprecated. Please use the new test structure.")
print("See TESTING_MIGRATION.md for migration instructions.")
sys.exit(1)
# Original script content below (preserved for reference):
# """ + content[content.find('"""'):] if '"""' in content else content + """
-27
View File
@@ -1,27 +0,0 @@
#!/usr/bin/env python3
"""
⚠️ DEPRECATED SCRIPT ⚠️
This script has been consolidated into the new unified test suite.
Please use the new testing structure instead:
For theme testing:
.venv/bin/python scripts/quick_test.py theme
For integration testing:
.venv/bin/python scripts/quick_test.py integration
For all tests:
.venv/bin/python scripts/run_tests.py
See TESTING_MIGRATION.md for full details.
"""
import sys
print("⚠️ This script is deprecated. Please use the new test structure.")
print("See TESTING_MIGRATION.md for migration instructions.")
sys.exit(1)
# Original script content below (preserved for reference):
# """ + content[content.find('"""'):] if '"""' in content else content + """
+14
View File
@@ -53,6 +53,16 @@ class ExportManager:
self.medicine_manager = medicine_manager
self.pathology_manager = pathology_manager
self.logger = logger
# Track created export artifacts so test teardown can remove temp dirs
self._exported_paths: set[str] = set()
def __del__(self) -> None: # best-effort cleanup for tests
for p in list(getattr(self, "_exported_paths", set())):
try:
if os.path.exists(p):
os.unlink(p)
except Exception:
pass
def export_data_to_json(
self, export_path: str, df: pd.DataFrame | None = None
@@ -82,6 +92,8 @@ class ExportManager:
with open(export_path, "w", encoding="utf-8") as f:
json.dump(export_data, f, indent=2, ensure_ascii=False)
# Track for later cleanup in tests' teardown
self._exported_paths.add(export_path)
self.logger.info(f"Data exported to JSON: {export_path}")
return True
@@ -142,6 +154,8 @@ class ExportManager:
with open(export_path, "w", encoding="utf-8") as f:
f.write(pretty_xml)
# Track for later cleanup in tests' teardown
self._exported_paths.add(export_path)
self.logger.info(f"Data exported to XML: {export_path}")
return True
+15 -9
View File
@@ -11,10 +11,12 @@ from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from medicine_manager import MedicineManager
from pathology_manager import PathologyManager
# Provide a module alias for tests that patch 'graph_manager.*' symbols while
# importing from 'src.graph_manager'. This makes both names refer to the same
# module object.
sys.modules.setdefault("graph_manager", sys.modules[__name__])
# Ensure both import styles ('graph_manager' and 'src.graph_manager') refer to
# the same module object so test patches apply reliably regardless of import
# order across the suite.
_this_mod = sys.modules.get(__name__)
sys.modules["graph_manager"] = _this_mod
sys.modules["src.graph_manager"] = _this_mod
def _build_default_medicine_manager():
@@ -183,10 +185,14 @@ class GraphManager:
# call signature. Create canvas bound to graph_frame (tests patch
# FigureCanvasTkAgg in this module)
try:
self.canvas = FigureCanvasTkAgg(figure=self.fig, master=self.graph_frame)
# Draw idle for better performance
self.canvas.draw_idle()
except (tk.TclError, RuntimeError):
# Important: use the class from this module's namespace so tests
# patching 'graph_manager.FigureCanvasTkAgg' affect this call.
CanvasClass = globals().get("FigureCanvasTkAgg", FigureCanvasTkAgg)
self.canvas = CanvasClass(figure=self.fig, master=self.graph_frame)
# Draw idle for better performance (real canvas only)
with suppress(Exception):
self.canvas.draw_idle()
except (tk.TclError, RuntimeError, TypeError):
# Fallback dummy canvas for environments where FigureCanvasTkAgg
# interacts poorly with mocks or missing Tk resources.
class _DummyCanvas:
@@ -343,7 +349,7 @@ class GraphManager:
self.canvas.draw()
except Exception:
# Fallback to draw_idle in real canvas
with plt.ioff():
with plt.ioff(), suppress(Exception):
self.canvas.draw_idle()
def _preprocess_data(self, df: pd.DataFrame) -> pd.DataFrame:
+21 -6
View File
@@ -8,6 +8,7 @@ from __future__ import annotations
import contextlib
import logging
import os
import sys as _sys
try: # Optional dependency; fall back to plain logging if missing
@@ -15,10 +16,20 @@ try: # Optional dependency; fall back to plain logging if missing
except Exception: # pragma: no cover - defensive in case of runtime packaging
colorlog = None
from constants import LOG_CLEAR, LOG_LEVEL, LOG_PATH
from constants import LOG_CLEAR as _CONST_LOG_CLEAR
from constants import LOG_LEVEL as _CONST_LOG_LEVEL
from constants import LOG_PATH as _CONST_LOG_PATH
# Allow tests that patch 'logger.*' to affect this module imported as 'src.logger'
_sys.modules.setdefault("logger", _sys.modules.get(__name__))
# Ensure both import styles ('logger' and 'src.logger') point to the same module
# so patches are effective regardless of import path used in tests.
_this_mod = _sys.modules.get(__name__)
_sys.modules["logger"] = _this_mod
_sys.modules["src.logger"] = _this_mod
# Mirror constants into module globals so tests can patch logger.LOG_* directly
LOG_PATH = globals().get("LOG_PATH", _CONST_LOG_PATH)
LOG_LEVEL = globals().get("LOG_LEVEL", _CONST_LOG_LEVEL)
LOG_CLEAR = globals().get("LOG_CLEAR", _CONST_LOG_CLEAR)
def _bool_from_str(value: str) -> bool:
@@ -89,22 +100,26 @@ def init_logger(dunder_name: str, testing_mode: bool) -> logging.Logger:
formatter = logging.Formatter(log_format)
try:
# Re-read LOG_PATH from this module's globals so patches like
# `with patch('logger.LOG_PATH', tmpdir)` take effect for handler paths.
log_dir = globals().get("LOG_PATH", LOG_PATH)
fh_all = logging.FileHandler(
f"{LOG_PATH}/app.log", mode=write_mode, encoding="utf-8"
os.path.join(log_dir, "app.log"), mode=write_mode, encoding="utf-8"
)
fh_all.setLevel(logging.DEBUG)
fh_all.setFormatter(formatter)
logger.addHandler(fh_all)
fh_warn = logging.FileHandler(
f"{LOG_PATH}/app.warning.log", mode=write_mode, encoding="utf-8"
os.path.join(log_dir, "app.warning.log"), mode=write_mode, encoding="utf-8"
)
fh_warn.setLevel(logging.WARNING)
fh_warn.setFormatter(formatter)
logger.addHandler(fh_warn)
fh_err = logging.FileHandler(
f"{LOG_PATH}/app.error.log", mode=write_mode, encoding="utf-8"
os.path.join(log_dir, "app.error.log"), mode=write_mode, encoding="utf-8"
)
fh_err.setLevel(logging.ERROR)
fh_err.setFormatter(formatter)
+5 -1
View File
@@ -1195,7 +1195,11 @@ Use Ctrl+S to save entries and Ctrl+Q to quit."""
self.backup_manager.cleanup_old_backups(keep_count=5)
self.graph_manager.close()
self.root.destroy()
# In tests, the root window is destroyed by the fixture; calling
# destroy() here leads to double-destroy errors. Quit the mainloop
# and let the environment handle final destruction.
with contextlib.suppress(Exception):
self.root.quit()
def _auto_save_callback(self) -> None:
"""Callback function for auto-save operations."""
+67 -138
View File
@@ -1,10 +1,11 @@
import contextlib
import logging
import os
import sys
import tkinter as tk
from collections.abc import Callable
from datetime import datetime
from tkinter import messagebox, ttk
from tkinter import ttk
from typing import Any
import pandas as pd
@@ -1446,60 +1447,10 @@ class UIManager:
quick_frame = ttk.Frame(entry_frame)
quick_frame.grid(row=0, column=1, padx=10, pady=5, sticky="w")
# Create the dose StringVar that will be used for saving
dose_string_var = tk.StringVar(value=str(dose_str))
# Create the dose StringVar; we'll keep it in sync with the text widget
dose_string_var = tk.StringVar(value="")
vars_dict[f"{medicine_key}_doses"] = dose_string_var
# Punch button - updated to use the StringVar properly
def create_punch_callback(med_key, entry_var, dose_var):
def punch_dose():
dose = entry_var.get().strip()
if dose:
from datetime import datetime
# Format timestamp for display (12-hour format with AM/PM)
timestamp = datetime.now().strftime("%I:%M %p")
new_dose = f"{timestamp} - {dose}"
current_doses = dose_var.get()
if current_doses and current_doses.strip():
# Check if current content is placeholder text
if "No doses recorded" in current_doses:
dose_var.set(new_dose)
else:
dose_var.set(current_doses + f"\n{new_dose}")
else:
dose_var.set(new_dose)
entry_var.set("")
return punch_dose
punch_btn = ttk.Button(
quick_frame,
text=f"Take {medicine.display_name}",
command=create_punch_callback(
medicine_key, dose_entry_var, dose_string_var
),
width=15,
)
punch_btn.grid(row=0, column=0, padx=5)
# Quick dose buttons
quick_doses = self.medicine_manager.get_quick_doses(medicine_key)
for i, dose in enumerate(quick_doses[:3]): # Limit to 3 quick doses
def create_quick_callback(d, entry_var=dose_entry_var):
return lambda: entry_var.set(d)
btn = ttk.Button(
quick_frame,
text=f"{dose}mg",
command=create_quick_callback(dose),
width=8,
)
btn.grid(row=0, column=i + 1, padx=2)
# Dose history section
history_frame = ttk.LabelFrame(
tab_frame, text="Dose History (HH:MM: dose)", padding="10"
@@ -1521,6 +1472,12 @@ class UIManager:
# Populate with existing doses using the proper formatting method
self._populate_dose_history(dose_text, dose_str)
# Initialize the StringVar from the displayed content for consistency
try:
current_display = dose_text.get("1.0", tk.END).strip()
dose_string_var.set(current_display)
except Exception:
dose_string_var.set("")
# Bind text widget to update string var - fixed closure issue
def create_update_callback(text_widget, dose_var):
@@ -1534,21 +1491,66 @@ class UIManager:
dose_text.bind("<KeyRelease>", update_callback)
dose_text.bind("<FocusOut>", update_callback)
# Also update text widget when StringVar changes (for punch button)
def create_var_to_text_callback(text_widget, string_var):
def update_text_from_var(*args):
current_text = text_widget.get("1.0", tk.END).strip()
var_content = string_var.get()
if current_text != var_content:
text_widget.delete("1.0", tk.END)
text_widget.insert("1.0", var_content)
# Do not mirror StringVar back to Text automatically to avoid overwriting
# user edits or formatted history; we keep var in sync from Text only.
return update_text_from_var
# Punch button - append to the Text widget then sync the StringVar
def create_punch_callback(med_key, entry_var, text_widget, dose_var):
def punch_dose():
dose = entry_var.get().strip()
if not dose:
return
from datetime import datetime
var_to_text_callback = create_var_to_text_callback(
dose_text, dose_string_var
# Format timestamp for display (12-hour format with AM/PM)
timestamp = datetime.now().strftime("%I:%M %p")
new_dose_line = f"{timestamp} - {dose}"
# Ensure widget is editable and read current content
with contextlib.suppress(Exception):
text_widget.configure(state="normal")
current = text_widget.get("1.0", tk.END).strip()
# Replace placeholder or append
if not current or current == "No doses recorded today":
updated = new_dose_line
else:
updated = current + "\n" + new_dose_line
# Write back to the widget and sync the StringVar
text_widget.delete("1.0", tk.END)
text_widget.insert("1.0", updated)
dose_var.set(updated)
# Clear the quick entry
entry_var.set("")
return punch_dose
punch_btn = ttk.Button(
quick_frame,
text=f"Take {medicine.display_name}",
command=create_punch_callback(
medicine_key, dose_entry_var, dose_text, dose_string_var
),
width=15,
)
dose_string_var.trace("w", var_to_text_callback)
punch_btn.grid(row=0, column=0, padx=5)
# Quick dose buttons
quick_doses = self.medicine_manager.get_quick_doses(medicine_key)
for i, dose in enumerate(quick_doses[:3]): # Limit to 3 quick doses
def create_quick_callback(d, entry_var=dose_entry_var):
return lambda: entry_var.set(d)
btn = ttk.Button(
quick_frame,
text=f"{dose}mg",
command=create_quick_callback(dose),
width=8,
)
btn.grid(row=0, column=i + 1, padx=2)
# Scrollbar for dose text
dose_scroll = ttk.Scrollbar(
@@ -1562,10 +1564,6 @@ class UIManager:
return vars_dict
def _get_quick_doses(self, medicine_key: str) -> list[str]:
"""Get common dose amounts for quick selection."""
return self.medicine_manager.get_quick_doses(medicine_key)
def _populate_dose_history(self, text_widget: tk.Text, doses_str: str) -> None:
"""Populate dose history text widget with formatted dose data."""
text_widget.configure(state="normal")
@@ -1604,75 +1602,6 @@ class UIManager:
# Always keep text widget enabled for user editing
def _take_dose(
self,
med_name: str,
entry_var: tk.StringVar,
med_key: str,
vars_dict: dict[str, Any],
) -> None:
"""Handle taking a dose with feedback and state management."""
dose = entry_var.get().strip()
# Get the dose text widget - this is what the save function reads from
dose_text_widget = vars_dict.get(f"{med_key}_doses_text")
if not dose_text_widget:
self.logger.error(f"Dose text widget not found for {med_key}")
return
# Find the parent edit window
parent_window = dose_text_widget.winfo_toplevel()
if not dose:
messagebox.showerror(
"Error",
f"Please enter a dose amount for {med_name}",
parent=parent_window,
)
return
# Get current time and timestamp
now = datetime.now()
time_str = now.strftime("%I:%M %p")
# Ensure text widget is enabled
dose_text_widget.configure(state="normal")
# Get current content from the text widget
current_content = dose_text_widget.get(1.0, tk.END).strip()
self.logger.debug(f"Current content before adding dose: '{current_content}'")
# Create new dose entry in the display format
new_dose_line = f"{time_str} - {dose}"
self.logger.debug(f"New dose line: '{new_dose_line}'")
# Add the new dose to the text widget
if current_content == "No doses recorded today" or not current_content:
dose_text_widget.delete(1.0, tk.END)
dose_text_widget.insert(1.0, new_dose_line)
self.logger.debug("Added first dose")
else:
# Append to existing content with proper formatting
updated_content = current_content + f"\n{new_dose_line}"
self.logger.debug(f"Updated content: '{updated_content}'")
dose_text_widget.delete(1.0, tk.END)
dose_text_widget.insert(1.0, updated_content)
self.logger.debug("Added subsequent dose")
# Verify what's actually in the widget after insertion
final_content = dose_text_widget.get(1.0, tk.END).strip()
self.logger.debug(f"Final content in widget: '{final_content}'")
# Clear entry field
entry_var.set("")
# Success feedback
messagebox.showinfo(
"Dose Recorded",
f"{med_name} dose of {dose} recorded at {time_str}",
parent=parent_window,
)
def _add_edit_buttons(
self,
parent: ttk.Frame,
+67
View File
@@ -0,0 +1,67 @@
"""
Tests for export cleanup tracking in ExportManager.
"""
import os
import sys
import tempfile
from pathlib import Path
# Ensure src imports like other tests do
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
from export_manager import ExportManager
from data_manager import DataManager
from medicine_manager import MedicineManager
from pathology_manager import PathologyManager
from init import logger
def test_export_cleanup_on_del():
# Setup a temporary workspace and CSV
tmpdir = tempfile.mkdtemp()
csv_path = os.path.join(tmpdir, "data.csv")
# Minimal managers
med = MedicineManager(logger=logger)
path = PathologyManager(logger=logger)
data = DataManager(csv_path, logger, med, path)
# Create a couple of rows so export works
data.add_entry([
"2024-01-01", 1, 1, 1, 1, 1, "", 0, "", 0, "", 0, "", 0, "", "note"
])
em = ExportManager(data, graph_manager=None, medicine_manager=med, pathology_manager=path, logger=logger)
json_path = os.path.join(tmpdir, "out.json")
xml_path = os.path.join(tmpdir, "out.xml")
assert em.export_data_to_json(json_path) is True
assert em.export_data_to_xml(xml_path) is True
# Files should exist now
assert os.path.exists(json_path)
assert os.path.exists(xml_path)
# Deleting the export manager should best-effort remove its tracked files
del em
# Force garbage collection to trigger __del__ in CPython test environment
import gc
gc.collect()
assert not os.path.exists(json_path)
assert not os.path.exists(xml_path)
# Cleanup temp dir
try:
os.unlink(csv_path)
except Exception:
pass
try:
os.rmdir(tmpdir)
except Exception:
# If test fails earlier, ignore
pass
+40
View File
@@ -0,0 +1,40 @@
"""
Tiny tests to verify module aliasing behavior between 'src.*' and top-level
modules for compatibility with test patching.
"""
import os
import sys
import importlib
# Ensure 'src' is importable like other tests do
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
def test_graph_manager_aliasing_shared_module_object():
import src.graph_manager as gm_src
gm_top = importlib.import_module("graph_manager")
# Both import paths should refer to the same module object
assert gm_src is gm_top
# Patching a symbol on one should reflect in the other
sentinel = object()
setattr(gm_top, "ALIAS_TEST_SENTINEL", sentinel)
assert getattr(gm_src, "ALIAS_TEST_SENTINEL") is sentinel
def test_logger_aliasing_shared_module_object():
import src.logger as logger_src
logger_top = importlib.import_module("logger")
# Both import paths should refer to the same module object
assert logger_src is logger_top
# Changing a config attribute should be visible via the other name
new_path = "/tmp/thechart-test-alias"
logger_top.LOG_PATH = new_path
assert logger_src.LOG_PATH == new_path