diff --git a/flask_restplus/api.py b/flask_restplus/api.py index 37276503..a2889ab4 100644 --- a/flask_restplus/api.py +++ b/flask_restplus/api.py @@ -126,12 +126,7 @@ def __init__(self, app=None, version='1.0', title=None, description=None, self._refresolver = None self.format_checker = format_checker self.namespaces = [] - self.default_namespace = self.namespace(default, default_label, - endpoint='{0}-declaration'.format(default), - validate=validate, - api=self, - path='/', - ) + self.ns_paths = dict() self.representations = OrderedDict(DEFAULT_REPRESENTATIONS) @@ -146,7 +141,14 @@ def __init__(self, app=None, version='1.0', title=None, description=None, self.resources = [] self.app = None self.blueprint = None - + # must come after self.app initialisation to prevent __getattr__ recursion + # in self._configure_namespace_logger + self.default_namespace = self.namespace(default, default_label, + endpoint='{0}-declaration'.format(default), + validate=validate, + api=self, + path='/', + ) if app is not None: self.app = app self.init_app(app) @@ -205,6 +207,9 @@ def _init_app(self, app): for resource, namespace, urls, kwargs in self.resources: self._register_view(app, resource, namespace, *urls, **kwargs) + for ns in self.namespaces: + self._configure_namespace_logger(app, ns) + self._register_apidoc(app) self._validate = self._validate if self._validate is not None else app.config.get('RESTPLUS_VALIDATE', False) app.config.setdefault('RESTPLUS_MASK_HEADER', 'X-Fields') @@ -266,6 +271,11 @@ def register_resource(self, namespace, resource, *urls, **kwargs): self.resources.append((resource, namespace, urls, kwargs)) return endpoint + def _configure_namespace_logger(self, app, namespace): + for handler in app.logger.handlers: + namespace.logger.addHandler(handler) + namespace.logger.setLevel(app.logger.level) + def _register_view(self, app, resource, namespace, *urls, **kwargs): endpoint = kwargs.pop('endpoint', None) or camel_to_dash(resource.__name__) resource_class_args = kwargs.pop('resource_class_args', ()) @@ -427,6 +437,8 @@ def add_namespace(self, ns, path=None): # Register models for name, definition in six.iteritems(ns.models): self.models[name] = definition + if not self.blueprint and self.app is not None: + self._configure_namespace_logger(self.app, ns) def namespace(self, *args, **kwargs): ''' diff --git a/tests/conftest.py b/tests/conftest.py index 2d21c0ad..0b10b95c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -53,6 +53,18 @@ def api(request, app): yield api +@pytest.fixture +def mock_app(mocker): + app = mocker.Mock(Flask) + # mock Flask app object doesn't have any real loggers -> mock logging + # set up on Api object + mocker.patch.object(restplus.Api, '_configure_namespace_logger') + app.view_functions = {} + app.extensions = {} + app.config = {} + return app + + @pytest.fixture(autouse=True) def _push_custom_request_context(request): app = request.getfixturevalue('app') diff --git a/tests/legacy/test_api_legacy.py b/tests/legacy/test_api_legacy.py index 7b10725b..79e70fa4 100644 --- a/tests/legacy/test_api_legacy.py +++ b/tests/legacy/test_api_legacy.py @@ -4,6 +4,7 @@ import flask import pytest import six +import logging from json import dumps, JSONEncoder @@ -126,21 +127,17 @@ def test_media_types_q(self, app): }): assert api.mediatypes() == ['application/json', 'application/xml'] - def test_decorator(self, mocker): + def test_decorator(self, mocker, mock_app): def return_zero(func): return 0 - app = mocker.Mock(flask.Flask) - app.view_functions = {} - app.extensions = {} - app.config = {} view = mocker.Mock() - api = restplus.Api(app) + api = restplus.Api(mock_app) api.decorators.append(return_zero) api.output = mocker.Mock() api.add_resource(view, '/foo', endpoint='bar') - app.add_url_rule.assert_called_with('/foo', view_func=0) + mock_app.add_url_rule.assert_called_with('/foo', view_func=0) def test_add_resource_endpoint(self, app, mocker): view = mocker.Mock(**{'as_view.return_value.__name__': str('test_view')}) @@ -181,28 +178,20 @@ def get(self): foo2 = client.get('/foo/toto') assert foo2.data == b'"foo1"\n' - def test_add_resource(self, mocker): - app = mocker.Mock(flask.Flask) - app.view_functions = {} - app.extensions = {} - app.config = {} - api = restplus.Api(app) + def test_add_resource(self, mocker, mock_app): + api = restplus.Api(mock_app) api.output = mocker.Mock() api.add_resource(views.MethodView, '/foo') - app.add_url_rule.assert_called_with('/foo', + mock_app.add_url_rule.assert_called_with('/foo', view_func=api.output()) - def test_add_resource_kwargs(self, mocker): - app = mocker.Mock(flask.Flask) - app.view_functions = {} - app.extensions = {} - app.config = {} - api = restplus.Api(app) + def test_add_resource_kwargs(self, mocker, mock_app): + api = restplus.Api(mock_app) api.output = mocker.Mock() api.add_resource(views.MethodView, '/foo', defaults={"bar": "baz"}) - app.add_url_rule.assert_called_with('/foo', + mock_app.add_url_rule.assert_called_with('/foo', view_func=api.output(), defaults={"bar": "baz"})