From 92b26673d86a663050c9a40a8ffd5b56c25be85f Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Sun, 24 Apr 2022 08:51:09 -0700 Subject: [PATCH] Fix blacken-docs errors and warnings, refs #1718 --- docs/authentication.rst | 25 ++-- docs/internals.rst | 98 ++++++++----- docs/json_api.rst | 2 +- docs/plugin_hooks.rst | 305 +++++++++++++++++++++++++++------------- 4 files changed, 289 insertions(+), 141 deletions(-) diff --git a/docs/authentication.rst b/docs/authentication.rst index 0d98cf82..24960733 100644 --- a/docs/authentication.rst +++ b/docs/authentication.rst @@ -381,11 +381,10 @@ Authentication plugins can set signed ``ds_actor`` cookies themselves like so: .. code-block:: python response = Response.redirect("/") - response.set_cookie("ds_actor", datasette.sign({ - "a": { - "id": "cleopaws" - } - }, "actor")) + response.set_cookie( + "ds_actor", + datasette.sign({"a": {"id": "cleopaws"}}, "actor"), + ) Note that you need to pass ``"actor"`` as the namespace to :ref:`datasette_sign`. @@ -412,12 +411,16 @@ To include an expiry, add a ``"e"`` key to the cookie value containing a `base62 expires_at = int(time.time()) + (24 * 60 * 60) response = Response.redirect("/") - response.set_cookie("ds_actor", datasette.sign({ - "a": { - "id": "cleopaws" - }, - "e": baseconv.base62.encode(expires_at), - }, "actor")) + response.set_cookie( + "ds_actor", + datasette.sign( + { + "a": {"id": "cleopaws"}, + "e": baseconv.base62.encode(expires_at), + }, + "actor", + ), + ) The resulting cookie will encode data that looks something like this: diff --git a/docs/internals.rst b/docs/internals.rst index 76e27e5f..aad608dc 100644 --- a/docs/internals.rst +++ b/docs/internals.rst @@ -70,10 +70,10 @@ And a class method that can be used to create fake request objects for use in te from datasette import Request from pprint import pprint - request = Request.fake("/fixtures/facetable/", url_vars={ - "database": "fixtures", - "table": "facetable" - }) + request = Request.fake( + "/fixtures/facetable/", + url_vars={"database": "fixtures", "table": "facetable"}, + ) pprint(request.scope) This outputs:: @@ -146,7 +146,7 @@ For example: response = Response( "This is XML", - content_type="application/xml; charset=utf-8" + content_type="application/xml; charset=utf-8", ) The quickest way to create responses is using the ``Response.text(...)``, ``Response.html(...)``, ``Response.json(...)`` or ``Response.redirect(...)`` helper methods: @@ -157,9 +157,13 @@ The quickest way to create responses is using the ``Response.text(...)``, ``Resp html_response = Response.html("This is HTML") json_response = Response.json({"this_is": "json"}) - text_response = Response.text("This will become utf-8 encoded text") + text_response = Response.text( + "This will become utf-8 encoded text" + ) # Redirects are served as 302, unless you pass status=301: - redirect_response = Response.redirect("https://latest.datasette.io/") + redirect_response = Response.redirect( + "https://latest.datasette.io/" + ) Each of these responses will use the correct corresponding content-type - ``text/html; charset=utf-8``, ``application/json; charset=utf-8`` or ``text/plain; charset=utf-8`` respectively. @@ -207,13 +211,17 @@ To set cookies on the response, use the ``response.set_cookie(...)`` method. The httponly=False, samesite="lax", ): + ... You can use this with :ref:`datasette.sign() ` to set signed cookies. Here's how you would set the :ref:`ds_actor cookie ` for use with Datasette :ref:`authentication `: .. code-block:: python response = Response.redirect("/") - response.set_cookie("ds_actor", datasette.sign({"a": {"id": "cleopaws"}}, "actor")) + response.set_cookie( + "ds_actor", + datasette.sign({"a": {"id": "cleopaws"}}, "actor"), + ) return response .. _internals_datasette: @@ -236,13 +244,16 @@ You can create your own instance of this - for example to help write tests for a datasette = Datasette(files=["/path/to/my-database.db"]) # Pass metadata as a JSON dictionary like this - datasette = Datasette(files=["/path/to/my-database.db"], metadata={ - "databases": { - "my-database": { - "description": "This is my database" + datasette = Datasette( + files=["/path/to/my-database.db"], + metadata={ + "databases": { + "my-database": { + "description": "This is my database" + } } - } - }) + }, + ) Constructor parameters include: @@ -345,7 +356,7 @@ This is useful when you need to check multiple permissions at once. For example, ("view-table", (database, table)), ("view-database", database), "view-instance", - ] + ], ) .. _datasette_check_visibilty: @@ -406,11 +417,13 @@ The ``db`` parameter should be an instance of the ``datasette.database.Database` from datasette.database import Database - datasette.add_database(Database( - datasette, - path="path/to/my-new-database.db", - is_mutable=True - )) + datasette.add_database( + Database( + datasette, + path="path/to/my-new-database.db", + is_mutable=True, + ) + ) This will add a mutable database and serve it at ``/my-new-database``. @@ -418,8 +431,12 @@ This will add a mutable database and serve it at ``/my-new-database``. .. code-block:: python - db = datasette.add_database(Database(datasette, memory_name="statistics")) - await db.execute_write("CREATE TABLE foo(id integer primary key)") + db = datasette.add_database( + Database(datasette, memory_name="statistics") + ) + await db.execute_write( + "CREATE TABLE foo(id integer primary key)" + ) .. _datasette_add_memory_database: @@ -438,10 +455,9 @@ This is a shortcut for the following: from datasette.database import Database - datasette.add_database(Database( - datasette, - memory_name="statistics" - )) + datasette.add_database( + Database(datasette, memory_name="statistics") + ) Using either of these pattern will result in the in-memory database being served at ``/statistics``. @@ -516,7 +532,9 @@ Returns the absolute URL for the given path, including the protocol and host. Fo .. code-block:: python - absolute_url = datasette.absolute_url(request, "/dbname/table.json") + absolute_url = datasette.absolute_url( + request, "/dbname/table.json" + ) # Would return "http://localhost:8001/dbname/table.json" The current request object is used to determine the hostname and protocol that should be used for the returned URL. The :ref:`setting_force_https_urls` configuration setting is taken into account. @@ -578,7 +596,9 @@ These methods can be used with :ref:`internals_datasette_urls` - for example: table_json = ( await datasette.client.get( - datasette.urls.table("fixtures", "facetable", format="json") + datasette.urls.table( + "fixtures", "facetable", format="json" + ) ) ).json() @@ -754,6 +774,7 @@ Example usage: "select sqlite_version()" ).fetchall()[0][0] + version = await db.execute_fn(get_version) .. _database_execute_write: @@ -789,7 +810,7 @@ Like ``execute_write()`` but uses the ``sqlite3`` `conn.executemany() 5") - return conn.execute("select count(*) from some_table").fetchone()[0] + return conn.execute( + "select count(*) from some_table" + ).fetchone()[0] + try: - num_rows_left = await database.execute_write_fn(delete_and_return_count) + num_rows_left = await database.execute_write_fn( + delete_and_return_count + ) except Exception as e: print("An error occurred:", e) @@ -1021,6 +1047,7 @@ This example uses trace to record the start, end and duration of any HTTP GET re from datasette.tracer import trace import httpx + async def fetch_url(url): with trace("fetch-url", url=url): async with httpx.AsyncClient() as client: @@ -1051,9 +1078,9 @@ This example uses the :ref:`register_routes() ` plugin h from datasette import hookimpl from datasette import tracer + @hookimpl def register_routes(): - async def parallel_queries(datasette): db = datasette.get_database() with tracer.trace_child_tasks(): @@ -1061,7 +1088,12 @@ This example uses the :ref:`register_routes() ` plugin h db.execute("select 1"), db.execute("select 2"), ) - return Response.json({"one": one.single_value(), "two": two.single_value()}) + return Response.json( + { + "one": one.single_value(), + "two": two.single_value(), + } + ) return [ (r"/parallel-queries$", parallel_queries), diff --git a/docs/json_api.rst b/docs/json_api.rst index aa6fcdaa..d3fdb1e4 100644 --- a/docs/json_api.rst +++ b/docs/json_api.rst @@ -446,7 +446,7 @@ Most of the HTML pages served by Datasette provide a mechanism for discovering t You can find this near the top of the source code of those pages, looking like this: -.. code-block:: python +.. code-block:: html `_, `datasette-vega `_ @@ -281,7 +301,7 @@ Use a dictionary if you want to specify that the code should be placed in a ```_, `datasette-publish-vercel `_ @@ -400,7 +422,9 @@ If the value matches that pattern, the plugin returns an HTML link element: if not isinstance(value, str): return None stripped = value.strip() - if not stripped.startswith("{") and stripped.endswith("}"): + if not stripped.startswith("{") and stripped.endswith( + "}" + ): return None try: data = json.loads(value) @@ -412,14 +436,18 @@ If the value matches that pattern, the plugin returns an HTML link element: return None href = data["href"] if not ( - href.startswith("/") or href.startswith("http://") + href.startswith("/") + or href.startswith("http://") or href.startswith("https://") ): return None - return markupsafe.Markup('{label}'.format( - href=markupsafe.escape(data["href"]), - label=markupsafe.escape(data["label"] or "") or " " - )) + return markupsafe.Markup( + '{label}'.format( + href=markupsafe.escape(data["href"]), + label=markupsafe.escape(data["label"] or "") + or " ", + ) + ) Examples: `datasette-render-binary `_, `datasette-render-markdown `__, `datasette-json-html `__ @@ -516,7 +544,7 @@ Here is a more complex example: return Response( "\n".join(lines), content_type="text/plain; charset=utf-8", - headers={"x-sqlite-version": result.first()[0]} + headers={"x-sqlite-version": result.first()[0]}, ) And here is an example ``can_render`` function which returns ``True`` only if the query results contain the columns ``atom_id``, ``atom_title`` and ``atom_updated``: @@ -524,7 +552,11 @@ And here is an example ``can_render`` function which returns ``True`` only if th .. code-block:: python def can_render_demo(columns): - return {"atom_id", "atom_title", "atom_updated"}.issubset(columns) + return { + "atom_id", + "atom_title", + "atom_updated", + }.issubset(columns) Examples: `datasette-atom `_, `datasette-ics `_, `datasette-geojson `__ @@ -548,16 +580,14 @@ Return a list of ``(regex, view_function)`` pairs, something like this: async def hello_from(request): name = request.url_vars["name"] - return Response.html("Hello from {}".format( - html.escape(name) - )) + return Response.html( + "Hello from {}".format(html.escape(name)) + ) @hookimpl def register_routes(): - return [ - (r"^/hello-from/(?P.*)$", hello_from) - ] + return [(r"^/hello-from/(?P.*)$", hello_from)] The view functions can take a number of different optional arguments. The corresponding argument will be passed to your function depending on its named parameters - a form of dependency injection. @@ -606,10 +636,13 @@ This example registers a new ``datasette verify file1.db file2.db`` command that import click import sqlite3 + @hookimpl def register_commands(cli): @cli.command() - @click.argument("files", type=click.Path(exists=True), nargs=-1) + @click.argument( + "files", type=click.Path(exists=True), nargs=-1 + ) def verify(files): "Verify that files can be opened by Datasette" for file in files: @@ -617,7 +650,9 @@ This example registers a new ``datasette verify file1.db file2.db`` command that try: conn.execute("select * from sqlite_master") except sqlite3.DatabaseError: - raise click.ClickException("Invalid database: {}".format(file)) + raise click.ClickException( + "Invalid database: {}".format(file) + ) The new command can then be executed like so:: @@ -656,15 +691,18 @@ Each Facet subclass implements a new type of facet operation. The class should l async def suggest(self): # Use self.sql and self.params to suggest some facets suggested_facets = [] - suggested_facets.append({ - "name": column, # Or other unique name - # Construct the URL that will enable this facet: - "toggle_url": self.ds.absolute_url( - self.request, path_with_added_args( - self.request, {"_facet": column} - ) - ), - }) + suggested_facets.append( + { + "name": column, # Or other unique name + # Construct the URL that will enable this facet: + "toggle_url": self.ds.absolute_url( + self.request, + path_with_added_args( + self.request, {"_facet": column} + ), + ), + } + ) return suggested_facets async def facet_results(self): @@ -678,18 +716,25 @@ Each Facet subclass implements a new type of facet operation. The class should l try: facet_results_values = [] # More calculations... - facet_results_values.append({ - "value": value, - "label": label, - "count": count, - "toggle_url": self.ds.absolute_url(self.request, toggle_path), - "selected": selected, - }) - facet_results.append({ - "name": column, - "results": facet_results_values, - "truncated": len(facet_rows_results) > facet_size, - }) + facet_results_values.append( + { + "value": value, + "label": label, + "count": count, + "toggle_url": self.ds.absolute_url( + self.request, toggle_path + ), + "selected": selected, + } + ) + facet_results.append( + { + "name": column, + "results": facet_results_values, + "truncated": len(facet_rows_results) + > facet_size, + } + ) except QueryInterrupted: facets_timed_out.append(column) @@ -728,21 +773,33 @@ This example plugin adds a ``x-databases`` HTTP header listing the currently att def asgi_wrapper(datasette): def wrap_with_databases_header(app): @wraps(app) - async def add_x_databases_header(scope, receive, send): + async def add_x_databases_header( + scope, receive, send + ): async def wrapped_send(event): if event["type"] == "http.response.start": - original_headers = event.get("headers") or [] + original_headers = ( + event.get("headers") or [] + ) event = { "type": event["type"], "status": event["status"], - "headers": original_headers + [ - [b"x-databases", - ", ".join(datasette.databases.keys()).encode("utf-8")] + "headers": original_headers + + [ + [ + b"x-databases", + ", ".join( + datasette.databases.keys() + ).encode("utf-8"), + ] ], } await send(event) + await app(scope, receive, wrapped_send) + return add_x_databases_header + return wrap_with_databases_header Examples: `datasette-cors `__, `datasette-pyinstrument `__ @@ -759,7 +816,9 @@ This hook fires when the Datasette application server first starts up. You can i @hookimpl def startup(datasette): config = datasette.plugin_config("my-plugin") or {} - assert "required-setting" in config, "my-plugin requires setting required-setting" + assert ( + "required-setting" in config + ), "my-plugin requires setting required-setting" Or you can return an async function which will be awaited on startup. Use this option if you need to make any database queries: @@ -770,9 +829,12 @@ Or you can return an async function which will be awaited on startup. Use this o async def inner(): db = datasette.get_database() if "my_table" not in await db.table_names(): - await db.execute_write(""" + await db.execute_write( + """ create table my_table (mycol text) - """) + """ + ) + return inner Potential use-cases: @@ -815,6 +877,7 @@ Ues this hook to return a dictionary of additional :ref:`canned query `__ @@ -888,9 +960,12 @@ Here's an example that authenticates the actor based on an incoming API key: SECRET_KEY = "this-is-a-secret" + @hookimpl def actor_from_request(datasette, request): - authorization = request.headers.get("authorization") or "" + authorization = ( + request.headers.get("authorization") or "" + ) expected = "Bearer {}".format(SECRET_KEY) if secrets.compare_digest(authorization, expected): @@ -906,6 +981,7 @@ Instead of returning a dictionary, this function can return an awaitable functio from datasette import hookimpl + @hookimpl def actor_from_request(datasette, request): async def inner(): @@ -914,7 +990,8 @@ Instead of returning a dictionary, this function can return an awaitable functio return None # Look up ?_token=xxx in sessions table result = await datasette.get_database().execute( - "select count(*) from sessions where token = ?", [token] + "select count(*) from sessions where token = ?", + [token], ) if result.first()[0]: return {"token": token} @@ -952,7 +1029,7 @@ The hook should return an instance of ``datasette.filters.FilterArguments`` whic where_clauses=["id > :max_id"], params={"max_id": 5}, human_descriptions=["max_id is greater than 5"], - extra_context={} + extra_context={}, ) The arguments to the ``FilterArguments`` class constructor are as follows: @@ -973,10 +1050,13 @@ This example plugin causes 0 results to be returned if ``?_nothing=1`` is added from datasette import hookimpl from datasette.filters import FilterArguments + @hookimpl def filters_from_request(self, request): if request.args.get("_nothing"): - return FilterArguments(["1 = 0"], human_descriptions=["NOTHING"]) + return FilterArguments( + ["1 = 0"], human_descriptions=["NOTHING"] + ) Example: `datasette-leaflet-freedraw `_ @@ -1006,6 +1086,7 @@ Here's an example plugin which randomly selects if a permission should be allowe from datasette import hookimpl import random + @hookimpl def permission_allowed(action): if action != "view-instance": @@ -1024,11 +1105,16 @@ Here's an example that allows users to view the ``admin_log`` table only if thei async def inner(): if action == "execute-sql" and resource == "staff": return False - if action == "view-table" and resource == ("staff", "admin_log"): + if action == "view-table" and resource == ( + "staff", + "admin_log", + ): if not actor: return False user_id = actor["id"] - return await datasette.get_database("staff").execute( + return await datasette.get_database( + "staff" + ).execute( "select count(*) from admin_users where user_id = :user_id", {"user_id": user_id}, ) @@ -1059,18 +1145,21 @@ This example registers two new magic parameters: ``:_request_http_version`` retu from uuid import uuid4 + def uuid(key, request): if key == "new": return str(uuid4()) else: raise KeyError + def request(key, request): if key == "http_version": return request.scope["http_version"] else: raise KeyError + @hookimpl def register_magic_parameters(datasette): return [ @@ -1103,9 +1192,12 @@ This example returns a redirect to a ``/-/login`` page: from datasette import hookimpl from urllib.parse import urlencode + @hookimpl def forbidden(request, message): - return Response.redirect("/-/login?=" + urlencode({"message": message})) + return Response.redirect( + "/-/login?=" + urlencode({"message": message}) + ) The function can alternatively return an awaitable function if it needs to make any asynchronous method calls. This example renders a template: @@ -1114,10 +1206,15 @@ The function can alternatively return an awaitable function if it needs to make from datasette import hookimpl from datasette.utils.asgi import Response + @hookimpl def forbidden(datasette): async def inner(): - return Response.html(await datasette.render_template("forbidden.html")) + return Response.html( + await datasette.render_template( + "forbidden.html" + ) + ) return inner @@ -1147,11 +1244,17 @@ This example adds a new menu item but only if the signed in user is ``"root"``: from datasette import hookimpl + @hookimpl def menu_links(datasette, actor): if actor and actor.get("id") == "root": return [ - {"href": datasette.urls.path("/-/edit-schema"), "label": "Edit schema"}, + { + "href": datasette.urls.path( + "/-/edit-schema" + ), + "label": "Edit schema", + }, ] Using :ref:`internals_datasette_urls` here ensures that links in the menu will take the :ref:`setting_base_url` setting into account. @@ -1188,13 +1291,20 @@ This example adds a new table action if the signed in user is ``"root"``: from datasette import hookimpl + @hookimpl def table_actions(datasette, actor): if actor and actor.get("id") == "root": - return [{ - "href": datasette.urls.path("/-/edit-schema/{}/{}".format(database, table)), - "label": "Edit schema for this table", - }] + return [ + { + "href": datasette.urls.path( + "/-/edit-schema/{}/{}".format( + database, table + ) + ), + "label": "Edit schema for this table", + } + ] Example: `datasette-graphql `_ @@ -1238,6 +1348,7 @@ This example will disable CSRF protection for that specific URL path: from datasette import hookimpl + @hookimpl def skip_csrf(scope): return scope["path"] == "/submit-comment" @@ -1278,7 +1389,9 @@ This hook is responsible for returning a dictionary corresponding to Datasette : "description": get_instance_description(datasette), "databases": [], } - for db_name, db_data_dict in get_my_database_meta(datasette, database, table, key): + for db_name, db_data_dict in get_my_database_meta( + datasette, database, table, key + ): metadata["databases"][db_name] = db_data_dict # whatever we return here will be merged with any other plugins using this hook and # will be overwritten by a local metadata.yaml if one exists!