From 649f48cd702fb76fed92eac1e5d2fd2ec28fbbf9 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Mon, 11 Jan 2021 13:32:58 -0800 Subject: [PATCH] request.full_path property, closes #1184 --- datasette/utils/asgi.py | 8 ++++++++ docs/internals.rst | 13 ++++++++----- tests/test_internals_request.py | 22 ++++++++++++++++++++++ 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/datasette/utils/asgi.py b/datasette/utils/asgi.py index 6811250b..7bfda201 100644 --- a/datasette/utils/asgi.py +++ b/datasette/utils/asgi.py @@ -89,6 +89,14 @@ class Request: def query_string(self): return (self.scope.get("query_string") or b"").decode("latin-1") + @property + def full_path(self): + qs = self.query_string + return "{}{}".format( + self.path, + ('?' + qs) if qs else '' + ) + @property def args(self): return MultiParams(parse_qs(qs=self.query_string)) diff --git a/docs/internals.rst b/docs/internals.rst index 05cb8bd7..f7b0cc0b 100644 --- a/docs/internals.rst +++ b/docs/internals.rst @@ -35,13 +35,16 @@ The request object is passed to various plugin hooks. It represents an incoming The host header from the incoming request, e.g. ``latest.datasette.io`` or ``localhost``. ``.path`` - string - The path of the request, e.g. ``/fixtures``. + The path of the request excluding the query string, e.g. ``/fixtures``. + +``.full_path`` - string + The path of the request including the query string if one is present, e.g. ``/fixtures?sql=select+sqlite_version()``. ``.query_string`` - string - The querystring component of the request, without the ``?`` - e.g. ``name__contains=sam&age__gt=10``. + The query string component of the request, without the ``?`` - e.g. ``name__contains=sam&age__gt=10``. ``.args`` - MultiParams - An object representing the parsed querystring parameters, see below. + An object representing the parsed query string parameters, see below. ``.url_vars`` - dictionary (str -> str) Variables extracted from the URL path, if that path was defined using a regular expression. See :ref:`plugin_register_routes`. @@ -62,9 +65,9 @@ The object also has two awaitable methods: The MultiParams class ===================== -``request.args`` is a ``MultiParams`` object - a dictionary-like object which provides access to querystring parameters that may have multiple values. +``request.args`` is a ``MultiParams`` object - a dictionary-like object which provides access to query string parameters that may have multiple values. -Consider the querystring ``?foo=1&foo=2&bar=3`` - with two values for ``foo`` and one value for ``bar``. +Consider the query string ``?foo=1&foo=2&bar=3`` - with two values for ``foo`` and one value for ``bar``. ``request.args[key]`` - string Returns the first value for that key, or raises a ``KeyError`` if the key is missing. For the above example ``request.args["foo"]`` would return ``"1"``. diff --git a/tests/test_internals_request.py b/tests/test_internals_request.py index a659262b..dbfe07d7 100644 --- a/tests/test_internals_request.py +++ b/tests/test_internals_request.py @@ -90,3 +90,25 @@ def test_request_url_vars(): assert {"name": "cleo"} == Request( dict(scope, url_route={"kwargs": {"name": "cleo"}}), None ).url_vars + + + +@pytest.mark.parametrize("path,query_string,expected_full_path", [ + ("/", "", "/"), + ("/", "foo=bar", "/?foo=bar"), + ("/foo", "bar", "/foo?bar") +]) +def test_request_properties(path, query_string, expected_full_path): + scope = { + "http_version": "1.1", + "method": "POST", + "path": path, + "raw_path": path.encode("latin-1"), + "query_string": query_string.encode("latin-1"), + "scheme": "http", + "type": "http", + } + request = Request(scope, None) + assert request.path == path + assert request.query_string == query_string + assert request.full_path == expected_full_path