From 8778103541f28fb563f3d9d0b4af38d98d80f94e Mon Sep 17 00:00:00 2001 From: kgriffs Date: Tue, 8 Apr 2014 18:11:19 -0500 Subject: [PATCH] doc(reference): Improved and annotated Request docstrings The Request docstrings were modified so that now attributes and properties are both documented in the class docstring, and class members were reordered to be grouped more logically so that browsing the source as well as reading the docs is easier. Also, None was annotated across multiple files, and some minor tweaks to introductory prose were completed. --- doc/_static/css/falcon.css | 7 + doc/_templates/layout.html | 4 + doc/api/api.rst | 2 +- doc/conf.py | 10 +- doc/index.rst | 8 +- doc/user/intro.rst | 2 +- falcon/api_helpers.py | 6 +- falcon/exceptions.py | 8 +- falcon/hooks.py | 26 +-- falcon/http_error.py | 16 +- falcon/request.py | 391 ++++++++++++++++++------------------- 11 files changed, 246 insertions(+), 234 deletions(-) create mode 100644 doc/_static/css/falcon.css create mode 100644 doc/_templates/layout.html diff --git a/doc/_static/css/falcon.css b/doc/_static/css/falcon.css new file mode 100644 index 0000000..7f429e0 --- /dev/null +++ b/doc/_static/css/falcon.css @@ -0,0 +1,7 @@ +.property { + margin-left: 1em; +} + +em { + margin-right: 0.15em; +} \ No newline at end of file diff --git a/doc/_templates/layout.html b/doc/_templates/layout.html new file mode 100644 index 0000000..dd2b204 --- /dev/null +++ b/doc/_templates/layout.html @@ -0,0 +1,4 @@ +{% extends "!layout.html" %} +{% block extrahead %} + +{% endblock %} \ No newline at end of file diff --git a/doc/api/api.rst b/doc/api/api.rst index 4b42a4b..81bc79e 100644 --- a/doc/api/api.rst +++ b/doc/api/api.rst @@ -3,7 +3,7 @@ API Class ========= -Falcon's API class is a WSGI callable "application" that you can host with any +Falcon's API class is a WSGI "application" that you can host with any standard-compliant WSGI server. .. code:: python diff --git a/doc/conf.py b/doc/conf.py index 4e6a171..e27e0be 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -101,9 +101,13 @@ pygments_style = 'sphinx' # -- Options for HTML output ---------------------------------------------- -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -html_theme = 'default' +try: + import sphinx_rtd_theme + + html_theme = "sphinx_rtd_theme" + html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] +except ImportError: + html_theme = 'default' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the diff --git a/doc/index.rst b/doc/index.rst index 1ed65d5..125eec4 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -8,9 +8,13 @@ Falcon: The Unladen WSGI Framework Release v\ |version|. (:ref:`Installation `) -Falcon is a minimalist WSGI library for building speedy web APIs and app backends. +Falcon is a minimalist WSGI library for building speedy web APIs and app +backends. -When it comes to building HTTP APIs, other frameworks weigh you down with tons of dependencies and unnecessary abstractions. Falcon cuts to the chase with a clean design that embraces HTTP. We like to think of Falcon as the Dieter Rams of web frameworks; functional, simple, and elegant. +When it comes to building HTTP APIs, other frameworks weigh you down with tons +of dependencies and unnecessary abstractions. Falcon cuts to the chase with a +clean design that `embraces` HTTP. We like to think of Falcon as the Dieter +Rams of web frameworks; functional, simple, and elegant. .. code:: python diff --git a/doc/user/intro.rst b/doc/user/intro.rst index e9ec8db..e5338c7 100644 --- a/doc/user/intro.rst +++ b/doc/user/intro.rst @@ -3,7 +3,7 @@ Introduction ============ -Falcon is a minimalist, high-performance web framework for building web services and app backends with Python. It's WSGI-based, and works great with Python 2.6, Python 2.7, Python 3.3, and PyPy, giving you a wide variety of deployment options. +Falcon is a minimalist, high-performance web framework for building web services and app backends with Python. It's WSGI-based, and works great with Python 2.6, Python 2.7, Python 3.3, Python 3.4 and PyPy, giving you a wide variety of deployment options. Yet Another Framework diff --git a/falcon/api_helpers.py b/falcon/api_helpers.py index a69234e..c99598e 100644 --- a/falcon/api_helpers.py +++ b/falcon/api_helpers.py @@ -97,10 +97,10 @@ def get_body(resp): resp: Instance of falcon.Response Returns: - * If resp.body is not None, returns [resp.body], encoded as UTF-8 if + * If resp.body is not *None*, returns [resp.body], encoded as UTF-8 if it is a Unicode string. Bytestrings are returned as-is. - * If resp.data is not None, returns [resp.data] - * If resp.stream is not None, returns resp.stream + * If resp.data is not *None*, returns [resp.data] + * If resp.stream is not *None*, returns resp.stream * Otherwise, returns [] """ diff --git a/falcon/exceptions.py b/falcon/exceptions.py index c90e441..288a66a 100644 --- a/falcon/exceptions.py +++ b/falcon/exceptions.py @@ -46,7 +46,7 @@ class HTTPUnauthorized(HTTPError): description (str): Human-friendly description of the error, along with a helpful suggestion or two. scheme (str): Authentication scheme to use as the value of the - WWW-Authenticate header in the response (default None). + WWW-Authenticate header in the response (default *None*). kwargs (optional): Same as for ``HTTPError``. """ @@ -92,6 +92,7 @@ class HTTPNotFound(HTTPError): do not wish to disclose exactly why a request was refused. """ + def __init__(self): HTTPError.__init__(self, status.HTTP_404, None, None) @@ -110,6 +111,7 @@ class HTTPMethodNotAllowed(HTTPError): kwargs (optional): Same as for ``HTTPError``. """ + def __init__(self, allowed_methods, **kwargs): headers = kwargs.setdefault('headers', {}) headers['Allow'] = ', '.join(allowed_methods) @@ -134,6 +136,7 @@ class HTTPNotAcceptable(HTTPError): kwargs (optional): Same as for ``HTTPError``. """ + def __init__(self, description, **kwargs): HTTPError.__init__(self, status.HTTP_406, 'Media type not acceptable', description, **kwargs) @@ -242,7 +245,8 @@ class HTTPRangeNotSatisfiable(HTTPError): resource_length: The maximum value for the last-byte-pos of a range request. Used to set the Content-Range header. media_type: Media type to use as the value of the Content-Type - header, or None to use the default passed to the API initializer. + header, or *None* to use the default passed to the API + initializer. """ diff --git a/falcon/hooks.py b/falcon/hooks.py index d9e8601..f67af04 100644 --- a/falcon/hooks.py +++ b/falcon/hooks.py @@ -22,19 +22,20 @@ def before(action): """Decorator to execute the given action function *before* the responder. Args: - action: A function with a similar signature to a resource responder - method, taking (req, resp, params), where params includes values for - URI template field names, if any. Hooks may also add pseudo-params - of their own. For example: + action (callable): A function of the form ``func(req, resp, params)``, + where params is a dict of URI Template field names, if any, + that will be passed into the resource responder as *kwargs*. - def do_something(req, resp, params): - try: - params['id'] = int(params['id']) - except ValueError: - raise falcon.HTTPBadRequest('Invalid ID', - 'ID was not valid.') + Hooks may inject extra params as needed. For example:: - params['answer'] = 42 + def do_something(req, resp, params): + try: + params['id'] = int(params['id']) + except ValueError: + raise falcon.HTTPBadRequest('Invalid ID', + 'ID was not valid.') + + params['answer'] = 42 """ @@ -87,8 +88,7 @@ def after(action): """Decorator to execute the given action function *after* the responder. Args: - action: A function with a similar signature to a resource responder - method, taking (req, resp). + action (callable): A function of the form ``func(req, resp)`` """ diff --git a/falcon/http_error.py b/falcon/http_error.py index 7914048..0e1bd05 100644 --- a/falcon/http_error.py +++ b/falcon/http_error.py @@ -44,17 +44,17 @@ class HTTPError(Exception): Args: status (str): HTTP status code and text, such as "400 Bad Request" - title (str): Human-friendly error title. Set to None if you wish Falcon - to return an empty response body (all remaining args will + title (str): Human-friendly error title. Set to *None* if you wish + Falcon to return an empty response body (all remaining args will be ignored except for headers.) Do this only when you don't wish to disclose sensitive information about why a request was refused, or if the status and headers are self-descriptive. description (str): Human-friendly description of the error, along with - a helpful suggestion or two (default None). + a helpful suggestion or two (default *None*). headers (dict): Extra headers to return in the - response to the client (default None). + response to the client (default *None*). href (str): A URL someone can visit to find out more information - (default None). Unicode characters are percent-encoded. + (default *None*). Unicode characters are percent-encoded. href_text (str): If href is given, use this as the friendly title/description for the link (defaults to "API documentation for this error"). @@ -91,13 +91,9 @@ class HTTPError(Exception): def json(self): """Returns a pretty JSON-encoded version of the exception - Note: - Excludes the HTTP status line, since the results of this call - are meant to be returned in the body of an HTTP response. - Returns: A JSON representation of the exception except the status line, or - NONE if title was set to None. + NONE if title was set to *None*. """ diff --git a/falcon/request.py b/falcon/request.py index ef86282..ac521be 100644 --- a/falcon/request.py +++ b/falcon/request.py @@ -55,16 +55,80 @@ class InvalidParamValueError(HTTPBadRequest): class Request(object): - """Represents a client's HTTP request + """Represents a client's HTTP request. + + Note: + Request is not meant to be instantiated directory by responders. + + Args: + env (dict): A WSGI environment dict passed in from the server. See + also the PEP-3333 spec. Attributes: + protocol (str): Either 'http' or 'https'. method (str): HTTP method requested (e.g., GET, POST, etc.) + user_agent (str): Value of the User-Agent header, or *None* if the + header is missing. + app (str): Name of the WSGI app (if using WSGI's notion of virtual + hosting). + env (dict): Reference to the WSGI *environ* dict passed in from the + server. See also PEP-3333. + uri (str): The fully-qualified URI for the request. + url (str): alias for ``uri``. + relative_uri (str): The path + query string portion of the full URI. path (str): Path portion of the request URL (not including query string). query_string (str): Query string portion of the request URL, without the preceding '?' character. - stream: Stream-like object for reading the body of the request, if any. + accept (str): Value of the Accept header, or '*/*' if the header is + missing. + auth (str): Value of the Authorization header, or *None* if the header + is missing. + client_accepts_json (bool): True if the Accept header includes JSON, + otherwise False. + client_accepts_xml (bool): True if the Accept header includes XML, + otherwise False. + content_type (str): Value of the Content-Type header, or *None* if + the header is missing. + content_length (int): Value of the Content-Length header converted + to an int, or *None* if the header is missing. + stream (io): File-like object for reading the body of the request, if + any. + date (datetime): Value of the Date header, converted to a + `datetime.datetime` instance. The header value is assumed to + conform to RFC 1123. + expect (str): Value of the Expect header, or *None* if the + header is missing. + range (tuple of int): A 2-member tuple parsed from the value of the + Range header. + The two members correspond to the first and last byte + positions of the requested resource, inclusive. Negative + indices indicate offset from the end of the resource, + where -1 is the last byte, -2 is the second-to-last byte, + and so forth. + + Only continous ranges are supported (e.g., "bytes=0-0,-1" would + result in an HTTPBadRequest exception when the attribute is + accessed.) + if_match (str): Value of the If-Match header, or *None* if the + header is missing. + if_none_match (str): Value of the If-None-Match header, or *None* + if the header is missing. + if_modified_since (str): Value of the If-Modified-Since header, or + None if the header is missing. + if_unmodified_since (str): Value of the If-Unmodified-Sinc header, + or *None* if the header is missing. + if_range (str): Value of the If-Range header, or *None* if the + header is missing. + + headers (dict): Raw HTTP headers from the request with + canonical dash-separated names. Parsing all the headers + to create this dict is done the first time this attribute + is accessed. This parsing can be costly, so unless you + need all the headers in this format, you should use the + ``get_header`` method or one of the convenience attributes + instead, to get a value for a specific header. """ __slots__ = ( @@ -81,15 +145,6 @@ class Request(object): ) def __init__(self, env): - """Initialize attributes based on a WSGI environment dict - - Note: Request is not meant to be instantiated directory by responders. - - Args: - env (dict): A WSGI environment dict passed in from the server. See - also the PEP-3333 spec. - - """ self.env = env self._wsgierrors = env['wsgi.errors'] @@ -143,100 +198,20 @@ class Request(object): # covered since the test that does so uses multiprocessing. self.stream = helpers.Body(self.stream, self.content_length) - # TODO(kgriffs): Use the nocover pragma only for the six.PY3 if..else - def log_error(self, message): # pragma: no cover - """Log an error to wsgi.error - - Prepends timestamp and request info to message, and writes the - result out to the WSGI server's error stream (wsgi.error). - - Args: - message (str): A string describing the problem. If a byte-string - it is simply written out as-is. Unicode strings will be - converted to UTF-8. - - """ - - if self.query_string: - query_string_formatted = '?' + self.query_string - else: - query_string_formatted = '' - - log_line = ( - DEFAULT_ERROR_LOG_FORMAT. - format(datetime.now(), self.method, self.path, - query_string_formatted) - ) - - if six.PY3: - self._wsgierrors.write(log_line + message + '\n') - else: - if isinstance(message, unicode): - message = message.encode('utf-8') - - self._wsgierrors.write(log_line.encode('utf-8')) - self._wsgierrors.write(message + '\n') + # ------------------------------------------------------------------------ + # Properties + # ------------------------------------------------------------------------ @property def client_accepts_json(self): - """Return True if the Accept header indicates JSON support.""" return self.client_accepts('application/json') @property def client_accepts_xml(self): - """Return True if the Accept header indicates XML support.""" return self.client_accepts('application/xml') - def client_accepts(self, media_type): - """Returns the client's preferred media type. - - Args: - media_type (str): Media type to check - - Returns: - bool: True IFF the client has indicated in the Accept header that - they accept at least one of the specified media types. - """ - - accept = self.accept - - # PERF(kgriffs): Usually the following will be true, so - # try it first. - if (accept == media_type) or (accept == '*/*'): - return True - - # Fall back to full-blown parsing - try: - return mimeparse.quality(media_type, accept) != 0.0 - except ValueError: - return False - - def client_prefers(self, media_types): - """Returns the client's preferred media type given several choices. - - Args: - media_types (iterable): One or more media types from which to - choose the client's preferred type. This value MUST be an - iterable collection of strings. - - Returns: - str: The client's preferred media type, based on the Accept header, - or None if the client does not accept any of the specified - types. - """ - - try: - # NOTE(kgriffs): best_match will return '' if no match is found - preferred_type = mimeparse.best_match(media_types, self.accept) - except ValueError: - # Value for the accept header was not formatted correctly - preferred_type = '' - - return (preferred_type if preferred_type else None) - @property def accept(self): - """Value of the Accept header, or */* if not found per RFC.""" accept = self._get_header_by_wsgi_name('HTTP_ACCEPT') # NOTE(kgriffs): Per RFC, missing accept header is @@ -244,26 +219,15 @@ class Request(object): return '*/*' if accept is None else accept @property - def app(self): - """Name of the WSGI app (if using WSGI's notion of virtual hosting).""" - return self.env['SCRIPT_NAME'] + def user_agent(self): + return self._get_header_by_wsgi_name('HTTP_USER_AGENT') @property def auth(self): - """Value of the Authorization header, or None if not found.""" return self._get_header_by_wsgi_name('HTTP_AUTHORIZATION') @property def content_length(self): - """Value of the Content-Length header - - Returns: - int: Value converted to an int, or None if missing. - - Raises: - HTTPBadRequest: The header had a value, but it wasn't - formatted correctly or was a negative number. - """ value = self._get_header_by_wsgi_name('HTTP_CONTENT_LENGTH') if value: try: @@ -284,24 +248,10 @@ class Request(object): @property def content_type(self): - """Value of the Content-Type header, or None if not found.""" return self._get_header_by_wsgi_name('HTTP_CONTENT_TYPE') @property def date(self): - """Value of the Date header, converted to a datetime instance. - - Returns: - datetime.datetime: An instance of datetime.datetime representing - the value of the Date header, or None if the Date header is - not present in the request. - - Raises: - HTTPBadRequest: The date value could not be parsed, likely - because it does not confrom to RFC 1123. - - """ - http_date = self._get_header_by_wsgi_name('HTTP_DATE') try: return util.http_date_to_dt(http_date) @@ -312,59 +262,30 @@ class Request(object): @property def expect(self): - """Value of the Expect header, or None if missing.""" return self._get_header_by_wsgi_name('HTTP_EXPECT') @property def if_match(self): - """Value of the If-Match header, or None if missing.""" return self._get_header_by_wsgi_name('HTTP_IF_MATCH') @property def if_none_match(self): - """Value of the If-None-Match header, or None if missing.""" return self._get_header_by_wsgi_name('HTTP_IF_NONE_MATCH') @property def if_modified_since(self): - """Value of the If-Modified-Since header, or None if missing.""" return self._get_header_by_wsgi_name('HTTP_IF_MODIFIED_SINCE') @property def if_unmodified_since(self): - """Value of the If-Unmodified-Since header, or None if missing.""" return self._get_header_by_wsgi_name('HTTP_IF_UNMODIFIED_SINCE') @property def if_range(self): - """Value of the If-Range header, or None if missing.""" return self._get_header_by_wsgi_name('HTTP_IF_RANGE') - @property - def protocol(self): - """Will be either 'http' or 'https'.""" - return self.env['wsgi.url_scheme'] - @property def range(self): - """A 2-member tuple representing the value of the Range header. - - The two members correspond to first and last byte positions of the - requested resource, inclusive. Negative indices indicate offset - from the end of the resource, where -1 is the last byte, -2 is the - second-to-last byte, and so forth. - - Only continous ranges are supported (e.g., "bytes=0-0,-1" would - result in an HTTPBadRequest exception.) - - Returns: - int: Parse range value, or None if the header is not present. - - Raises: - HTTPBadRequest: The header had a value, but it wasn't - formatted correctly. - """ - value = self._get_header_by_wsgi_name('HTTP_RANGE') if value: @@ -394,9 +315,15 @@ class Request(object): return None @property - def uri(self): - """The fully-qualified URI for the request.""" + def app(self): + return self.env['SCRIPT_NAME'] + @property + def protocol(self): + return self.env['wsgi.url_scheme'] + + @property + def uri(self): if self._cached_uri is None: # PERF: For small numbers of items, '+' is faster # than ''.join(...). Concatenation is also generally @@ -414,12 +341,9 @@ class Request(object): return self._cached_uri url = uri - """Alias for uri""" @property def relative_uri(self): - """The path + query string portion of the full URI.""" - if self._cached_relative_uri is None: if self.query_string: self._cached_relative_uri = (self.app + self.path + '?' + @@ -429,25 +353,8 @@ class Request(object): return self._cached_relative_uri - @property - def user_agent(self): - """Value of the User-Agent string, or None if missing.""" - return self._get_header_by_wsgi_name('HTTP_USER_AGENT') - @property def headers(self): - """Get raw HTTP headers - - Build a temporary dictionary of dash-separated HTTP headers, - which can be used as a whole, like, to perform an HTTP request. - - If you want to lookup a header, please use `get_header` instead. - - Returns: - dict: A new dictionary of HTTP headers. - - """ - # NOTE(kgriffs: First time here will cache the dict so all we # have to do is clone it in the future. if not self._cached_headers: @@ -463,17 +370,69 @@ class Request(object): return self._cached_headers.copy() + # ------------------------------------------------------------------------ + # Methods + # ------------------------------------------------------------------------ + + def client_accepts(self, media_type): + """Determines whether or not the client accepts a given media type. + + Args: + media_type (str): An Internet media type to check. + + Returns: + bool: True if the client has indicated in the Accept header that + it accepts the specified media type. Otherwise, returns + False. + """ + + accept = self.accept + + # PERF(kgriffs): Usually the following will be true, so + # try it first. + if (accept == media_type) or (accept == '*/*'): + return True + + # Fall back to full-blown parsing + try: + return mimeparse.quality(media_type, accept) != 0.0 + except ValueError: + return False + + def client_prefers(self, media_types): + """Returns the client's preferred media type given several choices. + + Args: + media_types (iterable of str): One or more Internet media types + from which to choose the client's preferred type. This value + **must** be an iterable collection of strings. + + Returns: + str: The client's preferred media type, based on the Accept + header. Returns *None* if the client does not accept any + of the given types. + """ + + try: + # NOTE(kgriffs): best_match will return '' if no match is found + preferred_type = mimeparse.best_match(media_types, self.accept) + except ValueError: + # Value for the accept header was not formatted correctly + preferred_type = '' + + return (preferred_type if preferred_type else None) + def get_header(self, name, required=False): - """Return a header value as a string + """Return a header value as a string. Args: name (str): Header name, case-insensitive (e.g., 'Content-Type') required (bool, optional): Set to True to raise HttpBadRequest instead of returning gracefully when the header is not found - (default False) + (default False). Returns: - str: The value of the specified header if it exists, or None if + str: The value of the specified header if it exists, or *None* if the header is not found and is not required. Raises: @@ -496,7 +455,7 @@ class Request(object): raise HTTPBadRequest('Missing header', description) def get_param(self, name, required=False, store=None): - """Return the value of a query string parameter as a string + """Return the value of a query string parameter as a string. Args: name (str): Parameter name, case-sensitive (e.g., 'sort') @@ -507,7 +466,7 @@ class Request(object): value of the param, but only if the param is found. Returns: - string: The value of the param as a string, or None if param is + string: The value of the param as a string, or *None* if param is not found and is not required. Raises: @@ -534,13 +493,13 @@ class Request(object): def get_param_as_int(self, name, required=False, min=None, max=None, store=None): - """Return the value of a query string parameter as an int + """Return the value of a query string parameter as an int. Args: name (str): Parameter name, case-sensitive (e.g., 'limit') required (bool, optional): Set to True to raise HTTPBadRequest instead of returning gracefully when the parameter is not - found or is not an integer (default False) + found or is not an integer (default False). min (int, optional): Set to the minimum value allowed for this param. If the param is found and it is less than min, an HTTPError is raised. @@ -549,12 +508,12 @@ class Request(object): max, an HTTPError is raised. store (dict, optional): A dict-like object in which to place the value of the param, but only if the param is found (default - None) + *None*). Returns: int: The value of the param if it is found and can be converted to - an integer. If the param is not found, returns None, unless - required is True. + an integer. If the param is not found, returns *None*, unless + ``required`` is True. Raises HTTPBadRequest: The param was not found in the request, even though @@ -601,10 +560,10 @@ class Request(object): def get_param_as_bool(self, name, required=False, store=None): """Return the value of a query string parameter as a boolean - The following bool-ish strings are supported: + The following bool-like strings are supported:: - True: ('true', 'True', 'yes') - False: ('false', 'False', 'no') + TRUE_STRINGS = ('true', 'True', 'yes') + FALSE_STRINGS = ('false', 'False', 'no') Args: name (str): Parameter name, case-sensitive (e.g., 'limit') @@ -613,12 +572,12 @@ class Request(object): found or is not a recognized bool-ish string (default False). store (dict, optional): A dict-like object in which to place the value of the param, but only if the param is found (default - None) + *None*). Returns: bool: The value of the param if it is found and can be converted - to a boolean. If the param is not found, returns None unless - required is True + to a boolean. If the param is not found, returns *None* unless + required is True. Raises HTTPBadRequest: The param was not found in the request, even though @@ -654,7 +613,7 @@ class Request(object): def get_param_as_list(self, name, transform=None, required=False, store=None): - """Return the value of a query string parameter as a list + """Return the value of a query string parameter as a list. Note that list items must be comma-separated. @@ -670,24 +629,24 @@ class Request(object): found or is not an integer (default False) store (dict, optional): A dict-like object in which to place the value of the param, but only if the param is found (default - None) + *None*). Returns: list: The value of the param if it is found. Otherwise, returns - None unless required is True. for partial lists, None will be - returned as a placeholder. For example: + *None* unless required is True. for partial lists, *None* will be + returned as a placeholder. For example:: things=1,,3 - would be returned as: + would be returned as:: ['1', None, '3'] - while this: + while this:: things=,,, - would just be retured as: + would just be retured as:: [None, None, None, None] @@ -729,9 +688,43 @@ class Request(object): raise HTTPBadRequest('Missing query parameter', 'The "' + name + '" query parameter is required.') - # ------------------------------------------------------------------------- + # TODO(kgriffs): Use the nocover pragma only for the six.PY3 if..else + def log_error(self, message): # pragma: no cover + """Write an error message to the server's log. + + Prepends timestamp and request info to message, and writes the + result out to the WSGI server's error stream (`wsgi.error`). + + Args: + message (str): A string describing the problem. If a byte-string + it is simply written out as-is. Unicode strings will be + converted to UTF-8. + + """ + + if self.query_string: + query_string_formatted = '?' + self.query_string + else: + query_string_formatted = '' + + log_line = ( + DEFAULT_ERROR_LOG_FORMAT. + format(datetime.now(), self.method, self.path, + query_string_formatted) + ) + + if six.PY3: + self._wsgierrors.write(log_line + message + '\n') + else: + if isinstance(message, unicode): + message = message.encode('utf-8') + + self._wsgierrors.write(log_line.encode('utf-8')) + self._wsgierrors.write(message + '\n') + + # ------------------------------------------------------------------------ # Helpers - # ------------------------------------------------------------------------- + # ------------------------------------------------------------------------ def _get_header_by_wsgi_name(self, name): """Looks up a header, assuming name is already UPPERCASE_UNDERSCORE @@ -741,8 +734,8 @@ class Request(object): underscored Returns: - str: Value of the specified header, or None if the header was not - found. Also returns None if the value of the header was blank. + str: Value of the specified header, or *None* if the header was not + found. Also returns *None* if the value of the header was blank. """ try: