From e5afd0b9cbd5b60d477d19ebcfb7b1ba71aeb694 Mon Sep 17 00:00:00 2001 From: Boden R Date: Thu, 25 Oct 2018 15:42:00 -0600 Subject: [PATCH] rehome the resource_extend db module This patch rehomes neutron.db._resource_extend into neutron-lib. While the module is private in neutron, it's used by consumers today [1]. The patch also includes a test fixture along with unit tests and a release note. [1] http://codesearch.openstack.org/?q=from%20neutron%5C.db%20import%20_resource_extend&i=nope&files=&repos= Change-Id: I2306ba92dcf4a989c7c73e5f0ef4bfb4f804b6cd --- neutron_lib/db/resource_extend.py | 146 ++++++++++++++++++ neutron_lib/fixture.py | 16 ++ .../tests/unit/db/test_resource_extend.py | 65 ++++++++ neutron_lib/tests/unit/test_fixture.py | 24 +++ ...home-resource-extend-7eee483ec4146801.yaml | 6 + 5 files changed, 257 insertions(+) create mode 100644 neutron_lib/db/resource_extend.py create mode 100644 neutron_lib/tests/unit/db/test_resource_extend.py create mode 100644 releasenotes/notes/rehome-resource-extend-7eee483ec4146801.yaml diff --git a/neutron_lib/db/resource_extend.py b/neutron_lib/db/resource_extend.py new file mode 100644 index 000000000..d625893df --- /dev/null +++ b/neutron_lib/db/resource_extend.py @@ -0,0 +1,146 @@ +# 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. + +""" +NOTE: This module shall not be used by external projects. It will be moved + to neutron-lib in due course, and then it can be used from there. +""" + +import collections +import inspect + +from neutron_lib.utils import helpers + + +# This dictionary will store methods for extending API resources. +# Extensions can add their own methods by invoking register_funcs(). +_resource_extend_functions = { + # : [, , ...], + # : [, , ...], + # ... +} + +# This dictionary will store @extends decorated methods with a list of +# resources that each method will extend on class initialization. +_DECORATED_EXTEND_METHODS = collections.defaultdict(list) +_DECORATED_METHODS_REGISTERED = '_DECORATED_METHODS_REGISTERED' + + +def register_funcs(resource, funcs): + """Add functions to extend a resource. + + :param resource: A resource collection name. + :type resource: str + + :param funcs: A list of functions. + :type funcs: list of callable + + These functions take a resource dict and a resource object and + update the resource dict with extension data (possibly retrieved + from the resource db object). + def _extend_foo_with_bar(foo_res, foo_db): + foo_res['bar'] = foo_db.bar_info # example + return foo_res + + """ + funcs = [helpers.make_weak_ref(f) if callable(f) else f + for f in funcs] + _resource_extend_functions.setdefault(resource, []).extend(funcs) + + +def get_funcs(resource): + """Retrieve a list of functions extending a resource. + + :param resource: A resource collection name. + :type resource: str + + :return: A list (possibly empty) of functions extending resource. + :rtype: list of callable + + """ + return _resource_extend_functions.get(resource, []) + + +def apply_funcs(resource_type, response, db_object): + """Appy registered functions for the said resource type. + + :param resource_type: The resource type to apply funcs for. + :param response: The response object. + :param db_object: The Database object. + :returns: None + """ + for func in get_funcs(resource_type): + resolved_func = helpers.resolve_ref(func) + if resolved_func: + resolved_func(response, db_object) + + +def extends(resources): + """Use to decorate methods on classes before initialization. + + Any classes that use this must themselves be decorated with the + @has_resource_extenders decorator to setup the __new__ method to + actually register the instance methods after initialization. + + :param resources: Resource collection names. The decorated method will + be registered with each resource as an extend function. + :type resources: list of str + + """ + def decorator(method): + _DECORATED_EXTEND_METHODS[method].extend(resources) + return method + return decorator + + +def has_resource_extenders(klass): + """Decorator to setup __new__ method in classes to extend resources. + + Any method decorated with @extends above is an unbound method on a class. + This decorator sets up the class __new__ method to add the bound + method to _resource_extend_functions after object instantiation. + """ + orig_new = klass.__new__ + new_inherited = '__new__' not in klass.__dict__ + + @staticmethod + def replacement_new(cls, *args, **kwargs): + if new_inherited: + # class didn't define __new__ so we need to call inherited __new__ + super_new = super(klass, cls).__new__ + if super_new is object.__new__: + # object.__new__ doesn't accept args nor kwargs + instance = super_new(cls) + else: + instance = super_new(cls, *args, **kwargs) + else: + instance = orig_new(cls, *args, **kwargs) + if getattr(instance, _DECORATED_METHODS_REGISTERED, False): + # Avoid running this logic twice for classes inheriting other + # classes with this same decorator. Only one needs to execute + # to subscribe all decorated methods. + return instance + for name, unbound_method in inspect.getmembers(cls): + if (not inspect.ismethod(unbound_method) and + not inspect.isfunction(unbound_method)): + continue + # Handle py27/py34 difference + method = getattr(unbound_method, 'im_func', unbound_method) + if method not in _DECORATED_EXTEND_METHODS: + continue + for resource in _DECORATED_EXTEND_METHODS[method]: + # Register the bound method for the resourse + register_funcs(resource, [method]) + setattr(instance, _DECORATED_METHODS_REGISTERED, True) + return instance + klass.__new__ = replacement_new + return klass diff --git a/neutron_lib/fixture.py b/neutron_lib/fixture.py index 2c224fc30..c1a3946d7 100644 --- a/neutron_lib/fixture.py +++ b/neutron_lib/fixture.py @@ -24,6 +24,7 @@ from neutron_lib.callbacks import registry from neutron_lib.db import api as db_api from neutron_lib.db import model_base from neutron_lib.db import model_query +from neutron_lib.db import resource_extend from neutron_lib.plugins import directory from neutron_lib import rpc from neutron_lib.tests.unit import fake_notifier @@ -270,3 +271,18 @@ class RPCFixture(fixtures.Fixture): self.addCleanup(rpc.cleanup) rpc.init(CONF) + + +class DBResourceExtendFixture(fixtures.Fixture): + + def __init__(self, extended_methods=None): + self.extended_methods = extended_methods or {} + + def _setUp(self): + self._backup = copy.deepcopy( + resource_extend._DECORATED_EXTEND_METHODS) + resource_extend._DECORATED_EXTEND_METHODS = self.extended_methods + self.addCleanup(self._restore) + + def _restore(self): + resource_extend._DECORATED_EXTEND_METHODS = self._backup diff --git a/neutron_lib/tests/unit/db/test_resource_extend.py b/neutron_lib/tests/unit/db/test_resource_extend.py new file mode 100644 index 000000000..429916238 --- /dev/null +++ b/neutron_lib/tests/unit/db/test_resource_extend.py @@ -0,0 +1,65 @@ +# 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 oslotest import base + +from neutron_lib.db import resource_extend +from neutron_lib import fixture + + +@resource_extend.has_resource_extenders +class _DBExtender(object): + + @resource_extend.extends('ExtendedA') + def _extend_a(self, resp, db_obj): + pass + + @resource_extend.extends('ExtendedB') + def _extend_b(self, resp, db_obj): + pass + + +class TestResourceExtendClass(base.BaseTestCase): + + def test_extends(self): + self.assertIsNotNone(resource_extend.get_funcs('ExtendedA')) + self.assertIsNotNone(resource_extend.get_funcs('ExtendedB')) + + +class TestResourceExtend(base.BaseTestCase): + + def setUp(self): + super(TestResourceExtend, self).setUp() + self.useFixture(fixture.DBResourceExtendFixture()) + + def test_register_funcs(self): + resources = ['A', 'B', 'C'] + for r in resources: + resource_extend.register_funcs(r, (lambda x: x,)) + + for r in resources: + self.assertIsNotNone(resource_extend.get_funcs(r)) + + def test_apply_funcs(self): + resources = ['A', 'B', 'C'] + callbacks = [] + + def _cb(resp, db_obj): + callbacks.append(resp) + + for r in resources: + resource_extend.register_funcs(r, (_cb,)) + + for r in resources: + resource_extend.apply_funcs(r, None, None) + + self.assertEqual(3, len(callbacks)) diff --git a/neutron_lib/tests/unit/test_fixture.py b/neutron_lib/tests/unit/test_fixture.py index e8cc73cc7..fe4ad1272 100644 --- a/neutron_lib/tests/unit/test_fixture.py +++ b/neutron_lib/tests/unit/test_fixture.py @@ -20,6 +20,7 @@ from neutron_lib.api import attributes from neutron_lib.api.definitions import port from neutron_lib.callbacks import registry from neutron_lib.db import model_base +from neutron_lib.db import resource_extend from neutron_lib import fixture from neutron_lib.placement import client as place_client from neutron_lib.plugins import directory @@ -154,3 +155,26 @@ class PlacementAPIClientFixtureTestCase(base.BaseTestCase): p_client, p_fixture = self._create_client_and_fixture() p_client.list_aggregates('resource') p_fixture.mock_get.assert_called_once() + + +class DBResourceExtendFixtureTestCase(base.BaseTestCase): + + def test_fixture_backup(self): + fake_methods = { + 'a': 'A', + 'b': 'B' + } + orig_methods = resource_extend._DECORATED_EXTEND_METHODS + self.assertNotEqual(fake_methods, orig_methods) + + db_fixture = fixture.DBResourceExtendFixture( + extended_methods=fake_methods) + db_fixture.setUp() + + resource_extend.register_funcs('C', (lambda x: x,)) + self.assertNotEqual( + orig_methods, resource_extend._DECORATED_EXTEND_METHODS) + + db_fixture.cleanUp() + self.assertEqual( + orig_methods, resource_extend._DECORATED_EXTEND_METHODS) diff --git a/releasenotes/notes/rehome-resource-extend-7eee483ec4146801.yaml b/releasenotes/notes/rehome-resource-extend-7eee483ec4146801.yaml new file mode 100644 index 000000000..671e6a534 --- /dev/null +++ b/releasenotes/notes/rehome-resource-extend-7eee483ec4146801.yaml @@ -0,0 +1,6 @@ +--- +features: + - The ``neutron.db._resource_extend`` is now available as + ``neutron_lib.db.resource_extend`` along with a new + ``DBResourceExtendFixture`` that allows tests to modify the + map of registered resource functions.