Revert the Proxy metaclass

Sometimes one can be too clever. The win of generating methods here
doesn't really outweigh the complexity or the increased difficulty
understanding the codebase. Additionally, we run in to an EXCELLENTLY
obtuse error:

  TypeError: metaclass conflict: the metaclass of a derived class must be
  a (non-strict) subclass of the metaclasses of all its bases

when trying to make an abstract base class for the image proxy base
class.

Just get rid of the metaclass and restore the (very few) generated
methods.

Change-Id: Ib53d7b29526a734f1dcf4088bf156e9a29746f5b
This commit is contained in:
Monty Taylor 2018-10-11 10:31:38 -05:00
parent 1370553e73
commit c9e337422d
No known key found for this signature in database
GPG Key ID: 7BAE94BC7141A594
7 changed files with 132 additions and 297 deletions

View File

@ -1,150 +0,0 @@
# Copyright 2018 Red Hat, Inc.
#
# 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.
"""Doc and Code templates to be used by the Proxy Metaclass.
The doc templates and code templates are stored separately because having
either of them templated is weird in the first place, but having a doc
string inside of a function definition that's inside of a triple-quoted
string is just hard on the eyeballs.
"""
_FIND_TEMPLATE = """Find a single {resource_name}
:param name_or_id: The name or ID of an {resource_name}.
:param bool ignore_missing: When set to ``False``
:class:`~openstack.exceptions.ResourceNotFound` will be
raised when the resource does not exist.
When set to ``True``, None will be returned when
attempting to find a nonexistent resource.
:returns: One :class:`~{resource_class}` or None
"""
_LIST_TEMPLATE = """Retrieve a generator of all {resource_name}
:param bool details: When ``True``, returns
:class:`~{detail_class}` objects,
otherwise :class:`~{resource_class}`.
*Default: ``True``*
:param kwargs \*\*query: Optional query parameters to be sent to limit
the flavors being returned.
:returns: A generator of {resource_name} instances.
:rtype: :class:`~{resource_class}`
"""
_DELETE_TEMPLATE = """Delete a {resource_name}
:param {name}:
The value can be either the ID of a {name} or a
:class:`~{resource_class}` instance.
:param bool ignore_missing:
When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound`
will be raised when the {name} does not exist.
When set to ``True``, no exception will be set when
attempting to delete a nonexistent {name}.
:returns: ``None``
"""
_FETCH_TEMPLATE = """Fetch a single {resource_name}
:param {name}:
The value can be the ID of a {name} or a
:class:`~{resource_class}` instance.
:returns: One :class:`~{resource_class}`
:raises: :class:`~openstack.exceptions.ResourceNotFound`
when no resource can be found.
"""
_CREATE_TEMPLATE = """Create a new {resource_name} from attributes
:param dict attrs:
Keyword arguments which will be used to create a
:class:`~{resource_class}`.
:returns: The results of {resource_name} creation
:rtype: :class:`~{resource_class}`
"""
_COMMIT_TEMPLATE = """Commit the state of a {resource_name}
:param {name}:
Either the ID of a {resource_name} or a :class:`~{resource_class}`
instance.
:attrs kwargs:
The attributes to commit on the {resource_name} represented by
``{name}``.
:returns: The updated server
:rtype: :class:`~{resource_class}`
"""
_DOC_TEMPLATES = {
'create': _CREATE_TEMPLATE,
'delete': _DELETE_TEMPLATE,
'find': _FIND_TEMPLATE,
'list': _LIST_TEMPLATE,
'fetch': _FETCH_TEMPLATE,
'commit': _COMMIT_TEMPLATE,
}
_FIND_SOURCE = """
def find(self, name_or_id, ignore_missing=True):
return self._find(
self.{resource_name}, name_or_id, ignore_missing=ignore_missing)
"""
_CREATE_SOURCE = """
def create(self, **attrs):
return self._create(self.{resource_name}, **attrs)
"""
_DELETE_SOURCE = """
def delete(self, {name}, ignore_missing=True):
self._delete(self.{resource_name}, {name}, ignore_missing=ignore_missing)
"""
_FETCH_SOURCE = """
def fetch(self, {name}):
return self._get(self.{resource_name}, {name})
"""
_LIST_SOURCE = """
def list(self, details=True, **query):
res_cls = self.{detail_name} if details else self.{resource_name}
return self._list(res_cls, paginated=True, **query)
"""
_COMMIT_SOURCE = """
def commit(self, {name}, **attrs):
return self._update(self.{resource_name}, {name}, **attrs)
"""
_SOURCE_TEMPLATES = {
'create': _CREATE_SOURCE,
'delete': _DELETE_SOURCE,
'find': _FIND_SOURCE,
'list': _LIST_SOURCE,
'fetch': _FETCH_SOURCE,
'commit': _COMMIT_SOURCE,
}
def get_source_template(action, **kwargs):
return _SOURCE_TEMPLATES[action].format(**kwargs)
def get_doc_template(action, **kwargs):
return _DOC_TEMPLATES[action].format(**kwargs)

