Add test for RST heading underline lengths, closes #2544

Added test_rst_heading_underlines_match_title_length() to verify that RST
heading underlines match their title lengths. The test properly handles:
- Overline+underline style headings (skips validation for those)
- Empty lines before underlines (ignores them)
- Minimum 5-character underline length (avoids false positives)

Running this test identified 14 heading underline mismatches which have
been fixed across 5 documentation files:
- docs/authentication.rst (3 headings)
- docs/plugin_hooks.rst (4 headings)
- docs/internals.rst (5 headings)
- docs/deploying.rst (1 heading)
- docs/changelog.rst (1 heading)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Simon Willison 2025-10-26 09:44:58 -07:00
commit 4fe1765dc3
6 changed files with 69 additions and 14 deletions

View file

@ -1093,7 +1093,7 @@ All three endpoints support both HTML and JSON responses. Visit the endpoint dir
.. _PermissionRulesView: .. _PermissionRulesView:
Permission rules view Permission rules view
====================== =====================
The ``/-/rules`` endpoint displays all permission rules (both allow and deny) for each candidate resource for the requested action. The ``/-/rules`` endpoint displays all permission rules (both allow and deny) for each candidate resource for the requested action.
@ -1106,7 +1106,7 @@ Pass ``?action=`` as a query parameter to specify which action to check.
.. _PermissionCheckView: .. _PermissionCheckView:
Permission check view Permission check view
====================== =====================
The ``/-/check`` endpoint evaluates a single action/resource pair and returns information indicating whether the access was allowed along with diagnostic information. The ``/-/check`` endpoint evaluates a single action/resource pair and returns information indicating whether the access was allowed along with diagnostic information.
@ -1212,7 +1212,7 @@ Default *allow*.
.. _permissions_view_database_download: .. _permissions_view_database_download:
view-database-download view-database-download
----------------------- ----------------------
Actor is allowed to download a database, e.g. https://latest.datasette.io/fixtures.db Actor is allowed to download a database, e.g. https://latest.datasette.io/fixtures.db

View file

@ -577,7 +577,7 @@ Documentation
.. _v0_62: .. _v0_62:
0.62 (2022-08-14) 0.62 (2022-08-14)
------------------- -----------------
Datasette can now run entirely in your browser using WebAssembly. Try out `Datasette Lite <https://lite.datasette.io/>`__, take a look `at the code <https://github.com/simonw/datasette-lite>`__ or read more about it in `Datasette Lite: a server-side Python web application running in a browser <https://simonwillison.net/2022/May/4/datasette-lite/>`__. Datasette can now run entirely in your browser using WebAssembly. Try out `Datasette Lite <https://lite.datasette.io/>`__, take a look `at the code <https://github.com/simonw/datasette-lite>`__ or read more about it in `Datasette Lite: a server-side Python web application running in a browser <https://simonwillison.net/2022/May/4/datasette-lite/>`__.

View file

@ -79,7 +79,7 @@ Datasette will not be accessible from outside the server because it is listening
.. _deploying_openrc: .. _deploying_openrc:
Running Datasette using OpenRC Running Datasette using OpenRC
=============================== ==============================
OpenRC is the service manager on non-systemd Linux distributions like `Alpine Linux <https://www.alpinelinux.org/>`__ and `Gentoo <https://www.gentoo.org/>`__. OpenRC is the service manager on non-systemd Linux distributions like `Alpine Linux <https://www.alpinelinux.org/>`__ and `Gentoo <https://www.gentoo.org/>`__.
Create an init script at ``/etc/init.d/datasette`` with the following contents: Create an init script at ``/etc/init.d/datasette`` with the following contents:

View file

@ -419,7 +419,7 @@ For legacy string/tuple based permission checking, use :ref:`datasette_permissio
.. _datasette_ensure_permission: .. _datasette_ensure_permission:
await .ensure_permission(action, resource=None, actor=None) await .ensure_permission(action, resource=None, actor=None)
------------------------------------------------------------ -----------------------------------------------------------
``action`` - string ``action`` - string
The action to check. See :ref:`permissions` for a list of available actions. The action to check. See :ref:`permissions` for a list of available actions.
@ -1047,7 +1047,7 @@ These methods each return a ``datasette.utils.PrefixedUrlString`` object, which
.. _internals_permission_classes: .. _internals_permission_classes:
Permission classes and utilities Permission classes and utilities
================================= ================================
.. _internals_permission_sql: .. _internals_permission_sql:
@ -1296,7 +1296,7 @@ Example usage:
.. _database_execute_write: .. _database_execute_write:
await db.execute_write(sql, params=None, block=True) await db.execute_write(sql, params=None, block=True)
----------------------------------------------------- ----------------------------------------------------
SQLite only allows one database connection to write at a time. Datasette handles this for you by maintaining a queue of writes to be executed against a given database. Plugins can submit write operations to this queue and they will be executed in the order in which they are received. SQLite only allows one database connection to write at a time. Datasette handles this for you by maintaining a queue of writes to be executed against a given database. Plugins can submit write operations to this queue and they will be executed in the order in which they are received.
@ -1313,7 +1313,7 @@ Each call to ``execute_write()`` will be executed inside a transaction.
.. _database_execute_write_script: .. _database_execute_write_script:
await db.execute_write_script(sql, block=True) await db.execute_write_script(sql, block=True)
----------------------------------------------- ----------------------------------------------
Like ``execute_write()`` but can be used to send multiple SQL statements in a single string separated by semicolons, using the ``sqlite3`` `conn.executescript() <https://docs.python.org/3/library/sqlite3.html#sqlite3.Cursor.executescript>`__ method. Like ``execute_write()`` but can be used to send multiple SQL statements in a single string separated by semicolons, using the ``sqlite3`` `conn.executescript() <https://docs.python.org/3/library/sqlite3.html#sqlite3.Cursor.executescript>`__ method.
@ -1322,7 +1322,7 @@ Each call to ``execute_write_script()`` will be executed inside a transaction.
.. _database_execute_write_many: .. _database_execute_write_many:
await db.execute_write_many(sql, params_seq, block=True) await db.execute_write_many(sql, params_seq, block=True)
--------------------------------------------------------- --------------------------------------------------------
Like ``execute_write()`` but uses the ``sqlite3`` `conn.executemany() <https://docs.python.org/3/library/sqlite3.html#sqlite3.Cursor.executemany>`__ method. This will efficiently execute the same SQL statement against each of the parameters in the ``params_seq`` iterator, for example: Like ``execute_write()`` but uses the ``sqlite3`` `conn.executemany() <https://docs.python.org/3/library/sqlite3.html#sqlite3.Cursor.executemany>`__ method. This will efficiently execute the same SQL statement against each of the parameters in the ``params_seq`` iterator, for example:

