diff --git a/CHANGES.txt b/CHANGES.txt index e8191ad75..b42ea27a8 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -21,6 +21,7 @@ Enhancements * Removed obsolete code (#112,#113) * Increased test coverage (#114) * Python 3 support (#115) + * Allow setting and resetting the global adaptation manager (#145) Fixes diff --git a/docs/source/traits_api_reference/traits.adaptation.rst b/docs/source/traits_api_reference/traits.adaptation.rst index 9cc499000..9765fe588 100644 --- a/docs/source/traits_api_reference/traits.adaptation.rst +++ b/docs/source/traits_api_reference/traits.adaptation.rst @@ -25,12 +25,6 @@ :undoc-members: :show-inheritance: -.. autofunction:: adapt -.. autofunction:: register_factory -.. autofunction:: register_offer -.. autofunction:: register_provides -.. autofunction:: supports_protocol - :mod:`adaptation_offer` Module ------------------------------ diff --git a/docs/source/traits_user_manual/advanced.rst b/docs/source/traits_user_manual/advanced.rst index 053452916..e941123ea 100644 --- a/docs/source/traits_user_manual/advanced.rst +++ b/docs/source/traits_user_manual/advanced.rst @@ -1017,6 +1017,25 @@ Gotchas the classes involved in adaptation are typically subclasses of :class:`~.HasTraits`, in which case this is not an issue. +2) The methods :func:`~traits.adaptation.adaptation_manager.register_factory`, + :func:`~traits.adaptation.adaptation_manager.adapt`, + etc. use a global adaptation manager, which is accessible through the + function + :func:`~traits.adaptation.adaptation_manager.get_global_adaptation_manager`. + The traits automatic adaptation features also use the global manager. + Having a global adaptation manager can get you into trouble, for the usual + reasons related to having a global state. If you want to have more control + over adaptation, we recommend creating a new + :class:`~traits.adaptation.adaptation_manager.AdaptationManager` + instance, use it directly in your application, and set it as the global + manager using + :func:`~traits.adaptation.adaptation_manager.set_global_adaptation_manager`. + A common issue with the global manager arises in unittesting, where adapters + registered in one test influence the outcome of other tests downstream. + Tests relying on adaptation should make sure to reset the state of the + global adapter using + :func:`~traits.adaptation.adaptation_manager.reset_global_adaptation_manager`. + Recommended readings about adaptation ````````````````````````````````````` diff --git a/traits/adaptation/adaptation_manager.py b/traits/adaptation/adaptation_manager.py index c8570be58..e22440107 100644 --- a/traits/adaptation/adaptation_manager.py +++ b/traits/adaptation/adaptation_manager.py @@ -361,20 +361,70 @@ def _by_weight_then_from_protocol_specificity(edge_1, edge_2): return 0 -#: The default global adaptation manager. +# The default global adaptation manager. +# PROVIDED FOR BACKWARD COMPATIBILITY ONLY, IT SHOULD NEVER BE USED DIRECTLY. +# If you must use a global adaptation manager, use the functions +# `get_global_adaptation_manager`, `reset_global_adaptation_manager`, +# `set_global_adaptation_manager`. adaptation_manager = AdaptationManager() +def set_global_adaptation_manager(new_adaptation_manager): + """ Set the global adaptation manager to the given instance. """ + global adaptation_manager + adaptation_manager = new_adaptation_manager + + +def reset_global_adaptation_manager(): + """ Set the global adaptation manager to a new AdaptationManager instance. + """ + global adaptation_manager + adaptation_manager = AdaptationManager() + + +def get_global_adaptation_manager(): + """ Set a reference to the global adaptation manager. """ + global adaptation_manager + return adaptation_manager + + # Convenience references to methods on the default adaptation manager. # # If you add a public method to the adaptation manager protocol then don't # forget to add a convenience function here! -adapt = adaptation_manager.adapt -register_factory = adaptation_manager.register_factory -register_offer = adaptation_manager.register_offer -register_provides = adaptation_manager.register_provides -supports_protocol = adaptation_manager.supports_protocol -provides_protocol = AdaptationManager.provides_protocol +def adapt(adaptee, to_protocol, default=AdaptationError): + """ Attempt to adapt an object to a given protocol. """ + manager = get_global_adaptation_manager() + return manager.adapt(adaptee, to_protocol, default) + + +def register_factory(factory, from_protocol, to_protocol): + """ Register an adapter factory. """ + manager = get_global_adaptation_manager() + return manager.register_factory(factory, from_protocol, to_protocol) + + +def register_offer(offer): + """ Register an offer to adapt from one protocol to another. """ + manager = get_global_adaptation_manager() + return manager.register_offer(offer) + + +def register_provides(provider_protocol, protocol): + """ Register that a protocol provides another. """ + manager = get_global_adaptation_manager() + return manager.register_provides(provider_protocol, protocol) + + +def supports_protocol(obj, protocol): + """ Does the object support a given protocol? """ + manager = get_global_adaptation_manager() + return manager.supports_protocol(obj, protocol) + + +def provides_protocol(type_, protocol): + """ Does the given type provide (i.e implement) a given protocol? """ + return AdaptationManager.provides_protocol(type_, protocol) #### EOF ###################################################################### diff --git a/traits/adaptation/api.py b/traits/adaptation/api.py index 465d10970..ebd28d4d5 100644 --- a/traits/adaptation/api.py +++ b/traits/adaptation/api.py @@ -3,7 +3,8 @@ from .adaptation_error import AdaptationError from .adaptation_manager import adapt, AdaptationManager, \ - provides_protocol, register_factory, register_offer, register_provides, \ - supports_protocol + get_global_adaptation_manager, provides_protocol, register_factory, \ + register_offer, register_provides, reset_global_adaptation_manager, \ + set_global_adaptation_manager, supports_protocol from .adaptation_offer import AdaptationOffer diff --git a/traits/adaptation/tests/test_adaptation_manager.py b/traits/adaptation/tests/test_adaptation_manager.py index 0060d3f80..995f971f2 100644 --- a/traits/adaptation/tests/test_adaptation_manager.py +++ b/traits/adaptation/tests/test_adaptation_manager.py @@ -1,4 +1,4 @@ -""" Test the adapter manager. """ +""" Test the adaptation manager. """ import sys @@ -9,7 +9,7 @@ class TestAdaptationManagerWithABC(unittest.TestCase): - """ Test the adapter manager. """ + """ Test the adaptation manager. """ #: Class attribute pointing at the module containing the example data examples = traits.adaptation.tests.abc_examples diff --git a/traits/adaptation/tests/test_global_adaptation_manager.py b/traits/adaptation/tests/test_global_adaptation_manager.py new file mode 100644 index 000000000..6cf256453 --- /dev/null +++ b/traits/adaptation/tests/test_global_adaptation_manager.py @@ -0,0 +1,123 @@ +""" Test the setting/getting/resetting/using the global adaptation manager. """ + +from traits.adaptation.api import adapt, AdaptationError, AdaptationManager, \ + AdaptationOffer, get_global_adaptation_manager, provides_protocol, \ + register_factory, register_provides, register_offer, \ + reset_global_adaptation_manager, set_global_adaptation_manager, \ + supports_protocol +import traits.adaptation.tests.abc_examples +from traits.testing.unittest_tools import unittest + + +class TestGlobalAdaptationManager(unittest.TestCase): + """ Test the setting/getting/resetting/using the global adaptation manager. + """ + + #: Class attribute pointing at the module containing the example data + examples = traits.adaptation.tests.abc_examples + + #### 'TestCase' protocol ################################################## + + def setUp(self): + """ Prepares the test fixture before each test method is called. """ + + reset_global_adaptation_manager() + + #### Tests ################################################################ + + def test_reset_adaptation_manager(self): + ex = self.examples + adaptation_manager = get_global_adaptation_manager() + + # UKStandard->EUStandard. + adaptation_manager.register_factory( + factory = ex.UKStandardToEUStandard, + from_protocol = ex.UKStandard, + to_protocol = ex.EUStandard, + ) + + # Create a UKPlug. + uk_plug = ex.UKPlug() + + reset_global_adaptation_manager() + adaptation_manager = get_global_adaptation_manager() + + with self.assertRaises(AdaptationError): + adaptation_manager.adapt(uk_plug, ex.EUStandard) + + def test_set_adaptation_manager(self): + ex = self.examples + adaptation_manager = AdaptationManager() + + # UKStandard->EUStandard. + adaptation_manager.register_factory( + factory = ex.UKStandardToEUStandard, + from_protocol = ex.UKStandard, + to_protocol = ex.EUStandard + ) + + # Create a UKPlug. + uk_plug = ex.UKPlug() + + set_global_adaptation_manager(adaptation_manager) + global_adaptation_manager = get_global_adaptation_manager() + + eu_plug = global_adaptation_manager.adapt(uk_plug, ex.EUStandard) + self.assertIsNotNone(eu_plug) + self.assertIsInstance(eu_plug, ex.UKStandardToEUStandard) + + def test_global_convenience_functions(self): + ex = self.examples + + # Global `register_factory`. + register_factory( + factory = ex.UKStandardToEUStandard, + from_protocol = ex.UKStandard, + to_protocol = ex.EUStandard + ) + + uk_plug = ex.UKPlug() + # Global `adapt`. + eu_plug = adapt(uk_plug, ex.EUStandard) + self.assertIsNotNone(eu_plug) + self.assertIsInstance(eu_plug, ex.UKStandardToEUStandard) + + # Global `provides_protocol`. + self.assertTrue(provides_protocol(ex.UKPlug, ex.UKStandard)) + + # Global `supports_protocol`. + self.assertTrue(supports_protocol(uk_plug, ex.EUStandard)) + + def test_global_register_provides(self): + from traits.api import Interface + + class IFoo(Interface): + pass + + obj = {} + # Global `register_provides`. + register_provides(dict, IFoo) + self.assertEqual(obj, adapt(obj, IFoo)) + + def test_global_register_offer(self): + ex = self.examples + + offer = AdaptationOffer( + factory = ex.UKStandardToEUStandard, + from_protocol = ex.UKStandard, + to_protocol = ex.EUStandard + ) + + # Global `register_offer`. + register_offer(offer) + + uk_plug = ex.UKPlug() + eu_plug = adapt(uk_plug, ex.EUStandard) + self.assertIsNotNone(eu_plug) + self.assertIsInstance(eu_plug, ex.UKStandardToEUStandard) + + +if __name__ == '__main__': + unittest.main() + +#### EOF ###################################################################### diff --git a/traits/tests/test_interface_checker.py b/traits/tests/test_interface_checker.py index aa78a6435..b7cbd8610 100644 --- a/traits/tests/test_interface_checker.py +++ b/traits/tests/test_interface_checker.py @@ -14,6 +14,7 @@ from traits.testing.unittest_tools import unittest # Enthought library imports. +from traits.adaptation.api import reset_global_adaptation_manager from traits.api import Adapter, HasTraits, Instance, Int, Interface, \ provides, register_factory @@ -35,6 +36,7 @@ class InterfaceCheckerTestCase(unittest.TestCase): def setUp(self): """ Prepares the test fixture before each test method is called. """ + reset_global_adaptation_manager() return diff --git a/traits/tests/test_interfaces.py b/traits/tests/test_interfaces.py index 0106e8350..8ebfe4186 100644 --- a/traits/tests/test_interfaces.py +++ b/traits/tests/test_interfaces.py @@ -20,6 +20,7 @@ from traits.api import (HasTraits, Adapter, AdaptsTo, Instance, Int, Interface, List, provides, register_factory, Supports, TraitError) +from traits.adaptation.api import reset_global_adaptation_manager class IFoo(Interface): @@ -144,14 +145,22 @@ def get_foo(self): def get_foo_plus(self): return (self.obj.get_foo() + 1) -register_factory(SampleListAdapter, Sample, IList) -register_factory(ListAverageAdapter, IList, IAverage) -register_factory(SampleFooAdapter, Sample, IFoo) -register_factory(FooPlusAdapter, IFoo, IFooPlus) - class InterfacesTest(unittest.TestCase): + #### 'TestCase' protocol ################################################## + + def setUp(self): + reset_global_adaptation_manager() + + # Register adapters. + register_factory(SampleListAdapter, Sample, IList) + register_factory(ListAverageAdapter, IList, IAverage) + register_factory(SampleFooAdapter, Sample, IFoo) + register_factory(FooPlusAdapter, IFoo, IFooPlus) + + #### Tests ################################################################ + def test_provides_none(self): @provides() class Test(HasTraits): diff --git a/traits/tests/test_interfaces_with_implements.py b/traits/tests/test_interfaces_with_implements.py index 79bc6646f..4d7ededd7 100644 --- a/traits/tests/test_interfaces_with_implements.py +++ b/traits/tests/test_interfaces_with_implements.py @@ -23,11 +23,19 @@ import sys from traits.testing.unittest_tools import unittest - +from traits.adaptation.api import get_global_adaptation_manager, \ + set_global_adaptation_manager from traits.api import HasTraits, Adapter, adapts, AdaptsTo, \ implements, Instance, Int, Interface, List, Supports, TraitError +# Using the deprecated class advisor "adapts", the registration of adapters +# occurs globally at class definition time. Since other tests will reset the +# global adaptation manager, the registration will be lost. +# That's why we save a reference to the current global adaptation manager. +_adaptation_manager = get_global_adaptation_manager() + + if sys.version_info[0] >= 3: import nose raise nose.SkipTest(""" @@ -174,6 +182,13 @@ def get_foo_plus(self): class InterfacesTest(unittest.TestCase): + #### 'TestCase' protocol ################################################## + + def setUp(self): + set_global_adaptation_manager(_adaptation_manager) + + #### Tests ################################################################ + def test_implements_none(self): class Test(HasTraits): implements()