Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
6c0ad9d
refactor bring_to_foreground to mywx module
ruck94301 Oct 28, 2025
98fc602
In probes.probes, Stub in a call to userdata_survey.survey.
ruck94301 Oct 28, 2025
fded217
Implement two custom wx.Dialogs with left-aligned msg.
ruck94301 Oct 29, 2025
3a23ca2
Implement verify_userdata_dir_separation.
ruck94301 Oct 29, 2025
79d3fe4
Implement offer_mkdir_userdata.
ruck94301 Oct 29, 2025
b8141df
Stub in check_for_internal_userdata_dirs.
ruck94301 Oct 29, 2025
0adcc92
Partially implement advise_on_internal_userdata_dirs.
ruck94301 Oct 29, 2025
255e703
Stub in warn_on_orphaned_userdata.
ruck94301 Oct 29, 2025
1386f4d
Enforce the expectation that path args are "full" not relative.
ruck94301 Oct 30, 2025
28640ca
Implement "advise" with two approaches to displaying instructions.
ruck94301 Nov 4, 2025
4a448b1
Implement warn_on_orphaned userdata
ruck94301 Nov 4, 2025
0b7be36
Merge branch 'master' into check-for-datafolders-within-tree
ruck94301 Nov 17, 2025
7a2ff79
Merge branch 'master' into check-for-datafolders-within-tree
ruck94301 Nov 26, 2025
993177b
Clean userdata_survey.py
ruck94301 Nov 26, 2025
48b5163
Resuscitate the single call to wx.App().
ruck94301 Nov 30, 2025
1fe87df
Replace mywx.App function with patch to wx.App.
ruck94301 Dec 4, 2025
7c00776
Implement userdata_survey.survey with warnings instead of gui dialogs…
ruck94301 Dec 5, 2025
92b722d
Fix unit tests.
ruck94301 Dec 5, 2025
3afe940
Use logger.warning() instead of logger.warn().
ruck94301 Dec 5, 2025
012d7a6
Move bring_wxapp_to_foreground to instantiation in __main__.py
ruck94301 Dec 9, 2025
8f8bdb9
Be robust to mymx being loaded twice.
ruck94301 Dec 9, 2025
d071263
Show warning msgs with wx Dialog.
ruck94301 Dec 9, 2025
f69568c
Mock userdata_survey.survey in test_probes.py.
ruck94301 Dec 9, 2025
ea33d91
Change usage comment in mywx.py
ruck94301 Dec 10, 2025
458bde9
Refactor module mywx.py into package mywx
ruck94301 Dec 10, 2025
2cdb9c1
Wordsmith comments. Small & benign.
ruck94301 Dec 10, 2025
6e46137
Add unit tests for userdata_survey.py
ruck94301 Dec 11, 2025
65be890
Scrub unused functions from userdata_survey.py
ruck94301 Dec 12, 2025
ce6ed83
Implement assess_userdata_folders with feature off, by default.
ruck94301 Dec 12, 2025
86d53f1
Bugfixed the unittest
ruck94301 Dec 12, 2025
db3da2f
remove an unused function and its unittest
ruck94301 Dec 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions LabGym/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,11 @@
# Collect logrecords and defer handling until logging is configured.
mylogging.defer()

# Log the loading of this module (by the module loader, on first import).
# Log the load of this module (by the module loader, on first import).
# Intentionally positioning these statements before other imports, against the
# guidance of PEP 8, to log the load before other imports log messages.
logger = logging.getLogger(__name__)
logger.debug('loading %s', __file__)
logger.debug('%s', f'loading {__name__}')

# Configure logging based on configfile, then handle collected logrecords.
mylogging.configure()
Expand All @@ -47,11 +49,13 @@
# Related third party imports.
from packaging import version # Core utilities for Python packages
import requests # Python HTTP for Humans.
from LabGym import mywx # on load, monkeypatch wx.App to be a strict-singleton
import wx # wxPython, Cross platform GUI toolkit for Python, "Phoenix" version

# Local application/library specific imports.
# pylint: disable-next=unused-import
from LabGym import mypkg_resources # replace deprecated pkg_resources
from LabGym import __version__, gui_main, mywx, probes
from LabGym import __version__, gui_main, probes


logger.debug('%s: %r', '(__name__, __package__)', (__name__, __package__))
Expand Down Expand Up @@ -85,7 +89,9 @@ def main() -> None:

