diff --git a/CHANGELOG.md b/CHANGELOG.md index 278d6fe..34f54a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ CHANGELOG - Adds a EntryPointLookup. - Adds a DelegatedLookup. - Adds a ProxyLookup. +- Adds a proper resolution for system default lookup Lookup.get_default(). - Fixes issue with listeners registration disappearing immediately when using object-bound methods. - Content of a GenericLookup can now behave like a Container (ie. you can do things like "obj in content"). - When an instance is not hashable, provides an alternative using id() of the object in order to be diff --git a/README.md b/README.md index c019c6a..8e82657 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,7 @@ my_content.remove(child1) ## Other lookups +* `lookups.Lookup.get_default()`: The default lookup in a system. * `lookups.ProxyLookup`: A lookup that merge results from several lookups. * `lookups.DelegatedLookup`: A lookup that redirects to another (dynamic) lookup, through a LookupProvider. * `lookups.EntryPointLookup`: A lookup loading its instances from a setuptools entry point group (ie. provided by any installed package). diff --git a/lookups/lookup.py b/lookups/lookup.py index ed592d4..2317a34 100644 --- a/lookups/lookup.py +++ b/lookups/lookup.py @@ -24,25 +24,66 @@ class Lookup(ABC): A general registry permitting clients to find instances of services (implementation of a given interface). - This class is inspired Netbeans Platform lookup mechanism. The Lookup API concentrates on the + This class is inspired by Netbeans Platform lookup mechanism. The Lookup API concentrates on the lookup, not on the registration. ''' + _DEFAULT_LOOKUP: Lookup = None # type: ignore + _DEFAULT_LOOKUP_PROVIDER: LookupProvider = None # type: ignore + _DEFAULT_ENTRY_POINT_GROUP = 'lookup.default' + @classmethod def get_default(cls) -> Lookup: ''' Method to obtain the global lookup in the whole system. The actual returned implementation can be different in different systems, but the default - one is based on lookup.Lookups.???. + one is based on lookups.ProxyLookup. + + The resolution is the following: + - If there is already a default lookup provider defined, returns its lookup. + - If there is already a default lookup defined, returns it. + - Loads on EntryPointLookup on 'lookup.default' group: + - If it finds a lookup which happens to also be a lookup provider, returns its lookup. + - If it finds a lookup, returns it. + - If it finds a lookup provider, returns DelegatedLookup from it. + - Otherwise, returns a ProxyLookup with just the EntryPointLookup as source. :return: The global lookup in the system :rtype: Lookup ''' - # Temporary solution - from .generic_lookup import GenericLookup - from .instance_content import InstanceContent - return GenericLookup(InstanceContent()) + + if (cls._DEFAULT_LOOKUP is not None) or (cls._DEFAULT_LOOKUP_PROVIDER is not None): + if cls._DEFAULT_LOOKUP_PROVIDER is not None: + lookup = cls._DEFAULT_LOOKUP_PROVIDER.get_lookup() + if lookup is not None: + return lookup + + return cls._DEFAULT_LOOKUP + + from .entry_point import EntryPointLookup + epl = EntryPointLookup(cls._DEFAULT_ENTRY_POINT_GROUP) + cls._DEFAULT_LOOKUP = epl.lookup(Lookup) + if cls._DEFAULT_LOOKUP is not None: + if isinstance(cls._DEFAULT_LOOKUP, LookupProvider): + cls._DEFAULT_LOOKUP_PROVIDER = cls._DEFAULT_LOOKUP + lookup = cls._DEFAULT_LOOKUP_PROVIDER.get_lookup() + if lookup is not None: + return lookup + + return cls._DEFAULT_LOOKUP + + provider = epl.lookup(LookupProvider) + if provider is not None: + from .delegated_lookup import DelegatedLookup + cls._DEFAULT_LOOKUP = DelegatedLookup(provider) + + return cls._DEFAULT_LOOKUP + + from .proxy_lookup import ProxyLookup + cls._DEFAULT_LOOKUP = ProxyLookup(epl) + + return cls._DEFAULT_LOOKUP @abstractmethod def lookup(self, cls: Type[object]) -> Optional[object]: diff --git a/setup.cfg b/setup.cfg index 39922c4..8d6c677 100644 --- a/setup.cfg +++ b/setup.cfg @@ -54,10 +54,21 @@ dev = check-manifest [options.entry_points] -lookup.tests = +lookups.test_entry_point = parent = tests.tools:TestParentObject child = tests.tools:TestChildObject other = tests.tools:TestOtherObject +lookups.test_default_lookup = + a_lookup = tests.test_lookup_default:DefaultLookup +lookups.test_default_lookup_lookup_provider = + a_lookup = tests.test_lookup_default:DefaultLookupLookupProvider +lookups.test_default_lookup_provider = + a_lookup = tests.test_lookup_default:DefaulLookupProvider +lookups.test_default_no_lookup = + parent = tests.tools:TestParentObject + child = tests.tools:TestChildObject + other = tests.tools:TestOtherObject +lookups.test_default_empty_entry_point_group = [flake8] max-line-length = 100 diff --git a/tests/test_entry_point.py b/tests/test_entry_point.py index 6a8b78d..6f088e6 100644 --- a/tests/test_entry_point.py +++ b/tests/test_entry_point.py @@ -51,7 +51,7 @@ def check_item(expected_classes, item): def test_instantiation(): - assert EntryPointLookup('lookup.tests') + assert EntryPointLookup('lookups.test_entry_point') def test_non_existant_group(): @@ -60,14 +60,14 @@ def test_non_existant_group(): @pytest.mark.parametrize('search, expected_classes', MEMBER_FIXTURES) def test_lookup(search, expected_classes): - lookup = EntryPointLookup('lookup.tests') + lookup = EntryPointLookup('lookups.test_entry_point') assert isinstance(lookup.lookup(search), expected_classes) @pytest.mark.parametrize('search, expected_classes', MEMBER_FIXTURES) def test_lookup_item(search, expected_classes): - lookup = EntryPointLookup('lookup.tests') + lookup = EntryPointLookup('lookups.test_entry_point') item = lookup.lookup_item(search) check_item(expected_classes, item) @@ -76,7 +76,7 @@ def test_lookup_item(search, expected_classes): @pytest.mark.parametrize('search, expected_classes', MEMBER_FIXTURES) def test_lookup_all(search, expected_classes): - lookup = EntryPointLookup('lookup.tests') + lookup = EntryPointLookup('lookups.test_entry_point') all_instances = lookup.lookup_all(search) check_all_instances(expected_classes, all_instances) @@ -84,7 +84,7 @@ def test_lookup_all(search, expected_classes): @pytest.mark.parametrize('search, expected_classes', MEMBER_FIXTURES) def test_lookup_result(search, expected_classes): - lookup = EntryPointLookup('lookup.tests') + lookup = EntryPointLookup('lookups.test_entry_point') if not isinstance(expected_classes, Sequence): expected_classes = (expected_classes, ) @@ -110,7 +110,7 @@ def test_lookup_result(search, expected_classes): @pytest.mark.parametrize('search, expected_classes', MEMBER_FIXTURES) def test_listeners(search, expected_classes): - lookup = EntryPointLookup('lookup.tests') + lookup = EntryPointLookup('lookups.test_entry_point') result = lookup.lookup_result(search) diff --git a/tests/test_lookup_default.py b/tests/test_lookup_default.py new file mode 100644 index 0000000..66fd7d4 --- /dev/null +++ b/tests/test_lookup_default.py @@ -0,0 +1,141 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021 Contributors as noted in the AUTHORS file +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +# System imports + +# Third-party imports +import pytest + +# Local imports +from lookups import Lookup, LookupProvider, DelegatedLookup, ProxyLookup +from lookups.singleton import SingletonLookup +from .tools import TestParentObject, TestChildObject, TestOtherObject + + +class DefaultLookup(SingletonLookup): + + def __init__(self): + super().__init__(TestParentObject()) + + +class DefaulLookupProvider(LookupProvider): + + def get_lookup(self): + return SingletonLookup(TestParentObject()) + + +class DefaultLookupLookupProvider(SingletonLookup, LookupProvider): + + def __init__(self): + super().__init__(TestOtherObject()) + self._lookup = SingletonLookup(TestParentObject()) + + def get_lookup(self): + return self._lookup + + +@pytest.fixture +def cleanup(): + Lookup._DEFAULT_LOOKUP = None + Lookup._DEFAULT_LOOKUP_PROVIDER = None + Lookup._DEFAULT_ENTRY_POINT_GROUP = 'lookup.default' + + yield + + Lookup._DEFAULT_LOOKUP = None + Lookup._DEFAULT_LOOKUP_PROVIDER = None + Lookup._DEFAULT_ENTRY_POINT_GROUP = 'lookup.default' + + +def test_default_lookup(cleanup): + Lookup._DEFAULT_ENTRY_POINT_GROUP = 'lookups.test_default_lookup' + dflt = Lookup.get_default() + assert dflt + assert isinstance(dflt, DefaultLookup) + + all_instances = dflt.lookup_all(object) + assert len(all_instances) == 1 + assert isinstance(all_instances[0], TestParentObject) + + assert Lookup.get_default() is dflt + + assert Lookup._DEFAULT_LOOKUP is dflt + assert Lookup._DEFAULT_LOOKUP_PROVIDER is None + + +def test_default_lookup_lookup_provider(cleanup): + Lookup._DEFAULT_ENTRY_POINT_GROUP = 'lookups.test_default_lookup_lookup_provider' + dflt = Lookup.get_default() + assert dflt + assert isinstance(dflt, SingletonLookup) + + all_instances = dflt.lookup_all(object) + assert len(all_instances) == 1 + assert isinstance(all_instances[0], TestParentObject) + + assert Lookup.get_default() is dflt + + assert isinstance(Lookup._DEFAULT_LOOKUP, DefaultLookupLookupProvider) + assert Lookup._DEFAULT_LOOKUP_PROVIDER is Lookup._DEFAULT_LOOKUP + + # Try have the provider return None + Lookup._DEFAULT_LOOKUP_PROVIDER._lookup = None + new_dflt = Lookup.get_default() + assert new_dflt is Lookup._DEFAULT_LOOKUP + + all_instances = new_dflt.lookup_all(object) + assert len(all_instances) == 1 + assert isinstance(all_instances[0], TestOtherObject) + + +def test_default_lookup_provider(cleanup): + Lookup._DEFAULT_ENTRY_POINT_GROUP = 'lookups.test_default_lookup_provider' + dflt = Lookup.get_default() + assert dflt + assert isinstance(dflt, DelegatedLookup) + + all_instances = dflt.lookup_all(object) + assert len(all_instances) == 1 + assert isinstance(all_instances[0], TestParentObject) + + assert Lookup.get_default() is dflt + + assert Lookup._DEFAULT_LOOKUP is dflt + assert Lookup._DEFAULT_LOOKUP_PROVIDER is None + + +def test_default_no_lookup(cleanup): + Lookup._DEFAULT_ENTRY_POINT_GROUP = 'lookups.test_default_no_lookup' + dflt = Lookup.get_default() + assert dflt + assert isinstance(dflt, ProxyLookup) + + all_instances = dflt.lookup_all(object) + assert all_instances + assert (len(all_instances) % 3) == 0 # Because pytest can double up our entry points + assert {type(instance) for instance in all_instances} == set([ + TestParentObject, TestChildObject, TestOtherObject]) + + assert Lookup.get_default() is dflt + + assert Lookup._DEFAULT_LOOKUP is dflt + assert Lookup._DEFAULT_LOOKUP_PROVIDER is None + + +def test_default_empty_entry_point_group(cleanup): + Lookup._DEFAULT_ENTRY_POINT_GROUP = 'lookups.test_default_empty_entry_point_group' + dflt = Lookup.get_default() + assert dflt + assert isinstance(dflt, ProxyLookup) + + all_instances = dflt.lookup_all(object) + assert not all_instances + + assert Lookup.get_default() is dflt + + assert Lookup._DEFAULT_LOOKUP is dflt + assert Lookup._DEFAULT_LOOKUP_PROVIDER is None