View file

@ -780,7 +780,7 @@ The plugin hook can then be used to register the new facet class like this:
.. _plugin_register_permissions: .. _plugin_register_permissions:
register_permissions(datasette) register_permissions(datasette)
-------------------------------- -------------------------------
.. note:: .. note::
This hook is deprecated. Use :ref:`plugin_register_actions` instead, which provides a more flexible resource-based permission system. This hook is deprecated. Use :ref:`plugin_register_actions` instead, which provides a more flexible resource-based permission system.
@ -830,7 +830,7 @@ The fields of the ``Permission`` class are as follows:
.. _plugin_register_actions: .. _plugin_register_actions:
register_actions(datasette) register_actions(datasette)
---------------------------- ---------------------------
If your plugin needs to register actions that can be checked with Datasette's new resource-based permission system, return a list of those actions from this hook. If your plugin needs to register actions that can be checked with Datasette's new resource-based permission system, return a list of those actions from this hook.
@ -931,7 +931,7 @@ The fields of the ``Action`` dataclass are as follows:
- Have an ``__init__`` method that accepts appropriate parameters and calls ``super().__init__(parent=..., child=...)`` - Have an ``__init__`` method that accepts appropriate parameters and calls ``super().__init__(parent=..., child=...)``
The ``resources_sql()`` method The ``resources_sql()`` method
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The ``resources_sql()`` classmethod is crucial to Datasette's permission system. It returns a SQL query that lists all resources of that type that exist in the system. The ``resources_sql()`` classmethod is crucial to Datasette's permission system. It returns a SQL query that lists all resources of that type that exist in the system.
@ -1445,7 +1445,7 @@ Example: `datasette-permissions-sql <https://datasette.io/plugins/datasette-perm
.. _plugin_hook_permission_resources_sql: .. _plugin_hook_permission_resources_sql:
permission_resources_sql(datasette, actor, action) permission_resources_sql(datasette, actor, action)
--------------------------------------------------- --------------------------------------------------
``datasette`` - :ref:`internals_datasette` ``datasette`` - :ref:`internals_datasette`
Access to the Datasette instance. Access to the Datasette instance.

View file

@ -106,6 +106,61 @@ def test_functions_marked_with_documented_are_documented(documented_fns, fn):
assert fn.__name__ in documented_fns assert fn.__name__ in documented_fns
def test_rst_heading_underlines_match_title_length():
"""Test that RST heading underlines are the same length as their titles."""
# Common RST underline characters
underline_chars = ['-', '=', '~', '^', '+', '*', '#']
errors = []
for rst_file in docs_path.glob("*.rst"):
content = rst_file.read_text()
lines = content.split('\n')
for i in range(len(lines) - 1):
current_line = lines[i]
next_line = lines[i + 1]
# Check if next line is entirely made of a single underline character
# and is at least 5 characters long (to avoid false positives)
if (next_line and
len(next_line) >= 5 and
len(set(next_line)) == 1 and
next_line[0] in underline_chars):
# Skip if the previous line is empty (blank line before underline)
if not current_line:
continue
# Check if this is an overline+underline style heading
# Look at the line before current_line to see if it's also an underline
if i > 0:
prev_line = lines[i - 1]
if (prev_line and
len(prev_line) >= 5 and
len(set(prev_line)) == 1 and
prev_line[0] in underline_chars and
len(prev_line) == len(next_line)):
# This is overline+underline style, skip it
continue
# This is a heading underline
title_length = len(current_line)
underline_length = len(next_line)
if title_length != underline_length:
errors.append(
f"{rst_file.name}:{i+1}: Title length {title_length} != underline length {underline_length}\n"
f" Title: {current_line!r}\n"
f" Underline: {next_line!r}"
)
if errors:
raise AssertionError(
f"Found {len(errors)} RST heading(s) with mismatched underline length:\n\n" +
"\n\n".join(errors)
)
# Tests for testing_plugins.rst documentation # Tests for testing_plugins.rst documentation
# fmt: off # fmt: off