# Create a single persistent, wx.App instance, as it may be
# needed for probe dialogs prior to calling gui_main.main_window.
mywx.App()
assert wx.GetApp() is None
wx.App()
mywx.bring_wxapp_to_foreground()

# Perform some pre-op sanity checks and probes of outside resources.
probes.probes()
Expand Down
3 changes: 3 additions & 0 deletions LabGym/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@
'enable': {
'central_logger': True, # to disable central logger, user must opt out
'registration': True, # to disable registration, user must opt out

# for now, user has to opt in for assessing locations of userdata
'assess_userdata_folders': False,
},

'anonymous': False,
Expand Down
3 changes: 3 additions & 0 deletions LabGym/gui_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,9 @@ def on_page_close(self, event):
def main_window():
"""Display the main window."""
app = wx.GetApp() # reference to the currently running wx.App instance
if app is None:
app = wx.App() # new wx.App object

app.SetAppName("LabGym") # Set app name to influence WM_CLASS
setup_application_icons() # Set up all platform-specific icons

Expand Down
59 changes: 0 additions & 59 deletions LabGym/mywx.py
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mywx.py is replaced by mywx package

This file was deleted.

89 changes: 89 additions & 0 deletions LabGym/mywx/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
"""
Monkeypatch wx.App, and provide wx utility functions and wx.Dialog subclasses.

Import this package before the first import of wx, to patch wx.App to be
a strict-singleton before an unpatched instance of wx.App can possibly
be created.

Example -- Display a modal dialog. In this example, mywx is a
subpackage of mypkg (not in a dir in sys.path).
from mypkg import mywx # on load, monkeypatch wx.App to be a strict-singleton
import wx # wxPython, Cross platform GUI toolkit for Python, "Phoenix" version

# Dialog obj requires that the wx.App obj exists already.
if not wx.GetApp():
wx.App()
mywx.bring_wxapp_to_foreground()

# Show modal dialog.
title = 'My Title'
msg = 'My message'
with mywx.OK_Dialog(None, title=title, msg=msg) as dlg:
result = dlg.ShowModal() # will return wx.ID_OK upon OK or dismiss

Notes
* Why patch wx.App?
Because it's easy to misuse, producing delayed consequences that may
be difficult to diagnose.

Scraped on 2025-12-10 from wxPython 4.2.3 documentation
https://docs.wxpython.org/wx.App.html
The wx.App class represents the application and is used to:
* bootstrap the wxPython system and initialize the underlying
gui toolkit
* set and get application-wide properties
* implement the native windowing system main message or event
loop, and to dispatch events to window instances
* etc.

Every wx application must have a single wx.App instance, and all
creation of UI objects should be delayed until after the wx.App
object has been created in order to ensure that the gui platform
and wxWidgets have been fully initialized.

Normally you would derive from this class and implement an
OnInit method that creates a frame and then calls
self.SetTopWindow(frame), however wx.App is also usable on its
own without derivation.

Scraped on 2025-12-10 from wxPython discussion
https://discuss.wxpython.org/t/two-wx-app-instances-one-appx-mainloop-runs-both-app-instances/24934
wx.App is supposed to be used as a singleton. It doesn't own
the frames that are created in its OnInit, but instead assumes
that it is supposed to manage and deliver events to all the
windows that exist in the application.

* Why patch wx.App to be a "strict singleton" (that raises an
exception on a second instantiation attempt) instead of a singleton
that returns the existing instance?
Because the intention is to prevent the misuse of wx (according to
the lib documentation), not to accommodate the misuse of wx.

* During development of a feature, more wx-related functions and
classes are being parked in mywx.custom out of convenience, but
there may be a better way to reorganize them after the feature code
is stable.
"""

# Standard library imports.
import logging

# Log the load of this module (by the module loader, on first import).
# Intentionally positioning these statements before other imports, against the
# guidance of PEP 8, to log the load before other imports log messages.
logger = logging.getLogger(__name__)
logger.debug('%s', f'loading {__name__}')

# Related third party imports.
# None

# Local application/library specific imports.
# On load of this package, import/load .patch, which monkeypatches wx.App.
from . import patch

# Expose custom functions and classes as attributes of this package.
from .custom import (
bring_wxapp_to_foreground,
OK_Dialog,
OK_Cancel_Dialog,
)
139 changes: 139 additions & 0 deletions LabGym/mywx/custom.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
"""Provide wx utility functions and wx.Dialog subclasses.

Public Functions
bring_wxapp_to_foreground -- Bring the wx app to the foreground.

Public Classes
OK_Dialog -- A wx.Dialog with left-aligned msg, and centered OK button.
OK_Cancel_Dialog -- A wx.Dialog with left-aligned msg, and centered
OK and Cancel buttons.
"""

