From 13ffa44ba6de85dfc84f4a411ce148a7bdb94bb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20C=C3=B4t=C3=A9?= Date: Mon, 23 Feb 2026 09:47:12 -0500 Subject: [PATCH 1/8] Modernize project structure to current Python packaging standards. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit High priority: - Fix version mismatch: __version__ now read from importlib.metadata instead of hardcoded "0.9.12" - Remove duplicate CellEntry import in __init__.py - Update setuptools constraint from <69 to >=70 - Unpin matplotlib (==3.9.2 → >=3.9), lowercase requests, remove opencv-python and raytracing from core deps - Add [project.optional-dependencies]: video (opencv-python), optics (raytracing) Medium priority: - Add requires-python = ">=3.11" (StrEnum requires 3.11+) - Fix classifiers: remove wrong Build Tools topic, add Science/Research, correct Environment and License entries - Add [project.urls] (Homepage, Repository, Bug Tracker) - Replace os.system() with subprocess.run(cwd=) in __main__.py; wrap CLI logic in main() - Add GitHub Actions CI workflow (Ubuntu/macOS/Windows × Python 3.11–3.13) - Add [tool.ruff] and [tool.pytest.ini_options] to pyproject.toml Low priority: - Remove vestigial stdlib re-exports from __init__.py (partial, platform, time, signal, etc.) - Add __all__ to __init__.py to define the explicit public API - Replace wildcard import in __main__.py with explicit `from mytk import Bindable` - Remove sys.path hack from mytk/tests/envtest.py (replaced by editable install) - Remove sys.path hack from docs/source/conf.py; switch theme to furo - Add docs optional dependency (furo, sphinx) Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/tests.yml | 40 ++++++++++ docs/source/conf.py | 10 +-- mytk/__init__.py | 76 ++++++++++++++---- mytk/__main__.py | 151 +++++++++++++++++++++++------------- mytk/tests/envtest.py | 11 --- pyproject.toml | 38 +++++++-- requirements.txt | 6 +- 7 files changed, 234 insertions(+), 98 deletions(-) create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..4a56c39 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,40 @@ +name: Tests + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + python-version: ["3.11", "3.12", "3.13"] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e . + + - name: Install Tk and virtual display (Linux only) + if: runner.os == 'Linux' + run: | + sudo apt-get install -y python3-tk xvfb + Xvfb :99 -screen 0 1024x768x24 & + + - name: Run tests + run: python -m unittest discover -s mytk/tests + env: + DISPLAY: ":99" diff --git a/docs/source/conf.py b/docs/source/conf.py index fd343df..af41ed7 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -30,12 +30,6 @@ autosummary_generate = True -html_theme = "sphinx_rtd_theme" +html_theme = "furo" html_static_path = ["_static"] - -import os -import sys - -sys.path.insert( - 0, os.path.abspath("../../mytk") -) # or your actual source folder +# mytk must be installed (pip install -e .) for autodoc to find the package. diff --git a/mytk/__init__.py b/mytk/__init__.py index 7d3703a..ecc3a61 100644 --- a/mytk/__init__.py +++ b/mytk/__init__.py @@ -1,18 +1,8 @@ +# Re-export tkinter so users can do `from mytk import *` and get a complete toolkit. from tkinter import * import tkinter.ttk as ttk import tkinter.font as tkFont -from functools import partial -import platform -import time -import signal -import subprocess -import sys -import weakref -import json -from enum import StrEnum -import importlib - from .modulesmanager import ModulesManager from .bindable import Bindable from .app import App @@ -31,7 +21,6 @@ CellEntry, NumericEntry, IntEntry, - CellEntry, LabelledEntry, ) from .controls import Slider @@ -44,4 +33,65 @@ from .figures import Figure, XYPlot, Histogram from .videoview import VideoView -__version__ = "0.9.12" +from importlib.metadata import version, PackageNotFoundError +try: + __version__ = version("mytk") +except PackageNotFoundError: + __version__ = "unknown" + +__all__ = [ + # Tkinter namespaces (re-exported for convenience) + "ttk", + "tkFont", + # Core + "ModulesManager", + "Bindable", + "App", + "Base", + "Window", + # Dialogs + "Dialog", + "SimpleDialog", + # Layout + "View", + "Box", + # Controls + "Button", + "Checkbox", + "RadioButton", + "PopupMenu", + "Slider", + # Labels + "Label", + "URLLabel", + # Entries + "Entry", + "FormattedEntry", + "CellEntry", + "NumericEntry", + "IntEntry", + "LabelledEntry", + # Canvas + "CanvasView", + # Indicators + "NumericIndicator", + "BooleanIndicator", + "Level", + # Images + "Image", + "DynamicImage", + "ImageWithGrid", + # Data + "TabularData", + "PostponeChangeCalls", + "TableView", + # File system + "FileTreeData", + "FileViewer", + # Plots + "Figure", + "XYPlot", + "Histogram", + # Video + "VideoView", +] diff --git a/mytk/__main__.py b/mytk/__main__.py index 51aaf82..fe732c3 100644 --- a/mytk/__main__.py +++ b/mytk/__main__.py @@ -2,70 +2,109 @@ import sys import argparse import subprocess -from mytk import * +from mytk import Bindable + def printClassHierarchy(aClass): def printAllChilds(aClass): for child in aClass.__subclasses__(): - print("\"{0}\" -> \"{1}\"".format(aClass.__name__, child.__name__)) + print('"{0}" -> "{1}"'.format(aClass.__name__, child.__name__)) printAllChilds(child) + print("# Paste this in the text field of http://www.webgraphviz.com/") print("digraph G {") - print(" rankdir=\"LR\";") + print(' rankdir="LR";') printAllChilds(aClass) print("}") -root = os.path.dirname(__file__) -examples_dir = os.path.join(root, "example_apps") -examples = [ f for f in sorted(os.listdir(examples_dir)) if f.endswith('.py') and not f.startswith('__') ] - -ap = argparse.ArgumentParser(prog='python -m mytk') -ap.add_argument("-e", "--examples", required=False, default='all', - help="Specific example numbers, separated by a comma") -ap.add_argument("-c", "--classes", required=False, action='store_const', - const=True, help="Print the class hierarchy in graphviz format") -ap.add_argument("-l", "--list", required=False, action='store_const', - const=True, help="List all the accessible examples") -ap.add_argument("-t", "--tests", required=False, action='store_const', - const=True, help="Run all Unit tests") - -args = vars(ap.parse_args()) -runExamples = args['examples'] -runTests = args['tests'] -printClasses = args['classes'] -listExamples = args['list'] - -if runExamples == 'all': - runExamples = range(1, len(examples)+1) -elif runExamples == '': - runExamples = [] -else: - runExamples = [int(y) for y in runExamples.split(',')] - -if printClasses: - printClassHierarchy(Bindable) - -elif runTests: - moduleDir = os.path.dirname(os.path.realpath(__file__)) - err = os.system('cd {0}/tests; {1} -m unittest'.format(moduleDir, sys.executable)) -elif listExamples: - for i, app in enumerate(sorted(examples)): - print(f"{i+1:2d}. {app}") -elif runExamples: - for i in runExamples: - entry = examples[i-1] - - filepath = os.path.join(examples_dir, entry) - title = f"# mytk example file: {filepath}" - - print(f"\n\n\n") - print("#"*len(title)) - print(title) - print("#"*len(title)) - print(f"\n") - - with open(filepath, "r") as file: - print(file.read()) - - subprocess.run([sys.executable, os.path.join(examples_dir, entry)]) +def main(): + root = os.path.dirname(__file__) + examples_dir = os.path.join(root, "example_apps") + examples = [ + f + for f in sorted(os.listdir(examples_dir)) + if f.endswith(".py") and not f.startswith("__") + ] + + ap = argparse.ArgumentParser(prog="python -m mytk") + ap.add_argument( + "-e", + "--examples", + required=False, + default="all", + help="Specific example numbers, separated by a comma", + ) + ap.add_argument( + "-c", + "--classes", + required=False, + action="store_const", + const=True, + help="Print the class hierarchy in graphviz format", + ) + ap.add_argument( + "-l", + "--list", + required=False, + action="store_const", + const=True, + help="List all the accessible examples", + ) + ap.add_argument( + "-t", + "--tests", + required=False, + action="store_const", + const=True, + help="Run all Unit tests", + ) + + args = vars(ap.parse_args()) + runExamples = args["examples"] + runTests = args["tests"] + printClasses = args["classes"] + listExamples = args["list"] + + if runExamples == "all": + runExamples = range(1, len(examples) + 1) + elif runExamples == "": + runExamples = [] + else: + runExamples = [int(y) for y in runExamples.split(",")] + + if printClasses: + printClassHierarchy(Bindable) + + elif runTests: + tests_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "tests") + result = subprocess.run( + [sys.executable, "-m", "unittest"], + cwd=tests_dir, + ) + sys.exit(result.returncode) + + elif listExamples: + for i, app in enumerate(sorted(examples)): + print(f"{i+1:2d}. {app}") + + elif runExamples: + for i in runExamples: + entry = examples[i - 1] + filepath = os.path.join(examples_dir, entry) + title = f"# mytk example file: {filepath}" + + print(f"\n\n\n") + print("#" * len(title)) + print(title) + print("#" * len(title)) + print(f"\n") + + with open(filepath, "r") as file: + print(file.read()) + + subprocess.run([sys.executable, os.path.join(examples_dir, entry)]) + + +if __name__ == "__main__": + main() diff --git a/mytk/tests/envtest.py b/mytk/tests/envtest.py index 7b654e4..04f2b33 100644 --- a/mytk/tests/envtest.py +++ b/mytk/tests/envtest.py @@ -1,14 +1,3 @@ -import sys -import os - -# append module root directory to sys.path -this_dir = os.path.dirname( - os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -) -sys.path.insert(0, this_dir) - -# print(this_dir) - import pathlib import unittest from mytk import App, View diff --git a/pyproject.toml b/pyproject.toml index af292b4..7a889a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [build-system] -requires = ["setuptools<69", "wheel"] +requires = ["setuptools>=70", "wheel"] build-backend = "setuptools.build_meta" @@ -10,6 +10,7 @@ version="0.9.14" description = "A wrapper for Tkinter for busy scientists" readme = "README.md" license = {file = "LICENSE"} +requires-python = ">=3.11" dynamic = ["dependencies"] authors = [ {name = "Daniel Côté", email = "dccote@cervo.ulaval.ca"} @@ -20,19 +21,44 @@ maintainers = [ classifiers = [ "Development Status :: 3 - Alpha", - - "Topic :: Software Development :: Build Tools", - - # Specify the Python versions you support here. + "Intended Audience :: Science/Research", + "Topic :: Scientific/Engineering", + "Topic :: Software Development :: Libraries :: Python Modules", + "Environment :: MacOS X", + "Environment :: Win32 (MS Windows)", + "Environment :: X11 Applications", + "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", ] -# py_modules=["mytk"] +[project.urls] +Homepage = "https://github.com/DCC-Lab/myTk" +Repository = "https://github.com/DCC-Lab/myTk" +"Bug Tracker" = "https://github.com/DCC-Lab/myTk/issues" + +[project.optional-dependencies] +video = ["opencv-python"] +optics = ["raytracing"] +docs = ["furo", "sphinx"] +all = ["mytk[video,optics]"] [tool.setuptools.package-data] "mytk.resources" = ["*.png"] [tool.setuptools.dynamic] dependencies = {file = ["requirements.txt"]} + +[tool.ruff] +target-version = "py311" +line-length = 100 + +[tool.ruff.lint] +select = ["E", "F", "W", "I"] +ignore = ["E501"] + +[tool.pytest.ini_options] +testpaths = ["mytk/tests"] diff --git a/requirements.txt b/requirements.txt index e895549..67ef9b1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,8 @@ -matplotlib==3.9.2 +matplotlib>=3.9 numpy packaging pyperclip -Requests +requests scipy pandas openpyxl -raytracing -opencv-python \ No newline at end of file From 14cff6e60a63495bc226d95d389e2771d5be2edf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20C=C3=B4t=C3=A9?= Date: Mon, 23 Feb 2026 10:22:49 -0500 Subject: [PATCH 2/8] Fix dead code, hardcoded path, and optional cv2 import. - Remove unused `timeout = 1000` variable in envtest.setUp() - Replace hardcoded absolute path assertion in testImages with exists()/is_dir() checks on self.resource_directory - Guard `import cv2` in videoview.py with try/except so the package imports cleanly when opencv-python is not installed (optional dep) Co-Authored-By: Claude Sonnet 4.6 --- mytk/tests/envtest.py | 1 - mytk/tests/testImages.py | 6 ++---- mytk/videoview.py | 5 ++++- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/mytk/tests/envtest.py b/mytk/tests/envtest.py index 04f2b33..ddb76c9 100644 --- a/mytk/tests/envtest.py +++ b/mytk/tests/envtest.py @@ -5,7 +5,6 @@ class MyTkTestCase(unittest.TestCase): def setUp(self): - timeout = 1000 self.app = App() testcase_id = self.id() self.app.window.widget.title(testcase_id) diff --git a/mytk/tests/testImages.py b/mytk/tests/testImages.py index 6cafa21..9f7f255 100644 --- a/mytk/tests/testImages.py +++ b/mytk/tests/testImages.py @@ -17,10 +17,8 @@ def test_init_empty(self): self.assertIsNotNone(Image()) def test_resource_directory(self): - resource_directory = pathlib.Path(__file__).parent.parent / "resources" - self.assertEqual( - resource_directory, pathlib.Path("/Users/dccote/GitHub/myTk/mytk/resources") - ) + self.assertTrue(self.resource_directory.exists()) + self.assertTrue(self.resource_directory.is_dir()) def test_init_with_path(self): self.assertIsNotNone(Image(filepath=self.resource_directory / "error.png")) diff --git a/mytk/videoview.py b/mytk/videoview.py index bcc872d..ffbe28c 100644 --- a/mytk/videoview.py +++ b/mytk/videoview.py @@ -7,7 +7,10 @@ from .popupmenu import PopupMenu import importlib import signal -import cv2 +try: + import cv2 +except ImportError: + cv2 = None class VideoView(Base): From a8b956003c2c0fa4905dc1f0db3d2bd290fac65c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20C=C3=B4t=C3=A9?= Date: Mon, 23 Feb 2026 10:28:01 -0500 Subject: [PATCH 3/8] Fix hanging test in testCustomDialogs. auto_click was commented out in test_custom_window, causing diag.run() to block indefinitely waiting for a manual button click. Restored auto_click as a tuple with a 100ms timeout, consistent with the other dialog tests. Co-Authored-By: Claude Sonnet 4.6 --- mytk/tests/testCustomDialogs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mytk/tests/testCustomDialogs.py b/mytk/tests/testCustomDialogs.py index 70c7dc8..845809d 100644 --- a/mytk/tests/testCustomDialogs.py +++ b/mytk/tests/testCustomDialogs.py @@ -68,7 +68,7 @@ def populate_widget_body(self): diag = MyDialog( title="Test Window", buttons_labels=[Dialog.Replies.Ok, Dialog.Replies.Cancel], - # auto_click=[Dialog.Replies.Ok, 1000], + auto_click=(Dialog.Replies.Ok, 100), ) self.assertIsNotNone(diag) reply = diag.run() From e9ab96ef3a0e032dc6e19880fbe1918237d42658 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20C=C3=B4t=C3=A9?= Date: Mon, 23 Feb 2026 11:06:10 -0500 Subject: [PATCH 4/8] Fix wildcard re-export and missing partial import. - Remove __all__ from __init__.py so that `from mytk import *` continues to re-export all tkinter names (IntVar, StringVar, etc.) as intended by the existing `from tkinter import *` statement. Adding __all__ had silently blocked those names, breaking 17 tests with NameError. - Add explicit `from functools import partial` to testMyApp.py; partial was previously available only via a vestigial re-export that we removed. Co-Authored-By: Claude Sonnet 4.6 --- mytk/__init__.py | 56 ----------------------------------------- mytk/tests/testMyApp.py | 1 + 2 files changed, 1 insertion(+), 56 deletions(-) diff --git a/mytk/__init__.py b/mytk/__init__.py index ecc3a61..7d2fb33 100644 --- a/mytk/__init__.py +++ b/mytk/__init__.py @@ -39,59 +39,3 @@ except PackageNotFoundError: __version__ = "unknown" -__all__ = [ - # Tkinter namespaces (re-exported for convenience) - "ttk", - "tkFont", - # Core - "ModulesManager", - "Bindable", - "App", - "Base", - "Window", - # Dialogs - "Dialog", - "SimpleDialog", - # Layout - "View", - "Box", - # Controls - "Button", - "Checkbox", - "RadioButton", - "PopupMenu", - "Slider", - # Labels - "Label", - "URLLabel", - # Entries - "Entry", - "FormattedEntry", - "CellEntry", - "NumericEntry", - "IntEntry", - "LabelledEntry", - # Canvas - "CanvasView", - # Indicators - "NumericIndicator", - "BooleanIndicator", - "Level", - # Images - "Image", - "DynamicImage", - "ImageWithGrid", - # Data - "TabularData", - "PostponeChangeCalls", - "TableView", - # File system - "FileTreeData", - "FileViewer", - # Plots - "Figure", - "XYPlot", - "Histogram", - # Video - "VideoView", -] diff --git a/mytk/tests/testMyApp.py b/mytk/tests/testMyApp.py index b2ee869..d46833f 100644 --- a/mytk/tests/testMyApp.py +++ b/mytk/tests/testMyApp.py @@ -1,5 +1,6 @@ import envtest import unittest +from functools import partial from mytk import * From b3d57b6327e6803e4ce51b0bc148f0107daef172 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20C=C3=B4t=C3=A9?= Date: Mon, 23 Feb 2026 11:12:35 -0500 Subject: [PATCH 5/8] Fix Entry.__init__ resetting initial value to empty string. Assigning StringVar() after the two-way binding was established caused self.value to be overwritten by the StringVar's empty default. Initialize StringVar with the provided value instead to prevent this. Co-Authored-By: Claude Sonnet 4.6 --- mytk/entries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mytk/entries.py b/mytk/entries.py index 28ffd99..8f1aadc 100644 --- a/mytk/entries.py +++ b/mytk/entries.py @@ -19,7 +19,7 @@ def __init__( self._widget_args["width"] = character_width self.value = value self.bind_properties("value", self, "value_variable") - self.value_variable = StringVar() + self.value_variable = StringVar(value=value) def create_widget(self, master): self.parent = master From 6747ea461df8e83a7a030c5b868e8200ffaf06a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20C=C3=B4t=C3=A9?= Date: Mon, 23 Feb 2026 12:07:00 -0500 Subject: [PATCH 6/8] Remove continuous integration workflow. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/tests.yml | 40 ------------------------------------- 1 file changed, 40 deletions(-) delete mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml deleted file mode 100644 index 4a56c39..0000000 --- a/.github/workflows/tests.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Tests - -on: - push: - branches: [main] - pull_request: - branches: [main] - -jobs: - test: - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - python-version: ["3.11", "3.12", "3.13"] - - steps: - - uses: actions/checkout@v4 - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -e . - - - name: Install Tk and virtual display (Linux only) - if: runner.os == 'Linux' - run: | - sudo apt-get install -y python3-tk xvfb - Xvfb :99 -screen 0 1024x768x24 & - - - name: Run tests - run: python -m unittest discover -s mytk/tests - env: - DISPLAY: ":99" From ff21eb7c6ff828704b788d6fefb286f9c2c23f90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20C=C3=B4t=C3=A9?= Date: Mon, 23 Feb 2026 16:04:18 -0500 Subject: [PATCH 7/8] Fix mutation-during-iteration bug in after_cancel_all. Passing a copy of scheduled_tasks prevents after_cancel from corrupting the iteration when it removes items from the same list, which was silently leaving half the tasks uncancelled and causing 'invalid command name' Tcl errors after widget destruction. Co-Authored-By: Claude Sonnet 4.6 --- mytk/eventcapable.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mytk/eventcapable.py b/mytk/eventcapable.py index 7f737bd..ed89ffc 100644 --- a/mytk/eventcapable.py +++ b/mytk/eventcapable.py @@ -119,7 +119,7 @@ def after_cancel_all(self): """ Cancels all currently scheduled tasks for this object. """ - self.after_cancel_many(self.scheduled_tasks) + self.after_cancel_many(list(self.scheduled_tasks)) def bind_event(self, event: str, callback: Callable): """ From 17b53b1ad79174d5f5c20d5cce51ccc3faa4c969 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20C=C3=B4t=C3=A9?= Date: Mon, 23 Feb 2026 16:08:26 -0500 Subject: [PATCH 8/8] Bump version to 0.9.15. Co-Authored-By: Claude Sonnet 4.6 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7a889a2..88caa11 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta" [project] name = "mytk" -version="0.9.14" +version="0.9.15" description = "A wrapper for Tkinter for busy scientists" readme = "README.md" license = {file = "LICENSE"}