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:
Permission rules view
======================
=====================
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:
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.
@ -1212,7 +1212,7 @@ Default *allow*.
.. _permissions_view_database_download:
view-database-download
-----------------------
----------------------
Actor is allowed to download a database, e.g. https://latest.datasette.io/fixtures.db

View file

@ -577,7 +577,7 @@ Documentation
.. _v0_62:
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/>`__.

View file

@ -79,7 +79,7 @@ Datasette will not be accessible from outside the server because it is listening
.. _deploying_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/>`__.
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:
await .ensure_permission(action, resource=None, actor=None)
------------------------------------------------------------
-----------------------------------------------------------
``action`` - string
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:
Permission classes and utilities
=================================
================================
.. _internals_permission_sql:
@ -1296,7 +1296,7 @@ Example usage:
.. _database_execute_write:
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.
@ -1313,7 +1313,7 @@ Each call to ``execute_write()`` will be executed inside a transaction.
.. _database_execute_write_script:
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.
@ -1322,7 +1322,7 @@ Each call to ``execute_write_script()`` will be executed inside a transaction.
.. _database_execute_write_many:
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:

View file

@ -780,7 +780,7 @@ The plugin hook can then be used to register the new facet class like this:
.. _plugin_register_permissions:
register_permissions(datasette)
--------------------------------
-------------------------------
.. note::
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:
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.
@ -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=...)``
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.
@ -1445,7 +1445,7 @@ Example: `datasette-permissions-sql <https://datasette.io/plugins/datasette-perm
.. _plugin_hook_permission_resources_sql:
permission_resources_sql(datasette, actor, action)
---------------------------------------------------
--------------------------------------------------
``datasette`` - :ref:`internals_datasette`
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
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
# fmt: off