View File

@ -1,129 +0,0 @@
# Copyright 2018 Red Hat, Inc.
#
# 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.
# Inspired by code from
# https://github.com/micheles/decorator/blob/master/src/decorator.py
# which is MIT licensed.
from openstack._meta import _proxy_templates
from openstack import resource
def compile_function(evaldict, action, module, **kwargs):
"Make a new functions"
src = _proxy_templates.get_source_template(action, **kwargs)
# Ensure each generated block of code has a unique filename for profilers
# (such as cProfile) that depend on the tuple of (<filename>,
# <definition line>, <function name>) being unique.
filename = '<generated-{module}>'.format(module=module)
code = compile(src, filename, 'exec')
exec(code, evaldict)
func = evaldict[action]
func.__source__ = src
return func
def add_function(dct, func, action, args, name_template='{action}_{name}'):
func_name = name_template.format(action=action, **args)
# If the class has the function already, don't override it
if func_name in dct:
func_name = '_generated_' + func_name
func.__name__ = func_name
func.__qualname__ = func_name
func.__doc__ = _proxy_templates.get_doc_template(action, **args)
func.__module__ = args['module']
dct[func_name] = func
def expand_classname(res):
return '{module}.{name}'.format(module=res.__module__, name=res.__name__)
class ProxyMeta(type):
"""Metaclass that generates standard methods based on Resources.
Each service has a set of Resources which define the fundamental
qualities of the remote resources. A large portion of the methods
on Proxy classes are boilerplate.
This metaclass reads the definition of the Proxy class and looks for
Resource classes attached to it. It then checks them to see which
operations are allowed by looking at the ``allow_`` flags. Based on that,
it generates the standard methods and adds them to the class.
If a method exists on the class when it is read, the generated method
does not overwrite the existing method. Instead, it is attached as
``_generated_{method_name}``. This allows people to either write
specific proxy methods and completely ignore the generated method,
or to write specialized methods that then delegate action to the generated
method.
Since this is done as a metaclass at class object creation time,
things like sphinx continue to work.
"""
def __new__(meta, name, bases, dct):
# Build up a list of resource classes attached to the Proxy
resources = {}
details = {}
for k, v in dct.items():
if isinstance(v, type) and issubclass(v, resource.Resource):
if v.detail_for:
details[v.detail_for.__name__] = v
else:
resources[v.__name__] = v
for resource_name, res in resources.items():
resource_class = expand_classname(res)
detail = details.get(resource_name, res)
detail_name = detail.__name__
detail_class = expand_classname(detail)
lower_name = resource_name.lower()
plural_name = getattr(res, 'plural_name', lower_name + 's')
args = dict(
resource_name=resource_name,
resource_class=resource_class,
name=lower_name,
module=res.__module__,
detail_name=detail_name,
detail_class=detail_class,
plural_name=plural_name,
)
# Generate unbound methods from the template strings.
# We have to do a compile step rather than using somthing
# like existing function objects wrapped with closures
# because of the argument naming pattern for delete and update.
# You can't really change an argument name programmatically,
# at least not that I've been able to find.
# We pass in a copy of the dct dict so that the exec step can
# be done in the context of the class the methods will be attached
# to. This allows name resolution to work properly.
for action in ('create', 'fetch', 'commit', 'delete'):
if getattr(res, 'allow_{action}'.format(action=action)):
func = compile_function(dct.copy(), action, **args)
kwargs = {}
if action == 'fetch':
kwargs['name_template'] = 'get_{name}'
elif action == 'commit':
kwargs['name_template'] = 'update_{name}'
add_function(dct, func, action, args, **kwargs)
if res.allow_list:
func = compile_function(dct.copy(), 'find', **args)
add_function(dct, func, 'find', args)
func = compile_function(dct.copy(), 'list', **args)
add_function(dct, func, 'list', args, plural_name)
return super(ProxyMeta, meta).__new__(meta, name, bases, dct)

