Skip to content

Commit 1e05404

Browse files
committed
Move name collision logic to the model
This change moves and formalizes the name collision logic from the factory to the resource model. The following has changed: * Documentation now better matches the factory output. * Collision renaming order is formalized in one place * Order: reserved names (meta), load action, identifiers, actions, subresources, references, collections, and then attributes. * Renaming resource model attributes/methods now happens at model loading time rather than class creation time. The way this works is by creating a mapping of (type, name) tuples to the renamed value, if it exists. Typically this mapping will be very sparse and it's fast to create / access. In practice we currently only have one or two names that collide across all the resource models. Tests have been updated, some of which needed to define proper Botocore shapes as the code now looks more closely at those at model load time.
1 parent 5cd57ce commit 1e05404

File tree

5 files changed

+200
-75
lines changed

5 files changed

+200
-75
lines changed

boto3/docs.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,12 @@ def docs_for(service_name):
177177
for name, model in sorted(data['resources'].items(),
178178
key=lambda i:i[0]):
179179
resource_model = ResourceModel(name, model, data['resources'])
180+
181+
shape = None
182+
if resource_model.shape:
183+
shape = service_model.shape_for(resource_model.shape)
184+
resource_model.load_rename_map(shape)
185+
180186
if name not in models:
181187
models[name] = {'type': 'resource', 'model': resource_model}
182188

