Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add wx toolkit support #246

Merged
merged 26 commits into from
Nov 26, 2020
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
494e21d
Add ci support for wxPython installation
mdickinson Nov 25, 2020
7f4af8a
Revert unrelated change
mdickinson Nov 25, 2020
2b2c8ec
Refactor to remove duplication
mdickinson Nov 25, 2020
0e54170
More refactoring and cleanup
mdickinson Nov 25, 2020
7360709
Add wx toolkit support
mdickinson Nov 25, 2020
abd8959
Merge branch 'build/wx-support' into feature/wxpython-support
mdickinson Nov 25, 2020
fc259e9
Fix ci install of wxPython to work independently of mode
mdickinson Nov 25, 2020
881d2a9
Enable faulthandler when running tests
mdickinson Nov 25, 2020
63807cd
Merge remote-tracking branch 'origin/feature/wxpython-support' into f…
mdickinson Nov 25, 2020
0265f92
FIX: use the given timeout rather than the default timeout for the timer
mdickinson Nov 25, 2020
fad35d5
Restrict wxPython to <4.1 to see if that solves the segfault
mdickinson Nov 25, 2020
4c8af13
Merge branch 'master' into feature/wxpython-support
mdickinson Nov 25, 2020
c110f98
Add quotes around 'wxPython<4.1'
mdickinson Nov 25, 2020
1828f3a
Add spaces around the '<'
mdickinson Nov 25, 2020
596af3d
Add some extra packages on Linux to see if it solves the segfault
mdickinson Nov 25, 2020
6cd2404
Merge branch 'master' into feature/wxpython-support
mdickinson Nov 25, 2020
e184437
Add comments to explain dependencies in .travis.yml
mdickinson Nov 26, 2020
1274dd7
Remove faulthandler
mdickinson Nov 26, 2020
e470da4
Add version number
mdickinson Nov 26, 2020
e0ad94f
Remove unnecessary(?) wxPython dependencies
mdickinson Nov 26, 2020
ef0b015
Add note about why we're creating our own timer class
mdickinson Nov 26, 2020
02e7049
Pacify the flake8 check
mdickinson Nov 26, 2020
597a7ee
Remove event_id argument to _PingEvent constructor
mdickinson Nov 26, 2020
b8871f3
Also get rid of the expectedIDs argument to PyEventBinder. It's not c…
mdickinson Nov 26, 2020
4f7e39f
Clarify _PING_EVENT_TYPE docstring
mdickinson Nov 26, 2020
bced91b
Remove doc note about lack of wxPython support
mdickinson Nov 26, 2020
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
7 changes: 7 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ services:
addons:
apt:
packages:
# Qt dependencies
- qt5-default
- libxkbcommon-x11-0
- libxcb-icccm4
Expand All @@ -15,6 +16,8 @@ addons:
- libxcb-randr0
- libxcb-render-util0
- libxcb-xinerama0
# wxPython dependencies
- libsdl2-2.0-0

env:
global:
Expand All @@ -35,6 +38,8 @@ matrix:
env: CI_PYTHON_VERSION="py36" CI_TOOLKIT="pyqt5"
- os: linux
env: CI_PYTHON_VERSION="py36" CI_TOOLKIT="pyside2"
- os: linux
env: CI_PYTHON_VERSION="py36" CI_TOOLKIT="wxpython"
- os: osx
env: CI_PYTHON_VERSION="py36" CI_TOOLKIT="null"
- os: osx
Expand All @@ -43,6 +48,8 @@ matrix:
env: CI_PYTHON_VERSION="py36" CI_TOOLKIT="pyqt5"
- os: osx
env: CI_PYTHON_VERSION="py36" CI_TOOLKIT="pyside2"
- os: osx
env: CI_PYTHON_VERSION="py36" CI_TOOLKIT="wxpython"

before_install:
- mkdir -p "${HOME}/.cache/download"
Expand Down
2 changes: 2 additions & 0 deletions appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ environment:
CI_TOOLKIT: "pyqt5"
- CI_PYTHON_VERSION: "py36"
CI_TOOLKIT: "pyside2"
- CI_PYTHON_VERSION: "py36"
CI_TOOLKIT: "wxpython"

cache:
- C:\Users\appveyor\.cache -> appveyor-clean-cache.txt
Expand Down
19 changes: 19 additions & 0 deletions ci/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,25 @@ def build(edm, python_version, toolkit, mode):
pyenv.create()
pyenv.install(dependencies)

