Esempio n. 1
0
    def __init__(self, name, config={}):
        self.name = name

        # Set up threadlocal storage.  We use this so we can process multiple
        # requests at the same time from one app.
        # NOTE: this needs to be done before logging, since InjectingFilter
        # will inject the request/response objects into a log message.
        self._locals = threading.local()
        self._locals.request = None
        self._locals.response = None
        self._locals.vars = SimpleNamespace()

        # If we're missing the root dir, we try and determine them here.
        app_file = config.get('APPLICATION_FILE')
        if app_file is None:
            import __main__

            # Get the file name if it exists.  It won't in, for example, the
            # interactive console.
            if hasattr(__main__, "__file__"):
                app_file = os.path.abspath(__main__.__file__)
            else:               # pragma: no cover
                app_file = os.path.abspath(".")

        # Given the application file, get the root directory.
        root_dir = config.get('ROOT_DIRECTORY')
        if root_dir is None:
            root_dir = os.path.dirname(app_file)

        # Create our config, and set values in it.
        self.config = ConfigDict(root_dir, defaults=self.DEFAULT_CONFIG)
        self.config.update(config)

        # Set our heuristically-determined things.
        self.config['APPLICATION_FILE'] = app_file
        self.config['ROOT_DIRECTORY'] = root_dir

        # Set other directory values.
        self.config.setdefault('VIEWS_DIRECTORY', os.path.join(
            self.config['ROOT_DIRECTORY'],
            "views"
        ))
        self.config.setdefault('STATIC_DIRECTORY', os.path.join(
            self.config['ROOT_DIRECTORY'],
            "static"
        ))

        # Routes array. We split this by method, both for speed and simplicity.
        self.routes = {}
        # TODO: Might be worth having a reverse-mapping array, so we can turn a
        # function into a route (e.g. for URL generation, and so on).

        for m in self.SUPPORTED_METHODS:
            self.routes[m] = []

        # Before and after filter arrays.  Note that these are also Routes
        self.before_filters = []
        self.after_filters = []

        # Create logger.
        self.logger = self.create_logger()

        # Create a lock which we might use to serialize requests.  Originally,
        # this was only created if the appropriate config value was set, but
        # this caused problems if the config value was then set after the
        # application was created.
        self.lock = threading.Lock()

        # Call other __init__ functions - this is needed for mixins to work.
        super(HobokenBaseApplication, self).__init__()

        # Done initialization.
        self.logger.info("Application initialized")
Esempio n. 2
0
 def setUp(self):
     self.d = tempfile.mkdtemp()
     self.c = ConfigDict(self.d)
     self.f = tempfile.NamedTemporaryFile(dir=self.d)