@@ -333,7 +339,7 @@ def document_resource(service_name, official_name, resource_model,
333339
docs += ' Attributes:\n\n'
334340
shape = service_model.shape_for(resource_model.shape)
335341

336-
for name, member in sorted(shape.members.items()):
342+
for name, member in sorted(resource_model.get_attributes(shape).items()):
337343
docs += (' .. py:attribute:: {0}\n\n (``{1}``)'
338344
' {2}\n\n').format(
339345
xform_name(name), py_type_name(member.type_name),

boto3/resources/factory.py

+7-54
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,11 @@ def load_from_definition(self, service_name, resource_name, model,
7474

7575
resource_model = ResourceModel(resource_name, model, resource_defs)
7676

77+
shape = None
78+
if resource_model.shape:
79+
shape = service_model.shape_for(resource_model.shape)
80+
resource_model.load_rename_map(shape)
81+
7782
self._load_identifiers(attrs, meta, resource_model)
7883
self._load_actions(attrs, resource_model, resource_defs,
7984
service_model)
@@ -99,8 +104,6 @@ def _load_identifiers(self, attrs, meta, model):
99104
"""
100105
for identifier in model.identifiers:
101106
snake_cased = xform_name(identifier.name)
102-
snake_cased = self._check_allowed_name(
103-
attrs, snake_cased, 'identifier', model.name)
104107
meta.identifiers.append(snake_cased)
105108
attrs[snake_cased] = None
106109

@@ -118,8 +121,6 @@ def _load_actions(self, attrs, model, resource_defs, service_model):
118121

119122
for action in model.actions:
120123
snake_cased = xform_name(action.name)
121-
snake_cased = self._check_allowed_name(
122-
attrs, snake_cased, 'action', model.name)
123124
attrs[snake_cased] = self._create_action(snake_cased,
124125
action, resource_defs, service_model)
125126

@@ -133,16 +134,8 @@ def _load_attributes(self, attrs, meta, model, service_model):
133134
if model.shape:
134135
shape = service_model.shape_for(model.shape)
135136

136-
for name, member in shape.members.items():
137-
snake_cased = xform_name(name)
138-
if snake_cased in meta.identifiers:
139-
# Skip identifiers, these are set through other means
140-
continue
141-
142-
snake_cased = self._check_allowed_name(
143-
attrs, snake_cased, 'attribute', model.name)
144-
attrs[snake_cased] = self._create_autoload_property(name,
145-
snake_cased)
137+
for name, member in model.get_attributes(shape).items():
138+
attrs[name] = self._create_autoload_property(member.name, name)
146139

147140
def _load_collections(self, attrs, model, resource_defs, service_model):
148141
"""
@@ -153,8 +146,6 @@ def _load_collections(self, attrs, model, resource_defs, service_model):
153146
"""
154147
for collection_model in model.collections:
155148
snake_cased = xform_name(collection_model.name)
156-
snake_cased = self._check_allowed_name(
157-
attrs, snake_cased, 'collection', model.name)
158149

159150
attrs[snake_cased] = self._create_collection(
160151
attrs['meta'].service_name, model.name, snake_cased,
@@ -177,8 +168,6 @@ def _load_has_relations(self, attrs, service_name, resource_name,
177168
# the data we need to create the resource, so
178169
# this instance becomes an attribute on the class.
179170
snake_cased = xform_name(reference.name)
180-
snake_cased = self._check_allowed_name(
181-
attrs, snake_cased, 'reference', model.name)
182171
attrs[snake_cased] = self._create_reference(
183172
reference.resource.type, snake_cased, reference,
184173
service_name, resource_name, model, resource_defs,
@@ -201,44 +190,8 @@ def _load_waiters(self, attrs, model):
201190
"""
202191
for waiter in model.waiters:
203192
snake_cased = xform_name(waiter.resource_waiter_name)
204-
snake_cased = self._check_allowed_name(
205-
attrs, snake_cased, 'waiter', model.name)
206193
attrs[snake_cased] = self._create_waiter(waiter, snake_cased)
207194

208-
def _check_allowed_name(self, attrs, name, category, resource_name):
209-
"""
210-
Determine if a given name is allowed on the instance, and if not,
211-
then raise an exception. This prevents public attributes of the
212-
class from being clobbered, e.g. since we define ``Resource.meta``,
213-
no identifier may be named ``meta``. Another example: no action
214-
named ``queue_items`` may be added after an identifier of the same
215-
name has been added.
216-
217-
One attempt is made in the event of a collision to remedy the
218-
situation. The ``category`` is appended to the name and the
219-
check is performed again. For example, if an action named
220-
``get_frobs`` fails the test, then we try ``get_frobs_action``
221-
after logging a warning.
222-
223-
:raises: ValueError
224-
"""
225-
if name in attrs:
226-
logger.warning('%s `%s` would clobber existing %s'
227-
' resource attribute, going to try'
228-
' %s instead...', category, name,
229-
resource_name, name + '_' + category)
230-
# TODO: Move this logic into the model and strictly
231-
# define the loading order of categories. This
232-
# will make documentation much simpler.
233-
name = name + '_' + category
234-
235-
if name in attrs:
236-
raise ValueError('{0} `{1}` would clobber existing '
237-
'{2} resource attribute'.format(
238-
category, name, resource_name))
239-
240-
return name
241-
242195
def _create_autoload_property(factory_self, name, snake_cased):
243196
"""
244197
Creates a new property on the resource to lazy-load its value

boto3/resources/model.py

+161-1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525

2626
import logging
2727

28+
from botocore import xform_name
29+
2830

2931
logger = logging.getLogger(__name__)
3032

@@ -250,12 +252,162 @@ class ResourceModel(object):
250252
def __init__(self, name, definition, resource_defs):
251253
self._definition = definition
252254
self._resource_defs = resource_defs
255+
self._renamed = {}
253256

254257
#: (``string``) The name of this resource
255258
self.name = name
256259
#: (``string``) The service shape name for this resource or ``None``
257260
self.shape = definition.get('shape')
258261

262+
def load_rename_map(self, shape=None):
263+
"""
264+
Load a name translation map given a shape. This will set
265+
up renamed values for any collisions, e.g. if the shape,
266+
an action, and a subresource all are all named ``foo``
267+
then the resource will have an action ``foo``, a subresource
268+
named ``Foo`` and a property named ``foo_attribute``.
269+
This is the order of precedence, from most important to
270+
least important:
271+
272+
* Load action (resource.load)
273+
* Identifiers
274+
* Actions
275+
* Subresources
276+
* References
277+
* Collections
278+
* Attributes (shape members)
279+
280+
Batch actions are only exposed on collections, so do not
281+
get modified here. Subresources use upper camel casing, so
282+
are unlikely to collide with anything but other subresources.
283+
284+
Creates a structure like this::
285+
286+
renames = {
287+
('action', 'id'): 'id_action',
288+
('collection', 'id'): 'id_collection',
289+
('attribute', 'id'): 'id_attribute'
290+
}
291+
292+
# Get the final name for an action named 'id'
293+
name = renames.get(('action', 'id'), 'id')
294+
295+
:type shape: botocore.model.Shape
296+
:param shape: The underlying shape for this resource.
297+
"""
298+
# Meta is a reserved name for resources
299+
names = set(['meta'])
300+
self._renamed = {}
301+
302+
if self._definition.get('load'):
303+
names.add('load')
304+
305+
for item in self._definition.get('identifiers', []):
306+
self._load_name_with_category(names, item['name'], 'identifier')
307+
308+
for name in self._definition.get('actions', {}).keys():
309+
self._load_name_with_category(names, name, 'action')
310+
311+
for name, ref in self._get_has_definition().items():
312+
# Subresources require no data members, just typically
313+
# identifiers and user input.
314+
data_required = False
315+
for identifier in ref['resource']['identifiers']:
316+
if identifier['source'] == 'data':
317+
data_required = True
318+
break
319+
320+
if not data_required:
321+
self._load_name_with_category(names, name, 'subresource',
322+
snake_case=False)
323+
else:
324+
self._load_name_with_category(names, name, 'reference')
325+
326+
for name in self._definition.get('hasMany', {}).keys():
327+
self._load_name_with_category(names, name, 'collection')
328+
329+
if shape is not None:
330+
for name in shape.members.keys():
331+
self._load_name_with_category(names, name, 'attribute')
332+
333+
def _load_name_with_category(self, names, name, category,
334+
snake_case=True):
335+
"""
336+
Load a name with a given category, possibly renaming it
337+
if that name is already in use. The name will be stored
338+
in ``names`` and possibly be set up in ``self._renamed``.
339+
340+
:type names: set
341+
:param names: Existing names (Python attributes, properties, or
342+
methods) on the resource.
343+
:type name: string
344+
:param name: The original name of the value.
345+
:type category: string
346+
:param category: The value type, such as 'identifier' or 'action'
347+
:type snake_case: bool
348+
:param snake_case: True (default) if the name should be snake cased.
349+
"""
350+
if snake_case:
351+
name = xform_name(name)
352+
353+
if name in names:
354+
logger.debug('Renaming %s %s %s' % (self.name, category, name))
355+
self._renamed[(category, name)] = name + '_' + category
356+
name += '_' + category
357+
358+
if name in names:
359+
# This isn't good, let's raise instead of trying to keep
360+
# renaming this value.
361+
raise ValueError('Problem renaming {0} {1} to {2}!'.format(
362+
self.name, category, name))
363+
364+
names.add(name)
365+
366+
def _get_name(self, category, name, snake_case=True):
367+
"""
368+
Get a possibly renamed value given a category and name. This
369+
uses the rename map set up in ``load_rename_map``, so that
370+
method must be called once first.
371+
372+
:type category: string
373+
:param category: The value type, such as 'identifier' or 'action'
374+
:type name: string
375+
:param name: The original name of the value
376+
:type snake_case: bool
377+
:param snake_case: True (default) if the name should be snake cased.
378+
:rtype: string
379+
:return: Either the renamed value if it is set, otherwise the
380+
original name.
381+
"""
382+
if snake_case:
383+
name = xform_name(name)
384+
385+
return self._renamed.get((category, name), name)
386+
387+
def get_attributes(self, shape):
388+
"""
389+
Get a dictionary of attribute names to shape models that
390+
represent the attributes of this resource.
391+
392+
:type shape: botocore.model.Shape
393+
:param shape: The underlying shape for this resource.
394+
:rtype: dict
395+
:return: Mapping of resource attributes.
396+
"""
397+
attributes = {}
398+
identifier_names = [i.name for i in self.identifiers]
399+
400+
for name, member in shape.members.items():
401+
snake_cased = xform_name(name)
402+
if snake_cased in identifier_names:
403+
# Skip identifiers, these are set through other means
404+
continue
405+
snake_cased = self._get_name('attribute', snake_cased,
406+
snake_case=False)
407+
attributes[snake_cased] = member
408+
409+
return attributes
410+
259411
@property
260412
def identifiers(self):
261413
"""
@@ -266,7 +418,8 @@ def identifiers(self):
266418
identifiers = []
267419

268420
for item in self._definition.get('identifiers', []):
269-
identifiers.append(Identifier(item['name']))
421+
name = self._get_name('identifier', item['name'])
422+
identifiers.append(Identifier(name))
270423

271424
return identifiers
272425

@@ -294,6 +447,7 @@ def actions(self):
294447
actions = []
295448

296449
for name, item in self._definition.get('actions', {}).items():
450+
name = self._get_name('action', name)
297451
actions.append(Action(name, item, self._resource_defs))
298452

299453
return actions
@@ -308,6 +462,7 @@ def batch_actions(self):
308462
actions = []
309463

310464
for name, item in self._definition.get('batchActions', {}).items():
465+
name = self._get_name('batch_action', name)
311466
actions.append(Action(name, item, self._resource_defs))
312467

313468
return actions
@@ -387,6 +542,10 @@ def _get_related_resources(self, subresources):
387542
resources = []
388543

389544
for name, definition in self._get_has_definition().items():
545+
if subresources:
546+
name = self._get_name('subresource', name, snake_case=False)
547+
else:
548+
name = self._get_name('reference', name)
390549
action = Action(name, definition, self._resource_defs)
391550

392551
data_required = False
@@ -430,6 +589,7 @@ def collections(self):
430589
collections = []
431590

432591
for name, item in self._definition.get('hasMany', {}).items():
592+
name = self._get_name('collection', name)
433593
collections.append(Collection(name, item, self._resource_defs))
434594

435595
return collections

0 commit comments

Comments
 (0)