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:
parent
1370553e73
commit
c9e337422d
@ -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)
|
@ -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)
|
@ -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
|
||||
|
@ -64,5 +64,3 @@ class FlavorDetail(Flavor):
|
||||
allow_commit = False
|
||||
allow_delete = False
|
||||
allow_list = True
|
||||
|
||||
detail_for = Flavor
|
||||
|
@ -447,5 +447,3 @@ class ServerDetail(Server):
|
||||
allow_commit = False
|
||||
allow_delete = False
|
||||
allow_list = True
|
||||
|
||||
detail_for = Server
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user