# wxPython installation needs special handling, especially on Linux
# Ref: https://wxpython.org/pages/downloads/
if toolkit == cfg.WXPYTHON:
wxpython_install_options = []
if current_platform() == cfg.LINUX:
wxpython_install_options += [
"-f",
"https://extras.wxpython.org/wxPython4/extras/linux/gtk3/ubuntu-16.04", # noqa: E501
kitchoi marked this conversation as resolved.
Show resolved Hide resolved
]

wxpython_install_cmd = [
"-m",
"pip",
"install",
*wxpython_install_options,
"wxPython",
]
pyenv.python(wxpython_install_cmd)

# Install local packages.
local_packages = ["./"]
pip_options = ["--editable"] if mode == "develop" else []
Expand Down
9 changes: 8 additions & 1 deletion ci/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@
PYQT = "pyqt" # Qt 4, PyQt
PYQT5 = "pyqt5" # Qt 5, PyQt
PYSIDE2 = "pyside2" # Qt 5, Qt for Python
TOOLKITS = [NULL, PYQT, PYQT5, PYSIDE2]
WXPYTHON = "wxpython" # wxPython 4
TOOLKITS = [NULL, PYQT, PYQT5, PYSIDE2, WXPYTHON]

# Default Python version and toolkit.
DEFAULT_PYTHON = PYTHON36
Expand Down Expand Up @@ -96,6 +97,9 @@
(MACOS, PYTHON36, PYSIDE2),
(LINUX, PYTHON36, PYSIDE2),
(WINDOWS, PYTHON36, PYSIDE2),
(MACOS, PYTHON36, WXPYTHON),
(LINUX, PYTHON36, WXPYTHON),
(WINDOWS, PYTHON36, WXPYTHON),
]

# Dependencies needed for all platforms, toolkits and Python versions.
Expand Down Expand Up @@ -123,6 +127,9 @@
PYQT: ["pyqt", "traitsui"],
PYQT5: ["pyqt5", "traitsui"],
PYSIDE2: ["pyside2", "traitsui"],
# wxPython is not yet available through EDM, and needs special
# handling in the main script.
WXPYTHON: ["traitsui"],
}

# Additional packages needed for local development, examples.
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ def get_long_description():
"null = traits_futures.null.init:toolkit_object",
"qt4 = traits_futures.qt.init:toolkit_object",
"qt = traits_futures.qt.init:toolkit_object",
"wx = traits_futures.wx.init:toolkit_object",
],
},
python_requires=">=3.6",
Expand Down
16 changes: 16 additions & 0 deletions traits_futures/tests/test_gui_test_assistant.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,42 +45,58 @@ def tearDown(self):
def test_run_until_timeout(self):
# Trait never fired, condition never true.
dummy = Dummy()

start_time = time.monotonic()
Copy link
Member Author

Choose a reason for hiding this comment

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

Note to reviewer: the changes in this file aren't specific to wx, and could be moved to a separate PR. They're regression tests for a bug I discovered in the wx implementation of the GuiTestAssistant.

with self.assertRaises(RuntimeError):
self.run_until(
dummy,
"never_fired",
condition=lambda object: False,
timeout=0.1,
)
actual_timeout = time.monotonic() - start_time
# We're being ultra-conservative here; on a typical system, the actual
# time taken to fail shouldn't be much more than the 0.1 timeout. But
# at least this test catches the bug of using the default timeout of 10
# seconds.
self.assertLess(actual_timeout, 1.0)

def test_run_until_timeout_trait_fired(self):
# Trait fired, but condition still never true.
executor = TraitsExecutor()
future = submit_call(executor, int, "111")
start_time = time.monotonic()
with self.assertRaises(RuntimeError):
self.run_until(
future,
"state",
condition=lambda future: future.state == CANCELLED,
timeout=0.1,
)
actual_timeout = time.monotonic() - start_time

executor.stop()
self.run_until(
executor,
"stopped",
condition=lambda executor: executor.stopped,
)
self.assertLess(actual_timeout, 1.0)

def test_run_until_timeout_with_true_condition(self):
# Trait never fired, but condition true anyway.
dummy = Dummy()
start_time = time.monotonic()
self.run_until(
dummy,
"never_fired",
condition=lambda object: True,
timeout=10.0,
)
actual_timeout = time.monotonic() - start_time
# The actual time take for the run_until call to return should
# be close to zero.
self.assertLess(actual_timeout, 1.0)

def test_run_until_success(self):
# Trait fired, condition starts false but becomes true.
Expand Down
Empty file added traits_futures/wx/__init__.py
Empty file.
176 changes: 176 additions & 0 deletions traits_futures/wx/gui_test_assistant.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
# (C) Copyright 2018-2020 Enthought, Inc., Austin, TX
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in LICENSE.txt and may be redistributed only under
# the conditions described in the aforementioned license. The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
#
# Thanks for using Enthought open source!

"""
Test support, providing the ability to run the event loop from tests.
"""

import wx


#: Default timeout, in seconds
TIMEOUT = 10.0

# XXX We should be using Pyface's own CallbackTimer instead of creating
# our own, but we were running into segfaults.
# xref: enthought/pyface#815, enthought/traits-futures#251


class TimeoutTimer(wx.Timer):
mdickinson marked this conversation as resolved.
Show resolved Hide resolved
"""
Single-shot timer that executes a given callback on completion.

Parameters
----------
timeout : float
Timeout in seconds.
callback : callable
Callable taking no arguments, to be executed when the timer
times out.
args : tuple, optional
Tuple of positional arguments to pass to the callable. If not
provided, no positional arguments are passed.
kwargs : dict, optional
Dictionary of keyword arguments to pass to the callable. If not
provided, no keyword arguments are passed.
"""

def __init__(self, timeout, callback, args=(), kwargs=None):
wx.Timer.__init__(self)
self.timeout = timeout
self.callback = callback
self.args = args
self.kwargs = {} if kwargs is None else kwargs

def start(self):
"""
Start the timer.
"""
timeout_in_ms = round(self.timeout * 1000)
self.StartOnce(milliseconds=timeout_in_ms)

def stop(self):
"""
Stop the timer if it hasn't already expired. The callback
will not be executed.
"""
self.Stop()

def Notify(self):
"""
Execute the callback when the timer completes.
"""
self.callback(*self.args, **self.kwargs)


class AppForTesting(wx.App):
"""
Subclass of wx.App used for testing.
"""

def OnInit(self):
"""
Override base class to ensure we have at least one window.
"""
# It's necessary to have at least one window to prevent the application
# exiting immediately.
self.frame = wx.Frame(None)
self.SetTopWindow(self.frame)
self.frame.Show(False)
return True

def exit(self, exit_code):
"""
Exit the application main event loop with a given exit code.

The event loop can be started and stopped several times for
a single AppForTesting object.
"""
self.exit_code = exit_code
self.ExitMainLoop()

def close(self):
"""
Clean up when the object is no longer needed.
"""
self.frame.Close()
del self.frame


class GuiTestAssistant:
mdickinson marked this conversation as resolved.
Show resolved Hide resolved
"""
Support for running the wx event loop in unit tests.
"""

def setUp(self):
self.wx_app = AppForTesting()

def tearDown(self):
self.wx_app.close()
del self.wx_app

def run_until(self, object, trait, condition, timeout=TIMEOUT):
"""
Run event loop until the given condition holds true, or until timeout.

The condition is re-evaluated, with the object as argument, every time
the trait changes.

Parameters
----------
object : traits.has_traits.HasTraits
Object whose trait we monitor.
trait : str
Name of the trait to monitor for changes.
condition : callable
Single-argument callable, returning a boolean. This will be
called with *object* as the only input.
timeout : float, optional
Number of seconds to allow before timing out with an exception.
The (somewhat arbitrary) default is 10 seconds.

Raises
------
RuntimeError
If timeout is reached, regardless of whether the condition is
true or not at that point.
"""

wx_app = self.wx_app

timeout_timer = TimeoutTimer(timeout, lambda: wx_app.exit(1))

def stop_if_condition():
if condition(object):
wx_app.exit(0)

object.on_trait_change(stop_if_condition, trait)
try:
# The condition may have become True before we
# started listening to changes. So start with a check.
if condition(object):
timed_out = 0
else:
timeout_timer.start()
try:
wx_app.MainLoop()
finally:
timed_out = wx_app.exit_code
timeout_timer.stop()
finally:
object.on_trait_change(stop_if_condition, trait, remove=True)

if timed_out:
raise RuntimeError(
"run_until timed out after {} seconds. "
"At timeout, condition was {}.".format(
timeout, condition(object)
)
)
20 changes: 20 additions & 0 deletions traits_futures/wx/init.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# (C) Copyright 2018-2020 Enthought, Inc., Austin, TX
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in LICENSE.txt and may be redistributed only under
# the conditions described in the aforementioned license. The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
#
# Thanks for using Enthought open source!

"""
Entry point for finding toolkit-specific classes.
"""
# Force an ImportError if wxPython is not installed.
import wx # noqa: F401

from pyface.base_toolkit import Toolkit

#: The toolkit object used to find toolkit-specific resources
toolkit_object = Toolkit("traits_futures", "wx", "traits_futures.wx")
Loading