diff --git a/README.md b/README.md
index af95b85e..83023443 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@
[](https://docs.datasette.io/en/latest/?badge=latest)
[](https://github.com/simonw/datasette/blob/main/LICENSE)
[](https://hub.docker.com/r/datasetteproject/datasette)
-[](https://discord.gg/ktd74dm5mw)
+[](https://datasette.io/discord)
*An open source multi-tool for exploring and publishing data*
@@ -22,7 +22,7 @@ Datasette is aimed at data journalists, museum curators, archivists, local gover
* Comprehensive documentation: https://docs.datasette.io/
* Examples: https://datasette.io/examples
* Live demo of current `main` branch: https://latest.datasette.io/
-* Questions, feedback or want to talk about the project? Join our [Discord](https://discord.gg/ktd74dm5mw)
+* Questions, feedback or want to talk about the project? Join our [Discord](https://datasette.io/discord)
Want to stay up-to-date with the project? Subscribe to the [Datasette newsletter](https://datasette.substack.com/) for tips, tricks and news on what's new in the Datasette ecosystem.
diff --git a/datasette/app.py b/datasette/app.py
index 03d1dacc..9ae4706a 100644
--- a/datasette/app.py
+++ b/datasette/app.py
@@ -190,6 +190,7 @@ class Datasette:
def __init__(
self,
files=None,
+ *,
immutables=None,
cache_headers=True,
cors=False,
@@ -410,7 +411,7 @@ class Datasette:
def unsign(self, signed, namespace="default"):
return URLSafeSerializer(self._secret, namespace).loads(signed)
- def get_database(self, name=None, route=None):
+ def get_database(self, name=None, *, route=None):
if route is not None:
matches = [db for db in self.databases.values() if db.route == route]
if not matches:
@@ -421,7 +422,7 @@ class Datasette:
name = [key for key in self.databases.keys() if key != "_internal"][0]
return self.databases[name]
- def add_database(self, db, name=None, route=None):
+ def add_database(self, db, name=None, *, route=None):
new_databases = self.databases.copy()
if name is None:
# Pick a unique name for this database
@@ -466,7 +467,7 @@ class Datasette:
orig[key] = upd_value
return orig
- def metadata(self, key=None, database=None, table=None, fallback=True):
+ def metadata(self, key=None, *, database=None, table=None, fallback=True):
"""
Looks up metadata, cascading backwards from specified level.
Returns None if metadata value is not found.
@@ -518,7 +519,7 @@ class Datasette:
def _metadata(self):
return self.metadata()
- def plugin_config(self, plugin_name, database=None, table=None, fallback=True):
+ def plugin_config(self, plugin_name, *, database=None, table=None, fallback=True):
"""Return config for plugin, falling back from specified database/table"""
plugins = self.metadata(
"plugins", database=database, table=table, fallback=fallback
@@ -714,6 +715,7 @@ class Datasette:
db_name,
sql,
params=None,
+ *,
truncate=False,
custom_time_limit=None,
page_size=None,
@@ -943,7 +945,7 @@ class Datasette:
)
async def render_template(
- self, templates, context=None, request=None, view_name=None
+ self, templates, context=None, *, request=None, view_name=None
):
if not self._startup_invoked:
raise Exception("render_template() called before await ds.invoke_startup()")
diff --git a/datasette/utils/asgi.py b/datasette/utils/asgi.py
index 8a2fa060..fca6e004 100644
--- a/datasette/utils/asgi.py
+++ b/datasette/utils/asgi.py
@@ -118,7 +118,9 @@ class Request:
return dict(parse_qsl(body.decode("utf-8"), keep_blank_values=True))
@classmethod
- def fake(cls, path_with_query_string, method="GET", scheme="http", url_vars=None):
+ def fake(
+ cls, path_with_query_string, *, method="GET", scheme="http", url_vars=None
+ ):
"""Useful for constructing Request objects for tests"""
path, _, query_string = path_with_query_string.partition("?")
scope = {
@@ -204,7 +206,7 @@ class AsgiWriter:
)
-async def asgi_send_json(send, info, status=200, headers=None):
+async def asgi_send_json(send, info, *, status=200, headers=None):
headers = headers or {}
await asgi_send(
send,
@@ -215,7 +217,7 @@ async def asgi_send_json(send, info, status=200, headers=None):
)
-async def asgi_send_html(send, html, status=200, headers=None):
+async def asgi_send_html(send, html, *, status=200, headers=None):
headers = headers or {}
await asgi_send(
send,
@@ -226,7 +228,7 @@ async def asgi_send_html(send, html, status=200, headers=None):
)
-async def asgi_send_redirect(send, location, status=302):
+async def asgi_send_redirect(send, location, *, status=302):
await asgi_send(
send,
"",
@@ -236,12 +238,12 @@ async def asgi_send_redirect(send, location, status=302):
)
-async def asgi_send(send, content, status, headers=None, content_type="text/plain"):
- await asgi_start(send, status, headers, content_type)
+async def asgi_send(send, content, status, *, headers=None, content_type="text/plain"):
+ await asgi_start(send, status=status, headers=headers, content_type=content_type)
await send({"type": "http.response.body", "body": content.encode("utf-8")})
-async def asgi_start(send, status, headers=None, content_type="text/plain"):
+async def asgi_start(send, status, *, headers=None, content_type="text/plain"):
headers = headers or {}
# Remove any existing content-type header
headers = {k: v for k, v in headers.items() if k.lower() != "content-type"}
@@ -259,7 +261,7 @@ async def asgi_start(send, status, headers=None, content_type="text/plain"):
async def asgi_send_file(
- send, filepath, filename=None, content_type=None, chunk_size=4096, headers=None
+ send, filepath, filename=None, *, content_type=None, chunk_size=4096, headers=None
):
headers = headers or {}
if filename:
@@ -270,9 +272,11 @@ async def asgi_send_file(
if first:
await asgi_start(
send,
- 200,
- headers,
- content_type or guess_type(str(filepath))[0] or "text/plain",
+ status=200,
+ headers=headers,
+ content_type=content_type
+ or guess_type(str(filepath))[0]
+ or "text/plain",
)
first = False
more_body = True
@@ -284,7 +288,7 @@ async def asgi_send_file(
)
-def asgi_static(root_path, chunk_size=4096, headers=None, content_type=None):
+def asgi_static(root_path, *, chunk_size=4096, headers=None, content_type=None):
root_path = Path(root_path)
async def inner_static(request, send):
@@ -292,28 +296,32 @@ def asgi_static(root_path, chunk_size=4096, headers=None, content_type=None):
try:
full_path = (root_path / path).resolve().absolute()
except FileNotFoundError:
- await asgi_send_html(send, "404: Directory not found", 404)
+ await asgi_send_html(send, "404: Directory not found", status=404)
return
if full_path.is_dir():
- await asgi_send_html(send, "403: Directory listing is not allowed", 403)
+ await asgi_send_html(
+ send, "403: Directory listing is not allowed", status=403
+ )
return
# Ensure full_path is within root_path to avoid weird "../" tricks
try:
full_path.relative_to(root_path.resolve())
except ValueError:
- await asgi_send_html(send, "404: Path not inside root path", 404)
+ await asgi_send_html(send, "404: Path not inside root path", status=404)
return
try:
await asgi_send_file(send, full_path, chunk_size=chunk_size)
except FileNotFoundError:
- await asgi_send_html(send, "404: File not found", 404)
+ await asgi_send_html(send, "404: File not found", status=404)
return
return inner_static
class Response:
- def __init__(self, body=None, status=200, headers=None, content_type="text/plain"):
+ def __init__(
+ self, body=None, *, status=200, headers=None, content_type="text/plain"
+ ):
self.body = body
self.status = status
self.headers = headers or {}
@@ -346,6 +354,7 @@ class Response:
self,
key,
value="",
+ *,
max_age=None,
expires=None,
path="/",
@@ -374,7 +383,7 @@ class Response:
self._set_cookie_headers.append(cookie.output(header="").strip())
@classmethod
- def html(cls, body, status=200, headers=None):
+ def html(cls, body, *, status=200, headers=None):
return cls(
body,
status=status,
@@ -383,7 +392,7 @@ class Response:
)
@classmethod
- def text(cls, body, status=200, headers=None):
+ def text(cls, body, *, status=200, headers=None):
return cls(
str(body),
status=status,
@@ -392,7 +401,7 @@ class Response:
)
@classmethod
- def json(cls, body, status=200, headers=None, default=None):
+ def json(cls, body, *, status=200, headers=None, default=None):
return cls(
json.dumps(body, default=default),
status=status,
@@ -401,7 +410,7 @@ class Response:
)
@classmethod
- def redirect(cls, path, status=302, headers=None):
+ def redirect(cls, path, *, status=302, headers=None):
headers = headers or {}
headers["Location"] = path
return cls("", status=status, headers=headers)
@@ -412,6 +421,7 @@ class AsgiFileDownload:
self,
filepath,
filename=None,
+ *,
content_type="application/octet-stream",
headers=None,
):
diff --git a/docs/changelog.rst b/docs/changelog.rst
index bd93f4cb..7f8cc879 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -28,7 +28,7 @@ Changelog
Datasette can now run entirely in your browser using WebAssembly. Try out `Datasette Lite `__, take a look `at the code `__ or read more about it in `Datasette Lite: a server-side Python web application running in a browser `__.
-Datasette now has a `Discord community `__ for questions and discussions about Datasette and its ecosystem of projects.
+Datasette now has a `Discord community `__ for questions and discussions about Datasette and its ecosystem of projects.
Features
~~~~~~~~
diff --git a/docs/index.rst b/docs/index.rst
index 5a9cc7ed..e80bec73 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -17,7 +17,7 @@ datasette| |discord|
.. |docker: datasette| image:: https://img.shields.io/badge/docker-datasette-blue
:target: https://hub.docker.com/r/datasetteproject/datasette
.. |discord| image:: https://img.shields.io/discord/823971286308356157?label=discord
- :target: https://discord.gg/ktd74dm5mw
+ :target: https://datasette.io/discord
*An open source multi-tool for exploring and publishing data*
diff --git a/tests/test_internals_database.py b/tests/test_internals_database.py
index 9e81c1d6..4e33beed 100644
--- a/tests/test_internals_database.py
+++ b/tests/test_internals_database.py
@@ -30,6 +30,14 @@ async def test_results_first(db):
assert isinstance(row, sqlite3.Row)
+@pytest.mark.asyncio
+@pytest.mark.parametrize("expected", (True, False))
+async def test_results_bool(db, expected):
+ where = "" if expected else "where pk = 0"
+ results = await db.execute("select * from facetable {}".format(where))
+ assert bool(results) is expected
+
+
@pytest.mark.parametrize(
"query,expected",
[