Skip to content

Commit

Permalink
Adds proper implementation of Lookup.get_default()
Browse files Browse the repository at this point in the history
  • Loading branch information
AxelVoitier committed Jul 3, 2021
1 parent 97b1bd9 commit ae053c5
Show file tree
Hide file tree
Showing 6 changed files with 208 additions and 13 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
53 changes: 47 additions & 6 deletions lookups/lookup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
Expand Down
13 changes: 12 additions & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 6 additions & 6 deletions tests/test_entry_point.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand All @@ -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)
Expand All @@ -76,15 +76,15 @@ 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)


@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, )

Expand All @@ -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)

Expand Down
141 changes: 141 additions & 0 deletions tests/test_lookup_default.py
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit ae053c5

Please sign in to comment.