Skip to content

Commit 3a9d807

Browse files
Poruri Sai Rahulmdickinson
andauthored
Backport PRs #421, #422 and #423 (#427)
* Revert PR #354 (#422) * Revert "Support observe("name:items") for ExtensionPoint [Requires Traits 6.1] (#354)" This reverts commit d62af08. * TST/FIX : Revert test that used observe to listen to changes to extension point items. The test now uses a static trait change handler like before modified: envisage/tests/test_plugin.py * Regression test for #417 (#421) * Add failing test * STY: Fix line spacing issues Co-authored-by: Poruri Sai Rahul <[email protected]> * Fix test suite dependence on ipykernel (#423) * Fix hard test suite dependence on ipykernel * Update envisage/tests/test_ids.py Co-authored-by: Poruri Sai Rahul <[email protected]> Co-authored-by: Poruri Sai Rahul <[email protected]> * DOC : Update changelog modified: CHANGES.rst Co-authored-by: Mark Dickinson <[email protected]>
1 parent 76111ea commit 3a9d807

File tree

7 files changed

+196
-599
lines changed

7 files changed

+196
-599
lines changed

CHANGES.rst

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,28 @@
22
Envisage CHANGELOG
33
====================
44

5+
Version 6.0.1
6+
=============
7+
8+
Released: 2021-06-18
9+
10+
This bugfix release fixes the issue where Extension Point resolution was
11+
happening too eagerly, which caused issues during application startup time in
12+
certain cases. We recommend all users of Envisage to upgrade to this bugfix
13+
version.
14+
15+
Fixes
16+
-----
17+
18+
- Revert PR #354, which caused the issue #417. (#422)
19+
20+
Tests
21+
-----
22+
23+
- Ensure that the testsuite passes with minimal dependencies. (#423)
24+
- Add a regression test for issue #417. (#421)
25+
26+
527
Version 6.0.0
628
=============
729

envisage/extension_point.py

Lines changed: 18 additions & 204 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,11 @@
1111

1212

1313
# Standard library imports.
14-
from functools import wraps
1514
import inspect
16-
import warnings
1715
import weakref
1816

1917
# Enthought library imports.
2018
from traits.api import List, TraitType, Undefined, provides
21-
from traits.trait_list_object import TraitList
2219

2320
# Local imports.
2421
from .i_extension_point import IExtensionPoint
@@ -118,14 +115,14 @@ def __repr__(self):
118115

119116
def get(self, obj, trait_name):
120117
""" Trait type getter. """
121-
cache_name = _get_cache_name(trait_name)
122-
if cache_name not in obj.__dict__:
123-
_update_cache(obj, trait_name)
124118

125-
value = obj.__dict__[cache_name]
126-
# validate again
127-
self.trait_type.validate(obj, trait_name, value[:])
128-
return value
119+
extension_registry = self._get_extension_registry(obj)
120+
121+
# Get the extensions to this extension point.
122+
extensions = extension_registry.get_extensions(self.id)
123+
124+
# Make sure the contributions are of the appropriate type.
125+
return self.trait_type.validate(obj, trait_name, extensions)
129126

130127
def set(self, obj, name, value):
131128
""" Trait type setter. """
@@ -154,41 +151,21 @@ def connect(self, obj, trait_name):
154151
"""
155152

156153
def listener(extension_registry, event):
157-
""" Listener called when an extension point is changed.
158-
159-
Parameters
160-
----------
161-
extension_registry : IExtensionRegistry
162-
Registry that maintains the extensions.
163-
event : ExtensionPointChangedEvent
164-
Event created for the change.
165-
If the event.index is None, this means the entire extensions
166-
list is set to a new value. If the event.index is not None,
167-
some portion of the list has been modified.
168-
"""
169-
if event.index is not None:
170-
# We know where in the list is changed.
154+
""" Listener called when an extension point is changed. """
171155

172-
# Mutate the _ExtensionPointValue to fire ListChangeEvent
173-
# expected from observing item change.
174-
getattr(obj, trait_name)._sync_values(event)
175-
176-
# For on_trait_change('name_items')
177-
obj.trait_property_changed(
178-
trait_name + "_items", Undefined, event
179-
)
156+
# If an index was specified then we fire an '_items' changed event.
157+
if event.index is not None:
158+
name = trait_name + "_items"
159+
old = Undefined
160+
new = event
180161

162+
# Otherwise, we fire a normal trait changed event.
181163
else:
182-
# The entire list has changed. We reset the cache and fire a
183-
# normal trait changed event.
184-
_update_cache(obj, trait_name)
164+
name = trait_name
165+
old = event.removed
166+
new = event.added
185167

186-
# In case the cache was created first and the registry is then mutated
187-
# before this ``connect`` is called, the internal cache would be in
188-
# an inconsistent state. This also has the side-effect of firing
189-
# another change event, hence allowing future changes to be observed
190-
# without having to access the trait first.
191-
_update_cache(obj, trait_name)
168+
obj.trait_property_changed(name, old, new)
192169

193170
extension_registry = self._get_extension_registry(obj)
194171

@@ -232,166 +209,3 @@ def _get_extension_registry(self, obj):
232209
)
233210

234211
return extension_registry
235-
236-
237-
def _warn_if_not_internal(func):
238-
""" Decorator for instance methods of _ExtensionPointValue such that its
239-
effect is nullified if the function is not called with the _internal_use
240-
flag set to true.
241-
"""
242-
243-
@wraps(func)
244-
def decorated(object, *args, **kwargs):
245-
if not object._internal_use:
246-
warnings.warn(
247-
"Extension point cannot be mutated directly.",
248-
RuntimeWarning,
249-
stacklevel=2,
250-
)
251-
# This restores the existing behavior where the operation
252-
# is acted on a list object that is not persisted.
253-
return func(TraitList(iter(object)), *args, **kwargs)
254-
return func(object, *args, **kwargs)
255-
256-
return decorated
257-
258-
259-
class _ExtensionPointValue(TraitList):
260-
""" _ExtensionPointValue is the list being returned while retrieving the
261-
attribute value for an ExtensionPoint trait.
262-
263-
This list returned for an ExtensionPoint acts as a proxy to query
264-
extensions in an ExtensionRegistry for a given extension point id. Users of
265-
ExtensionPoint expect to handle a list-like object, and expect to be able
266-
to listen to "mutation" on the list. The ExtensionRegistry remains to be
267-
the source of truth as to what extensions are available for a given
268-
extension point ID.
269-
270-
Users are not expected to mutate the list directly. All mutations to
271-
extensions are expected to go through the extension registry to maintain
272-
consistency. With that, all methods for mutating the list are nullified,
273-
unless it is used internally.
274-
275-
The requirement to support ``observe("name:items")`` means this list,
276-
associated with `name`, cannot be a property that gets recomputed on every
277-
access (enthought/traits#624), it needs to be cached. As with any
278-
cached quantity, it needs to be synchronized with the ExtensionRegistry.
279-
280-
Note that the list can only be synchronized with the extension registry
281-
when the listeners are connected (see ``ExtensionPoint.connect``).
282-
283-
Parameters
284-
----------
285-
iterable : iterable
286-
Iterable providing the items for the list
287-
"""
288-
289-
def __new__(cls, *args, **kwargs):
290-
# Methods such as 'append' or 'extend' may be called during unpickling.
291-
# Initialize internal flag to true which gets changed back to false
292-
# in __init__.
293-
self = super().__new__(cls)
294-
self._internal_use = True
295-
return self
296-
297-
def __init__(self, *args, **kwargs):
298-
super().__init__(*args, **kwargs)
299-
300-
# Flag to control access for mutating the list. Only internal
301-
# code can mutate the list. See _sync_values
302-
self._internal_use = False
303-
304-
def _sync_values(self, event):
305-
""" Given an ExtensionPointChangedEvent, modify the values in this list
306-
to match. This is an internal method only used by Envisage code.
307-
308-
Parameters
309-
----------
310-
event : ExtenstionPointChangedEvent
311-
Event being fired for extension point values changed (typically
312-
via the extension registry)
313-
"""
314-
self._internal_use = True
315-
try:
316-
if isinstance(event.index, slice):
317-
if event.added:
318-
self[event.index] = event.added
319-
else:
320-
del self[event.index]
321-
else:
322-
slice_ = slice(
323-
event.index, event.index + len(event.removed)
324-
)
325-
self[slice_] = event.added
326-
finally:
327-
self._internal_use = False
328-
329-
__delitem__ = _warn_if_not_internal(TraitList.__delitem__)
330-
__iadd__ = _warn_if_not_internal(TraitList.__iadd__)
331-
__imul__ = _warn_if_not_internal(TraitList.__imul__)
332-
__setitem__ = _warn_if_not_internal(TraitList.__setitem__)
333-
append = _warn_if_not_internal(TraitList.append)
334-
clear = _warn_if_not_internal(TraitList.clear)
335-
extend = _warn_if_not_internal(TraitList.extend)
336-
insert = _warn_if_not_internal(TraitList.insert)
337-
pop = _warn_if_not_internal(TraitList.pop)
338-
remove = _warn_if_not_internal(TraitList.remove)
339-
reverse = _warn_if_not_internal(TraitList.reverse)
340-
sort = _warn_if_not_internal(TraitList.sort)
341-
342-
343-
def _get_extensions(object, name):
344-
""" Return the extensions reported by the extension registry for the
345-
given object and the name of a trait whose type is an ExtensionPoint.
346-
347-
Parameters
348-
----------
349-
object : HasTraits
350-
Object on which an ExtensionPoint is defined
351-
name : str
352-
Name of the trait whose trait type is an ExtensionPoint.
353-
354-
Returns
355-
-------
356-
extensions : list
357-
All the extensions for the extension point.
358-
"""
359-
extension_point = object.trait(name).trait_type
360-
extension_registry = extension_point._get_extension_registry(object)
361-
362-
# Get the extensions to this extension point.
363-
return extension_registry.get_extensions(extension_point.id)
364-
365-
366-
def _get_cache_name(trait_name):
367-
""" Return the attribute name on the object for storing the cached
368-
extension point value associated with a given trait.
369-
370-
Parameters
371-
----------
372-
trait_name : str
373-
The name of the trait for which ExtensionPoint is defined.
374-
"""
375-
return "__envisage_{}".format(trait_name)
376-
377-
378-
def _update_cache(obj, trait_name):
379-
""" Update the internal cached value for the extension point and
380-
fire change event.
381-
382-
Parameters
383-
----------
384-
obj : HasTraits
385-
The object on which an ExtensionPoint is defined.
386-
trait_name : str
387-
The name of the trait for which ExtensionPoint is defined.
388-
"""
389-
cache_name = _get_cache_name(trait_name)
390-
old = obj.__dict__.get(cache_name, Undefined)
391-
new = (
392-
_ExtensionPointValue(
393-
_get_extensions(obj, trait_name)
394-
)
395-
)
396-
obj.__dict__[cache_name] = new
397-
obj.trait_property_changed(trait_name, old, new)

envisage/tests/test_application.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,53 @@ class PluginC(Plugin):
111111
x = List(Int, [98, 99, 100], contributes_to="a.x")
112112

113113

114+
# PluginD and PluginE each contribute to the other's extension points, but both
115+
# expect to be started before contributions are made.
116+
# xref: enthought/envisage#417
117+
118+
119+
class PluginD(Plugin):
120+
""" Plugin that expects to be started before contributing to
121+
extension points. """
122+
123+
id = "D"
124+
x = ExtensionPoint(List, id="d.x")
125+
126+
y = List(Int, contributes_to="e.x")
127+
128+
started = Bool(False)
129+
130+
def start(self):
131+
self.started = True
132+
133+
def _y_default(self):
134+
if self.started:
135+
return [4, 5, 6]
136+
else:
137+
return []
138+
139+
140+
class PluginE(Plugin):
141+
""" Another plugin that expects to be started before contributing to
142+
extension points. """
143+
144+
id = "E"
145+
x = ExtensionPoint(List, id="e.x")
146+
147+
y = List(Int, contributes_to="d.x")
148+
149+
started = Bool(False)
150+
151+
def start(self):
152+
self.started = True
153+
154+
def _y_default(self):
155+
if self.started:
156+
return [1, 2, 3]
157+
else:
158+
return []
159+
160+
114161
class ApplicationTestCase(unittest.TestCase):
115162
""" Tests for applications and plugins. """
116163

@@ -265,6 +312,27 @@ def test_extension_point(self):
265312
self.assertEqual(6, len(extensions))
266313
self.assertEqual([1, 2, 3, 98, 99, 100], extensions)
267314

315+
def test_extension_point_resolution_occurs_after_plugin_start(self):
316+
# Regression test for enthought/envisage#417
317+
318+
# Given
319+
d = PluginD()
320+
e = PluginE()
321+
application = TestApplication(plugins=[d, e])
322+
323+
# When
324+
application.start()
325+
326+
# Then
327+
self.assertEqual(
328+
application.get_extensions("d.x"),
329+
[1, 2, 3],
330+
)
331+
self.assertEqual(
332+
application.get_extensions("e.x"),
333+
[4, 5, 6],
334+
)
335+
268336
def test_add_extension_point_listener(self):
269337
""" add extension point listener """
270338

0 commit comments

Comments
 (0)