Esempio n. 3
0
class HobokenBaseApplication(with_metaclass(HobokenMetaclass)):
    # These are the supported HTTP methods.  They can be overridden in
    # subclasses to add additional methods (e.g. "TRACE", "CONNECT", etc.)
    SUPPORTED_METHODS = ("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS",
                         "HEAD")

    DEFAULT_CONFIG = {
        'DEBUG': False,
        'APPLICATION_FILE': None,
        'SERIALIZE_REQUESTS': False,
    }

    # The application's debug setting.
    debug = ConfigProperty('DEBUG')

    def __init__(self, name, config={}):
        self.name = name

        # Set up threadlocal storage.  We use this so we can process multiple
        # requests at the same time from one app.
        # NOTE: this needs to be done before logging, since InjectingFilter
        # will inject the request/response objects into a log message.
        self._locals = threading.local()
        self._locals.request = None
        self._locals.response = None
        self._locals.vars = SimpleNamespace()

        # If we're missing the root dir, we try and determine them here.
        app_file = config.get('APPLICATION_FILE')
        if app_file is None:
            import __main__

            # Get the file name if it exists.  It won't in, for example, the
            # interactive console.
            if hasattr(__main__, "__file__"):
                app_file = os.path.abspath(__main__.__file__)
            else:               # pragma: no cover
                app_file = os.path.abspath(".")

        # Given the application file, get the root directory.
        root_dir = config.get('ROOT_DIRECTORY')
        if root_dir is None:
            root_dir = os.path.dirname(app_file)

        # Create our config, and set values in it.
        self.config = ConfigDict(root_dir, defaults=self.DEFAULT_CONFIG)
        self.config.update(config)

        # Set our heuristically-determined things.
        self.config['APPLICATION_FILE'] = app_file
        self.config['ROOT_DIRECTORY'] = root_dir

        # Set other directory values.
        self.config.setdefault('VIEWS_DIRECTORY', os.path.join(
            self.config['ROOT_DIRECTORY'],
            "views"
        ))
        self.config.setdefault('STATIC_DIRECTORY', os.path.join(
            self.config['ROOT_DIRECTORY'],
            "static"
        ))

        # Routes array. We split this by method, both for speed and simplicity.
        self.routes = {}
        # TODO: Might be worth having a reverse-mapping array, so we can turn a
        # function into a route (e.g. for URL generation, and so on).

        for m in self.SUPPORTED_METHODS:
            self.routes[m] = []

        # Before and after filter arrays.  Note that these are also Routes
        self.before_filters = []
        self.after_filters = []

        # Create logger.
        self.logger = self.create_logger()

        # Create a lock which we might use to serialize requests.  Originally,
        # this was only created if the appropriate config value was set, but
        # this caused problems if the config value was then set after the
        # application was created.
        self.lock = threading.Lock()

        # Call other __init__ functions - this is needed for mixins to work.
        super(HobokenBaseApplication, self).__init__()

        # Done initialization.
        self.logger.info("Application initialized")

    def create_logger(self):
        logger = logging.getLogger('hoboken.applications.' + self.name)

        # Override the __class__ of the logger so we can deal with the debug
        # setting.
        logger.app = self
        logger.__class__ = DebugLogger

        # Add a filter that will store the request and response object on each
        # log record.
        logger.addFilter(InjectingFilter(self))

        return logger

    @property
    def request(self):
        return self._locals.__dict__.get('request')

    @request.setter
    def request(self, val):
        self._locals.request = val

    @request.deleter
    def request(self):
        self._locals.request = None

    @property
    def response(self):
        return self._locals.__dict__.get('response')

    @response.setter
    def response(self, val):
        self._locals.response = val

    @response.deleter
    def response(self):
        self._locals.response = None

    @property
    def g(self):
        v = self._locals.__dict__.get('vars')
        if v is None:       # pragma: no cover
            v = self._locals.vars = SimpleNamespace()
        return v

    @g.deleter
    def g(self):
        self._locals.vars = SimpleNamespace()

    def delegate(self, app, catch_exceptions=False):
        """
        Delegates processing of the current request to another WSGI
        application.  Will set the current response to the response that was
        recieved from the other application.
        """
        if self.request is None:
            return False

        # Make the request on the subapp.
        resp = self.request.get_response(app, catch_exc_info=catch_exceptions)

        # Set our response.
        self.response = resp
        return True

    def _make_route(self, match, func):
        if isinstance(match, string_types):
            matcher = HobokenRouteMatcher(match)
        elif isinstance(match, RegexType):
            # match is a regex, so we extract any named groups.
            keys = [None] * match.groups
            types = [False] * match.groups
            for name, index in iteritems(match.groupindex):
                types[index - 1] = True
                keys[index - 1] = name

            # Append the route with these keys.
            matcher = RegexMatcher(match, types, keys)

        elif hasattr(match, "match") and callable(getattr(match, "match")):
            # Don't know what type it is, but it has a callable "match"
            # attribute, so we use that.
            matcher = match

        else:
            # Unknown type!
            raise InvalidMatchTypeException("Unknown type: %r" % (match,))

        return Route(matcher, func)

    def add_route(self, method, match, func):
        # Methods are uppercase.
        method = method.upper()

        # Check for valid method.
        if not method in self.SUPPORTED_METHODS:
            raise HobokenException("Invalid method type given: %s" % (method,))

        route = self._make_route(match, func)
        route.method = method
        self.routes[method].append(route)

    def find_route_with_method(self, method, func):
        for route in self.routes[method]:
            if route.func == func:
                return route

        return None

    def find_route(self, func):
        for method in self.SUPPORTED_METHODS:
            route = self.find_route_with_method(method, func)
            if route:
                return route

        return None

    def url_for(self, function, *args, **kwargs):
        route = self.find_route(function)
        if route is None:
            return None

        path = route.reverse(*args, **kwargs)
        return path

    def redirect(self, location, code=None, body=None, headers=None):
        """
        This is a helper function for redirection.
        """

        # If a code is specified, we take that.
        if code is None:
            # If no code, we send a 303 if it's supported and we aren't already
            # using GET.
            if (self.request.http_version == b'HTTP/1.1' and
                    self.request.method != 'GET'):
                code = 303
            else:
                code = 302

        # Ensure we have the 'headers' dict.
        headers = headers or {}

        # Set the 'location' argument, which sets the 'Location' header.
        headers['Location'] = location

        # Halt routing with these parameters.
        halt(code=code, body=body, headers=headers)

    def _decorate_and_route(self, method, match):
        def internal_decorator(func):
            # We only allow one route for each function.
            if is_route(func):
                logger.error("Function %s is already a route", func.__name__)
                raise RouteExistsException()

            # This allows us to add conditions!
            def add_condition(condition_func):
                route = self.find_route(func)
                route.add_condition(condition_func)
                self.logger.debug("Added condition '%s' for func %s/%s",
                                  condition_func.__name__,
                                  str(method),
                                  func.__name__)

            # Add the route.
            self.add_route(method, match, func)

            # Add each of the existing conditions.
            conditions = get_func_attr(func, 'hoboken.conditions', default=[],
                                       delete=True)
            for c in conditions:
                add_condition(c)

            # Mark this function as a route.
            set_func_attr(func, 'hoboken.route', True)

            # Add a function to add future conditions. This is so the order
            # of conditions being added doesn't matter.
            set_func_attr(func, 'hoboken.add_condition', add_condition)
            return func

        return internal_decorator

    def add_before_filter(self, match, func):
        filter_tuple = self._make_route(match, func)
        self.before_filters.append(filter_tuple)

    def before(self, match=None):
        # If the match isn't provided, we match anything.
        if match is None:
            match = re.compile(b".*")

        def internal_decorator(func):
            self.add_before_filter(match, func)
            return func
        return internal_decorator

    def add_after_filter(self, match, func):
        filter_tuple = self._make_route(match, func)
        self.after_filters.append(filter_tuple)

    def after(self, match=None):
        # If the match isn't provided, we match anything.
        if match is None:
            match = re.compile(b".*")

        def internal_decorator(func):
            self.add_after_filter(match, func)
            return func
        return internal_decorator

    def on_returned_body(self, request, resp, value):
        """
        This function is used to turn a value that's been returned from a
        route function into the request body.  Override this in a subclass
        to customize how values are returned.
        """
        if isinstance(value, text_type):
            resp.text = value
        elif isinstance(value, binary_type):
            resp.body = value
        else:
            logger.error("Unknown return type: %r", type(value))
            raise ValueError("Unknown return type: {0!r}".format(type(value)))

    def wsgi_entrypoint(self, environ, start_response):
        # Flag stating whether we've acquired our lock.  Defaults to False,
        # since we (by default) do not serialize requests.
        locked = False

        try:
            if self.config['SERIALIZE_REQUESTS']:
                # Acquire, then set our flag.  Note that order matters here,
                # since we only want to set the 'locked' flag when it is safe
                # to unlock (see below for more notes).
                self.lock.acquire()
                locked = True

            # Create our request object.
            self.request = Request(environ)

            # Create an empty response.
            self.response = Response()

            # Create our variables object.
            self._locals.vars = SimpleNamespace()

            # Set default values on the response.
            self._prepare_response()

            # Actually handle this request.
            self._handle_request()

            # Finally, given our response, we finish the WSGI request.
            return self.response(environ, start_response)
        finally:
            # Note that we don't automatically release, since there might be
            # an error with accessing self.config, above, and so we might not
            # have acquired the lock.
            if locked:
                self.lock.release()

            # After each request, we remove the request and response objects.
            del self.request
            del self.response

            # We also reset our request config.
            del self.g

    def _prepare_response(self):
        # We default to setting the current (UTC) date on the response.
        if hasattr(self.response, 'date'):
            self.response.date = datetime.utcnow()

    def _run_routes(self, method):
        # Since these are thread-locals, we grab them as locals.
        request = self.request
        response = self.response

        # For each route of the specified type, try to match it.
        for route in self.routes[method]:
            matches, ret = route(request, response)
            if ret is not None:
                self.on_returned_body(request, response, ret)

            if matches:
                return True

        return False

    def _handle_request(self):
        # Since these are thread-locals, we grab them as locals.
        request = self.request
        response = self.response
        self.logger.debug("Handling: %s %s", request.method, request.url)

        # Check for valid method.
        # TODO: Should this call our after filters?
        if request.method not in self.SUPPORTED_METHODS:
            self.logger.warn("Called with invalid method: %s", request.method)

            # TODO: hook.

            # Send "invalid method" exception.
            response.status_int = 405
            return

        matched = False
        try:
            # Call before filters.
            for filter in self.before_filters:
                filter(request, response)

            # For each route of the specified type, try to match it.
            matched = self._run_routes(request.method)

            # We special-case the HEAD method to fallback to GET.
            if request.method == 'HEAD' and not matched:
                # Run our routes against the 'GET' method.
                matched = self._run_routes('GET')

        except HaltRoutingException as ex:
            # Set the various parameters.
            if ex.code is not None:
                response.status_int = ex.code

            if ex.body is not None:
                # We pass the body through to on_returned_body.
                self.on_returned_body(request, response, ex.body)

            if ex.headers is not None:
                # Set each header.
                for header, value in iteritems(ex.headers):
                    # Set this header.
                    response.headers[header] = value

            # Must set this, or we get clobbered by the 404 handler.
            matched = True

        except Exception as e:
            # Also, check if the exception has other information attached,
            # like a code/body.
            self.on_exception(e)

            # Must set this, or we get clobbered by the 404 handler.
            matched = True

        finally:
            # Call our after filters
            for route in self.after_filters:
                route(request, response)

        if not matched:
            self.on_route_missing()

    def __call__(self, environ, start_response):
        return self.wsgi_entrypoint(environ, start_response)

    def on_route_missing(self):
        """
        This function is called when a route to handle a request is not found.
        Override this function to provide custom not-found logic.
        """
        # By default, return a 404 request.
        self.response.status_int = 404

    def on_exception(self, exception):
        self.response.status_int = 500
        if self.config['DEBUG']:
            # Format the current traceback
            tb = traceback.format_exc()

            # Return the traceback as text.
            self.response.content_type = 'text/plain'
            if sys.version_info[0] >= 3:
                self.response.text = tb
            else:
                self.response.body = tb

            print(tb, file=sys.stderr)

    def test_server(self, port=8000):                   # pragma: no cover
        """
        This method lets you start a test server for development purposes.
        Note: There is deliberately no option to set the address to listen on.
              The server will always listen on 'localhost', and should never
              be used in production.
        """

        self.logger.info("Starting test server on port %d", port)

        from wsgiref.simple_server import make_server
        httpd = make_server('localhost', port, self)

        try:
            httpd.serve_forever()
        except KeyboardInterrupt:
            self.logger.info("Stopped test server due to keyboard interrupt")

    def __str__(self):
        """
        Some help for debugging: str(app) will get a summary of the app and
        it's defined before/after/routes.
        """

        body = []
        body.append("Application {0} (Debug: {1})".format(
            self.name,
            self.config['DEBUG']
        ))
        body.append("")

        def dump_filter_array(arr):
            body.append("=" * 79)
            body.append("Function        Match                     Conditions")
            body.append("-" * 79)
            for filter in arr:
                conds = ", ".join([f.__name__ for f in filter.conditions])
                body.append("{0:<15} {1:<25} {2:<35}".format(
                    filter.func.__name__, str(filter.match), conds
                ))

        def dump_route_array(arr):
            body.append("=" * 79)
            body.append(
                "Method  Function        Match                     Conditions"
            )
            body.append("-" * 79)
            for method in self.routes:
                for route in self.routes[method]:
                    conds = ", ".join([f.__name__ for f in route.conditions])
                    body.append("{0:<7} {1:<15} {2:<25} {3:<28}".format(
                        method,
                        route.func.__name__,
                        str(route.matcher),
                        conds
                    ))

        body.append("BEFORE FILTERS")
        dump_filter_array(self.before_filters)
        body.append("")

        body.append("ROUTES")
        dump_route_array(self.before_filters)
        body.append("")

        body.append("AFTER FILTERS")
        dump_filter_array(self.after_filters)
        body.append("")

        return '\n'.join(body)

    def __repr__(self):
        return "HobokenApplication(name={!r}, debug={!r})".format(
            self.name,
            self.config['DEBUG']
        )
Esempio n. 4
0
class TestConfigDict(unittest.TestCase):
    def setUp(self):
        self.d = tempfile.mkdtemp()
        self.c = ConfigDict(self.d)
        self.f = tempfile.NamedTemporaryFile(dir=self.d)

    def write(self, val):
        if isinstance(val, text_type):
            val = val.encode('utf-8')
        self.f.write(val)
        self.f.flush()

    def test_from_object(self):
        class Test(object):
            not_set = 1
            Not_set = 2
            IS_SET = 3

        t = Test()
        self.c.from_object(t)

        self.assertIn('IS_SET', self.c)
        self.assertNotIn('not_set', self.c)
        self.assertNotIn('Not_set', self.c)

    def test_from_dict(self):
        d = {
            'not_set': 1,
            'Not_set': 2,
            'IS_SET': 3,
        }

        self.c.from_dict(d)

        self.assertIn('IS_SET', self.c)
        self.assertNotIn('not_set', self.c)
        self.assertNotIn('Not_set', self.c)

    def test_from_json(self):
        j = '{"not_set": 1, "IS_SET": 3, "Not_set": 2}'
        self.write(j)

        self.c.from_json(self.f.name)

        self.assertIn('IS_SET', self.c)
        self.assertNotIn('not_set', self.c)
        self.assertNotIn('Not_set', self.c)

    def test_from_json_invalid(self):
        self.write('foobar::asdf')

        self.assertFalse(self.c.from_json(self.f.name, silent=True))

        with self.assertRaises(ValueError):
            self.c.from_json(self.f.name, silent=False)

        self.assertFalse(self.c.from_json('invalid_name', silent=True))

        with self.assertRaises(IOError):
            self.c.from_json('invalid_name', silent=False)

    def test_from_yaml(self):
        y = """
not_set: 1
Not_set: 2
IS_SET: 3
        """
        self.write(y)

        self.c.from_yaml(self.f.name)

        self.assertIn('IS_SET', self.c)
        self.assertNotIn('not_set', self.c)
        self.assertNotIn('Not_set', self.c)

    def test_from_yaml_invalid(self):
        self.write('foobar: asdf\nanother: {')

        self.assertFalse(self.c.from_yaml(self.f.name, silent=True))

        with self.assertRaises(yaml.YAMLError):
            self.c.from_yaml(self.f.name, silent=False)

        self.assertFalse(self.c.from_yaml('invalid_name', silent=True))

        with self.assertRaises(IOError):
            self.c.from_yaml('invalid_name', silent=False)

    def test_from_pyfile(self):
        py = """
not_set = 1
Not_set = 2
IS_SET = 3
        """
        self.write(py)

        self.c.from_pyfile(self.f.name)

        self.assertIn('IS_SET', self.c)
        self.assertNotIn('not_set', self.c)
        self.assertNotIn('Not_set', self.c)

    def test_from_pyfile_invalid(self):
        self.assertFalse(self.c.from_pyfile('invalid_name', silent=True))

        with self.assertRaises(IOError):
            self.c.from_pyfile('invalid_name', silent=False)

    def test_from_envvar(self):
        py = """
not_set = 1
Not_set = 2
IS_SET = 3
        """
        self.write(py)
        os.environ['MY_ENVIRONMENT_VAR'] = self.f.name

        self.c.from_envvar('MY_ENVIRONMENT_VAR')

        self.assertIn('IS_SET', self.c)
        self.assertNotIn('not_set', self.c)
        self.assertNotIn('Not_set', self.c)

    def test_from_envvar_invalid(self):
        self.assertFalse(self.c.from_envvar('INVALID_ENV_VAR', silent=True))

        with self.assertRaises(RuntimeError):
            self.assertFalse(
                self.c.from_envvar('INVALID_ENV_VAR', silent=False))
Esempio n. 5
0
 def setUp(self):
     self.d = tempfile.mkdtemp()
     self.c = ConfigDict(self.d)
     self.f = tempfile.NamedTemporaryFile(dir=self.d)
Esempio n. 6
0
class TestConfigDict(unittest.TestCase):
    def setUp(self):
        self.d = tempfile.mkdtemp()
        self.c = ConfigDict(self.d)
        self.f = tempfile.NamedTemporaryFile(dir=self.d)

    def write(self, val):
        if isinstance(val, text_type):
            val = val.encode('utf-8')
        self.f.write(val)
        self.f.flush()

    def test_from_object(self):
        class Test(object):
            not_set = 1
            Not_set = 2
            IS_SET  = 3

        t = Test()
        self.c.from_object(t)

        self.assertIn('IS_SET', self.c)
        self.assertNotIn('not_set', self.c)
        self.assertNotIn('Not_set', self.c)

    def test_from_dict(self):
        d = {
            'not_set': 1,
            'Not_set': 2,
            'IS_SET':  3,
        }

        self.c.from_dict(d)

        self.assertIn('IS_SET', self.c)
        self.assertNotIn('not_set', self.c)
        self.assertNotIn('Not_set', self.c)

    def test_from_json(self):
        j = '{"not_set": 1, "IS_SET": 3, "Not_set": 2}'
        self.write(j)

        self.c.from_json(self.f.name)

        self.assertIn('IS_SET', self.c)
        self.assertNotIn('not_set', self.c)
        self.assertNotIn('Not_set', self.c)

    def test_from_json_invalid(self):
        self.write('foobar::asdf')

        self.assertFalse(self.c.from_json(self.f.name, silent=True))

        with self.assertRaises(ValueError):
            self.c.from_json(self.f.name, silent=False)

        self.assertFalse(self.c.from_json('invalid_name', silent=True))

        with self.assertRaises(IOError):
            self.c.from_json('invalid_name', silent=False)

    def test_from_yaml(self):
        y = """
not_set: 1
Not_set: 2
IS_SET: 3
        """
        self.write(y)

        self.c.from_yaml(self.f.name)

        self.assertIn('IS_SET', self.c)
        self.assertNotIn('not_set', self.c)
        self.assertNotIn('Not_set', self.c)

    def test_from_yaml_invalid(self):
        self.write('foobar: asdf\nanother: {')

        self.assertFalse(self.c.from_yaml(self.f.name, silent=True))

        with self.assertRaises(yaml.YAMLError):
            self.c.from_yaml(self.f.name, silent=False)

        self.assertFalse(self.c.from_yaml('invalid_name', silent=True))

        with self.assertRaises(IOError):
            self.c.from_yaml('invalid_name', silent=False)

    def test_from_pyfile(self):
        py = """
not_set = 1
Not_set = 2
IS_SET = 3
        """
        self.write(py)

        self.c.from_pyfile(self.f.name)

        self.assertIn('IS_SET', self.c)
        self.assertNotIn('not_set', self.c)
        self.assertNotIn('Not_set', self.c)

    def test_from_pyfile_invalid(self):
        self.assertFalse(self.c.from_pyfile('invalid_name', silent=True))

        with self.assertRaises(IOError):
            self.c.from_pyfile('invalid_name', silent=False)

    def test_from_envvar(self):
        py = """
not_set = 1
Not_set = 2
IS_SET = 3
        """
        self.write(py)
        os.environ['MY_ENVIRONMENT_VAR'] = self.f.name

        self.c.from_envvar('MY_ENVIRONMENT_VAR')

        self.assertIn('IS_SET', self.c)
        self.assertNotIn('not_set', self.c)
        self.assertNotIn('Not_set', self.c)

    def test_from_envvar_invalid(self):
        self.assertFalse(self.c.from_envvar('INVALID_ENV_VAR', silent=True))

        with self.assertRaises(RuntimeError):
            self.assertFalse(self.c.from_envvar('INVALID_ENV_VAR', silent=False))
Esempio n. 7
0
    def __init__(self, name, config={}):
        self.name = name

        # Set up threadlocal storage.  We use this so we can process multiple
        # requests at the same time from one app.
        # NOTE: this needs to be done before logging, since InjectingFilter
        # will inject the request/response objects into a log message.
        self._locals = threading.local()
        self._locals.request = None
        self._locals.response = None
        self._locals.vars = SimpleNamespace()

        # If we're missing the root dir, we try and determine them here.
        app_file = config.get('APPLICATION_FILE')
        if app_file is None:
            import __main__

            # Get the file name if it exists.  It won't in, for example, the
            # interactive console.
            if hasattr(__main__, "__file__"):
                app_file = os.path.abspath(__main__.__file__)
            else:  # pragma: no cover
                app_file = os.path.abspath(".")

        # Given the application file, get the root directory.
        root_dir = config.get('ROOT_DIRECTORY')
        if root_dir is None:
            root_dir = os.path.dirname(app_file)

        # Create our config, and set values in it.
        self.config = ConfigDict(root_dir, defaults=self.DEFAULT_CONFIG)
        self.config.update(config)

        # Set our heuristically-determined things.
        self.config['APPLICATION_FILE'] = app_file
        self.config['ROOT_DIRECTORY'] = root_dir

        # Set other directory values.
        self.config.setdefault(
            'VIEWS_DIRECTORY',
            os.path.join(self.config['ROOT_DIRECTORY'], "views"))
        self.config.setdefault(
            'STATIC_DIRECTORY',
            os.path.join(self.config['ROOT_DIRECTORY'], "static"))

        # Routes array. We split this by method, both for speed and simplicity.
        self.routes = {}
        # TODO: Might be worth having a reverse-mapping array, so we can turn a
        # function into a route (e.g. for URL generation, and so on).

        for m in self.SUPPORTED_METHODS:
            self.routes[m] = []

        # Before and after filter arrays.  Note that these are also Routes
        self.before_filters = []
        self.after_filters = []

        # Create logger.
        self.logger = self.create_logger()

        # Create a lock which we might use to serialize requests.  Originally,
        # this was only created if the appropriate config value was set, but
        # this caused problems if the config value was then set after the
        # application was created.
        self.lock = threading.Lock()

        # Call other __init__ functions - this is needed for mixins to work.
        super(HobokenBaseApplication, self).__init__()

        # Done initialization.
        self.logger.info("Application initialized")
Esempio n. 8
0
class HobokenBaseApplication(with_metaclass(HobokenMetaclass)):
    # These are the supported HTTP methods.  They can be overridden in
    # subclasses to add additional methods (e.g. "TRACE", "CONNECT", etc.)
    SUPPORTED_METHODS = ("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS",
                         "HEAD")

    DEFAULT_CONFIG = {
        'DEBUG': False,
        'APPLICATION_FILE': None,
        'SERIALIZE_REQUESTS': False,
    }

    # The application's debug setting.
    debug = ConfigProperty('DEBUG')

    def __init__(self, name, config={}):
        self.name = name

        # Set up threadlocal storage.  We use this so we can process multiple
        # requests at the same time from one app.
        # NOTE: this needs to be done before logging, since InjectingFilter
        # will inject the request/response objects into a log message.
        self._locals = threading.local()
        self._locals.request = None
        self._locals.response = None
        self._locals.vars = SimpleNamespace()

        # If we're missing the root dir, we try and determine them here.
        app_file = config.get('APPLICATION_FILE')
        if app_file is None:
            import __main__

            # Get the file name if it exists.  It won't in, for example, the
            # interactive console.
            if hasattr(__main__, "__file__"):
                app_file = os.path.abspath(__main__.__file__)
            else:  # pragma: no cover
                app_file = os.path.abspath(".")

        # Given the application file, get the root directory.
        root_dir = config.get('ROOT_DIRECTORY')
        if root_dir is None:
            root_dir = os.path.dirname(app_file)

        # Create our config, and set values in it.
        self.config = ConfigDict(root_dir, defaults=self.DEFAULT_CONFIG)
        self.config.update(config)

        # Set our heuristically-determined things.
        self.config['APPLICATION_FILE'] = app_file
        self.config['ROOT_DIRECTORY'] = root_dir

        # Set other directory values.
        self.config.setdefault(
            'VIEWS_DIRECTORY',
            os.path.join(self.config['ROOT_DIRECTORY'], "views"))
        self.config.setdefault(
            'STATIC_DIRECTORY',
            os.path.join(self.config['ROOT_DIRECTORY'], "static"))

        # Routes array. We split this by method, both for speed and simplicity.
        self.routes = {}
        # TODO: Might be worth having a reverse-mapping array, so we can turn a
        # function into a route (e.g. for URL generation, and so on).

        for m in self.SUPPORTED_METHODS:
            self.routes[m] = []

        # Before and after filter arrays.  Note that these are also Routes
        self.before_filters = []
        self.after_filters = []

        # Create logger.
        self.logger = self.create_logger()

        # Create a lock which we might use to serialize requests.  Originally,
        # this was only created if the appropriate config value was set, but
        # this caused problems if the config value was then set after the
        # application was created.
        self.lock = threading.Lock()

        # Call other __init__ functions - this is needed for mixins to work.
        super(HobokenBaseApplication, self).__init__()

        # Done initialization.
        self.logger.info("Application initialized")

    def create_logger(self):
        logger = logging.getLogger('hoboken.applications.' + self.name)

        # Override the __class__ of the logger so we can deal with the debug
        # setting.
        logger.app = self
        logger.__class__ = DebugLogger

        # Add a filter that will store the request and response object on each
        # log record.
        logger.addFilter(InjectingFilter(self))

        return logger

    @property
    def request(self):
        return self._locals.__dict__.get('request')

    @request.setter
    def request(self, val):
        self._locals.request = val

    @request.deleter
    def request(self):
        self._locals.request = None

    @property
    def response(self):
        return self._locals.__dict__.get('response')

    @response.setter
    def response(self, val):
        self._locals.response = val

    @response.deleter
    def response(self):
        self._locals.response = None

    @property
    def g(self):
        v = self._locals.__dict__.get('vars')
        if v is None:  # pragma: no cover
            v = self._locals.vars = SimpleNamespace()
        return v

    @g.deleter
    def g(self):
        self._locals.vars = SimpleNamespace()

    def delegate(self, app, catch_exceptions=False):
        """
        Delegates processing of the current request to another WSGI
        application.  Will set the current response to the response that was
        recieved from the other application.
        """
        if self.request is None:
            return False

        # Make the request on the subapp.
        resp = self.request.get_response(app, catch_exc_info=catch_exceptions)

        # Set our response.
        self.response = resp
        return True

    def _make_route(self, match, func):
        if isinstance(match, string_types):
            matcher = HobokenRouteMatcher(match)
        elif isinstance(match, RegexType):
            # match is a regex, so we extract any named groups.
            keys = [None] * match.groups
            types = [False] * match.groups
            for name, index in iteritems(match.groupindex):
                types[index - 1] = True
                keys[index - 1] = name

            # Append the route with these keys.
            matcher = RegexMatcher(match, types, keys)

        elif hasattr(match, "match") and callable(getattr(match, "match")):
            # Don't know what type it is, but it has a callable "match"
            # attribute, so we use that.
            matcher = match

        else:
            # Unknown type!
            raise InvalidMatchTypeException("Unknown type: %r" % (match, ))

        return Route(matcher, func)

    def add_route(self, method, match, func):
        # Methods are uppercase.
        method = method.upper()

        # Check for valid method.
        if not method in self.SUPPORTED_METHODS:
            raise HobokenException("Invalid method type given: %s" %
                                   (method, ))

        route = self._make_route(match, func)
        route.method = method
        self.routes[method].append(route)

    def find_route_with_method(self, method, func):
        for route in self.routes[method]:
            if route.func == func:
                return route

        return None

    def find_route(self, func):
        for method in self.SUPPORTED_METHODS:
            route = self.find_route_with_method(method, func)
            if route:
                return route

        return None

    def url_for(self, function, *args, **kwargs):
        route = self.find_route(function)
        if route is None:
            return None

        path = route.reverse(*args, **kwargs)
        return path

    def redirect(self, location, code=None, body=None, headers=None):
        """
        This is a helper function for redirection.
        """

        # If a code is specified, we take that.
        if code is None:
            # If no code, we send a 303 if it's supported and we aren't already
            # using GET.
            if (self.request.http_version == b'HTTP/1.1'
                    and self.request.method != 'GET'):
                code = 303
            else:
                code = 302

        # Ensure we have the 'headers' dict.
        headers = headers or {}

        # Set the 'location' argument, which sets the 'Location' header.
        headers['Location'] = location

        # Halt routing with these parameters.
        halt(code=code, body=body, headers=headers)

    def _decorate_and_route(self, method, match):
        def internal_decorator(func):
            # We only allow one route for each function.
            if is_route(func):
                logger.error("Function %s is already a route", func.__name__)
                raise RouteExistsException()

            # This allows us to add conditions!
            def add_condition(condition_func):
                route = self.find_route(func)
                route.add_condition(condition_func)
                self.logger.debug("Added condition '%s' for func %s/%s",
                                  condition_func.__name__, str(method),
                                  func.__name__)

            # Add the route.
            self.add_route(method, match, func)

            # Add each of the existing conditions.
            conditions = get_func_attr(func,
                                       'hoboken.conditions',
                                       default=[],
                                       delete=True)
            for c in conditions:
                add_condition(c)

            # Mark this function as a route.
            set_func_attr(func, 'hoboken.route', True)

            # Add a function to add future conditions. This is so the order
            # of conditions being added doesn't matter.
            set_func_attr(func, 'hoboken.add_condition', add_condition)
            return func

        return internal_decorator

    def add_before_filter(self, match, func):
        filter_tuple = self._make_route(match, func)
        self.before_filters.append(filter_tuple)

    def before(self, match=None):
        # If the match isn't provided, we match anything.
        if match is None:
            match = re.compile(b".*")

        def internal_decorator(func):
            self.add_before_filter(match, func)
            return func

        return internal_decorator

    def add_after_filter(self, match, func):
        filter_tuple = self._make_route(match, func)
        self.after_filters.append(filter_tuple)

    def after(self, match=None):
        # If the match isn't provided, we match anything.
        if match is None:
            match = re.compile(b".*")

        def internal_decorator(func):
            self.add_after_filter(match, func)
            return func

        return internal_decorator

    def on_returned_body(self, request, resp, value):
        """
        This function is used to turn a value that's been returned from a
        route function into the request body.  Override this in a subclass
        to customize how values are returned.
        """
        if isinstance(value, text_type):
            resp.text = value
        elif isinstance(value, binary_type):
            resp.body = value
        else:
            logger.error("Unknown return type: %r", type(value))
            raise ValueError("Unknown return type: {0!r}".format(type(value)))

    def wsgi_entrypoint(self, environ, start_response):
        # Flag stating whether we've acquired our lock.  Defaults to False,
        # since we (by default) do not serialize requests.
        locked = False

        try:
            if self.config['SERIALIZE_REQUESTS']:
                # Acquire, then set our flag.  Note that order matters here,
                # since we only want to set the 'locked' flag when it is safe
                # to unlock (see below for more notes).
                self.lock.acquire()
                locked = True

            # Create our request object.
            self.request = Request(environ)

            # Create an empty response.
            self.response = Response()

            # Create our variables object.
            self._locals.vars = SimpleNamespace()

            # Set default values on the response.
            self._prepare_response()

            # Actually handle this request.
            self._handle_request()

            # Finally, given our response, we finish the WSGI request.
            return self.response(environ, start_response)
        finally:
            # Note that we don't automatically release, since there might be
            # an error with accessing self.config, above, and so we might not
            # have acquired the lock.
            if locked:
                self.lock.release()

            # After each request, we remove the request and response objects.
            del self.request
            del self.response

            # We also reset our request config.
            del self.g

    def _prepare_response(self):
        # We default to setting the current (UTC) date on the response.
        if hasattr(self.response, 'date'):
            self.response.date = datetime.utcnow()

    def _run_routes(self, method):
        # Since these are thread-locals, we grab them as locals.
        request = self.request
        response = self.response

        # For each route of the specified type, try to match it.
        for route in self.routes[method]:
            matches, ret = route(request, response)
            if ret is not None:
                self.on_returned_body(request, response, ret)

            if matches:
                return True

        return False

    def _handle_request(self):
        # Since these are thread-locals, we grab them as locals.
        request = self.request
        response = self.response
        self.logger.debug("Handling: %s %s", request.method, request.url)

        # Check for valid method.
        # TODO: Should this call our after filters?
        if request.method not in self.SUPPORTED_METHODS:
            self.logger.warn("Called with invalid method: %s", request.method)

            # TODO: hook.

            # Send "invalid method" exception.
            response.status_int = 405
            return

        matched = False
        try:
            # Call before filters.
            for filter in self.before_filters:
                filter(request, response)

            # For each route of the specified type, try to match it.
            matched = self._run_routes(request.method)

            # We special-case the HEAD method to fallback to GET.
            if request.method == 'HEAD' and not matched:
                # Run our routes against the 'GET' method.
                matched = self._run_routes('GET')

        except HaltRoutingException as ex:
            # Set the various parameters.
            if ex.code is not None:
                response.status_int = ex.code

            if ex.body is not None:
                # We pass the body through to on_returned_body.
                self.on_returned_body(request, response, ex.body)

            if ex.headers is not None:
                # Set each header.
                for header, value in iteritems(ex.headers):
                    # Set this header.
                    response.headers[header] = value

            # Must set this, or we get clobbered by the 404 handler.
            matched = True

        except Exception as e:
            # Also, check if the exception has other information attached,
            # like a code/body.
            self.on_exception(e)

            # Must set this, or we get clobbered by the 404 handler.
            matched = True

        finally:
            # Call our after filters
            for route in self.after_filters:
                route(request, response)

        if not matched:
            self.on_route_missing()

    def __call__(self, environ, start_response):
        return self.wsgi_entrypoint(environ, start_response)

    def on_route_missing(self):
        """
        This function is called when a route to handle a request is not found.
        Override this function to provide custom not-found logic.
        """
        # By default, return a 404 request.
        self.response.status_int = 404

    def on_exception(self, exception):
        self.response.status_int = 500
        if self.config['DEBUG']:
            # Format the current traceback
            tb = traceback.format_exc()

            # Return the traceback as text.
            self.response.content_type = 'text/plain'
            if sys.version_info[0] >= 3:
                self.response.text = tb
            else:
                self.response.body = tb

            print(tb, file=sys.stderr)

    def test_server(self, port=8000):  # pragma: no cover
        """
        This method lets you start a test server for development purposes.
        Note: There is deliberately no option to set the address to listen on.
              The server will always listen on 'localhost', and should never
              be used in production.
        """

        self.logger.info("Starting test server on port %d", port)

        from wsgiref.simple_server import make_server
        httpd = make_server('localhost', port, self)

        try:
            httpd.serve_forever()
        except KeyboardInterrupt:
            self.logger.info("Stopped test server due to keyboard interrupt")

    def __str__(self):
        """
        Some help for debugging: str(app) will get a summary of the app and
        it's defined before/after/routes.
        """

        body = []
        body.append("Application {0} (Debug: {1})".format(
            self.name, self.config['DEBUG']))
        body.append("")

        def dump_filter_array(arr):
            body.append("=" * 79)
            body.append("Function        Match                     Conditions")
            body.append("-" * 79)
            for filter in arr:
                conds = ", ".join([f.__name__ for f in filter.conditions])
                body.append("{0:<15} {1:<25} {2:<35}".format(
                    filter.func.__name__, str(filter.match), conds))

        def dump_route_array(arr):
            body.append("=" * 79)
            body.append(
                "Method  Function        Match                     Conditions")
            body.append("-" * 79)
            for method in self.routes:
                for route in self.routes[method]:
                    conds = ", ".join([f.__name__ for f in route.conditions])
                    body.append("{0:<7} {1:<15} {2:<25} {3:<28}".format(
                        method, route.func.__name__, str(route.matcher),
                        conds))

        body.append("BEFORE FILTERS")
        dump_filter_array(self.before_filters)
        body.append("")

        body.append("ROUTES")
        dump_route_array(self.before_filters)
        body.append("")

        body.append("AFTER FILTERS")
        dump_filter_array(self.after_filters)
        body.append("")

        return '\n'.join(body)

    def __repr__(self):
        return "HobokenApplication(name={!r}, debug={!r})".format(
            self.name, self.config['DEBUG'])