diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 25bf1d6434..e1fb01881c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,8 +1,31 @@ Changelog ========= -Unreleased ----------- +0.0.10 - 2015-03-05 +------------------- + +* bugfix:Documentation: Name collisions are now handled at the resource + model layer instead of the factory, meaning that the documentation + now uses the correct names. + (`issue 67 `__) +* feature:Session: Add a ``region_name`` option when creating a session. + (`issue 69 `__, + `issue 21 `__) +* feature:Botocore: Update to Botocore 0.94.0 + + * Update to the latest Amazon CloudeSearch API. + * Add support for near-realtime data updates and exporting historical + data from Amazon Cognito Sync. + * **Removed** the ability to clone a low-level client. Instead, create + a new client with the same parameters. + * Add support for URL paths in an endpoint URL. + * Multithreading signature fixes. + * Add support for listing hosted zones by name and getting hosted zone + counts from Amazon Route53. + * Add support for tagging to AWS Data Pipeline. + +0.0.9 - 2015-02-19 +------------------ * feature:Botocore: Update to Botocore 0.92.0 diff --git a/boto3/__init__.py b/boto3/__init__.py index 78a0b2c8c7..d7c0c5eeda 100644 --- a/boto3/__init__.py +++ b/boto3/__init__.py @@ -17,7 +17,7 @@ __author__ = 'Amazon Web Services' -__version__ = '0.0.9' +__version__ = '0.0.10' # The default Boto3 session; autoloaded when needed. diff --git a/boto3/docs.py b/boto3/docs.py index 02ab776d81..9aace7848a 100644 --- a/boto3/docs.py +++ b/boto3/docs.py @@ -177,6 +177,12 @@ def docs_for(service_name): for name, model in sorted(data['resources'].items(), key=lambda i:i[0]): resource_model = ResourceModel(name, model, data['resources']) + + shape = None + if resource_model.shape: + shape = service_model.shape_for(resource_model.shape) + resource_model.load_rename_map(shape) + if name not in models: models[name] = {'type': 'resource', 'model': resource_model} @@ -333,7 +339,8 @@ def document_resource(service_name, official_name, resource_model, docs += ' Attributes:\n\n' shape = service_model.shape_for(resource_model.shape) - for name, member in sorted(shape.members.items()): + attributes = resource_model.get_attributes(shape) + for name, (orig_name, member) in sorted(attributes.items()): docs += (' .. py:attribute:: {0}\n\n (``{1}``)' ' {2}\n\n').format( xform_name(name), py_type_name(member.type_name), @@ -403,7 +410,7 @@ def document_resource(service_name, official_name, resource_model, ' to reach a specific state.\n\n') service_waiter_model = session.get_waiter_model(service_name) for waiter in sorted(resource_model.waiters, - key=lambda i: i.resource_waiter_name): + key=lambda i: i.name): docs += document_waiter(waiter, service_name, resource_model, service_model, service_waiter_model) @@ -467,7 +474,7 @@ def document_waiter(waiter, service_name, resource_model, service_model, ' This method calls ``wait()`` on' ' :py:meth:`{2}.Client.get_waiter` using `{3}`_ .').format( resource_model.name, - xform_name(waiter.name).replace('_', ' '), + ' '.join(waiter.name.split('_')[2:]), service_name, xform_name(waiter.waiter_name)) @@ -476,7 +483,7 @@ def document_waiter(waiter, service_name, resource_model, service_model, return document_operation( operation_model=operation_model, service_name=service_name, - operation_name=xform_name(waiter.resource_waiter_name), + operation_name=xform_name(waiter.name), description=description, example_instance = xform_name(resource_model.name), ignore_params=ignore_params, rtype=None) diff --git a/boto3/resources/factory.py b/boto3/resources/factory.py index c637c4e8f8..7794fb21f4 100644 --- a/boto3/resources/factory.py +++ b/boto3/resources/factory.py @@ -14,8 +14,6 @@ import logging from functools import partial -from botocore import xform_name - from .action import ServiceAction from .action import WaiterAction from .base import ResourceMeta, ServiceResource @@ -74,6 +72,11 @@ def load_from_definition(self, service_name, resource_name, model, resource_model = ResourceModel(resource_name, model, resource_defs) + shape = None + if resource_model.shape: + shape = service_model.shape_for(resource_model.shape) + resource_model.load_rename_map(shape) + self._load_identifiers(attrs, meta, resource_model) self._load_actions(attrs, resource_model, resource_defs, service_model) @@ -98,11 +101,8 @@ def _load_identifiers(self, attrs, meta, model): operations on the resource. """ for identifier in model.identifiers: - snake_cased = xform_name(identifier.name) - snake_cased = self._check_allowed_name( - attrs, snake_cased, 'identifier', model.name) - meta.identifiers.append(snake_cased) - attrs[snake_cased] = None + meta.identifiers.append(identifier.name) + attrs[identifier.name] = None def _load_actions(self, attrs, model, resource_defs, service_model): """ @@ -112,16 +112,12 @@ def _load_actions(self, attrs, model, resource_defs, service_model): """ if model.load: attrs['load'] = self._create_action( - 'load', model.load, resource_defs, service_model, - is_load=True) + model.load, resource_defs, service_model, is_load=True) attrs['reload'] = attrs['load'] for action in model.actions: - snake_cased = xform_name(action.name) - snake_cased = self._check_allowed_name( - attrs, snake_cased, 'action', model.name) - attrs[snake_cased] = self._create_action(snake_cased, - action, resource_defs, service_model) + attrs[action.name] = self._create_action(action, resource_defs, + service_model) def _load_attributes(self, attrs, meta, model, service_model): """ @@ -133,16 +129,9 @@ def _load_attributes(self, attrs, meta, model, service_model): if model.shape: shape = service_model.shape_for(model.shape) - for name, member in shape.members.items(): - snake_cased = xform_name(name) - if snake_cased in meta.identifiers: - # Skip identifiers, these are set through other means - continue - - snake_cased = self._check_allowed_name( - attrs, snake_cased, 'attribute', model.name) - attrs[snake_cased] = self._create_autoload_property(name, - snake_cased) + attributes = model.get_attributes(shape) + for name, (orig_name, member) in attributes.items(): + attrs[name] = self._create_autoload_property(orig_name, name) def _load_collections(self, attrs, model, resource_defs, service_model): """ @@ -152,12 +141,8 @@ def _load_collections(self, attrs, model, resource_defs, service_model): through the collection's items. """ for collection_model in model.collections: - snake_cased = xform_name(collection_model.name) - snake_cased = self._check_allowed_name( - attrs, snake_cased, 'collection', model.name) - - attrs[snake_cased] = self._create_collection( - attrs['meta'].service_name, model.name, snake_cased, + attrs[collection_model.name] = self._create_collection( + attrs['meta'].service_name, model.name, collection_model, resource_defs, service_model) def _load_has_relations(self, attrs, service_name, resource_name, @@ -176,11 +161,8 @@ def _load_has_relations(self, attrs, service_name, resource_name, # This is a dangling reference, i.e. we have all # the data we need to create the resource, so # this instance becomes an attribute on the class. - snake_cased = xform_name(reference.name) - snake_cased = self._check_allowed_name( - attrs, snake_cased, 'reference', model.name) - attrs[snake_cased] = self._create_reference( - reference.resource.type, snake_cased, reference, + attrs[reference.name] = self._create_reference( + reference.resource.type, reference, service_name, resource_name, model, resource_defs, service_model) @@ -200,44 +182,7 @@ def _load_waiters(self, attrs, model): of the resource. """ for waiter in model.waiters: - snake_cased = xform_name(waiter.resource_waiter_name) - snake_cased = self._check_allowed_name( - attrs, snake_cased, 'waiter', model.name) - attrs[snake_cased] = self._create_waiter(waiter, snake_cased) - - def _check_allowed_name(self, attrs, name, category, resource_name): - """ - Determine if a given name is allowed on the instance, and if not, - then raise an exception. This prevents public attributes of the - class from being clobbered, e.g. since we define ``Resource.meta``, - no identifier may be named ``meta``. Another example: no action - named ``queue_items`` may be added after an identifier of the same - name has been added. - - One attempt is made in the event of a collision to remedy the - situation. The ``category`` is appended to the name and the - check is performed again. For example, if an action named - ``get_frobs`` fails the test, then we try ``get_frobs_action`` - after logging a warning. - - :raises: ValueError - """ - if name in attrs: - logger.warning('%s `%s` would clobber existing %s' - ' resource attribute, going to try' - ' %s instead...', category, name, - resource_name, name + '_' + category) - # TODO: Move this logic into the model and strictly - # define the loading order of categories. This - # will make documentation much simpler. - name = name + '_' + category - - if name in attrs: - raise ValueError('{0} `{1}` would clobber existing ' - '{2} resource attribute'.format( - category, name, resource_name)) - - return name + attrs[waiter.name] = self._create_waiter(waiter) def _create_autoload_property(factory_self, name, snake_cased): """ @@ -262,22 +207,22 @@ def property_loader(self): property_loader.__doc__ = 'TODO' return property(property_loader) - def _create_waiter(factory_self, waiter_model, snake_cased): + def _create_waiter(factory_self, waiter_model): """ Creates a new wait method for each resource where both a waiter and resource model is defined. """ - waiter = WaiterAction(waiter_model, waiter_resource_name=snake_cased) + waiter = WaiterAction(waiter_model, + waiter_resource_name=waiter_model.name) def do_waiter(self, *args, **kwargs): waiter(self, *args, **kwargs) - do_waiter.__name__ = str(snake_cased) + do_waiter.__name__ = str(waiter_model.name) do_waiter.__doc__ = 'TODO' return do_waiter def _create_collection(factory_self, service_name, resource_name, - snake_cased, collection_model, - resource_defs, service_model): + collection_model, resource_defs, service_model): """ Creates a new property on the resource to lazy-load a collection. """ @@ -289,13 +234,12 @@ def get_collection(self): return cls(collection_model, self, factory_self, resource_defs, service_model) - get_collection.__name__ = str(snake_cased) + get_collection.__name__ = str(collection_model.name) get_collection.__doc__ = 'TODO' return property(get_collection) - def _create_reference(factory_self, name, snake_cased, reference, - service_name, resource_name, model, resource_defs, - service_model): + def _create_reference(factory_self, name, reference, service_name, + resource_name, model, resource_defs, service_model): """ Creates a new property on the resource to lazy-load a reference. """ @@ -313,7 +257,7 @@ def get_reference(self): # identifiers to instantiate the resource reference. return handler(self, {}, {}) - get_reference.__name__ = str(snake_cased) + get_reference.__name__ = str(reference.name) get_reference.__doc__ = 'TODO' return property(get_reference) @@ -352,7 +296,7 @@ def create_resource(self, *args, **kwargs): create_resource.__doc__ = 'TODO' return create_resource - def _create_action(factory_self, snake_cased, action_model, resource_defs, + def _create_action(factory_self, action_model, resource_defs, service_model, is_load=False): """ Creates a new method which makes a request to the underlying @@ -386,6 +330,6 @@ def do_action(self, *args, **kwargs): return response - do_action.__name__ = str(snake_cased) + do_action.__name__ = str(action_model.name) do_action.__doc__ = 'TODO' return do_action diff --git a/boto3/resources/model.py b/boto3/resources/model.py index 3dd60c42ce..70611831e0 100644 --- a/boto3/resources/model.py +++ b/boto3/resources/model.py @@ -25,6 +25,8 @@ import logging +from botocore import xform_name + logger = logging.getLogger(__name__) @@ -152,15 +154,14 @@ class Waiter(DefinitionWithParams): :type definition: dict :param definition: The JSON definition """ + PREFIX = 'WaitUntil' + def __init__(self, name, definition): super(Waiter, self).__init__(definition) #: (``string``) The name of this waiter self.name = name - #: (``string``) The name of the waiter in the resource - self.resource_waiter_name = 'WaitUntil' + name - #: (``string``) The name of the underlying event waiter self.waiter_name = definition.get('waiterName') @@ -250,12 +251,172 @@ class ResourceModel(object): def __init__(self, name, definition, resource_defs): self._definition = definition self._resource_defs = resource_defs + self._renamed = {} #: (``string``) The name of this resource self.name = name #: (``string``) The service shape name for this resource or ``None`` self.shape = definition.get('shape') + def load_rename_map(self, shape=None): + """ + Load a name translation map given a shape. This will set + up renamed values for any collisions, e.g. if the shape, + an action, and a subresource all are all named ``foo`` + then the resource will have an action ``foo``, a subresource + named ``Foo`` and a property named ``foo_attribute``. + This is the order of precedence, from most important to + least important: + + * Load action (resource.load) + * Identifiers + * Actions + * Subresources + * References + * Collections + * Waiters + * Attributes (shape members) + + Batch actions are only exposed on collections, so do not + get modified here. Subresources use upper camel casing, so + are unlikely to collide with anything but other subresources. + + Creates a structure like this:: + + renames = { + ('action', 'id'): 'id_action', + ('collection', 'id'): 'id_collection', + ('attribute', 'id'): 'id_attribute' + } + + # Get the final name for an action named 'id' + name = renames.get(('action', 'id'), 'id') + + :type shape: botocore.model.Shape + :param shape: The underlying shape for this resource. + """ + # Meta is a reserved name for resources + names = set(['meta']) + self._renamed = {} + + if self._definition.get('load'): + names.add('load') + + for item in self._definition.get('identifiers', []): + self._load_name_with_category(names, item['name'], 'identifier') + + for name in self._definition.get('actions', {}): + self._load_name_with_category(names, name, 'action') + + for name, ref in self._get_has_definition().items(): + # Subresources require no data members, just typically + # identifiers and user input. + data_required = False + for identifier in ref['resource']['identifiers']: + if identifier['source'] == 'data': + data_required = True + break + + if not data_required: + self._load_name_with_category(names, name, 'subresource', + snake_case=False) + else: + self._load_name_with_category(names, name, 'reference') + + for name in self._definition.get('hasMany', {}): + self._load_name_with_category(names, name, 'collection') + + for name in self._definition.get('waiters', {}): + self._load_name_with_category(names, Waiter.PREFIX + name, + 'waiter') + + if shape is not None: + for name in shape.members.keys(): + self._load_name_with_category(names, name, 'attribute') + + def _load_name_with_category(self, names, name, category, + snake_case=True): + """ + Load a name with a given category, possibly renaming it + if that name is already in use. The name will be stored + in ``names`` and possibly be set up in ``self._renamed``. + + :type names: set + :param names: Existing names (Python attributes, properties, or + methods) on the resource. + :type name: string + :param name: The original name of the value. + :type category: string + :param category: The value type, such as 'identifier' or 'action' + :type snake_case: bool + :param snake_case: True (default) if the name should be snake cased. + """ + if snake_case: + name = xform_name(name) + + if name in names: + logger.debug('Renaming %s %s %s' % (self.name, category, name)) + self._renamed[(category, name)] = name + '_' + category + name += '_' + category + + if name in names: + # This isn't good, let's raise instead of trying to keep + # renaming this value. + raise ValueError('Problem renaming {0} {1} to {2}!'.format( + self.name, category, name)) + + names.add(name) + + def _get_name(self, category, name, snake_case=True): + """ + Get a possibly renamed value given a category and name. This + uses the rename map set up in ``load_rename_map``, so that + method must be called once first. + + :type category: string + :param category: The value type, such as 'identifier' or 'action' + :type name: string + :param name: The original name of the value + :type snake_case: bool + :param snake_case: True (default) if the name should be snake cased. + :rtype: string + :return: Either the renamed value if it is set, otherwise the + original name. + """ + if snake_case: + name = xform_name(name) + + return self._renamed.get((category, name), name) + + def get_attributes(self, shape): + """ + Get a dictionary of attribute names to original name and shape + models that represent the attributes of this resource. Looks + like the following: + + { + 'some_name': ('SomeName', ) + } + + :type shape: botocore.model.Shape + :param shape: The underlying shape for this resource. + :rtype: dict + :return: Mapping of resource attributes. + """ + attributes = {} + identifier_names = [i.name for i in self.identifiers] + + for name, member in shape.members.items(): + snake_cased = xform_name(name) + if snake_cased in identifier_names: + # Skip identifiers, these are set through other means + continue + snake_cased = self._get_name('attribute', snake_cased, + snake_case=False) + attributes[snake_cased] = (name, member) + + return attributes + @property def identifiers(self): """ @@ -266,7 +427,8 @@ def identifiers(self): identifiers = [] for item in self._definition.get('identifiers', []): - identifiers.append(Identifier(item['name'])) + name = self._get_name('identifier', item['name']) + identifiers.append(Identifier(name)) return identifiers @@ -294,6 +456,7 @@ def actions(self): actions = [] for name, item in self._definition.get('actions', {}).items(): + name = self._get_name('action', name) actions.append(Action(name, item, self._resource_defs)) return actions @@ -308,6 +471,7 @@ def batch_actions(self): actions = [] for name, item in self._definition.get('batchActions', {}).items(): + name = self._get_name('batch_action', name) actions.append(Action(name, item, self._resource_defs)) return actions @@ -387,6 +551,10 @@ def _get_related_resources(self, subresources): resources = [] for name, definition in self._get_has_definition().items(): + if subresources: + name = self._get_name('subresource', name, snake_case=False) + else: + name = self._get_name('reference', name) action = Action(name, definition, self._resource_defs) data_required = False @@ -430,6 +598,7 @@ def collections(self): collections = [] for name, item in self._definition.get('hasMany', {}).items(): + name = self._get_name('collection', name) collections.append(Collection(name, item, self._resource_defs)) return collections @@ -444,6 +613,7 @@ def waiters(self): waiters = [] for name, item in self._definition.get('waiters', {}).items(): + name = self._get_name('waiter', Waiter.PREFIX + name) waiters.append(Waiter(name, item)) return waiters diff --git a/boto3/session.py b/boto3/session.py index efd9fb7faf..26fcadfe1f 100644 --- a/boto3/session.py +++ b/boto3/session.py @@ -37,10 +37,13 @@ class Session(object): :type botocore_session: botocore.session.Session :param botocore_session: Use this Botocore session instead of creating a new default one. + :type profile_name: string + :param profile_name: The name of a profile to use. If not given, then + the default profile is used. """ def __init__(self, aws_access_key_id=None, aws_secret_access_key=None, aws_session_token=None, region_name=None, - botocore_session=None): + botocore_session=None, profile_name=None): if botocore_session is not None: self._session = botocore_session else: @@ -58,6 +61,9 @@ def __init__(self, aws_access_key_id=None, aws_secret_access_key=None, self._session.user_agent_name = 'Boto3' self._session.user_agent_version = boto3.__version__ + if profile_name is not None: + self._session.profile = profile_name + if aws_access_key_id or aws_secret_access_key or aws_session_token: self._session.set_credentials(aws_access_key_id, aws_secret_access_key, aws_session_token) @@ -72,6 +78,13 @@ def __repr__(self): return 'Session(region={0})'.format( repr(self._session.get_config_variable('region'))) + @property + def profile_name(self): + """ + The **read-only** profile name. + """ + return self._session.profile or 'default' + def _setup_loader(self): """ Setup loader paths so that we can load resources. diff --git a/setup.py b/setup.py index 6e027ed336..6327aa92f3 100644 --- a/setup.py +++ b/setup.py @@ -20,13 +20,13 @@ def get_version(): init = open(os.path.join(ROOT, 'boto3', '__init__.py')).read() return VERSION_RE.search(init).group(1) -packages = [ +packages = [ 'boto3', 'boto3.resources', ] requires = [ - 'botocore==0.92.0', + 'botocore==0.94.0', 'bcdoc==0.12.2', 'jmespath==0.6.1', ] diff --git a/tests/unit/resources/test_factory.py b/tests/unit/resources/test_factory.py index c320e875eb..17b38b11cd 100644 --- a/tests/unit/resources/test_factory.py +++ b/tests/unit/resources/test_factory.py @@ -11,7 +11,7 @@ # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. -from botocore.model import ServiceModel, StructureShape +from botocore.model import DenormalizedStructureBuilder, ServiceModel from boto3.exceptions import ResourceLoadException from boto3.resources.base import ServiceResource from boto3.resources.collection import CollectionManager @@ -20,15 +20,14 @@ from tests import BaseTestCase, mock -class TestResourceFactory(BaseTestCase): +class BaseTestResourceFactory(BaseTestCase): def setUp(self): - super(TestResourceFactory, self).setUp() + super(BaseTestResourceFactory, self).setUp() self.factory = ResourceFactory() self.load = self.factory.load_from_definition - def tearDown(self): - super(TestResourceFactory, self).tearDown() +class TestResourceFactory(BaseTestResourceFactory): def test_get_service_returns_resource_class(self): TestResource = self.load('test', 'test', {}, {}, None) @@ -127,11 +126,14 @@ def test_factory_creates_properties(self): } } } - shape = mock.Mock() - shape.members = { - 'ETag': None, - 'LastModified': None, - } + shape = DenormalizedStructureBuilder().with_members({ + 'ETag': { + 'type': 'string', + }, + 'LastModified': { + 'type': 'string' + } + }).build_model() service_model = mock.Mock() service_model.shape_for.return_value = shape @@ -358,12 +360,20 @@ def test_resource_lazy_loads_properties(self, action_cls): } } } - shape = mock.Mock() - shape.members = { - 'Url': None, - 'ETag': None, - 'LastModified': None, - } + shape = DenormalizedStructureBuilder().with_members({ + 'ETag': { + 'type': 'string', + 'shape_name': 'ETag' + }, + 'LastModified': { + 'type': 'string', + 'shape_name': 'LastModified' + }, + 'Url': { + 'type': 'string', + 'shape_name': 'Url' + } + }).build_model() service_model = mock.Mock() service_model.shape_for.return_value = shape @@ -402,12 +412,17 @@ def test_resource_lazy_properties_missing_load(self, action_cls): # Note the lack of a `load` method. These resources # are usually loaded via a call on a parent resource. } - shape = mock.Mock() - shape.members = { - 'Url': None, - 'ETag': None, - 'LastModified': None, - } + shape = DenormalizedStructureBuilder().with_members({ + 'ETag': { + 'type': 'string', + }, + 'LastModified': { + 'type': 'string' + }, + 'Url': { + 'type': 'string' + } + }).build_model() service_model = mock.Mock() service_model.shape_for.return_value = shape @@ -518,7 +533,7 @@ def test_resource_loads_collections(self, mock_model): 'Queue': {} } service_model = ServiceModel({}) - mock_model.return_value.name = 'Queues' + mock_model.return_value.name = 'queues' resource = self.load('test', 'test', model, defs, service_model)() @@ -574,7 +589,7 @@ def test_resource_waiter_calls_waiter_method(self, waiter_action_cls): waiter_action.assert_called_with(resource, 'arg1', arg2=2) -class TestResourceFactoryDanglingResource(TestResourceFactory): +class TestResourceFactoryDanglingResource(BaseTestResourceFactory): def setUp(self): super(TestResourceFactoryDanglingResource, self).setUp() @@ -672,7 +687,7 @@ def test_dangling_resource_inequality(self): self.assertNotEqual(q1, m) -class TestServiceResourceSubresources(TestResourceFactory): +class TestServiceResourceSubresources(BaseTestResourceFactory): def setUp(self): super(TestServiceResourceSubresources, self).setUp() diff --git a/tests/unit/resources/test_model.py b/tests/unit/resources/test_model.py index edeea4b6c8..5936385a14 100644 --- a/tests/unit/resources/test_model.py +++ b/tests/unit/resources/test_model.py @@ -11,6 +11,8 @@ # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. +from botocore.model import DenormalizedStructureBuilder + from boto3.resources.model import ResourceModel, Action, Collection, Waiter from tests import BaseTestCase @@ -182,7 +184,7 @@ def test_resource_references(self): self.assertEqual(len(model.references), 1) ref = model.references[0] - self.assertEqual(ref.name, 'Frob') + self.assertEqual(ref.name, 'frob') self.assertEqual(ref.resource.type, 'Frob') self.assertEqual(ref.resource.identifiers[0].target, 'Id') self.assertEqual(ref.resource.identifiers[0].source, 'data') @@ -230,7 +232,194 @@ def test_waiter(self): waiter = model.waiters[0] self.assertIsInstance(waiter, Waiter) - self.assertEqual(waiter.name, 'Exists') + self.assertEqual(waiter.name, 'wait_until_exists') self.assertEqual(waiter.waiter_name, 'ObjectExists') - self.assertEqual(waiter.resource_waiter_name, 'WaitUntilExists') self.assertEqual(waiter.params[0].target, 'Bucket') + +class TestRenaming(BaseTestCase): + def test_multiple(self): + # This tests a bunch of different renames working together + model = ResourceModel('test', { + 'identifiers': [{'name': 'Foo'}], + 'actions': { + 'Foo': {} + }, + 'has': { + 'Foo': { + 'resource': { + 'type': 'Frob', + 'identifiers': [ + {'target':'Id', 'source':'data', + 'path': 'FrobId'} + ] + } + } + }, + 'hasMany': { + 'Foo': {} + }, + 'waiters': { + 'Foo': {} + } + }, { + 'Frob': {} + }) + + shape = DenormalizedStructureBuilder().with_members({ + 'Foo': { + 'type': 'string', + }, + 'Bar': { + 'type': 'string' + } + }).build_model() + + model.load_rename_map(shape) + + self.assertEqual(model.identifiers[0].name, 'foo') + self.assertEqual(model.actions[0].name, 'foo_action') + self.assertEqual(model.references[0].name, 'foo_reference') + self.assertEqual(model.collections[0].name, 'foo_collection') + self.assertEqual(model.waiters[0].name, 'wait_until_foo') + + # If an identifier and an attribute share the same name, then + # the attribute is essentially hidden. + self.assertNotIn('foo_attribute', model.get_attributes(shape)) + + # Other attributes need to be there, though + self.assertIn('bar', model.get_attributes(shape)) + + # The rest of the tests below ensure the correct order of precedence + # for the various categories of attributes/properties/methods on the + # resource model. + def test_meta_beats_identifier(self): + model = ResourceModel('test', { + 'identifiers': [{'name': 'Meta'}] + }, {}) + + model.load_rename_map() + + self.assertEqual(model.identifiers[0].name, 'meta_identifier') + + def test_load_beats_identifier(self): + model = ResourceModel('test', { + 'identifiers': [{'name': 'Load'}], + 'load': { + 'request': { + 'operation': 'GetFrobs' + } + } + }, {}) + + model.load_rename_map() + + self.assertTrue(model.load) + self.assertEqual(model.identifiers[0].name, 'load_identifier') + + def test_identifier_beats_action(self): + model = ResourceModel('test', { + 'identifiers': [{'name': 'foo'}], + 'actions': { + 'Foo': { + 'request': { + 'operation': 'GetFoo' + } + } + } + }, {}) + + model.load_rename_map() + + self.assertEqual(model.identifiers[0].name, 'foo') + self.assertEqual(model.actions[0].name, 'foo_action') + + def test_action_beats_reference(self): + model = ResourceModel('test', { + 'actions': { + 'Foo': { + 'request': { + 'operation': 'GetFoo' + } + } + }, + 'has': { + 'Foo': { + 'resource': { + 'type': 'Frob', + 'identifiers': [ + {'target':'Id', 'source':'data', + 'path': 'FrobId'} + ] + } + } + } + }, {'Frob': {}}) + + model.load_rename_map() + + self.assertEqual(model.actions[0].name, 'foo') + self.assertEqual(model.references[0].name, 'foo_reference') + + def test_reference_beats_collection(self): + model = ResourceModel('test', { + 'has': { + 'Foo': { + 'resource': { + 'type': 'Frob', + 'identifiers': [ + {'target':'Id', 'source':'data', + 'path': 'FrobId'} + ] + } + } + }, + 'hasMany': { + 'Foo': { + 'resource': { + 'type': 'Frob' + } + } + } + }, {'Frob': {}}) + + model.load_rename_map() + + self.assertEqual(model.references[0].name, 'foo') + self.assertEqual(model.collections[0].name, 'foo_collection') + + def test_collection_beats_waiter(self): + model = ResourceModel('test', { + 'hasMany': { + 'WaitUntilFoo': { + 'resource': { + 'type': 'Frob' + } + } + }, + 'waiters': { + 'Foo': {} + } + }, {'Frob': {}}) + + model.load_rename_map() + + self.assertEqual(model.collections[0].name, 'wait_until_foo') + self.assertEqual(model.waiters[0].name, 'wait_until_foo_waiter') + + def test_waiter_beats_attribute(self): + model = ResourceModel('test', { + 'waiters': { + 'Foo': {} + } + }, {'Frob': {}}) + + shape = DenormalizedStructureBuilder().with_members({ + 'WaitUntilFoo': { + 'type': 'string', + } + }).build_model() + + model.load_rename_map(shape) + + self.assertEqual(model.waiters[0].name, 'wait_until_foo') + self.assertIn('wait_until_foo_attribute', model.get_attributes(shape)) diff --git a/tests/unit/test_session.py b/tests/unit/test_session.py index 3555b132be..d014518be2 100644 --- a/tests/unit/test_session.py +++ b/tests/unit/test_session.py @@ -66,6 +66,23 @@ def test_credentials_can_be_set(self): bc_session.set_credentials.assert_called_with( 'key', 'secret', 'token') + def test_profile_can_be_set(self): + bc_session = self.bc_session_cls.return_value + + session = Session(profile_name='foo') + + self.assertEqual(bc_session.profile, 'foo') + + # We should also be able to read the value + self.assertEqual(session.profile_name, 'foo') + + def test_profile_default(self): + self.bc_session_cls.return_value.profile = None + + session = Session() + + self.assertEqual(session.profile_name, 'default') + def test_custom_session(self): bc_session = self.bc_session_cls() self.bc_session_cls.reset_mock()