From b5bb5871452278d077d8fbc844570f83a0968e56 Mon Sep 17 00:00:00 2001 From: Robin Dunn Date: Sun, 24 Jun 2018 21:36:52 -0700 Subject: [PATCH] Merge pull request #897 from RobinD42/add-windowidref Add wx.WindowIDRef and wx.NewIdRef --- CHANGES.rst | 10 ++- buildtools/build_wxwidgets.py | 1 + docs/sphinx/_downloads/i18nwxapp/sampleapp.py | 6 +- docs/sphinx/itemToModuleMap.json | 2 + .../overviews/events_overview.rst | 2 +- .../overviews/standard_event_identifiers.rst | 2 +- etg/utils.py | 14 +++- etg/windowid.py | 73 +++++++++++++++++++ etgtools/extractors.py | 21 ++++-- etgtools/sphinx_generator.py | 4 +- src/core_ex.cpp | 4 + unittests/test_windowid.py | 50 ++++++++++++- 12 files changed, 170 insertions(+), 19 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 47f69c2c..b5be1353 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -53,7 +53,7 @@ Pip: ``pip install wxPython==4.0.3`` Changes in this release include the following: * Fixed a linking problem on macOS. The new waf added an explicit link to the - Python shared library which menat that it would try to load it at runtime, + Python shared library which meant that it would try to load it at runtime, even if a different Python (such as Anaconda, EDM or Homebrew) was used to import wxPython. This, of course, caused runtime errors. (#892) @@ -63,6 +63,14 @@ Changes in this release include the following: * Added Vagrant configuration for Fedora-28. Removed Fedora-23 (#884) +* Added wrappers for the wx.WindowIDRef class and added the wx.NewIdRef + function. These will make it possible to create reserved Window IDs using the + same mechanism which is used when passing wx.ID_ANY to a widget constructor. + The object returned by wx.NewIdRef will automatically convert to an int when + passing it to a window constructor, and can also be used as the source in a + Bind(). + + 4.0.2 "Cute as a June bug!" --------------------------- diff --git a/buildtools/build_wxwidgets.py b/buildtools/build_wxwidgets.py index a54aea6f..62e1e476 100644 --- a/buildtools/build_wxwidgets.py +++ b/buildtools/build_wxwidgets.py @@ -290,6 +290,7 @@ def main(wxDir, args): "--enable-optimise", "--disable-debugreport", "--enable-uiactionsim", + "--enable-autoidman", ] if sys.platform.startswith("darwin"): diff --git a/docs/sphinx/_downloads/i18nwxapp/sampleapp.py b/docs/sphinx/_downloads/i18nwxapp/sampleapp.py index 567c026f..5dd740db 100644 --- a/docs/sphinx/_downloads/i18nwxapp/sampleapp.py +++ b/docs/sphinx/_downloads/i18nwxapp/sampleapp.py @@ -24,7 +24,7 @@ class AppI18N(sc.SizedFrame): # file menu fileMenu = wx.Menu() - closeMenuItem = fileMenu.Append(wx.NewId(), + closeMenuItem = fileMenu.Append(wx.ID_ANY, _(u"Close"), _(u"Close the application")) self.Bind(wx.EVT_MENU, self.onClose, closeMenuItem) @@ -32,7 +32,7 @@ class AppI18N(sc.SizedFrame): # edit menu manageMenu = wx.Menu() - manageSomethingMenuItem = manageMenu.Append(wx.NewId(), + manageSomethingMenuItem = manageMenu.Append(wx.ID_ANY, _(u"Edit something"), _(u"Edit an entry of something")) self.Bind(wx.EVT_MENU, self.doEditSomething, manageSomethingMenuItem) @@ -41,7 +41,7 @@ class AppI18N(sc.SizedFrame): # help menu helpMenu = wx.Menu() - aboutMenuItem = helpMenu.Append(wx.NewId(), + aboutMenuItem = helpMenu.Append(wx.ID_ANY, _(u"&About"), _(u"About the program")) self.Bind(wx.EVT_MENU, self.doAboutBox, aboutMenuItem) diff --git a/docs/sphinx/itemToModuleMap.json b/docs/sphinx/itemToModuleMap.json index a2966faa..c1ade90d 100644 --- a/docs/sphinx/itemToModuleMap.json +++ b/docs/sphinx/itemToModuleMap.json @@ -2378,6 +2378,7 @@ "NavigationKeyEventFlags":"wx.NavigationKeyEvent.", "NewEventType":"wx.", "NewId":"wx.", +"NewIdRef":"wx.", "NonOwnedWindow":"wx.", "Notebook":"wx.", "NotebookNameStr":"wx.", @@ -6674,6 +6675,7 @@ "WindowDestroyEvent":"wx.", "WindowDisabler":"wx.", "WindowID":"wx.", +"WindowIDRef":"wx.", "WindowModalDialogEvent":"wx.", "WindowVariant":"wx.", "WithImages":"wx.", diff --git a/docs/sphinx/rest_substitutions/overviews/events_overview.rst b/docs/sphinx/rest_substitutions/overviews/events_overview.rst index 7e09b1aa..6e7b2139 100644 --- a/docs/sphinx/rest_substitutions/overviews/events_overview.rst +++ b/docs/sphinx/rest_substitutions/overviews/events_overview.rst @@ -408,6 +408,6 @@ user-specified identifiers which must be always positive. You can use ``wx.ID_HIGHEST`` to determine the number above which it is safe to define your own identifiers. Or, you can use identifiers below ``wx.ID_LOWEST``. Finally, you can allocate identifiers dynamically -using :func:`wx.NewId` function too. If you use :func:`wx.NewId` +using :func:`wx.NewIdRef` function too. If you use :func:`wx.NewIdRef` consistently in your application, you can be sure that your identifiers don't conflict accidentally. diff --git a/docs/sphinx/rest_substitutions/overviews/standard_event_identifiers.rst b/docs/sphinx/rest_substitutions/overviews/standard_event_identifiers.rst index 7eba2141..f5eda8be 100644 --- a/docs/sphinx/rest_substitutions/overviews/standard_event_identifiers.rst +++ b/docs/sphinx/rest_substitutions/overviews/standard_event_identifiers.rst @@ -26,7 +26,7 @@ used by the user code and also are sometimes used by wxPython itself. These reserved identifiers are all in the range between ``wx.ID_LOWEST`` and ``wx.ID_HIGHEST`` and, accordingly, the user code should avoid defining its own constants in this range (e.g. by using -:func:`wx.NewId` ()). +:func:`wx.NewIdRef` ()). Refer to :ref:`the list of stock items ` for the subset of standard IDs which are stock IDs as well. diff --git a/etg/utils.py b/etg/utils.py index 405bdfbb..1bf00922 100644 --- a/etg/utils.py +++ b/etg/utils.py @@ -87,6 +87,15 @@ def run(): c.addPyMethod('__enter__', '(self)', 'return self') c.addPyMethod('__exit__', '(self, exc_type, exc_val, exc_tb)', 'pass') + f = module.find('wxNewId') + f.clearDeprecated() + f.deprecated = """\ +IDs generated by this function can possibly conflict with IDs used elsewhere in +the application code. It is recommended to instead use the ``wx.ID_ANY`` ID to +assign generated IDs for the controls menu items and etc. that you create in the +application. These IDs are guaranteed to not conflict with the other IDs that +are in use in the application. For those cases where you need to create an ID +that can be used more than once then please see :func:`wx.NewIdRef`.""".replace('\n', ' ') for funcname in ['wxBell', 'wxBeginBusyCursor', @@ -98,9 +107,10 @@ def run(): 'wxGetKeyState', 'wxGetMouseState', ]: - c = module.find(funcname) - c.mustHaveApp() + f = module.find(funcname) + f.mustHaveApp() + #----------------------------------------------------------------- tools.doCommonTweaks(module) diff --git a/etg/windowid.py b/etg/windowid.py index 099c6d4e..1060ac1d 100644 --- a/etg/windowid.py +++ b/etg/windowid.py @@ -9,6 +9,7 @@ import etgtools import etgtools.tweaker_tools as tools +from etgtools import ClassDef, MethodDef, ParamDef PACKAGE = "wx" MODULE = "_core" @@ -30,10 +31,82 @@ def run(): # Tweak the parsed meta objects in the module object as needed for # customizing the generated code and docstrings. + c = module.find('wxIdManager') + assert isinstance(c, etgtools.ClassDef) + # no tweaks needed for this class + # wxWindowIDRef is not documented (and probably rightly so) but we're going + # to use it from Python anyway to help with preallocating IDs in a way that + # allows them to be reused and be also be protected from conflicts from + # other auto allocated IDs. + + # First, add defintions of the existing C++ class and its elements + klass = ClassDef(name='wxWindowIDRef', bases = [], + briefDoc="""\ + A wxWindowIDRef object wraps an ID value and marks it as being in-use until all references to that ID are gone. + """, + items = [ + MethodDef(name='wxWindowIDRef', className='wxWindowIDRef', isCtor=True, + briefDoc='Default constructor', + overloads=[ + MethodDef(name='wxWindowIDRef', className='wxWindowIDRef', isCtor=True, + briefDoc='Create reference from an ID', + items=[ ParamDef(type='int', name='id') ]), + + MethodDef(name='wxWindowIDRef', className='wxWindowIDRef', isCtor=True, + briefDoc='Copy an ID reference', + items=[ ParamDef(type='const wxWindowIDRef&', name='idref') ]), + ]), + + MethodDef(name='~wxWindowIDRef', className='wxWindowIDRef', isDtor=True), + + MethodDef(type='int', name='GetValue', + briefDoc='Get the ID value'), + ]) + + # Now tweak it a bit + klass.addCppMethod('int', 'GetId', '()', + doc="Alias for GetValue allowing the IDRef to be passed as the source parameter to :meth:`wx.EvtHandler.Bind`.", + body="""\ + return self->GetValue(); + """) + klass.addCppMethod('int', '__int__', '()', + doc="Alias for GetValue allowing the IDRef to be passed as the WindowID parameter when creating widgets or etc.", + body="""\ + return self->GetValue(); + """) + + klass.addPyMethod('__repr__', '(self)', 'return "WindowIDRef: {}".format(self.GetId())') + + # and finish it up by adding it to the module + module.addItem(klass) + + # Now, let's add a new Python function to the global scope that reserves an + # ID (or range) and returns a ref object for it. + module.addPyFunction('NewIdRef', '(count=1)', + doc="""\ + Reserves a new Window ID (or range of WindowIDs) and returns a + :class:`wx.WindowIDRef` object (or list of them) that will help + manage the reservation of that ID. + + This function is intended to be a drop-in replacement of the old + and deprecated :func:`wx.NewId` function, with the added benefit + that the ID should never conflict with an in-use ID or other IDs + generated by this function. + """, + body="""\ + if count == 1: + return WindowIDRef(IdManager.ReserveId()) + else: + start = IdManager.ReserveId(count) + IDRefs = [] + for id in range(start, start+count): + IDRefs.append(WindowIDRef(id)) + return IDRefs + """) #----------------------------------------------------------------- tools.doCommonTweaks(module) diff --git a/etgtools/extractors.py b/etgtools/extractors.py index ea04899a..ffdffac7 100644 --- a/etgtools/extractors.py +++ b/etgtools/extractors.py @@ -26,10 +26,6 @@ from sphinxtools.utilities import findDescendants #--------------------------------------------------------------------------- # These classes simply hold various bits of information about the classes, # methods, functions and other items in the C/C++ API being wrapped. -# -# NOTE: Currently very little is being done with the docstrings. They can -# either be reprocessed later by the document generator or we can do more -# tinkering with them here. It just depends on decisions not yet made... #--------------------------------------------------------------------------- class BaseDef(object): @@ -83,10 +79,21 @@ class BaseDef(object): itemid = item.get('id') if itemid and itemid.startswith('deprecated'): self.deprecated = True - break + return - if self.deprecated: - break + + def clearDeprecated(self): + """ + Remove the deprecation notice from the detailedDoc, if any, and reset + self.deprecated to False. + """ + self.deprecated = False + for para in self.detailedDoc: + for item in para.iter(): + itemid = item.get('id') + if itemid and itemid.startswith('deprecated'): + self.detailedDoc.remove(para) + return def ignore(self, val=True): diff --git a/etgtools/sphinx_generator.py b/etgtools/sphinx_generator.py index 256aa889..61b9c581 100644 --- a/etgtools/sphinx_generator.py +++ b/etgtools/sphinx_generator.py @@ -1979,13 +1979,13 @@ class XMLDocString(object): else: raise Exception('Unhandled docstring kind for %s'%xml_item.__class__.__name__) - # Some of the Exctractors (xml item) will set deprecated themselves, in which case it is set as a + # Some of the Extractors (xml item) will set deprecated themselves, in which case it is set as a # non-empty string. In such cases, this branch will insert a deprecated section into the xml tree # so that the Node Tree (see classes above) will generate the deprecated tag on their own in self.RecurseXML if hasattr(xml_item, 'deprecated') and xml_item.deprecated and isinstance(xml_item.deprecated, string_base): element = et.Element('deprecated', kind='deprecated') element.text = xml_item.deprecated - + deprecated_section = Section(element, None, self.kind, self.is_overload, self.share_docstrings) self.root.AddSection(deprecated_section) diff --git a/src/core_ex.cpp b/src/core_ex.cpp index 29e92bad..bb6def49 100644 --- a/src/core_ex.cpp +++ b/src/core_ex.cpp @@ -199,6 +199,10 @@ void wxPyCoreModuleInject(PyObject* moduleDict) PyList_Append(PlatformInfo, obj); Py_DECREF(obj); +#if wxUSE_AUTOID_MANAGEMENT + _AddInfoString("autoidman"); +#endif + #undef _AddInfoString PyObject* PlatformInfoTuple = PyList_AsTuple(PlatformInfo); diff --git a/unittests/test_windowid.py b/unittests/test_windowid.py index 1146c786..02633495 100644 --- a/unittests/test_windowid.py +++ b/unittests/test_windowid.py @@ -1,18 +1,64 @@ import unittest import wx +from unittests import wtc #--------------------------------------------------------------------------- -class IdManagerTest(unittest.TestCase): +class IdManagerTest(wtc.WidgetTestCase): def test_idManager(self): id = wx.IdManager.ReserveId(5) self.assertTrue(id != wx.ID_NONE) - wx.IdManager.UnreserveId(id, 5) + def test_newIdRef01(self): + id = wx.NewIdRef() + assert isinstance(id, wx.WindowIDRef) + id.GetValue() + id.GetId() + id.__int__() + + + def test_newIdRef02(self): + refs = wx.NewIdRef(5) + assert len(refs) == 5 + for ref in refs: + assert isinstance(ref, wx.WindowIDRef) + + + def test_newIdRef03(self): + """Check that Auto ID Management is enabled (--enable-autoidman)""" + # This test is expected to fail if autoID mangagement is turned on + # because a reference to the ID is not being saved, so it will be + # unreserved when the first widget is destroyed. + + id = wx.Window.NewControlId() + b = wx.Button(self.frame, id, 'button') + b.Destroy() + + self.myYield() + + with self.assertRaises(wx.wxAssertionError): + b = wx.Button(self.frame, id, 'button') + b.Destroy() + + + def test_newIdRef04(self): + """Ensure that an ID can be used more than once""" + id = wx.NewIdRef() # but using this one should succeed + + b = wx.Button(self.frame, id, 'button') + b.Destroy() + + self.myYield() + + b = wx.Button(self.frame, id, 'button') + b.Destroy() + + + #---------------------------------------------------------------------------