diff --git a/Makefile b/Makefile index 8aec323..3bef3ab 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,11 @@ test: python setup.py test +.PHONY: tests-prep +tests-prep: + pip install tox tox-pyenv + pyenv local 2.7.15 3.5.6 3.6.6 pypy2.7-6.0.0 + .PHONY: tests tests: tox diff --git a/prestans/__init__.py b/prestans/__init__.py index 758884e..3903611 100644 --- a/prestans/__init__.py +++ b/prestans/__init__.py @@ -33,5 +33,5 @@ __all__ = ['http', 'types', 'rest', 'parser', 'serializer', 'deserializer', 'provider', 'ext', 'exception'] -__version_info__ = (2, 2, 1) +__version_info__ = (2, 3, 0) __version__ = '.'.join(str(v) for v in __version_info__) diff --git a/prestans/exception.py b/prestans/exception.py index f2c705b..687377b 100644 --- a/prestans/exception.py +++ b/prestans/exception.py @@ -320,7 +320,7 @@ class AttributeFilterDiffers(RequestException): def __init__(self, attribute_list): _code = STATUS.BAD_REQUEST - _message = "attribute filter does not contain attributes (%s) that are not part of template" % ( + _message = "attribute filter contains attributes (%s) that are not part of template" % ( ', '.join(attribute_list) ) @@ -495,6 +495,27 @@ def __init__(self, message="Moved Permanently", response_model=None): super(MovedPermanently, self).__init__(_code, message, response_model) +class Redirect(ResponseException): + + def __init__(self, code, url): + self._url = url + super(Redirect, self).__init__(code, "Redirect") + + @property + def url(self): + return self._url + + +class TemporaryRedirect(Redirect): + def __init__(self, url): + super(TemporaryRedirect, self).__init__(STATUS.TEMPORARY_REDIRECT, url) + + +class PermanentRedirect(Redirect): + def __init__(self, url): + super(PermanentRedirect, self).__init__(STATUS.PERMANENT_REDIRECT, url) + + class PaymentRequired(ResponseException): def __init__(self, message="Payment Required", response_model=None): diff --git a/prestans/ext/data/adapters/__init__.py b/prestans/ext/data/adapters/__init__.py index 1527030..d868f55 100644 --- a/prestans/ext/data/adapters/__init__.py +++ b/prestans/ext/data/adapters/__init__.py @@ -31,14 +31,20 @@ # import inspect -from prestans.types import Model +from prestans import exception +from prestans import parser +from prestans import types class ModelAdapter(object): def __init__(self, rest_model_class, persistent_model_class): + """ + :param rest_model_class: + :param persistent_model_class: + """ - if issubclass(rest_model_class, Model): + if issubclass(rest_model_class, types.Model): self._rest_model_class = rest_model_class else: raise TypeError("rest_model_class must be sub class of prestans.types.Model") @@ -52,18 +58,184 @@ def persistent_model_class(self): @property def rest_model_class(self): return self._rest_model_class - - def adapt_persistent_to_rest(self, persistent_object): - raise NotImplementedError("adapt_persistent_to_rest direct use not allowed") + + def adapt_persistent_to_rest(self, persistent_object, attribute_filter=None): + """ + adapts a persistent model to a rest model by inspecting + """ + + rest_model_instance = self.rest_model_class() + + for attribute_key in rest_model_instance.get_attribute_keys(): + + rest_attr = getattr(self.rest_model_class, attribute_key) + + # don't bother processing if the persistent model doesn't have this attribute + if not hasattr(persistent_object, attribute_key): + + if isinstance(rest_attr, types.Model): + #: If the attribute is a Model, then we set it to None otherwise we get a model + #: with default values, which is invalid when constructing responses + try: + setattr(rest_model_instance, attribute_key, None) + # catch any exception thrown from setattr to give a usable error message + except TypeError as exp: + raise TypeError('Attribute %s, %s' % (attribute_key, str(exp))) + + continue + # ignore class methods + elif inspect.ismethod(getattr(persistent_object, attribute_key)): + import logging + logging.error("ignoring method: "+attribute_key) + continue + # attribute is not visible don't bother processing + elif isinstance(attribute_filter, parser.AttributeFilter) and \ + not attribute_filter.is_attribute_visible(attribute_key): + continue + + # handles prestans array population from SQLAlchemy relationships + elif isinstance(rest_attr, types.Array): + + persistent_attr_value = getattr(persistent_object, attribute_key) + rest_model_array_handle = getattr(rest_model_instance, attribute_key) + + # iterator uses the .append method exposed by prestans arrays to validate + # and populate the collection in the instance. + for collection_element in persistent_attr_value: + if isinstance(rest_attr.element_template, types.Boolean) or \ + isinstance(rest_attr.element_template, types.Float) or \ + isinstance(rest_attr.element_template, types.Integer) or \ + isinstance(rest_attr.element_template, types.String): + rest_model_array_handle.append(collection_element) + else: + element_adapter = registry.get_adapter_for_rest_model(rest_attr.element_template) + + # check if there is a sub model filter + sub_attribute_filter = None + if attribute_filter and attribute_key in attribute_filter: + sub_attribute_filter = getattr(attribute_filter, attribute_key) + + adapted_rest_model = element_adapter.adapt_persistent_to_rest( + collection_element, + sub_attribute_filter + ) + rest_model_array_handle.append(adapted_rest_model) + + elif isinstance(rest_attr, types.Model): + + try: + persistent_attr_value = getattr(persistent_object, attribute_key) + + if persistent_attr_value is None: + adapted_rest_model = None + else: + model_adapter = registry.get_adapter_for_rest_model(rest_attr) + + # check if there is a sub model filter + sub_attribute_filter = None + if isinstance(attribute_filter, parser.AttributeFilter) and \ + attribute_key in attribute_filter: + sub_attribute_filter = getattr(attribute_filter, attribute_key) + + adapted_rest_model = model_adapter.adapt_persistent_to_rest( + persistent_attr_value, + sub_attribute_filter + ) + + setattr(rest_model_instance, attribute_key, adapted_rest_model) + + except TypeError as exp: + raise TypeError('Attribute %s, %s' % (attribute_key, str(exp))) + except exception.DataValidationException as exp: + raise exception.InconsistentPersistentDataError(attribute_key, str(exp)) + + else: + + # otherwise copy the value to the rest model + try: + persistent_attr_value = getattr(persistent_object, attribute_key) + setattr(rest_model_instance, attribute_key, persistent_attr_value) + except TypeError as exp: + raise TypeError('Attribute %s, %s' % (attribute_key, str(exp))) + except exception.ValidationError as exp: + raise exception.InconsistentPersistentDataError(attribute_key, str(exp)) + + return rest_model_instance + + +def adapt_persistent_instance(persistent_object, target_rest_class=None, attribute_filter=None): + """ + Adapts a single persistent instance to a REST model; at present this is a + common method for all persistent backends. + + Refer to: https://groups.google.com/forum/#!topic/prestans-discuss/dO1yx8f60as + for discussion on this feature + """ + + # try and get the adapter and the REST class for the persistent object + if target_rest_class is None: + adapter_instance = registry.get_adapter_for_persistent_model(persistent_object) + else: + if inspect.isclass(target_rest_class): + target_rest_class = target_rest_class() + + adapter_instance = registry.get_adapter_for_persistent_model(persistent_object, target_rest_class) + + # would raise an exception if the attribute_filter differs from the target_rest_class + if attribute_filter is not None and isinstance(attribute_filter, parser.AttributeFilter): + parser.AttributeFilter.from_model(target_rest_class).conforms_to_template_filter(attribute_filter) + + return adapter_instance.adapt_persistent_to_rest(persistent_object, attribute_filter) + + +def adapt_persistent_collection(persistent_collection, target_rest_class=None, attribute_filter=None): + # ensure that collection is iterable and has at least one element + persistent_collection_length = 0 + + # attempt to detect the length of the persistent_collection + if persistent_collection and isinstance(persistent_collection, (list, tuple)): + persistent_collection_length = len(persistent_collection) + # SQLAlchemy query + elif persistent_collection and persistent_collection.__module__ == "sqlalchemy.orm.query": + persistent_collection_length = persistent_collection.count() + # Google App Engine NDB + elif persistent_collection and persistent_collection.__module__ == "google.appengine.ext.ndb": + persistent_collection_length = persistent_collection.count() + + # if the persistent_collection is empty then return a blank array + if persistent_collection_length == 0: + return types.Array(element_template=target_rest_class()) + + # try and get the adapter and the REST class for the persistent object + if target_rest_class is None: + adapter_instance = registry.get_adapter_for_persistent_model( + persistent_collection[0] + ) + else: + if inspect.isclass(target_rest_class): + target_rest_class = target_rest_class() + + adapter_instance = registry.get_adapter_for_persistent_model( + persistent_collection[0], + target_rest_class + ) + + adapted_models = types.Array(element_template=adapter_instance.rest_model_class()) + + for persistent_object in persistent_collection: + adapted_models.append(adapter_instance.adapt_persistent_to_rest(persistent_object, attribute_filter)) + + return adapted_models class AdapterRegistryManager(object): """ AdapterRegistryManager keeps track of rest to persistent model maps - AdapterRegistryManager should not be instantiated by the applications, a singleton - instance supplied by this package. + New AdapterRegistryManager's should not be instantiated by the application, a singleton + instance is supplied by this package. """ + DEFAULT_REST_ADAPTER = "prestans_rest_default_adapter" def __init__(self): self._persistent_map = dict() @@ -85,22 +257,55 @@ def register_adapter(self, model_adapter): rest_class_signature = self.generate_signature(model_adapter.rest_model_class) persistent_class_signature = self.generate_signature(model_adapter.persistent_model_class) - # store references to how a rest model maps to a persistent model and vice versa - self._persistent_map[persistent_class_signature] = model_adapter + # store references to rest model self._rest_map[rest_class_signature] = model_adapter - - def get_adapter_for_persistent_model(self, persistent_model): + + if persistent_class_signature not in self._persistent_map: + self._persistent_map[persistent_class_signature] = dict() + + # store a reference to the adapter under both REST signature and default key + # the default is always the last registered model (to match behaviour before this was patched) + self._persistent_map[persistent_class_signature][self.DEFAULT_REST_ADAPTER] = model_adapter + self._persistent_map[persistent_class_signature][rest_class_signature] = model_adapter + + def register_persistent_rest_pair(self, persistent_model_class, rest_model_class): + """ + :param persistent_model_class: + :param rest_model_class: + """ + self.register_adapter(ModelAdapter( + rest_model_class=rest_model_class, + persistent_model_class=persistent_model_class + )) + + def clear_registered_adapters(self): + """ + Clears all of the currently registered model adapters + """ + self._persistent_map.clear() + self._rest_map.clear() + + def get_adapter_for_persistent_model(self, persistent_model, rest_model=None): """ :param persistent_model: instance of persistent model + :param rest_model: specific REST model :return: the matching model adapter :rtype: ModelAdapter """ - class_signature = self.generate_signature(persistent_model) + persistent_signature = self.generate_signature(persistent_model) - if class_signature not in self._persistent_map: - raise TypeError("No registered Data Adapter for class %s" % class_signature) + if persistent_signature in self._persistent_map: + sub_map = self._persistent_map[persistent_signature] + + # return the first match if REST model was not specified + if rest_model is None: + return self._persistent_map[persistent_signature][self.DEFAULT_REST_ADAPTER] + else: + rest_sig = self.generate_signature(rest_model) + if rest_sig in sub_map: + return self._persistent_map[persistent_signature][rest_sig] - return self._persistent_map[class_signature] + raise TypeError("No registered Data Adapter for class %s" % persistent_signature) def get_adapter_for_rest_model(self, rest_model): """ diff --git a/prestans/ext/data/adapters/ndb.py b/prestans/ext/data/adapters/ndb.py index a2e2bf2..17e4e7e 100644 --- a/prestans/ext/data/adapters/ndb.py +++ b/prestans/ext/data/adapters/ndb.py @@ -31,160 +31,30 @@ # __all__ = ['ModelAdapter'] - -import inspect - -import prestans.types -import prestans.parser - from prestans.ext.data import adapters def adapt_persistent_instance(persistent_object, target_rest_class=None, attribute_filter=None): """ - Adapts a single persistent instance to a REST model; at present this is a - common method for all persistent backends. - - This might be moved to backend specific packages if the need arrises - - Refer to: https://groups.google.com/forum/#!topic/prestans-discuss/dO1yx8f60as - for discussion on this feature + Wrapper on adapters.adapt_persistent_instance for Google App Engine NDB """ - - #: Try and get the adapter and the REST class for the persistent object - if target_rest_class is None: - adapter_instance = adapters.registry.get_adapter_for_persistent_model(persistent_object) - else: - if inspect.isclass(target_rest_class): - target_rest_class = target_rest_class() - - adapter_instance = adapters.registry.get_adapter_for_rest_model(target_rest_class) - - #: Would raise an exception if the attribute_filter differs from the target_rest_class - if attribute_filter is not None and isinstance(attribute_filter, prestans.parser.AttributeFilter): - prestans.parser.AttributeFilter.from_model(target_rest_class).conforms_to_template_filter(attribute_filter) - - return adapter_instance.adapt_persistent_to_rest(persistent_object, attribute_filter) + return adapters.adapt_persistent_instance(persistent_object, target_rest_class, attribute_filter) def adapt_persistent_collection(persistent_collection, target_rest_class=None, attribute_filter=None): - - # ensure that collection is iterable and has at least one element - persistent_collection_length = 0 - - # attempt to reliably detect the length of the persistent_collection - if persistent_collection and isinstance(persistent_collection, (list, tuple)): - persistent_collection_length = len(persistent_collection) - elif persistent_collection: - persistent_collection_length = persistent_collection.count() - - # if the persistent_collection is empty then return a blank array - if persistent_collection_length == 0: - return prestans.types.Array(element_template=target_rest_class()) - - # try and get the adapter and the REST class for the persistent object - if target_rest_class is None: - adapter_instance = adapters.registry.get_adapter_for_persistent_model(persistent_collection[0]) - else: - if inspect.isclass(target_rest_class): - target_rest_class = target_rest_class() - - adapter_instance = adapters.registry.get_adapter_for_rest_model(target_rest_class) - - adapted_models = prestans.types.Array(element_template=adapter_instance.rest_model_class()) - - for persistent_object in persistent_collection: - adapted_models.append(adapter_instance.adapt_persistent_to_rest(persistent_object, attribute_filter)) - - return adapted_models - - -class ModelAdapter(adapters.ModelAdapter): """ - Provides a bridge between REST models and Google Datastore objects + Wrapper on adapters.adapt_persistent_collection for Google App Engine NDB """ + return adapters.adapt_persistent_collection(persistent_collection, target_rest_class, attribute_filter) - def adapt_persistent_to_rest(self, persistent_object, attribute_filter=None): - """ - adapts a persistent model to a rest model by inspecting - - :param self: - :param persistent_object: - :param attribute_filter: - :return: - """ - - rest_model_instance = self.rest_model_class() - - for attribute_key in rest_model_instance.get_attribute_keys(): - - rest_attr = getattr(self.rest_model_class, attribute_key) - - if not hasattr(persistent_object, attribute_key): - - #: Don't bother processing if the persistent model doesn't have this attribute - if issubclass(rest_attr.__class__, prestans.types.Model): - #: If the attribute is a Model, then we set it to None otherwise we get a model - #: with default values, which is invalid when constructing responses - setattr(rest_model_instance, attribute_key, None) - - continue - - #: Attribute not visible don't bother processing - elif isinstance(attribute_filter, prestans.parser.AttributeFilter) and\ - not attribute_filter.is_attribute_visible(attribute_key): - continue - elif isinstance(rest_attr, prestans.types.Array): - - persistent_attr_value = getattr(persistent_object, attribute_key) - rest_model_array_handle = getattr(rest_model_instance, attribute_key) - - #: Iterator uses the .append method exposed by prestans arrays to validate - #: and populate the collection in the instance. - for collection_element in persistent_attr_value: - - if isinstance(rest_attr.element_template, prestans.types.String): - rest_model_array_handle.append(collection_element) - elif isinstance(rest_attr.element_template, prestans.types.Integer): - rest_model_array_handle.append(collection_element) - elif isinstance(rest_attr.element_template, prestans.types.Float): - rest_model_array_handle.append(collection_element) - elif isinstance(rest_attr.element_template, prestans.types.Boolean): - rest_model_array_handle.append(collection_element) - else: - element_adapter = adapters.registry.get_adapter_for_rest_model(rest_attr._element_template) - adapted_rest_model = element_adapter.adapt_persistent_to_rest(collection_element) - rest_model_array_handle.append(adapted_rest_model) - - elif isinstance(rest_attr, prestans.types.Model): - - try: - - persistent_attr_value = getattr(persistent_object, attribute_key) - - if persistent_attr_value is None: - adapted_rest_model = None - else: - model_adapter = adapters.registry.get_adapter_for_rest_model(rest_attr) - adapted_rest_model = model_adapter.adapt_persistent_to_rest(persistent_attr_value) - - setattr(rest_model_instance, attribute_key, adapted_rest_model) - - except TypeError as exp: - raise TypeError('Attribute %s, %s' % (attribute_key, str(exp))) - except prestans.exception.DataValidationException as exp: - raise prestans.exception.InconsistentPersistentDataError(attribute_key, str(exp)) +class ModelAdapter(adapters.ModelAdapter): - else: - - # Default operation is to copy the value and set it, validate will ensure its an acceptable value - try: - persistent_attr_value = getattr(persistent_object, attribute_key) - setattr(rest_model_instance, attribute_key, persistent_attr_value) - except TypeError as exp: - raise TypeError('Attribute %s, %s' % (attribute_key, str(exp))) - except prestans.exception.DataValidationException as exp: - raise prestans.exception.InconsistentPersistentDataError(attribute_key, str(exp)) - - return rest_model_instance + def __init__(self, rest_model_class, persistent_model_class): + import logging + logger = logging.getLogger("prestans") + logger.warn("direct use of %s has been deprecated please use %s instead" % ( + self.__module__ + "." + self.__class__.__name__, + adapters.ModelAdapter.__module__ + "." + adapters.ModelAdapter.__name__ + )) + super(ModelAdapter, self).__init__(rest_model_class, persistent_model_class) diff --git a/prestans/ext/data/adapters/sqlalchemy.py b/prestans/ext/data/adapters/sqlalchemy.py index 470b3ac..4ccfc27 100644 --- a/prestans/ext/data/adapters/sqlalchemy.py +++ b/prestans/ext/data/adapters/sqlalchemy.py @@ -31,177 +31,30 @@ # __all__ = ['ModelAdapter'] -import inspect - -from prestans import exception -from prestans import types -from prestans import parser - from prestans.ext.data import adapters def adapt_persistent_instance(persistent_object, target_rest_class=None, attribute_filter=None): """ - Adapts a single persistent instance to a REST model; at present this is a - common method for all persistent backends. - - This might be moved to backend specific packages if the need arises - - Refer to: https://groups.google.com/forum/#!topic/prestans-discuss/dO1yx8f60as - for discussion on this feature + Wrapper on adapters.adapt_persistent_instance for SQLAlchemy """ - - # try and get the adapter and the REST class for the persistent object - if target_rest_class is None: - adapter_instance = adapters.registry.get_adapter_for_persistent_model(persistent_object) - else: - if inspect.isclass(target_rest_class): - target_rest_class = target_rest_class() - - adapter_instance = adapters.registry.get_adapter_for_rest_model(target_rest_class) - - # would raise an exception if the attribute_filter differs from the target_rest_class - if attribute_filter is not None and isinstance(attribute_filter, parser.AttributeFilter): - parser.AttributeFilter.from_model(target_rest_class).conforms_to_template_filter(attribute_filter) - - return adapter_instance.adapt_persistent_to_rest(persistent_object, attribute_filter) + return adapters.adapt_persistent_instance(persistent_object, target_rest_class, attribute_filter) def adapt_persistent_collection(persistent_collection, target_rest_class=None, attribute_filter=None): - - # ensure that collection is iterable and has at least one element - persistent_collection_length = 0 - - # attempt to reliably detect the length of the persistent_collection - if persistent_collection and isinstance(persistent_collection, (list, tuple)): - persistent_collection_length = len(persistent_collection) - elif persistent_collection and persistent_collection.__module__ == "sqlalchemy.orm.query": - persistent_collection_length = persistent_collection.count() - - # if the persistent_collection is empty then return a blank array - if persistent_collection_length == 0: - return types.Array(element_template=target_rest_class()) - - # try and get the adapter and the REST class for the persistent object - if target_rest_class is None: - adapter_instance = adapters.registry.get_adapter_for_persistent_model(persistent_collection[0]) - else: - if inspect.isclass(target_rest_class): - target_rest_class = target_rest_class() - - adapter_instance = adapters.registry.get_adapter_for_rest_model(target_rest_class) - - adapted_models = types.Array(element_template=adapter_instance.rest_model_class()) - - for persistent_object in persistent_collection: - adapted_models.append(adapter_instance.adapt_persistent_to_rest(persistent_object, attribute_filter)) - - return adapted_models - - -class ModelAdapter(adapters.ModelAdapter): """ - Provide a bridge between REST models and SQLAlchemy objects + Wrapper on adapters.adapt_persistent_collection for SQLAlchemy """ + return adapters.adapt_persistent_collection(persistent_collection, target_rest_class, attribute_filter) - def adapt_persistent_to_rest(self, persistent_object, attribute_filter=None): - """ - adapts a persistent model to a rest model by inspecting - """ - - rest_model_instance = self.rest_model_class() - for attribute_key in rest_model_instance.get_attribute_keys(): - - rest_attr = getattr(self.rest_model_class, attribute_key) - - # don't bother processing if the persistent model doesn't have this attribute - if not hasattr(persistent_object, attribute_key): - - if isinstance(rest_attr, types.Model): - #: If the attribute is a Model, then we set it to None otherwise we get a model - #: with default values, which is invalid when constructing responses - try: - setattr(rest_model_instance, attribute_key, None) - # catch any exception thrown from setattr to give a usable error message - except TypeError as exp: - raise TypeError('Attribute %s, %s' % (attribute_key, str(exp))) - - continue - - # attribute is not visible don't bother processing - elif isinstance(attribute_filter, parser.AttributeFilter) and \ - not attribute_filter.is_attribute_visible(attribute_key): - continue - - # handles prestans array population from SQLAlchemy relationships - elif isinstance(rest_attr, types.Array): - - persistent_attr_value = getattr(persistent_object, attribute_key) - rest_model_array_handle = getattr(rest_model_instance, attribute_key) - - # iterator uses the .append method exposed by prestans arrays to validate - # and populate the collection in the instance. - for collection_element in persistent_attr_value: - if isinstance(rest_attr.element_template, types.String): - rest_model_array_handle.append(collection_element) - elif isinstance(rest_attr.element_template, types.Integer): - rest_model_array_handle.append(collection_element) - elif isinstance(rest_attr.element_template, types.Float): - rest_model_array_handle.append(collection_element) - elif isinstance(rest_attr.element_template, types.Boolean): - rest_model_array_handle.append(collection_element) - else: - element_adapter = adapters.registry.get_adapter_for_rest_model(rest_attr.element_template) - - # check if there is a sub model filter - sub_attribute_filter = None - if attribute_filter and attribute_key in attribute_filter: - sub_attribute_filter = getattr(attribute_filter, attribute_key) - - adapted_rest_model = element_adapter.adapt_persistent_to_rest( - collection_element, - sub_attribute_filter - ) - rest_model_array_handle.append(adapted_rest_model) - - elif isinstance(rest_attr, types.Model): - - try: - persistent_attr_value = getattr(persistent_object, attribute_key) - - if persistent_attr_value is None: - adapted_rest_model = None - else: - model_adapter = adapters.registry.get_adapter_for_rest_model(rest_attr) - - # check if there is a sub model filter - sub_attribute_filter = None - if isinstance(attribute_filter, parser.AttributeFilter) and \ - attribute_key in attribute_filter: - sub_attribute_filter = getattr(attribute_filter, attribute_key) - - adapted_rest_model = model_adapter.adapt_persistent_to_rest( - persistent_attr_value, - sub_attribute_filter - ) - - setattr(rest_model_instance, attribute_key, adapted_rest_model) - - except TypeError as exp: - raise TypeError('Attribute %s, %s' % (attribute_key, str(exp))) - except exception.DataValidationException as exp: - raise exception.InconsistentPersistentDataError(attribute_key, str(exp)) - - else: - - # otherwise copy the value to the rest model - try: - persistent_attr_value = getattr(persistent_object, attribute_key) - setattr(rest_model_instance, attribute_key, persistent_attr_value) - except TypeError as exp: - raise TypeError('Attribute %s, %s' % (attribute_key, str(exp))) - except exception.DataValidationException as exp: - raise exception.InconsistentPersistentDataError(attribute_key, str(exp)) +class ModelAdapter(adapters.ModelAdapter): - return rest_model_instance + def __init__(self, rest_model_class, persistent_model_class): + import logging + logger = logging.getLogger("prestans") + logger.warn("direct use of %s has been deprecated please use %s instead" % ( + self.__module__ + "." + self.__class__.__name__, + adapters.ModelAdapter.__module__ + "." + adapters.ModelAdapter.__name__ + )) + super(ModelAdapter, self).__init__(rest_model_class, persistent_model_class) diff --git a/prestans/provider/auth.py b/prestans/provider/auth.py index 930e04e..8ec40bf 100644 --- a/prestans/provider/auth.py +++ b/prestans/provider/auth.py @@ -99,7 +99,7 @@ def login_required(http_method_handler): """ @wraps(http_method_handler) - def secure_http_method_handler(self, *args): + def secure_http_method_handler(self, *args, **kwargs): if not self.__provider_config__.authentication: _message = "Service available to authenticated users only, no auth context provider set in handler" @@ -112,7 +112,7 @@ def secure_http_method_handler(self, *args): authentication_error.request = self.request raise authentication_error - http_method_handler(self, *args) + http_method_handler(self, *args, **kwargs) return secure_http_method_handler @@ -128,7 +128,7 @@ def role_required(role_name=None): def _role_required(http_method_handler): @wraps(http_method_handler) - def secure_http_method_handler(self, *args): + def secure_http_method_handler(self, *args, **kwargs): # role name must be provided if role_name is None: @@ -150,7 +150,7 @@ def secure_http_method_handler(self, *args): authorization_error.request = self.request raise authorization_error - http_method_handler(self, *args) + http_method_handler(self, *args, **kwargs) return wraps(http_method_handler)(secure_http_method_handler) @@ -164,7 +164,7 @@ def access_required(config=None): def _access_required(http_method_handler): - def secure_http_method_handler(self, *args): + def secure_http_method_handler(self, *args, **kwargs): # authentication context must be set if not self.__provider_config__.authentication: @@ -180,7 +180,7 @@ def secure_http_method_handler(self, *args): authorization_error.request = self.request raise authorization_error - http_method_handler(self, *args) + http_method_handler(self, *args, **kwargs) return wraps(http_method_handler)(secure_http_method_handler) diff --git a/prestans/rest/request.py b/prestans/rest/request.py index 32190c3..32800f6 100644 --- a/prestans/rest/request.py +++ b/prestans/rest/request.py @@ -44,8 +44,10 @@ def logger(self): def parsed_body(self): if self.body_template is None: - raise AttributeError("access to request.parsed_body is not \ - allowed when body_template is set to None") + msg = "access to request.parsed_body is not allowed when body_template is set to None" + raise AttributeError(msg) + + self.parse_body() return self._parsed_body @@ -65,8 +67,13 @@ def selected_deserializer(self): def default_deserializer(self): return self._default_deserializer - #: Used by content_type_set to set get a referencey to the serializer object def set_deserializer_by_mime_type(self, mime_type): + """ + :param mime_type: + :return: + + Used by content_type_set to set get a reference to the serializer object + """ for deserializer in self._deserializers: if deserializer.content_type() == mime_type: @@ -122,20 +129,26 @@ def body_template(self, value): return if not isinstance(value, DataCollection): - raise AssertionError("body_template must be an instance of \ - prestans.types.DataCollection") + msg = "body_template must be an instance of %s.%s" % ( + DataCollection.__module__, + DataCollection.__name__ + ) + raise AssertionError(msg) self._body_template = value - #: Get a deserializer based on the Content-Type header - #: Do this here so the handler gets a chance to setup extra serializers + # get a deserializer based on the Content-Type header + # do this here so the handler gets a chance to setup extra serializers self.set_deserializer_by_mime_type(self.content_type) - #: Parse the body using the deserializer - unserialized_body = self.selected_deserializer.loads(self.body) + def parse_body(self): + + if self._parsed_body is None and self._body_template is not None: + # parse the body using the deserializer + unserialized_body = self.selected_deserializer.loads(self.body) - #: Parse the body using the template and attribute_filter - self._parsed_body = value.validate(unserialized_body, self.attribute_filter, self.is_minified) + # valiate the body using the template and attribute_filter + self._parsed_body = self._body_template.validate(unserialized_body, self.attribute_filter, self.is_minified) def register_deserializers(self, deserializers): @@ -147,8 +160,9 @@ def register_deserializers(self, deserializers): for new_deserializer in deserializers: if not isinstance(new_deserializer, deserializer.Base): - msg = "registered deserializer %s does not inherit from prestans.serializer.DeSerializer" % \ + msg = "registered deserializer %s does not inherit from prestans.serializer.DeSerializer" % ( new_deserializer.__class__.__name__ + ) raise TypeError(msg) self._deserializers = self._deserializers + deserializers diff --git a/prestans/rest/request_handler.py b/prestans/rest/request_handler.py index cbf5df9..c3e7a45 100644 --- a/prestans/rest/request_handler.py +++ b/prestans/rest/request_handler.py @@ -21,7 +21,7 @@ class RequestHandler(object): __provider_config__ = None __parser_config__ = None - def __init__(self, args, request, response, logger, debug): + def __init__(self, args, kwargs, request, response, logger, debug): if self.__provider_config__ is None: self.__provider_config__ = provider.Config() @@ -29,6 +29,7 @@ def __init__(self, args, request, response, logger, debug): self.__parser_config__ = parser.Config() self._args = args + self._kwargs = kwargs self._request = request self._response = response self._logger = logger @@ -110,37 +111,32 @@ def blueprint(self): return handler_blueprint def _setup_serializers(self): + """ + Auto set the return serializer based on Accept headers + http://docs.webob.org/en/latest/reference.html#header-getters - #: - #: Auto set the return serializer based on Accept headers - #: http://docs.webob.org/en/latest/reference.html#header-getters - #: - - #: Intersection of requested types and supported types tells us if we - #: can in fact respond in one of the request formats + Intersection of requested types and supported types tells us if we + can in fact respond in one of the request formats + """ best_accept_match = self.request.accept.best_match( self.response.supported_mime_types, default_match=self.response.default_serializer.content_type() ) - if best_accept_match is None: - self.logger.error("unsupported mime type in request; accept header reads %s" % \ - self.request.accept) - raise exception.UnsupportedVocabularyError( - self.request.accept, - self.response.supported_mime_types_str - ) + self.logger.info("%s determined as best match for accept header: %s" % ( + best_accept_match, + self.request.accept + )) - #: If content_type is not acceptable it will raise UnsupportedVocabulary + # if content_type is not acceptable it will raise UnsupportedVocabulary self.response.content_type = best_accept_match def __call__(self, environ, start_response): self.logger.info("handler %s.%s; callable execution start" % (self.__module__, self.__class__.__name__)) - self.logger.info("setting default response to %s" % self.request.accept) try: - #: Register additional serializers and de-serializers + # register additional serializers and de-serializers self.request.register_deserializers(self.register_deserializers()) self.response.register_serializers(self.register_serializers()) @@ -229,19 +225,21 @@ def __call__(self, environ, start_response): #: prestans sets a sensible HTTP status code #: if request_method == VERB.GET: - self.get(*self._args) + self.get(*self._args, **self._kwargs) elif request_method == VERB.HEAD: - self.head(*self._args) + self.head(*self._args, **self._kwargs) elif request_method == VERB.POST: - self.post(*self._args) + self.post(*self._args, **self._kwargs) elif request_method == VERB.PUT: - self.put(*self._args) + self.put(*self._args, **self._kwargs) elif request_method == VERB.PATCH: - self.patch(*self._args) + self.patch(*self._args, **self._kwargs) elif request_method == VERB.DELETE: - self.delete(*self._args) + self.delete(*self._args, **self._kwargs) elif request_method == VERB.OPTIONS: - self.options(*self._args) + self.options(*self._args, **self._kwargs) + except (exception.PermanentRedirect, exception.TemporaryRedirect) as exp: + self._redirect(exp.url, exp.http_status) # re-raise all prestans exceptions except exception.Base as exp: if isinstance(exception, exception.HandlerException): @@ -255,8 +253,10 @@ def __call__(self, environ, start_response): finally: self.handler_did_run() - self.logger.info("handler %s.%s; callable execution ends" % \ - (self.__module__, self.__class__.__name__)) + self.logger.info("handler %s.%s; callable execution ends" % ( + self.__module__, + self.__class__.__name__ + )) return self.response(environ, start_response) @@ -296,42 +296,50 @@ def handler_raised_exception(self, exp): #: if not overridden prestans returns a Not Implemented error #: - def get(self, *args): + def get(self, *args, **kwargs): unimplemented_verb_error = exception.UnimplementedVerbError(VERB.GET) unimplemented_verb_error.request = self.request raise unimplemented_verb_error - def head(self, *args): + def head(self, *args, **kwargs): unimplemented_verb_error = exception.UnimplementedVerbError(VERB.HEAD) unimplemented_verb_error.request = self.request raise unimplemented_verb_error - def post(self, *args): + def post(self, *args, **kwargs): unimplemented_verb_error = exception.UnimplementedVerbError(VERB.POST) unimplemented_verb_error.request = self.request raise unimplemented_verb_error - def put(self, *args): + def put(self, *args, **kwargs): unimplemented_verb_error = exception.UnimplementedVerbError(VERB.PUT) unimplemented_verb_error.request = self.request raise unimplemented_verb_error - def patch(self, *args): + def patch(self, *args, **kwargs): unimplemented_verb_error = exception.UnimplementedVerbError(VERB.PATCH) unimplemented_verb_error.request = self.request raise unimplemented_verb_error - def delete(self, *args): + def delete(self, *args, **kwargs): unimplemented_verb_error = exception.UnimplementedVerbError(VERB.DELETE) unimplemented_verb_error.request = self.request raise unimplemented_verb_error - def options(self, *args): + def options(self, *args, **kwargs): unimplemented_verb_error = exception.UnimplementedVerbError(VERB.OPTIONS) unimplemented_verb_error.request = self.request raise unimplemented_verb_error - def redirect(self, url, status=STATUS.TEMPORARY_REDIRECT): - + def _redirect(self, url, status=STATUS.TEMPORARY_REDIRECT): self._response.status = status self._response.headers.add("Location", url) + + def redirect(self, url, status=STATUS.TEMPORARY_REDIRECT): + + self.logger.warn("direct use of %s.%s has been deprecated please raise an exception instead" % ( + self.__module__, + "redirect" + )) + + self._redirect(url, status) diff --git a/prestans/rest/request_router.py b/prestans/rest/request_router.py index d9b99fa..beb0150 100644 --- a/prestans/rest/request_router.py +++ b/prestans/rest/request_router.py @@ -92,18 +92,21 @@ def application_name(self): def __call__(self, environ, start_response): # say hello - self.logger.info("%s exposes %i end-points; prestans %s; charset %s; debug %s" % \ - (self._application_name, len(self._routes), __version__, \ - self._charset, self._debug)) + self.logger.info("%s exposes %i end-points; prestans %s; charset %s; debug %s" % ( + self._application_name, len(self._routes), __version__, + self._charset, self._debug + )) # validate serializers and deserializers; are subclasses of prestans.serializer.Base _default_outgoing_mime_types = list() for available_serializer in self._serializers: if not isinstance(available_serializer, serializer.Base): - raise TypeError("registered serializer %s.%s does not \ - inherit from prestans.serializer.Serializer" % (available_serializer.__module__, \ - available_serializer.__class__.__name__)) + msg = "registered serializer %s.%s does not inherit from prestans.serializer.Serializer" % ( + available_serializer.__module__, + available_serializer.__class__.__name__ + ) + raise TypeError(msg) _default_outgoing_mime_types.append(available_serializer.content_type()) @@ -111,10 +114,11 @@ def __call__(self, environ, start_response): for available_deserializer in self._deserializers: if not isinstance(available_deserializer, deserializer.Base): - raise TypeError( - "registered deserializer %s.%s does not inherit from \ - prestans.serializer.DeSerializer" \ - % (available_deserializer.__module__, available_deserializer.__class__.__name__)) + msg = "registered deserializer %s.%s does not inherit from prestans.serializer.DeSerializer" % ( + available_deserializer.__module__, + available_deserializer.__class__.__name__ + ) + raise TypeError(msg) _default_incoming_mime_types.append(available_deserializer.content_type()) @@ -133,8 +137,8 @@ def __call__(self, environ, start_response): default_deserializer=self._default_deserializer ) - # initialise the Route map - route_map = self._init_route_map(self._routes) + # initialise the route map + route_map = self.generate_route_map(self._routes) try: @@ -148,6 +152,18 @@ def __call__(self, environ, start_response): # if we've found a match; ensure its a handler subclass and return it's callable if match: + # assemble the args and kwargs + args = match.groups() + kwargs = {} + for key, value in iter(regexp.groupindex.items()): + kwargs[key] = args[value - 1] + + if len(kwargs) > 0: + args = () + + self.logger.info(args) + self.logger.info(kwargs) + if issubclass(handler_class, BlueprintHandler): response = DictionaryResponse( @@ -157,7 +173,8 @@ def __call__(self, environ, start_response): ) request_handler = handler_class( - args=match.groups(), + args=args, + kwargs=kwargs, request=request, response=response, logger=self._logger, @@ -174,7 +191,8 @@ def __call__(self, environ, start_response): response.minify = request.is_minified request_handler = handler_class( - args=match.groups(), + args=args, + kwargs=kwargs, request=request, response=response, logger=self._logger, @@ -183,7 +201,7 @@ def __call__(self, environ, start_response): return request_handler(environ, start_response) - #: Request does not have a matched handler + # request does not have a matched handler no_endpoint = exception.NoEndpointError() no_endpoint.request = request raise no_endpoint @@ -193,24 +211,29 @@ def __call__(self, environ, start_response): error_response = ErrorResponse(exp, self._default_serializer) return error_response(environ, start_response) - def _init_route_map(self, routes): + @classmethod + def generate_route_map(cls, routes): parsed_handler_map = [] - for regexp, handler in routes: + for url, handler in routes: - try: - handler_name = handler.__name__ - except AttributeError: - pass + regexp = url - #: Patch regular expression if its incomplete + # patch regular expression if it is incomplete if not regexp.startswith('^'): regexp = '^' + regexp if not regexp.endswith('$'): regexp += '$' compiled_regex = re.compile(regexp) - parsed_handler_map.append((compiled_regex, handler)) - return parsed_handler_map \ No newline at end of file + arg_count = compiled_regex.groups + kwarg_count = len(compiled_regex.groupindex.items()) + + if arg_count != kwarg_count and kwarg_count > 0: + raise ValueError("%s URL is invalid, cannot mix named and un-named groups" % url) + else: + parsed_handler_map.append((compiled_regex, handler)) + + return parsed_handler_map diff --git a/prestans/rest/response.py b/prestans/rest/response.py index f7eed7c..d03cf2a 100644 --- a/prestans/rest/response.py +++ b/prestans/rest/response.py @@ -1,7 +1,5 @@ -import sys import webob -from prestans import __version__ from prestans import exception from prestans.http import STATUS from prestans.parser import AttributeFilter @@ -75,23 +73,33 @@ def selected_serializer(self): def default_serializer(self): return self._default_serializer - #: Used by content_type_set to set get a reference to the serializer object def _set_serializer_by_mime_type(self, mime_type): + """ + :param mime_type: + :return: + + used by content_type_set to set get a reference to the appropriate serializer + """ + + # ignore if binary response + if isinstance(self._app_iter, BinaryResponse): + self.logger.info("ignoring setting serializer for binary response") + return for available_serializer in self._serializers: if available_serializer.content_type() == mime_type: self._selected_serializer = available_serializer + self.logger.info("set serializer for mime type: %s" % mime_type) return + self.logger.info("could not find serializer for mime type: %s" % mime_type) raise exception.UnsupportedVocabularyError(mime_type, self.supported_mime_types_str) - #: - #: is an instance of prestans.types.DataType; mostly a subclass of - #: prestans.types.Model - #: - @property def template(self): + """ + is an instance of prestans.types.DataType; mostly a subclass of prestans.types.Model + """ return self._template @template.setter @@ -115,15 +123,11 @@ def attribute_filter(self): def attribute_filter(self, value): if value is not None and not isinstance(value, AttributeFilter): - raise TypeError("attribue_filter in response must be of \ - type prestans.types.AttributeFilter") + msg = "attribute_filter in response must be of type prestans.types.AttributeFilter" + raise TypeError(msg) self._attribute_filter = value - #: - #: content_type; overrides webob.Response line 606 - #: - def _content_type__get(self): """ Get/set the Content-Type header (or None), *without* the @@ -140,27 +144,26 @@ def _content_type__get(self): def _content_type__set(self, value): - #: Check to see if response can support the requested mime type - if not isinstance(self._app_iter, BinaryResponse) and value not in self.supported_mime_types: - raise exception.UnsupportedVocabularyError(value, self.supported_mime_types_str) - - #: Keep a reference to the selected serializer - if not isinstance(self._app_iter, BinaryResponse): + # skip for responses that have no body + if self.status_code in [STATUS.NO_CONTENT, STATUS.PERMANENT_REDIRECT, STATUS.TEMPORARY_REDIRECT]: + self.logger.info("attempt to set Content-Type to %s being ignored due to empty response" % value) + self._content_type__del() + else: self._set_serializer_by_mime_type(value) - if not value: - self._content_type__del() - return - if ';' not in value: - header = self.headers.get('Content-Type', '') - if ';' in header: - params = header.split(';', 1)[1] - value += ';' + params - self.headers['Content-Type'] = value + if ';' not in value: + header = self.headers.get('Content-Type', '') + if ';' in header: + params = header.split(';', 1)[1] + value += ';' + params + self.headers['Content-Type'] = value + + self.logger.info("Content-Type set to: %s" % value) def _content_type__del(self): self.headers.pop('Content-Type', None) + # content_type; overrides webob.Response line 606 content_type = property( _content_type__get, _content_type__set, @@ -168,11 +171,7 @@ def _content_type__del(self): doc=_content_type__get.__doc__ ) - - #: - #: body; overrides webob.Response line 324 - #: - + # body; overrides webob.Response line 324 @property def body(self): """ @@ -181,7 +180,7 @@ def body(self): body getter will return the validated prestans model. - Webob does the heavy lifiting with headers. + webob does the heavy lifting with headers. """ #: If template is null; return an empty iterable @@ -201,20 +200,27 @@ def body(self, value): #: value should be a subclass prestans.types.DataCollection if not isinstance(value, DataCollection) and \ not isinstance(value, BinaryResponse): - raise TypeError("%s is not a prestans.types.DataCollection \ - or prestans.types.BinaryResponse subclass" % value.__class__.__name__) + msg = "%s is not a prestans.types.DataCollection or prestans.types.BinaryResponse subclass" % ( + value.__class__.__name__ + ) + raise TypeError(msg) #: Ensure that it matches the return type template if not value.__class__ == self.template.__class__: - raise TypeError("body must of be type %s, given %s" % \ - (self.template.__class__.__name__, value.__class__.__name__)) + msg = "body must of be type %s, given %s" % ( + self.template.__class__.__name__, + value.__class__.__name__ + ) + raise TypeError(msg) #: If it's an array then ensure that element_template matches up if isinstance(self.template, Array) and \ - not isinstance(value.element_template, self.template.element_template.__class__): - raise TypeError("array elements must of be \ - type %s, given %s" % (self.template.element_template.__class__.__name__, \ - value.element_template.__class__.__name__)) + not isinstance(value.element_template, self.template.element_template.__class__): + msg = "array elements must of be type %s, given %s" % ( + self.template.element_template.__class__.__name__, + value.element_template.__class__.__name__ + ) + raise TypeError(msg) #: _app_iter assigned to value #: we need to serialize the contents before we know the length @@ -230,9 +236,11 @@ def register_serializers(self, serializers): for new_serializer in serializers: if not isinstance(new_serializer, serializer.Base): - raise TypeError("registered serializer %s.%s does not inherit from \ - prestans.serializer.Serializer" % (new_serializer.__module__, \ - new_serializer.__class__.__name__)) + msg = "registered serializer %s.%s does not inherit from prestans.serializer.Serializer" % ( + new_serializer.__module__, + new_serializer.__class__.__name__ + ) + raise TypeError(msg) self._serializers = self._serializers + serializers @@ -241,27 +249,31 @@ def __call__(self, environ, start_response): Overridden WSGI application interface """ - #: prestans' equivalent of webob.Response line 1022 + # prestans equivalent of webob.Response line 1022 if self.template is None or self.status_code == STATUS.NO_CONTENT: + self.content_type = None + start_response(self.status, self.headerlist) if self.template is not None: - self.logger.warn("handler returns No Content but has a \ - response_template; set template to None") + self.logger.warn("handler returns No Content but has a response_template; set template to None") return [] - #: Ensure what we are able to serialize is serializable + # ensure what we are able to serialize is serializable if not isinstance(self._app_iter, DataCollection) and \ - not isinstance(self._app_iter, BinaryResponse): + not isinstance(self._app_iter, BinaryResponse): if isinstance(self._app_iter, list): - type = "list" + app_iter_type = "list" else: - type = self._app_iter.__name__ + app_iter_type = self._app_iter.__name__ - raise TypeError("handler returns content of type %s; not a prestans.types.DataCollection subclass" % type) + msg = "handler returns content of type %s; not a prestans.types.DataCollection subclass" % ( + app_iter_type + ) + raise TypeError(msg) if isinstance(self._app_iter, DataCollection): @@ -325,7 +337,7 @@ def __call__(self, environ, start_response): self.content_type = "text/plain" return [] - #: Content type + # set the content type self.content_type = self._app_iter.mime_type #: Add content disposition header diff --git a/setup.py b/setup.py index 0af19b6..b535317 100644 --- a/setup.py +++ b/setup.py @@ -86,7 +86,8 @@ 'pytest-cov', 'mock', 'tox', - 'tox-pyenv' + 'tox-pyenv', + 'webtest' ], setup_requires=['pytest-runner'], extras_require={ diff --git a/tests/ext/data/adapters/sqlalchemy/test_adapter_registry_manager.py b/tests/ext/data/adapters/sqlalchemy/test_adapter_registry_manager.py index fff1f05..19fec90 100644 --- a/tests/ext/data/adapters/sqlalchemy/test_adapter_registry_manager.py +++ b/tests/ext/data/adapters/sqlalchemy/test_adapter_registry_manager.py @@ -91,4 +91,43 @@ def test_unknown_adapter_raises_exception(self): self.assertRaises(TypeError, registry_manager.get_adapter_for_rest_model, RESTModelB) self.assertRaises(TypeError, registry_manager.get_adapter_for_persistent_model, PersistentModelB) + def test_register_persistent_rest_pair(self): + registry_manager = adapters.AdapterRegistryManager() + registry_manager.register_persistent_rest_pair( + persistent_model_class=PersistentModelA, + rest_model_class=RESTModelA + ) + + # fetch via the REST model + found_adapter = registry_manager.get_adapter_for_rest_model(RESTModelA()) + self.assertEquals(found_adapter.rest_model_class, RESTModelA) + self.assertEquals(found_adapter.persistent_model_class, PersistentModelA) + + # fetch via the persistent model + found_adapter = registry_manager.get_adapter_for_persistent_model(PersistentModelA()) + self.assertEquals(found_adapter.rest_model_class, RESTModelA) + self.assertEquals(found_adapter.persistent_model_class, PersistentModelA) + + def test_clear_registered_adapters(self): + registry_manager = adapters.AdapterRegistryManager() + registry_manager.register_adapter(adapters.ModelAdapter( + rest_model_class=RESTModelA, + persistent_model_class=PersistentModelA + )) + + # fetch via the REST model + found_adapter = registry_manager.get_adapter_for_rest_model(RESTModelA()) + self.assertEquals(found_adapter.rest_model_class, RESTModelA) + self.assertEquals(found_adapter.persistent_model_class, PersistentModelA) + + # fetch via the persistent model + found_adapter = registry_manager.get_adapter_for_persistent_model(PersistentModelA()) + self.assertEquals(found_adapter.rest_model_class, RESTModelA) + self.assertEquals(found_adapter.persistent_model_class, PersistentModelA) + + # clear the registry + registry_manager.clear_registered_adapters() + # check they have been cleared + self.assertRaises(TypeError, registry_manager.get_adapter_for_rest_model, RESTModelA()) + self.assertRaises(TypeError, registry_manager.get_adapter_for_persistent_model, PersistentModelA()) diff --git a/tests/ext/data/adapters/sqlalchemy/test_model_adapter.py b/tests/ext/data/adapters/sqlalchemy/test_model_adapter.py index 40174aa..1bc39ce 100644 --- a/tests/ext/data/adapters/sqlalchemy/test_model_adapter.py +++ b/tests/ext/data/adapters/sqlalchemy/test_model_adapter.py @@ -1,39 +1,229 @@ import unittest -from prestans.ext.data.adapters import ModelAdapter +from prestans import exception +from prestans.ext.data import adapters +from prestans import parser from prestans import types -class ModelAdapterTest(unittest.TestCase): +class Address(object): + def __init__(self): + self.street = "street" - def test_rest_model_class(self): - class PythonObject(object): - pass +class Person(object): - class RESTModel(types.Model): - name = types.String() + def __init__(self): + self.first_name = "first_name" + self.last_name = "last_name" - model_adapter = ModelAdapter(rest_model_class=RESTModel, persistent_model_class=PythonObject) - self.assertEquals(model_adapter.rest_model_class, RESTModel) - self.assertEquals(model_adapter.persistent_model_class, PythonObject) - self.assertRaises(TypeError, ModelAdapter, rest_model_class=PythonObject, persistent_model_class=None) +class PersonWithAddress(Person): - def test_persistent_model_class(self): - class PythonObject(object): - pass + def __init__(self): + super(PersonWithAddress, self).__init__() + self.address = Address() - class RESTModel(types.Model): - name = types.String() - model_adapter = ModelAdapter(rest_model_class=RESTModel, persistent_model_class=PythonObject) - self.assertEquals(model_adapter.rest_model_class, RESTModel) - self.assertEquals(model_adapter.persistent_model_class, PythonObject) +class PersonWithAddresses(Person): - def test_adapt_persistent_to_rest(self): - class MyModel(types.Model): - name = types.String() + def __init__(self): + super(PersonWithAddresses, self).__init__() + self.addresses = [] - model_adapter = ModelAdapter(MyModel, None) - self.assertRaises(NotImplementedError, model_adapter.adapt_persistent_to_rest, None) + +class PersonWithBasicArrays(Person): + + def __init__(self): + super(PersonWithBasicArrays, self).__init__() + self.booleans = [] + self.floats = [] + self.integers = [] + self.strings = [] + + +class AddressREST(types.Model): + street = types.String() + short_string = types.String(required=False, max_length=10) + + +class PersonREST(types.Model): + first_name = types.String() + last_name = types.String() + short_string = types.String(max_length=10) + + address = AddressREST(required=False) + + addresses = types.Array(element_template=AddressREST()) + + booleans = types.Array(element_template=types.Boolean()) + floats = types.Array(element_template=types.Float()) + integers = types.Array(element_template=types.Integer()) + strings = types.Array(element_template=types.String()) + + +class ModelAdapterUnitTest(unittest.TestCase): + + def setUp(self): + adapters.registry.register_persistent_rest_pair(Address, AddressREST) + + def tearDown(self): + adapters.registry.clear_registered_adapters() + + def test_init_and_getters(self): + + model_adapter = adapters.ModelAdapter(rest_model_class=PersonREST, persistent_model_class=Person) + self.assertEquals(model_adapter.rest_model_class, PersonREST) + self.assertEquals(model_adapter.persistent_model_class, Person) + + def test_init_raises_type_error_for_invalid_rest_model(self): + self.assertRaises(TypeError, adapters.ModelAdapter, rest_model_class=Person, persistent_model_class=None) + + def test_adapt_persistent_to_rest_no_filter(self): + model_adapter = adapters.ModelAdapter(rest_model_class=PersonREST, persistent_model_class=Person) + + person = Person() + person.first_name = "John" + person.last_name = "Doe" + + person_rest = model_adapter.adapt_persistent_to_rest(person) + self.assertEquals(person.first_name, "John") + self.assertEquals(person.last_name, "Doe") + self.assertEquals(person.first_name, person_rest.first_name) + self.assertEquals(person.last_name, person_rest.last_name) + + def test_adapt_persistent_to_rest_with_filter(self): + model_adapter = adapters.ModelAdapter(rest_model_class=PersonREST, persistent_model_class=Person) + + person = Person() + person.first_name = "John" + person.last_name = "Doe" + + attribute_filter = parser.AttributeFilter.from_model(PersonREST(), default_value=False) + attribute_filter.last_name = True + + person_rest = model_adapter.adapt_persistent_to_rest(person, attribute_filter) + self.assertEquals(person.first_name, "John") + self.assertEquals(person.last_name, "Doe") + self.assertIsNone(person_rest.first_name) + self.assertEquals(person.last_name, person_rest.last_name) + + def test_adapt_persistent_to_rest_with_model_as_child(self): + model_adapter = adapters.ModelAdapter(rest_model_class=PersonREST, persistent_model_class=Person) + + person = PersonWithAddress() + person.first_name = "John" + person.last_name = "Doe" + person.address.street = "123 Street Address" + + attribute_filter = parser.AttributeFilter.from_model(PersonREST(), default_value=False) + attribute_filter.last_name = True + attribute_filter.address.street = True + + person_rest = model_adapter.adapt_persistent_to_rest(person, attribute_filter) + self.assertEquals(person.first_name, "John") + self.assertEquals(person.last_name, "Doe") + self.assertEquals(person.address.street, "123 Street Address") + self.assertIsNone(person_rest.first_name) + self.assertEquals(person.last_name, person_rest.last_name) + self.assertEquals(person.address.street, person_rest.address.street) + + def test_adapt_persistent_to_rest_with_model_missing(self): + model_adapter = adapters.ModelAdapter(rest_model_class=PersonREST, persistent_model_class=Person) + + person = PersonWithAddress() + person.first_name = "John" + person.last_name = "Doe" + person.address = None + + person_rest = model_adapter.adapt_persistent_to_rest(person) + self.assertEquals(person.first_name, "John") + self.assertEquals(person.last_name, "Doe") + self.assertEquals(person.first_name, person_rest.first_name) + self.assertEquals(person.last_name, person_rest.last_name) + + def test_adapt_persistent_to_rest_with_basic_arrays_as_children(self): + model_adapter = adapters.ModelAdapter(rest_model_class=PersonREST, persistent_model_class=PersonWithBasicArrays) + + person = PersonWithBasicArrays() + person.first_name = "John" + person.last_name = "Doe" + person.booleans.append(True) + person.floats.append(1.1) + person.integers.append(2) + person.strings.append("string") + + person_rest = model_adapter.adapt_persistent_to_rest(person) + self.assertEquals(person.first_name, "John") + self.assertEquals(person.last_name, "Doe") + self.assertEquals(person.booleans, [True]) + self.assertEquals(person.floats, [1.1]) + self.assertEquals(person.integers, [2]) + self.assertEquals(person.strings, ["string"]) + + self.assertEquals(person.first_name, person_rest.first_name) + self.assertEquals(person.last_name, person_rest.last_name) + self.assertEquals(person.booleans, person_rest.booleans.as_serializable()) + self.assertEquals(person.floats, person_rest.floats.as_serializable()) + self.assertEquals(person.integers, person_rest.integers.as_serializable()) + self.assertEquals(person.strings, person_rest.strings.as_serializable()) + + def test_adapt_persistent_to_rest_with_model_array_as_child(self): + model_adapter = adapters.ModelAdapter(rest_model_class=PersonREST, persistent_model_class=PersonWithAddresses) + + address = Address() + address.street = "123 Street Address" + + person = PersonWithAddresses() + person.first_name = "John" + person.last_name = "Doe" + person.addresses.append(address) + + person_rest = model_adapter.adapt_persistent_to_rest(person) + self.assertEquals(person.first_name, "John") + self.assertEquals(person.last_name, "Doe") + self.assertEquals(person.addresses, [address]) + + self.assertEquals(person.first_name, person_rest.first_name) + self.assertEquals(person.last_name, person_rest.last_name) + self.assertEquals([{"short_string": None, "street": "123 Street Address"}], person_rest.addresses.as_serializable()) + + def test_adapt_persistent_to_rest_with_model_array_as_child_filtered(self): + model_adapter = adapters.ModelAdapter(rest_model_class=PersonREST, persistent_model_class=PersonWithAddresses) + + address = Address() + address.street = "123 Street Address" + + person = PersonWithAddresses() + person.first_name = "John" + person.last_name = "Doe" + person.addresses.append(address) + + attribute_filter = parser.AttributeFilter.from_model(PersonREST(), default_value=False) + attribute_filter.last_name = True + attribute_filter.addresses.street = True + + person_rest = model_adapter.adapt_persistent_to_rest(person, attribute_filter) + self.assertEquals(person.first_name, "John") + self.assertEquals(person.last_name, "Doe") + self.assertEquals(person.addresses, [address]) + + self.assertIsNone(person_rest.first_name) + self.assertEquals(person.last_name, person_rest.last_name) + self.assertEquals([{"short_string": None, "street": "123 Street Address"}], person_rest.addresses.as_serializable()) + + def test_adapt_persistent_to_rest_inconsistent_data_exception_raised_model(self): + model_adapter = adapters.ModelAdapter(rest_model_class=PersonREST, persistent_model_class=Person) + + person = PersonWithAddress() + person.address.short_string = "a longer string" + + self.assertRaises(exception.InconsistentPersistentDataError, model_adapter.adapt_persistent_to_rest, person) + + def test_adapt_persistent_to_rest_inconsistent_data_exception_raised_array(self): + model_adapter = adapters.ModelAdapter(rest_model_class=PersonREST, persistent_model_class=Person) + + person = Person() + person.short_string = "a longer string" + + self.assertRaises(exception.InconsistentPersistentDataError, model_adapter.adapt_persistent_to_rest, person) diff --git a/tests/issues/test_issue102.py b/tests/issues/test_issue102.py new file mode 100644 index 0000000..b08fb1e --- /dev/null +++ b/tests/issues/test_issue102.py @@ -0,0 +1,46 @@ +from prestans import exception +from prestans.http import STATUS +from prestans import rest + +import pytest +import unittest + + +class PermanentRedirectHandler(rest.RequestHandler): + + def get(self): + raise exception.PermanentRedirect("/permanent-new") + + +class TemporaryRedirectHandler(rest.RequestHandler): + + def get(self): + raise exception.TemporaryRedirect("/temporary-new") + + +@pytest.fixture +def test_app(): + from webtest import TestApp + from prestans.rest import RequestRouter + + api = RequestRouter([ + ('/permanent', PermanentRedirectHandler), + ('/temporary', TemporaryRedirectHandler) + ], application_name="api", debug=True) + + return TestApp(app=api) + + +class Issue155(unittest.TestCase): + + def test_permanent_redirect(self): + app = test_app() + + resp = app.get("/permanent") + self.assertEquals(resp.status_int, STATUS.PERMANENT_REDIRECT) + + def test_temporary_redirect(self): + app = test_app() + + resp = app.get("/temporary") + self.assertEquals(resp.status_int, STATUS.TEMPORARY_REDIRECT) diff --git a/tests/issues/test_issue125.py b/tests/issues/test_issue125.py new file mode 100644 index 0000000..b709052 --- /dev/null +++ b/tests/issues/test_issue125.py @@ -0,0 +1,42 @@ +from prestans.ext.data import adapters +from prestans import types + +from mock import patch +import unittest + + +class Person(object): + def __init__(self): + self.first_name = "John" + + @property + def last_name(self): + return "Doe" + + @classmethod + def class_method(cls): + return "class_method" + + +class PersonREST(types.Model): + first_name = types.String() + last_name = types.String() + class_method = types.String() + + +class Issue125(unittest.TestCase): + + def setUp(self): + adapters.registry.register_persistent_rest_pair(Person, PersonREST) + + def tearDown(self): + adapters.registry.clear_registered_adapters() + + def test_class_method_not_called(self): + + person = Person() + + person_rest = adapters.adapt_persistent_instance(person, PersonREST) + self.assertEquals(person_rest.first_name, person.first_name) + self.assertEquals(person_rest.last_name, person.last_name) + self.assertIsNone(person_rest.class_method) diff --git a/tests/issues/test_issue154.py b/tests/issues/test_issue154.py new file mode 100644 index 0000000..fb7f62d --- /dev/null +++ b/tests/issues/test_issue154.py @@ -0,0 +1,35 @@ +from prestans.http import STATUS +from prestans.rest import RequestHandler + +import pytest +import unittest + + +class NoContentHandler(RequestHandler): + + def get(self): + self.response.status = STATUS.NO_CONTENT + + +@pytest.fixture +def test_app(): + from webtest import TestApp + from prestans.rest import RequestRouter + + api = RequestRouter([ + ('/no-content', NoContentHandler) + ], application_name="api", debug=True) + + return TestApp(app=api) + + +class Issue154(unittest.TestCase): + + def test_204_header_omitted(self): + """ + Request should return no content with header omitted + """ + app = test_app() + resp = app.get('/no-content') + self.assertEqual(resp.status_int, STATUS.NO_CONTENT) + self.assertIsNone(resp.content_type) diff --git a/tests/issues/test_issue155.py b/tests/issues/test_issue155.py new file mode 100644 index 0000000..3e8c530 --- /dev/null +++ b/tests/issues/test_issue155.py @@ -0,0 +1,109 @@ +from prestans.http import STATUS +from prestans import parser +from prestans.provider import auth +from prestans import rest +from prestans import types + +import json +from mock import patch, PropertyMock +import pytest +import unittest + + +class PersonREST(types.Model): + first_name = types.String() + last_name = types.String() + + +BODY = { + "first_name": "Jane", + "last_name": "Doe" +} + + +class AuthContextProvider(auth.Base): + + def current_user_has_role(self, role_name): + return False + + def get_current_user(self): + return None + + def is_authorized_user(self, config): + return False + + def is_authenticated_user(self): + return False + + +class AuthHandler(rest.RequestHandler): + + def handler_will_run(self): + self.__provider_config__.authentication = AuthContextProvider() + + __parser_config__ = parser.Config( + POST=parser.VerbConfig( + body_template=PersonREST(), + response_attribute_filter_default_value=True, + response_template=PersonREST() + ) + ) + + @auth.login_required + def post(self): + self.response.status = STATUS.NO_CONTENT + + +class NoAuthHandler(rest.RequestHandler): + + __parser_config__ = parser.Config( + POST=parser.VerbConfig( + body_template=PersonREST(), + response_attribute_filter_default_value=True, + response_template=PersonREST() + ) + ) + + def post(self): + person = self.request.parsed_body + + self.response.status = STATUS.OK + self.response.body = person + + +@pytest.fixture +def test_app(): + from webtest import TestApp + from prestans.rest import RequestRouter + + api = RequestRouter([ + ('/auth', AuthHandler), + ('/no-auth', NoAuthHandler) + ], application_name="api", debug=True) + + return TestApp(app=api) + + +class Issue155(unittest.TestCase): + + def test_no_auth_parses_body(self): + """ + """ + app = test_app() + resp = app.post_json(url="/no-auth", params=BODY, status="*") + resp_body = json.loads(resp.body if isinstance(resp.body, str) else resp.body.decode()) + + self.assertEquals(resp.status_int, STATUS.OK) + self.assertEquals(resp_body["first_name"], BODY["first_name"]) + self.assertEquals(resp_body["last_name"], BODY["last_name"]) + + @patch("prestans.rest.request.Request.parsed_body", new_callable=PropertyMock) + @patch("prestans.rest.request.Request.parse_body") + def test_unauthenticated_ignores_body(self, parsed_body, parse_body): + """ + """ + app = test_app() + resp = app.post_json(url="/auth", params=BODY, status="*") + self.assertEqual(resp.status_int, STATUS.UNAUTHORIZED) + parsed_body.assert_not_called() + parse_body.assert_not_called() diff --git a/tests/issues/test_issue166.py b/tests/issues/test_issue166.py new file mode 100644 index 0000000..a923332 --- /dev/null +++ b/tests/issues/test_issue166.py @@ -0,0 +1,56 @@ +from prestans.ext.data import adapters +from prestans.ext.data.adapters import sqlalchemy +from prestans import types + +import unittest + + +class UserPersistent(object): + first_name = "first" + last_name = "last" + + +class UserREST(types.Model): + first_name = types.String() + last_name = types.String() + + +class AuthorREST(types.Model): + first_name = types.String() + last_name = types.String() + + +adapters.registry.register_adapter(sqlalchemy.ModelAdapter( + rest_model_class=UserREST, + persistent_model_class=UserPersistent +)) + +adapters.registry.register_adapter(sqlalchemy.ModelAdapter( + rest_model_class=AuthorREST, + persistent_model_class=UserPersistent +)) + + +class Issue166(unittest.TestCase): + + def setUp(self): + adapters.registry.register_persistent_rest_pair(UserPersistent, UserREST) + adapters.registry.register_persistent_rest_pair(UserPersistent, AuthorREST) + + def tearDown(self): + adapters.registry.clear_registered_adapters() + + def test_default_rest_model_lookup(self): + + model_adapter = adapters.registry.get_adapter_for_persistent_model(UserPersistent()) + self.assertEquals(model_adapter.rest_model_class, AuthorREST) + self.assertNotEquals(model_adapter.rest_model_class, UserREST) + + def test_specific_rest_model_lookup(self): + model_adapter1 = adapters.registry.get_adapter_for_persistent_model(UserPersistent(), UserREST) + self.assertEquals(model_adapter1.rest_model_class, UserREST) + self.assertNotEquals(model_adapter1.rest_model_class, AuthorREST) + + model_adapter2 = adapters.registry.get_adapter_for_persistent_model(UserPersistent(), AuthorREST) + self.assertEquals(model_adapter2.rest_model_class, AuthorREST) + self.assertNotEquals(model_adapter2.rest_model_class, UserREST) diff --git a/tests/issues/test_issue84.py b/tests/issues/test_issue84.py new file mode 100644 index 0000000..197c5d0 --- /dev/null +++ b/tests/issues/test_issue84.py @@ -0,0 +1,82 @@ +from prestans.ext.data import adapters +from prestans.ext.data.adapters import sqlalchemy +from prestans.parser.attribute_filter import AttributeFilter +from prestans import types + +import unittest + + +class UserPersistent(object): + first_name = "first" + last_name = "last" + + +class AddressPersistent(object): + street = "street" + + +class UserREST(types.Model): + first_name = types.String() + last_name = types.String() + + +class AuthorREST(types.Model): + first_name = types.String() + last_name = types.String() + + +class AddressREST(types.Model): + street = types.String() + + +class Issue84(unittest.TestCase): + + def setUp(self): + adapters.registry.register_persistent_rest_pair(UserPersistent, UserREST) + adapters.registry.register_persistent_rest_pair(UserPersistent, AuthorREST) + + def tearDown(self): + adapters.registry.clear_registered_adapters() + + def test_correct_adaption_collection(self): + + user = UserPersistent() + user.first_name = "James" + user.last_name = "Hetfield" + + users = [user] + + attribute_filter = AttributeFilter.from_model(UserREST(), False) + attribute_filter.first_name = True + + adapted_users = sqlalchemy.adapt_persistent_collection(users, UserREST, attribute_filter) + self.assertEquals(adapted_users[0].first_name, "James") + self.assertIsNone(adapted_users[0].last_name) + + def test_correct_adaption_instance(self): + + user = UserPersistent() + user.first_name = "James" + user.last_name = "Hetfield" + + attribute_filter = AttributeFilter.from_model(UserREST(), False) + attribute_filter.first_name = True + + adapted_user = sqlalchemy.adapt_persistent_instance(user, UserREST, attribute_filter) + self.assertEquals(adapted_user.first_name, "James") + self.assertIsNone(adapted_user.last_name) + + def test_incorrect_adaption_raises_exception_collection(self): + address = AddressPersistent() + address.street = "123 Fake Street" + + addresses = [address] + + self.assertRaises(TypeError, sqlalchemy.adapt_persistent_collection, addresses, UserREST) + + def test_incorrect_adaption_raises_exception_instance(self): + address = AddressPersistent() + address.street = "123 Fake Street" + + self.assertRaises(TypeError, sqlalchemy.adapt_persistent_instance, address, UserREST) + diff --git a/tests/provider/test_auth.py b/tests/provider/test_auth.py index 5006241..675bd53 100644 --- a/tests/provider/test_auth.py +++ b/tests/provider/test_auth.py @@ -214,6 +214,7 @@ def get(self): def test_login_required_no_provider_raises_exception(self): handler = self.handler_without_provider( args=[], + kwargs={}, request=self.get_request, response=self.response, logger=self.logger, @@ -225,6 +226,7 @@ def test_login_required_no_provider_raises_exception(self): def test_login_required_unauthenticated_raises_exception(self): handler = self.unauthenticated_handler( args=[], + kwargs={}, request=self.get_request, response=self.response, logger=self.logger, @@ -236,6 +238,7 @@ def test_login_required_unauthenticated_raises_exception(self): def test_login_required_authenticated(self): handler = self.authenticated_handler( args=[], + kwargs={}, request=self.get_request, response=self.response, logger=self.logger, @@ -328,6 +331,7 @@ def put(self): def test_role_required_no_provider_raises_exception(self): handler = self.handler_without_provider( args=[], + kwargs={}, request=self.get_request, response=self.response, logger=self.logger, @@ -339,6 +343,7 @@ def test_role_required_no_provider_raises_exception(self): def test_role_required_none_raises_authorization_error(self): handler = self.handler_with_provider( args=[], + kwargs={}, request=self.get_request, response=self.response, logger=self.logger, @@ -350,6 +355,7 @@ def test_role_required_none_raises_authorization_error(self): def test_role_required_incorrect_role_raises_exception(self): handler = self.handler_with_provider( args=[], + kwargs={}, request=self.post_request, response=self.response, logger=self.logger, @@ -361,6 +367,7 @@ def test_role_required_incorrect_role_raises_exception(self): def test_role_required_success(self): handler = self.handler_with_provider( args=[], + kwargs={}, request=self.put_request, response=self.response, logger=self.logger, @@ -439,6 +446,7 @@ def post(self): def test_access_required_no_provider_raises_exception(self): handler = self.handler_without_provider( args=[], + kwargs={}, request=self.get_request, response=self.response, logger=self.logger, @@ -450,6 +458,7 @@ def test_access_required_no_provider_raises_exception(self): def test_access_required_unauthorized_raises_exception(self): handler = self.handler_with_provider( args=[], + kwargs={}, request=self.post_request, response=self.response, logger=self.logger, @@ -461,6 +470,7 @@ def test_access_required_unauthorized_raises_exception(self): def test_access_required_success(self): handler = self.handler_with_provider( args=[], + kwargs={}, request=self.get_request, response=self.response, logger=self.logger, diff --git a/tests/rest/test_request_router.py b/tests/rest/test_request_router.py index 90c59e4..d8635a3 100644 --- a/tests/rest/test_request_router.py +++ b/tests/rest/test_request_router.py @@ -29,23 +29,21 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # - -import logging import unittest -import prestans.rest - -LOGGER_MCLOGFACE = logging.Logger("temp", level='ERROR') -LOGGER_MCLOGFACE.disabled = 50 # silence the logger +from prestans.http import VERB +from prestans import parser +from prestans import rest +from prestans import types -class MyModel(prestans.types.Model): - id = prestans.types.Integer() +class MyModel(types.Model): + id = types.Integer() -class _UserHandler(prestans.rest.RequestHandler): - __parser_config__ = prestans.parser.Config( - GET=prestans.parser.VerbConfig( +class _UserHandler(rest.RequestHandler): + __parser_config__ = parser.Config( + GET=parser.VerbConfig( response_template=MyModel(), response_attribute_filter_default_value=True ) @@ -63,7 +61,7 @@ def __call__(cls, status, response_headers, exc_info=None): pass -class RequestRouterTest(unittest.TestCase): +class RequestRouterTestCase(unittest.TestCase): def test_script_alias_match_with_global_match_group_should_not_pass_call(self): """ WSGIScriptAliasMatch /mountpoint(.*) script.wsgi @@ -73,8 +71,8 @@ def test_script_alias_match_with_global_match_group_should_not_pass_call(self): expected_value = 123 - self._test_routing_behavour(environ={ - "REQUEST_METHOD": prestans.http.VERB.GET, + self._test_routing_behaviour(environ={ + "REQUEST_METHOD": VERB.GET, "SCRIPT_NAME": "/mountpoint/some/path/{}".format(expected_value), "PATH_INFO": "", "wsgi.version": (1, 0), @@ -91,8 +89,8 @@ def test_script_alias_match_with_match_group_should_pass_call(self): with router ``TestRequestRouter`` :return: """ - self._test_routing_behavour(environ={ - "REQUEST_METHOD": prestans.http.VERB.GET, + self._test_routing_behaviour(environ={ + "REQUEST_METHOD": VERB.GET, "SCRIPT_NAME": "/mountpoint", "PATH_INFO": "/some/path/{}".format(123), "wsgi.url_scheme": "http", @@ -105,8 +103,8 @@ def test_script_alias_match_with_solidus_should_pass_call(self): WSGIScriptAliasMatch /mountpoint/(.*) script.wsgi/$1 :return: """ - self._test_routing_behavour(environ={ - "REQUEST_METHOD": prestans.http.VERB.GET, + self._test_routing_behaviour(environ={ + "REQUEST_METHOD": VERB.GET, "SCRIPT_NAME": "/mountpoint/", "PATH_INFO": "/some/path/{}".format(123), "wsgi.url_scheme": "http", @@ -122,8 +120,8 @@ def test_router_should_not_crash_with_no_path_info_field(self): """ expected_value = 123 - self._test_routing_behavour(environ={ - "REQUEST_METHOD": prestans.http.VERB.GET, + self._test_routing_behaviour(environ={ + "REQUEST_METHOD": VERB.GET, "SCRIPT_NAME": "/mountpoint/some/path/{}".format(123), "wsgi.url_scheme": "http", "SERVER_NAME": "localhost", @@ -140,21 +138,23 @@ def test_router_should_not_crash_with_no_script_name_field(self): expected_value = 123 - self._test_routing_behavour(environ={ - "REQUEST_METHOD": prestans.http.VERB.GET, + self._test_routing_behaviour(environ={ + "REQUEST_METHOD": VERB.GET, "PATH_INFO": "/some/path/{}".format(123), "wsgi.url_scheme": "http", "SERVER_NAME": "localhost", "SERVER_PORT": "1234"}) - def _test_routing_behavour(self, environ, should_pass=True, expected_value=123, match=r"/some/path/([0-9]+)"): + def _test_routing_behaviour(self, environ, should_pass=True, expected_value=123, match=r"/some/path/([0-9]+)"): + + from prestans.deserializer import JSON - test_router = prestans.rest.RequestRouter([ + test_router = rest.RequestRouter([ (match, _UserHandler) - ], application_name="test-router", logger=LOGGER_MCLOGFACE) + ], application_name="test-router") response = test_router(environ=environ, start_response=MockStartResponse.__call__) - response_parsed = prestans.deserializer.JSON().loads(response[0]) + response_parsed = JSON().loads(response[0]) if should_pass: self.assert_should_find_route(environ, expected_value, match, response_parsed) else: @@ -178,7 +178,3 @@ def gen_route_match_failure_message(self, environ, match, assertion="SHOULD"): route=match, assertion=assertion ) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_exception.py b/tests/test_exception.py index 4c7c817..c00bd0f 100644 --- a/tests/test_exception.py +++ b/tests/test_exception.py @@ -201,7 +201,7 @@ def test_init(self): self.assertEquals(attribute_filter_differs.http_status, STATUS.BAD_REQUEST) self.assertEquals( attribute_filter_differs.message, - "attribute filter does not contain attributes (cat, dog) that are not part of template" + "attribute filter contains attributes (cat, dog) that are not part of template" ) diff --git a/tox.ini b/tox.ini index 9fe8ad1..67be850 100644 --- a/tox.ini +++ b/tox.ini @@ -8,5 +8,6 @@ envlist = deps = jinja2 mock pytest + webtest commands = py.test tests \ No newline at end of file