# Allow use of newer syntax Python 3.10 type hints in Python 3.9.
from __future__ import annotations

# Standard library imports.
import logging
import sys

# Log the load of this module (by the module loader, on first import).
# Intentionally positioning these statements before other imports, against the
# guidance of PEP 8, to log the load before other imports log messages.
logger = logging.getLogger(__name__)
logger.debug('%s', f'loading {__name__}')

# Related third party imports.
if sys.platform == 'darwin': # macOS
# AppKit is from package pyobjc-framework-Cocoa, "Wrappers for the
# Cocoa frameworks on macOS".
from AppKit import NSApp, NSApplication

import wx # wxPython, Cross platform GUI toolkit for Python, "Phoenix" version

# Local application/library specific imports.
# None


def bring_wxapp_to_foreground() -> None:
"""Bring the wx app to the foreground.

We want the wx app displayed to the user, not initially obscured by
windows from other apps.

On macOS 12.7, with LabGym started from terminal, I'm seeing:
* the registration dialog is displayed, but doesn't have focus
and is under the windows of the active app (terminology?),
potentially hidden completely.
* a bouncing python rocketship icon in the tray. The bouncing
stops when you mouseover the icon. Also some other actions
can stop the bouncing or just pause the bouncing... weird.

I wasn't able to resolve this undesirable behavior on macOS using
the wx.Dialog object's Raise, SetFocus, and Restore methods.
Instead, this approach worked for me...
"Calling NSApp().activateIgnoringOtherApps_(True) via AppKit:
This macOS workaround uses the AppKit module to explicitly
activate the application, ignoring other running applications.
This is typically done after your wxPython application has
started and its main window is shown."
"""

if sys.platform == 'darwin': # macOS
NSApplication.sharedApplication()
NSApp().activateIgnoringOtherApps_(True)


class OK_Dialog(wx.Dialog):
"""An OK dialog object, with the message text left-aligned.

Why use a custom class instead of using wx.MessageDialog?
Because left-alignment of the message is preferred, and a
wx.MessageDialog cannot be customized to control the alignment of
its message text or buttons.

(class purpose and functionality, and optionally its attributes and methods)
"""

def __init__(self, parent, title='', msg=''):
super().__init__(parent, title=title)

panel = wx.Panel(self)
main_sizer = wx.BoxSizer(wx.VERTICAL)

# Add the msg
main_sizer.Add(wx.StaticText(panel, label=msg),
0, # proportion (int). 0 means the item won't expand
# beyond its minimal size.

# border on all sides, and align left
wx.ALL | wx.LEFT,

10, # width (in pixels) of the borders specified
)

# Create sizers for layout.
button_sizer = wx.StdDialogButtonSizer()

# Create buttons, Add buttons to sizers, and Bind event handlers
self.add_buttons(panel, button_sizer)

# Realize the sizer to apply platform-specific layout
button_sizer.Realize()

# Add the button sizer to the main sizer
main_sizer.Add(button_sizer, 0, wx.ALL | wx.EXPAND, 10)

panel.SetSizer(main_sizer)
main_sizer.Fit(self)
# self.SetSizerAndFit(main_sizer) # is this equivalent??

def add_buttons(self, panel, button_sizer):
"""Create and add buttons to sizers, and bind event handlers.

Create standard buttons with their respective IDs.
Add buttons to the StdDialogButtonSizer.
Bind event handlers for the buttons.
"""
# Create/Add/Bind for the OK button
ok_button = wx.Button(panel, wx.ID_OK)
button_sizer.AddButton(ok_button)
self.Bind(wx.EVT_BUTTON, self.on_ok, id=wx.ID_OK)

def on_ok(self, event):
self.EndModal(wx.ID_OK)


class OK_Cancel_Dialog(OK_Dialog):
"""An OK/Cancel dialog object, with the message text left-aligned."""

def add_buttons(self, panel, button_sizer):
# Create/Add/Bind for the OK button
super().add_buttons(panel, button_sizer)

# Create/Add/Bind for the Cancel button
cancel_button = wx.Button(panel, wx.ID_CANCEL)
button_sizer.AddButton(cancel_button)
self.Bind(wx.EVT_BUTTON, self.on_cancel, id=wx.ID_CANCEL)

def on_cancel(self, event):
self.EndModal(wx.ID_CANCEL)
Loading