From bd598d63f9bcfdebba4e7f33034b40e648e6a5dd Mon Sep 17 00:00:00 2001 From: William Valentin Date: Fri, 8 Aug 2025 18:33:51 -0700 Subject: [PATCH] feat: Add package structure for TheChart, implement entry points for console scripts and module execution --- README.md | 5 +++- pyproject.toml | 15 ++++++++++ src/main.py | 13 ++++++-- src/thechart/__init__.py | 26 ++++++++++++++++ src/thechart/__main__.py | 64 ++++++++++++++++++++++++++++++++++++++++ uv.lock | 4 +-- 6 files changed, 122 insertions(+), 5 deletions(-) create mode 100644 src/thechart/__init__.py create mode 100644 src/thechart/__main__.py diff --git a/README.md b/README.md index 19eedbc..fb1c777 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ make install # Run the application make run +# Or use the package entry point +python -m thechart # Run tests (consolidated test suite) make test @@ -96,8 +98,9 @@ python -m venv .venv source .venv/bin/activate # On Windows: .venv\Scripts\activate pip install -r requirements.txt -# Run the application +# Run the application (any of the following) python src/main.py +python -m thechart ``` ## 🧪 Testing diff --git a/pyproject.toml b/pyproject.toml index e47e603..2b4ba42 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,9 @@ dependencies = [ "ttkthemes>=3.2.2", ] +[project.scripts] +thechart = "thechart.__main__:main" + [dependency-groups] dev = [ "pre-commit>=4.2.0", @@ -104,3 +107,15 @@ indent-style = "space" # Use spaces for indentation [tool.ruff.lint.pycodestyle] max-line-length = 88 + +[build-system] +requires = ["setuptools>=68", "wheel"] +build-backend = "setuptools.build_meta" + +[tool.setuptools] +package-dir = { "" = "src" } + +[tool.setuptools.packages.find] +where = ["src"] +include = ["thechart*"] +exclude = ["tests*"] diff --git a/src/main.py b/src/main.py index 3f97b39..040ad8e 100644 --- a/src/main.py +++ b/src/main.py @@ -1675,7 +1675,16 @@ Use Ctrl+S to save entries and Ctrl+Q to quit.""" logger.error(f"Error updating tree efficiently: {e}") -if __name__ == "__main__": +def run() -> None: + """Start the TheChart Tkinter application. + + Provided to support `python -m thechart` and a console-script entry point + without changing import-time behavior (tests patch symbols on `main`). + """ root: tk.Tk = tk.Tk() - app: MedTrackerApp = MedTrackerApp(root) + _ = MedTrackerApp(root) root.mainloop() + + +if __name__ == "__main__": + run() diff --git a/src/thechart/__init__.py b/src/thechart/__init__.py new file mode 100644 index 0000000..8cc1563 --- /dev/null +++ b/src/thechart/__init__.py @@ -0,0 +1,26 @@ +"""TheChart package. + +This package provides the main application and components for the +TheChart (medication tracker) desktop app. + +Notes +----- +- This package layer is introduced to follow Python packaging best + practices while keeping backward compatibility with existing + imports used in tests (e.g., ``src.*``). The original modules under + ``src/`` remain available; this package enables ``python -m thechart`` + and console-script usage. +""" + +from __future__ import annotations + +from importlib import metadata as _metadata + +try: # Prefer installed package version if available + __version__ = _metadata.version("thechart") +except Exception: # Fallback in editable/dev mode + __version__ = "0.0.0.dev" + +__all__ = [ + "__version__", +] diff --git a/src/thechart/__main__.py b/src/thechart/__main__.py new file mode 100644 index 0000000..a7426ed --- /dev/null +++ b/src/thechart/__main__.py @@ -0,0 +1,64 @@ +"""Module entry-point for `python -m thechart` and console scripts. + +This dynamically locates and runs the existing application start-up code +without imposing a hard packaging dependency on the development layout. +""" + +from __future__ import annotations + +import importlib +import os +import tkinter as tk + + +def _load_main_module(): + """Load the existing main module from common locations. + + Tries in order: + - importlib.import_module("src.main") + - importlib.import_module("main") + - Load from file path next to this package (.. / main.py) + """ + + try: + return importlib.import_module("src.main") + except Exception: + pass + try: + return importlib.import_module("main") + except Exception: + pass + + # File-based fallback for src layout in editable/dev mode + try: + from importlib.machinery import SourceFileLoader + + here = os.path.dirname(__file__) + candidate = os.path.abspath(os.path.join(here, os.pardir, "main.py")) + if os.path.exists(candidate): + return SourceFileLoader("main", candidate).load_module() # type: ignore[deprecated-import] + except Exception: + pass + raise ImportError("Could not locate application main module") + + +def main() -> None: + """Start the TheChart application.""" + mod = _load_main_module() + # Prefer a run() entry if available + try: + run_fn = mod.run # type: ignore[attr-defined] + except AttributeError: + run_fn = None # type: ignore[assignment] + if callable(run_fn): # type: ignore[truthy-function] + run_fn() + return + # Very old fallback: directly instantiate if run() is absent + root = tk.Tk() + app_cls = mod.MedTrackerApp # type: ignore[attr-defined] + _ = app_cls(root) + root.mainloop() + + +if __name__ == "__main__": # pragma: no cover - simple dispatcher + main() diff --git a/uv.lock b/uv.lock index f9d7e07..9561ed6 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.13" [[package]] @@ -758,7 +758,7 @@ wheels = [ [[package]] name = "thechart" version = "1.14.9" -source = { virtual = "." } +source = { editable = "." } dependencies = [ { name = "colorlog" }, { name = "dotenv" },