View File

@ -29,11 +29,96 @@ from openstack import resource
class Proxy(proxy.Proxy):
Extension = extension.Extension
Flavor = _flavor.Flavor
FlavorDetail = _flavor.FlavorDetail
Server = _server.Server
ServerDetail = _server.ServerDetail
def find_extension(self, name_or_id, ignore_missing=True):
"""Find a single extension
:param name_or_id: The name or ID of an extension.
:param bool ignore_missing: When set to ``False``
:class:`~openstack.exceptions.ResourceNotFound` will be
raised when the resource does not exist.
When set to ``True``, None will be returned when
attempting to find a nonexistent resource.
:returns: One :class:`~openstack.compute.v2.extension.Extension` or
None
"""
return self._find(extension.Extension, name_or_id,
ignore_missing=ignore_missing)
def extensions(self):
"""Retrieve a generator of extensions
:returns: A generator of extension instances.
:rtype: :class:`~openstack.compute.v2.extension.Extension`
"""
return self._list(extension.Extension, paginated=True)
def find_flavor(self, name_or_id, ignore_missing=True):
"""Find a single flavor
:param name_or_id: The name or ID of a flavor.
:param bool ignore_missing: When set to ``False``
:class:`~openstack.exceptions.ResourceNotFound` will be
raised when the resource does not exist.
When set to ``True``, None will be returned when
attempting to find a nonexistent resource.
:returns: One :class:`~openstack.compute.v2.flavor.Flavor` or None
"""
return self._find(_flavor.Flavor, name_or_id,
ignore_missing=ignore_missing)
def create_flavor(self, **attrs):
"""Create a new flavor from attributes
:param dict attrs: Keyword arguments which will be used to create
a :class:`~openstack.compute.v2.flavor.Flavor`,
comprised of the properties on the Flavor class.
:returns: The results of flavor creation
:rtype: :class:`~openstack.compute.v2.flavor.Flavor`
"""
return self._create(_flavor.Flavor, **attrs)
def delete_flavor(self, flavor, ignore_missing=True):
"""Delete a flavor
:param flavor: The value can be either the ID of a flavor or a
:class:`~openstack.compute.v2.flavor.Flavor` instance.
:param bool ignore_missing: When set to ``False``
:class:`~openstack.exceptions.ResourceNotFound` will be
raised when the flavor does not exist.
When set to ``True``, no exception will be set when
attempting to delete a nonexistent flavor.
:returns: ``None``
"""
self._delete(_flavor.Flavor, flavor, ignore_missing=ignore_missing)
def get_flavor(self, flavor):
"""Get a single flavor
:param flavor: The value can be the ID of a flavor or a
:class:`~openstack.compute.v2.flavor.Flavor` instance.
:returns: One :class:`~openstack.compute.v2.flavor.Flavor`
:raises: :class:`~openstack.exceptions.ResourceNotFound`
when no resource can be found.
"""
return self._get(_flavor.Flavor, flavor)
def flavors(self, details=True, **query):
"""Return a generator of flavors
:param bool details: When ``True``, returns
:class:`~openstack.compute.v2.flavor.FlavorDetail` objects,
otherwise :class:`~openstack.compute.v2.flavor.Flavor`.
*Default: ``True``*
:param kwargs \*\*query: Optional query parameters to be sent to limit
the flavors being returned.
:returns: A generator of flavor objects
"""
flv = _flavor.FlavorDetail if details else _flavor.Flavor
return self._list(flv, paginated=True, **query)
def delete_image(self, image, ignore_missing=True):
"""Delete an image
@ -227,6 +312,18 @@ class Proxy(proxy.Proxy):
"""
return self._get(limits.Limits)
def create_server(self, **attrs):
"""Create a new server from attributes
:param dict attrs: Keyword arguments which will be used to create
a :class:`~openstack.compute.v2.server.Server`,
comprised of the properties on the Server class.
:returns: The results of server creation
:rtype: :class:`~openstack.compute.v2.server.Server`
"""
return self._create(_server.Server, **attrs)
def delete_server(self, server, ignore_missing=True, force=False):
"""Delete a server
@ -246,8 +343,33 @@ class Proxy(proxy.Proxy):
server = self._get_resource(_server.Server, server)
server.force_delete(self)
else:
self._generated_delete_server(
server, ignore_missing=ignore_missing)
self._delete(_server.Server, server, ignore_missing=ignore_missing)
def find_server(self, name_or_id, ignore_missing=True):
"""Find a single server
:param name_or_id: The name or ID of a server.
:param bool ignore_missing: When set to ``False``
:class:`~openstack.exceptions.ResourceNotFound` will be
raised when the resource does not exist.
When set to ``True``, None will be returned when
attempting to find a nonexistent resource.
:returns: One :class:`~openstack.compute.v2.server.Server` or None
"""
return self._find(_server.Server, name_or_id,
ignore_missing=ignore_missing)
def get_server(self, server):
"""Get a single server
:param server: The value can be the ID of a server or a
:class:`~openstack.compute.v2.server.Server` instance.
:returns: One :class:`~openstack.compute.v2.server.Server`
:raises: :class:`~openstack.exceptions.ResourceNotFound`
when no resource can be found.
"""
return self._get(_server.Server, server)
def servers(self, details=True, **query):
"""Retrieve a generator of servers
@ -286,7 +408,8 @@ class Proxy(proxy.Proxy):
:returns: A generator of server instances.
"""
return self._generated_servers(details=details, **query)
srv = _server.ServerDetail if details else _server.Server
return self._list(srv, paginated=True, **query)
def update_server(self, server, **attrs):
"""Update a server

View File

@ -64,5 +64,3 @@ class FlavorDetail(Flavor):
allow_commit = False
allow_delete = False
allow_list = True
detail_for = Flavor

View File

@ -447,5 +447,3 @@ class ServerDetail(Server):
allow_commit = False
allow_delete = False
allow_list = True
detail_for = Server

View File

@ -10,10 +10,7 @@
# License for the specific language governing permissions and limitations
# under the License.
import six
from openstack import _adapter
from openstack._meta import proxy as _meta
from openstack import exceptions
from openstack import resource
@ -42,7 +39,7 @@ def _check_resource(strict=False):
return wrap
class Proxy(six.with_metaclass(_meta.ProxyMeta, _adapter.OpenStackSDKAdapter)):
class Proxy(_adapter.OpenStackSDKAdapter):
"""Represents a service."""
def _get_resource(self, resource_type, value, **attrs):

View File

@ -382,8 +382,6 @@ class Resource(dict):
requires_id = True
#: Do responses for this resource have bodies
has_body = True
#: Is this a detailed version of another Resource
detail_for = None
#: Maximum microversion to use for getting/creating/updating the Resource
_max_microversion = None