Fix blacken-docs errors and warnings, refs #1718

This commit is contained in:
Simon Willison 2022-04-24 08:51:09 -07:00
commit 92b26673d8
4 changed files with 289 additions and 141 deletions

View file

@ -44,9 +44,12 @@ aggregates and collations. For example:
from datasette import hookimpl
import random
@hookimpl
def prepare_connection(conn):
conn.create_function('random_integer', 2, random.randint)
conn.create_function(
"random_integer", 2, random.randint
)
This registers a SQL function called ``random_integer`` which takes two
arguments and can be called like this::
@ -72,9 +75,10 @@ example:
from datasette import hookimpl
@hookimpl
def prepare_jinja2_environment(env):
env.filters['uppercase'] = lambda u: u.upper()
env.filters["uppercase"] = lambda u: u.upper()
You can now use this filter in your custom templates like so::
@ -127,9 +131,7 @@ Here's an example plugin that adds a ``"user_agent"`` variable to the template c
@hookimpl
def extra_template_vars(request):
return {
"user_agent": request.headers.get("user-agent")
}
return {"user_agent": request.headers.get("user-agent")}
This example returns an awaitable function which adds a list of ``hidden_table_names`` to the context:
@ -140,9 +142,12 @@ This example returns an awaitable function which adds a list of ``hidden_table_n
async def hidden_table_names():
if database:
db = datasette.databases[database]
return {"hidden_table_names": await db.hidden_table_names()}
return {
"hidden_table_names": await db.hidden_table_names()
}
else:
return {}
return hidden_table_names
And here's an example which adds a ``sql_first(sql_query)`` function which executes a SQL statement and returns the first column of the first row of results:
@ -152,8 +157,15 @@ And here's an example which adds a ``sql_first(sql_query)`` function which execu
@hookimpl
def extra_template_vars(datasette, database):
async def sql_first(sql, dbname=None):
dbname = dbname or database or next(iter(datasette.databases.keys()))
return (await datasette.execute(dbname, sql)).rows[0][0]
dbname = (
dbname
or database
or next(iter(datasette.databases.keys()))
)
return (await datasette.execute(dbname, sql)).rows[
0
][0]
return {"sql_first": sql_first}
You can then use the new function in a template like so::
@ -178,6 +190,7 @@ This can be a list of URLs:
from datasette import hookimpl
@hookimpl
def extra_css_urls():
return [
@ -191,10 +204,12 @@ Or a list of dictionaries defining both a URL and an
@hookimpl
def extra_css_urls():
return [{
"url": "https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css",
"sri": "sha384-9gVQ4dYFwwWSjIDZnLEWnxCjeSWFphJiwGPXr1jddIhOegiu1FwO5qRGvFXOdJZ4",
}]
return [
{
"url": "https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css",
"sri": "sha384-9gVQ4dYFwwWSjIDZnLEWnxCjeSWFphJiwGPXr1jddIhOegiu1FwO5qRGvFXOdJZ4",
}
]
This function can also return an awaitable function, useful if it needs to run any async code:
@ -204,7 +219,9 @@ This function can also return an awaitable function, useful if it needs to run a
def extra_css_urls(datasette):
async def inner():
db = datasette.get_database()
results = await db.execute("select url from css_files")
results = await db.execute(
"select url from css_files"
)
return [r[0] for r in results]
return inner
@ -225,12 +242,15 @@ return a list of URLs, a list of dictionaries or an awaitable function that retu
from datasette import hookimpl
@hookimpl
def extra_js_urls():
return [{
"url": "https://code.jquery.com/jquery-3.3.1.slim.min.js",
"sri": "sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo",
}]
return [
{
"url": "https://code.jquery.com/jquery-3.3.1.slim.min.js",
"sri": "sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo",
}
]
You can also return URLs to files from your plugin's ``static/`` directory, if
you have one:
@ -239,9 +259,7 @@ you have one:
@hookimpl
def extra_js_urls():
return [
"/-/static-plugins/your-plugin/app.js"
]
return ["/-/static-plugins/your-plugin/app.js"]
Note that `your-plugin` here should be the hyphenated plugin name - the name that is displayed in the list on the `/-/plugins` debug page.
@ -251,9 +269,11 @@ If your code uses `JavaScript modules <https://developer.mozilla.org/en-US/docs/
@hookimpl
def extra_js_urls():
return [{
"url": "/-/static-plugins/your-plugin/app.js",
"module": True
return [
{
"url": "/-/static-plugins/your-plugin/app.js",
"module": True,
}
]
Examples: `datasette-cluster-map <https://datasette.io/plugins/datasette-cluster-map>`_, `datasette-vega <https://datasette.io/plugins/datasette-vega>`_
@ -281,7 +301,7 @@ Use a dictionary if you want to specify that the code should be placed in a ``<s
def extra_body_script():
return {
"module": True,
"script": "console.log('Your JavaScript goes here...')"
"script": "console.log('Your JavaScript goes here...')",
}
This will add the following to the end of your page:
@ -311,7 +331,9 @@ Let's say you want to build a plugin that adds a ``datasette publish my_hosting_
.. code-block:: python
from datasette import hookimpl
from datasette.publish.common import add_common_publish_arguments_and_options
from datasette.publish.common import (
add_common_publish_arguments_and_options,
)
import click
@ -345,7 +367,7 @@ Let's say you want to build a plugin that adds a ``datasette publish my_hosting_
about_url,
api_key,
):
# Your implementation goes here
...
Examples: `datasette-publish-fly <https://datasette.io/plugins/datasette-publish-fly>`_, `datasette-publish-vercel <https://datasette.io/plugins/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('<a href="{href}">{label}</a>'.format(
href=markupsafe.escape(data["href"]),
label=markupsafe.escape(data["label"] or "") or "&nbsp;"
))
return markupsafe.Markup(
'<a href="{href}">{label}</a>'.format(
href=markupsafe.escape(data["href"]),
label=markupsafe.escape(data["label"] or "")
or "&nbsp;",
)
)
Examples: `datasette-render-binary <https://datasette.io/plugins/datasette-render-binary>`_, `datasette-render-markdown <https://datasette.io/plugins/datasette-render-markdown>`__, `datasette-json-html <https://datasette.io/plugins/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 <https://datasette.io/plugins/datasette-atom>`_, `datasette-ics <https://datasette.io/plugins/datasette-ics>`_, `datasette-geojson <https://datasette.io/plugins/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<name>.*)$", hello_from)
]
return [(r"^/hello-from/(?P<name>.*)$", 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 <https://datasette.io/plugins/datasette-cors>`__, `datasette-pyinstrument <https://datasette.io/plugins/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 <canned_qu
from datasette import hookimpl
@hookimpl
def canned_queries(datasette, database):
if database == "mydb":
@ -830,15 +893,20 @@ The hook can alternatively return an awaitable function that returns a list. Her
from datasette import hookimpl
@hookimpl
def canned_queries(datasette, database):
async def inner():
db = datasette.get_database(database)
if await db.table_exists("saved_queries"):
results = await db.execute("select name, sql from saved_queries")
return {result["name"]: {
"sql": result["sql"]
} for result in results}
results = await db.execute(
"select name, sql from saved_queries"
)
return {
result["name"]: {"sql": result["sql"]}
for result in results
}
return inner
The actor parameter can be used to include the currently authenticated actor in your decision. Here's an example that returns saved queries that were saved by that actor:
@ -847,19 +915,23 @@ The actor parameter can be used to include the currently authenticated actor in
from datasette import hookimpl
@hookimpl
def canned_queries(datasette, database, actor):
async def inner():
db = datasette.get_database(database)
if actor is not None and await db.table_exists("saved_queries"):
if actor is not None and await db.table_exists(
"saved_queries"
):
results = await db.execute(
"select name, sql from saved_queries where actor_id = :id", {
"id": actor["id"]
}
"select name, sql from saved_queries where actor_id = :id",
{"id": actor["id"]},
)
return {result["name"]: {
"sql": result["sql"]
} for result in results}
return {
result["name"]: {"sql": result["sql"]}
for result in results
}
return inner
Example: `datasette-saved-queries <https://datasette.io/plugins/datasette-saved-queries>`__
@ -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 <https://datasette.io/plugins/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 <https://datasette.io/plugins/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!