Add support for entry points

Change-Id: I11fd40645479a815b29afaac0b12d45f6f7e6c21
This commit is contained in:
Yves-Gwenael Bourhis 2019-03-12 16:04:51 +01:00
parent 98a0fb2405
commit 02bcffc174
5 changed files with 144 additions and 12 deletions

View File

@ -245,10 +245,44 @@ How to extend
Given the ever-widening OpenStack ecosystem, OSPurge can't support every Given the ever-widening OpenStack ecosystem, OSPurge can't support every
OpenStack services. We intend to support in-tree, only the 'core' services. OpenStack services. We intend to support in-tree, only the 'core' services.
Fortunately, OSPurge is easily extensible. All you have to do is add a new Fortunately, OSPurge is easily extensible. There are 2 methods and you can
Python module in the ``resources`` package and define one or more Python chose the one you prefer:
class(es) that subclass ``ospurge.resources.base.ServiceResource``. Your module
will automatically be loaded and your methods called. Have a look at the 1: Add a new Python module in the ``resources`` package and define one or more
Python class(es) that subclass ``ospurge.resources.base.ServiceResource``.
Your module will automatically be loaded and your methods called.
2: Create your standalone python modules and in your module's setup.py or
setup.cfg file add an entry point to ``ospurge_resources`` pointing to the
python module in which you subclass ``ospurge.resources.base.ServiceResource``.
setup.py example::
from setuptools import setup
setup(
name='my_ospurge_extension',
entry_points={
'ospurge_resources': [
'foo = my_module.submodule_with_subclass',
],
}
)
setup.cfg example::
[entry_points]
ospurge_resources =
foo = my_module.submodule_with_subclass
Once your module installed, it will automatically be loaded and your methods
called.
More examples on entry points:
https://amir.rachum.com/blog/2017/07/28/python-entry-points/
Have a look
at the
``main.main`` and ``main.runner`` functions to fully understand the mechanism. ``main.main`` and ``main.runner`` functions to fully understand the mechanism.
Note: We won't accept any patch that broaden what OSPurge supports, beyond Note: We won't accept any patch that broaden what OSPurge supports, beyond

View File

@ -0,0 +1,28 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from ospurge.resources.base import ServiceResource
class Foo(ServiceResource):
ORDER = 15
def list(self):
return []
def delete(self, resource):
pass
@staticmethod
def to_str(resource):
return "Foo"

View File

@ -10,9 +10,15 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import logging import logging
import os
import types
import typing import typing
import unittest import unittest
import pkg_resources
import six
import shade import shade
from ospurge.resources.base import ServiceResource from ospurge.resources.base import ServiceResource
@ -20,6 +26,21 @@ from ospurge.tests import mock
from ospurge import utils from ospurge import utils
def register_test_entry_point():
test_resource_file = os.path.abspath(
os.path.join(
os.path.dirname(__file__), 'resources/entry_points.py'
)
)
distribution = pkg_resources.Distribution.from_filename(test_resource_file)
entry_point = pkg_resources.EntryPoint(
'foo', 'ospurge.tests.resources.entry_points', dist=distribution
)
distribution._ep_map = {utils.ENTRY_POINTS_NAME: {'foo': entry_point}}
pkg_resources.working_set.add(distribution, 'foo')
return entry_point
class TestUtils(unittest.TestCase): class TestUtils(unittest.TestCase):
def test_replace_project_info_in_config(self): def test_replace_project_info_in_config(self):
config = { config = {
@ -43,6 +64,25 @@ class TestUtils(unittest.TestCase):
} }
}) })
def test_load_ospurge_resource_modules(self):
modules = utils.load_ospurge_resource_modules()
self.assertIsInstance(modules, typing.Dict)
for name, module in six.iteritems(modules):
# assertIsInstance(name, typing.AnyStr) fails with:
# TypeError: Type variables cannot be used with isinstance().
self.assertIsInstance(name, six.string_types)
self.assertIsInstance(module, types.ModuleType)
def test_load_entry_points_modules(self):
register_test_entry_point()
modules = utils.load_entry_points_modules()
self.assertIsInstance(modules, typing.Dict)
for name, module in six.iteritems(modules):
# assertIsInstance(name, typing.AnyStr) fails with:
# TypeError: Type variables cannot be used with isinstance().
self.assertIsInstance(name, six.string_types)
self.assertIsInstance(module, types.ModuleType)
def test_get_all_resource_classes(self): def test_get_all_resource_classes(self):
classes = utils.get_resource_classes() classes = utils.get_resource_classes()
self.assertIsInstance(classes, typing.List) self.assertIsInstance(classes, typing.List)

View File

@ -17,24 +17,45 @@ import os
import pkgutil import pkgutil
import re import re
import pkg_resources
from ospurge.resources import base from ospurge.resources import base
def get_resource_classes(resources=None): ENTRY_POINTS_NAME = 'ospurge_resources'
"""
Import all the modules in the `resources` package and return all the
subclasses of the `ServiceResource` ABC that match the `resources` arg.
This way we can easily extend OSPurge by just adding a new file in the
`resources` dir. def load_ospurge_resource_modules():
""" """Import all the modules in the `resources` package."""
modules = {}
iter_modules = pkgutil.iter_modules( iter_modules = pkgutil.iter_modules(
[os.path.join(os.path.dirname(__file__), 'resources')], [os.path.join(os.path.dirname(__file__), 'resources')],
prefix='ospurge.resources.' prefix='ospurge.resources.'
) )
for (_, name, ispkg) in iter_modules: for (_, name, ispkg) in iter_modules:
if not ispkg: if not ispkg:
importlib.import_module(name) modules[name] = importlib.import_module(name)
return modules
def load_entry_points_modules(name=ENTRY_POINTS_NAME):
"""Import all modules in the `name` entry point."""
entry_points = {}
for entry_point in pkg_resources.iter_entry_points(name):
entry_points[entry_point.name] = entry_point.load()
return entry_points
def get_resource_classes(resources=None):
"""
Load all ospurge resource and entry point modules and return all the
subclasses of the `ServiceResource` ABC that match the `resources` arg.
This way we can easily extend OSPurge by just adding a new file in the
`resources` dir or a package with `ENTRY_POINTS_NAME` entry point.
"""
load_ospurge_resource_modules()
load_entry_points_modules()
all_classes = base.ServiceResource.__subclasses__() all_classes = base.ServiceResource.__subclasses__()

View File

@ -17,6 +17,15 @@ from typing import Iterable
from typing import List from typing import List
from typing import Optional from typing import Optional
from typing import TypeVar from typing import TypeVar
from typing import AnyStr
def load_ospurge_resource_modules() -> Dict:
...
def load_entry_points_modules(name: AnyStr) -> Dict:
...
def get_resource_classes(resources: Optional[Iterable[str]]=None) -> List: def get_resource_classes(resources: Optional[Iterable[str]]=None) -> List: