def test_events_on_decorator(): e = Events('foo') m = mock.MagicMock() @e.on("foo") def doit(): m() yield from e.emit("foo") assert m.called
def test_events_on_async(): e = Events('foo') m = mock.MagicMock() @asyncio.coroutine def foo(): m() e.on('foo', foo()) yield from e.emit('foo') assert m.called
def test_events_on_typecheck(): e = Events('foo') with pytest.raises(ValueError): e.on('anything', 10)
def test_events_constructor_nonempty(): e = Events('foo') with pytest.raises(KeyError): e.on('anything', lambda: print())
def test_events_constructor_empty(): e = Events() e.on('anything', lambda: print())
def test_events_on(): e = Events('foo') m = mock.MagicMock() e.on('foo', m) yield from e.emit('foo') assert m.called
def __init__(self, protocol, EOL="\r\n"): self.protocol = protocol self.EOL = EOL self.headers = Headers() self.events = Events()
class HTTPResponse: """ Response class which handles HTTP formatting and sending a response to the client. If the response has sent a message (the `has_ended` property evaluates to True) the middleware chain will stop. There are many convenience functions such as `send_json` and `send_html` which will handle formatting data, setting headers and sending objects and strings for you. A typical use is the modification of the response object by the standard Renderer middleware, which adds a `render` method to the response object. Any middleware after this one (i.e. your routes) can then call res.render("template_name", data) to automatically render a web view and send it to. Parameters ---------- protocol : GrowlerHTTPProtocol Protocol object creating the response EOL : str The string with which to end lines """ SERVER_INFO = 'Growler/{growler_version} Python/{py_version}'.format( py_version=".".join(map(str, sys.version_info[:2])), growler_version=growler.__version__, ) protocol = None has_sent_headers = False has_ended = False status_code = 200 headers = None message = '' EOL = '' phrase = None def __init__(self, protocol, EOL="\r\n"): self.protocol = protocol self.EOL = EOL self.headers = Headers() self.events = Events() def _set_default_headers(self): """ Create some default headers that should be sent along with every HTTP response """ self.headers.setdefault('Date', self.get_current_time) self.headers.setdefault('Server', self.SERVER_INFO) self.headers.setdefault('Content-Length', "%d" % len(self.message)) if self.app.enabled('x-powered-by'): self.headers.setdefault('X-Powered-By', 'Growler') def send_headers(self): """ Sends the headers to the client """ self.events.sync_emit('headers') self._set_default_headers() header_str = self.status_line + self.EOL + str(self.headers) self.protocol.transport.write(header_str.encode()) self.events.sync_emit('after_headers') def write(self, msg=None): msg = self.message if msg is None else msg msg = msg.encode() if isinstance(msg, str) else msg self.protocol.transport.write(msg) def write_eof(self): self.stream.write_eof() self.has_ended = True self.events.sync_emit('after_send') @property def status_line(self): """ Returns the first line of response, including http version, status and a phrase (OK). """ if not self.phrase: self.phrase = Status.Phrase(self.status_code) return "{} {} {}".format("HTTP/1.1", self.status_code, self.phrase) def end(self): """ Ends the response. Useful for quickly ending connection with no data sent """ self.send_headers() self.write() self.write_eof() self.has_ended = True def redirect(self, url, status=None): """ Redirect to the specified url, optional status code defaults to 302. """ self.status_code = 302 if status is None else status self.headers = Headers([('location', url)]) self.message = '' self.end() def set(self, header, value=None): """Set header to the value""" if value is None: for k, v in header.items(): self.headers[k] = v else: self.headers[header] = value def header(self, header, value=None): """Alias for 'set()'""" self.set(header, value) def set_type(self, res_type): self.set('Content-Type', res_type) def get(self, field): """Get a header""" return self.headers[field] # def cookie(self, name, value, options={}): # """Set cookie name to value""" # self.cookies[name] = value # # def clear_cookie(self, name, options={}): # """Removes a cookie""" # options.setdefault("path", "/") # del self.cookies[name] def location(self, location): """Set the location header""" self.headers['location'] = location def links(self, links): """Sets the Link """ s = ['<{}>; rel="{}"'.format(link, rel) for link, rel in links.items()] self.headers['Link'] = ','.join(s) def json(self, body, status=200): """Alias of send_json""" return self.send_json(body, status) def send_json(self, obj, status=200): """ Sends a stringified JSON response to client. Automatically sets the content-type header to application/json. Parameters ---------- obj : mixed Any object which will be serialized by the json.dumps module function status : int, optional The HTTP status code, defaults to 200 (OK) """ self.headers['Content-Type'] = 'application/json' self.status_code = status message = json.dumps(obj) self.send_text(message) def send_html(self, html, status=200): """ Sends html response to client. Automatically sets the content-type header to text/html. Parameters ---------- html : str The raw html string to be sent back to the client status : int, optional The HTTP status code, defaults to 200 (OK) """ self.headers.setdefault('Content-Type', 'text/html') self.message = html self.status_code = status self.send_headers() self.write() self.write_eof() def send_text(self, txt, status=200): """ Sends plaintext response to client. Automatically sets the content-type header to text/plain. If txt is not a string, it will be formatted as one. Parameters ---------- txt : str The plaintext string to be sent back to the client status : int, optional The HTTP status code, defaults to 200 (OK) """ self.headers.setdefault('content-type', 'text/plain') if not isinstance(txt, bytes): txt = str(txt).encode() self.message = txt self.status_code = status self.end() def send_file(self, filename, status=200): """ Reads in the file 'filename' and sends bytes to client Parameters ---------- filename : str Filename of the file to read status : int, optional The HTTP status code, defaults to 200 (OK) """ if isinstance(filename, Path) and sys.version_info >= (3, 5): self.message = filename.read_bytes() else: with io.FileIO(str(filename)) as f: self.message = f.read() self.status_code = status self.send_headers() self.write() self.write_eof() def send(self, *args, **kwargs): raise NotImplementedError # return self.write(*args, **kwargs) @property def info(self): return self.SERVER_INFO @property def stream(self): return self.protocol.transport @property def app(self): return self.protocol.http_application @staticmethod def get_current_time(): # return time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime()) return format_RFC_1123(time.mktime(datetime.now().timetuple()))
def __init__(self, name=__name__, loop=None, debug=True, request_class=HTTPRequest, response_class=HTTPResponse, protocol_factory=GrowlerHTTPProtocol.get_factory, middleware_chain_factory=MiddlewareChain, **kw): """ Creates an application object. Args: name (str): Does nothing right now except identify object loop (asyncio.AbstractEventLoop): The event loop to run on debug (bool): (de)Activates the loop's debug setting request_class (type or callable): The factory of request objects, the default of which is growler.HTTPRequest. This should only be set in special cases, like debugging or if the dev doesn't want to modify default request objects via middleware. response_class (type or callable): The factory of response objects, the default of which is growler.HTTPResponse. This should only be set in special cases, like debugging or if the dev doesn't want to modify default response objects via middleware. protocol_factory (callable): Factory function this application uses to construct the asyncio protocol object which responds to client connections. The default is the GrowlerHTTPProtocol.get_factory method, which simply returns a lambda returning new GrowlerHTTPProtocol objects. middleware_chain_factory (type or callable): Called upon with no arguments to create the middleware chain used by the application. This is accessible via the attribute 'middleware'. Keyword Args: Any other custom variables for the application. This dict is stored as the attribute 'config' in the application. These variables are accessible by the application's dict-access, as in: .. code:: python app = app(..., val='VALUE') app['val'] #=> VALUE """ self.name = name self.config = kw self.loop = asyncio.get_event_loop() if (loop is None) else loop self.loop.set_debug(debug) self.middleware = middleware_chain_factory() self.enable('x-powered-by') self['env'] = os.getenv('GROWLER_ENV', 'development') self.events = Events() self.strict_router_check = False self._request_class = request_class self._response_class = response_class self._protocol_factory = protocol_factory self.handle_404 = self.default_404_handler