Switch to ruff and fix all lint errors, refs #2630

This commit is contained in:
Simon Willison 2026-01-23 20:43:16 -08:00
commit 66d2a033f8
21 changed files with 44 additions and 101 deletions

View file

@ -35,6 +35,8 @@ jobs:
tests/test_datasette_https_server.sh
- name: Black
run: black --check .
- name: Ruff
run: ruff check datasette tests
- name: Check if cog needs to be run
run: |
cog --check docs/*.rst

View file

@ -17,12 +17,16 @@ export DATASETTE_SECRET := "not_a_secret"
uv run codespell datasette -S datasette/static --ignore-words docs/codespell-ignore-words.txt
uv run codespell tests --ignore-words docs/codespell-ignore-words.txt
# Run linters: black, flake8, mypy, cog
# Run linters: black, ruff, cog
@lint: codespell
uv run black . --check
uv run flake8
uv run black datasette tests --check
uv run ruff check datasette tests
uv run cog --check README.md docs/*.rst
# Apply ruff fixes
@fix:
uv run ruff check --fix datasette tests
# Rebuild docs with cog
@cog:
uv run cog -r README.md docs/*.rst
@ -37,7 +41,7 @@ export DATASETTE_SECRET := "not_a_secret"
# Apply Black
@black:
uv run black .
uv run black datasette tests
# Apply blacken-docs
@blacken-docs:

View file

@ -6,7 +6,7 @@ import contextvars
from typing import TYPE_CHECKING, Any, Dict, Iterable, List
if TYPE_CHECKING:
from datasette.permissions import AllowedResource, Resource
from datasette.permissions import Resource
import asgi_csrf
import collections
import dataclasses
@ -1144,7 +1144,7 @@ class Datasette:
# Validate that resource is a Resource object or None
if resource is not None and not isinstance(resource, Resource):
raise TypeError(f"resource must be a Resource subclass instance or None.")
raise TypeError("resource must be a Resource subclass instance or None.")
# Check if actor can see it
if not await self.allowed(action=action, resource=resource, actor=actor):

View file

@ -26,18 +26,18 @@ from datasette import hookimpl
# Re-export all hooks and public utilities
from .restrictions import (
actor_restrictions_sql,
restrictions_allow_action,
ActorRestrictions,
actor_restrictions_sql as actor_restrictions_sql,
restrictions_allow_action as restrictions_allow_action,
ActorRestrictions as ActorRestrictions,
)
from .root import root_user_permissions_sql
from .config import config_permissions_sql
from .root import root_user_permissions_sql as root_user_permissions_sql
from .config import config_permissions_sql as config_permissions_sql
from .defaults import (
default_allow_sql_check,
default_action_permissions_sql,
DEFAULT_ALLOW_ACTIONS,
default_allow_sql_check as default_allow_sql_check,
default_action_permissions_sql as default_action_permissions_sql,
DEFAULT_ALLOW_ACTIONS as DEFAULT_ALLOW_ACTIONS,
)
from .tokens import actor_from_signed_api_token
from .tokens import actor_from_signed_api_token as actor_from_signed_api_token
@hookimpl

View file

@ -1,7 +1,6 @@
import asyncio
import csv
import hashlib
import json
import sys
import textwrap
import time

View file

@ -66,6 +66,7 @@ dev = [
"pytest-timeout>=1.4.2",
"trustme>=0.7",
"cogapp>=3.3.0",
"ruff>=0.9",
# docs
"Sphinx==7.4.7",
"furo==2025.9.25",
@ -94,5 +95,9 @@ datasette = ["templates/*.html"]
[tool.setuptools.dynamic]
version = {attr = "datasette.version.__version__"}
[tool.ruff]
line-length = 160
select = ["E", "F", "W"]
[tool.uv]
package = true

View file

@ -1,5 +1,2 @@
[aliases]
test=pytest
[flake8]
max-line-length = 160

View file

@ -117,7 +117,6 @@ async def test_tables_endpoint_database_restriction(test_ds):
# Bob should only see analytics tables
analytics_tables = [m for m in result if m["name"].startswith("analytics/")]
production_tables = [m for m in result if m["name"].startswith("production/")]
assert len(analytics_tables) == 3
table_names = {m["name"] for m in analytics_tables}

View file

@ -1,21 +1,7 @@
from datasette.app import Datasette
from datasette.plugins import DEFAULT_PLUGINS
from datasette.version import __version__
from .fixtures import ( # noqa
app_client,
app_client_no_files,
app_client_with_dot,
app_client_shorter_time_limit,
app_client_two_attached_databases_one_immutable,
app_client_larger_cache_size,
app_client_with_cors,
app_client_two_attached_databases,
app_client_conflicting_database_names,
app_client_immutable_and_inspect_file,
make_app_client,
EXPECTED_PLUGINS,
METADATA,
)
from .fixtures import make_app_client, EXPECTED_PLUGINS
import pathlib
import pytest
import sys
@ -815,14 +801,14 @@ def test_databases_json(app_client_two_attached_databases_one_immutable):
assert 2 == len(databases)
extra_database, fixtures_database = databases
assert "extra database" == extra_database["name"]
assert None == extra_database["hash"]
assert True == extra_database["is_mutable"]
assert False == extra_database["is_memory"]
assert extra_database["hash"] is None
assert extra_database["is_mutable"] is True
assert extra_database["is_memory"] is False
assert "fixtures" == fixtures_database["name"]
assert fixtures_database["hash"] is not None
assert False == fixtures_database["is_mutable"]
assert False == fixtures_database["is_memory"]
assert fixtures_database["is_mutable"] is False
assert fixtures_database["is_memory"] is False
@pytest.mark.asyncio

View file

@ -87,7 +87,7 @@ def test_invalid_settings(config_dir):
)
try:
with pytest.raises(StartupError) as ex:
ds = Datasette([], config_dir=config_dir)
Datasette([], config_dir=config_dir)
assert ex.value.args[0] == "Invalid setting 'invalid' in config file"
finally:
(config_dir / "datasette.json").write_text(previous, "utf-8")

View file

@ -67,7 +67,7 @@ def test_crossdb_attached_database_list_display(
):
app_client = app_client_two_attached_databases_crossdb_enabled
response = app_client.get("/_memory")
response2 = app_client.get("/")
app_client.get("/")
for fragment in (
"databases are attached to this connection",
"<li><strong>fixtures</strong> - ",

View file

@ -1,12 +1,6 @@
from datasette.app import Datasette
from bs4 import BeautifulSoup as Soup
import pytest
from .fixtures import ( # noqa
app_client,
app_client_csv_max_mb_one,
app_client_with_cors,
app_client_with_trace,
)
import urllib.parse
EXPECTED_TABLE_CSV = """id,content

View file

@ -103,27 +103,6 @@ async def test_through_filters_from_request(ds_client):
assert filter_args.extra_context == {}
@pytest.mark.asyncio
async def test_through_filters_from_request(ds_client):
request = Request.fake(
'/?_through={"table":"roadside_attraction_characteristics","column":"characteristic_id","value":"1"}'
)
filter_args = await through_filters(
request=request,
datasette=ds_client.ds,
table="roadside_attractions",
database="fixtures",
)()
assert filter_args.where_clauses == [
"pk in (select attraction_id from roadside_attraction_characteristics where characteristic_id = :p0)"
]
assert filter_args.params == {"p0": "1"}
assert filter_args.human_descriptions == [
'roadside_attraction_characteristics.characteristic_id = "1"'
]
assert filter_args.extra_context == {}
@pytest.mark.asyncio
async def test_where_filters_from_request(ds_client):
await ds_client.ds.invoke_startup()

View file

@ -1,14 +1,7 @@
from bs4 import BeautifulSoup as Soup
from datasette.app import Datasette
from datasette.utils import allowed_pragmas
from .fixtures import ( # noqa
app_client,
app_client_base_url_prefix,
app_client_shorter_time_limit,
app_client_two_attached_databases,
make_app_client,
METADATA,
)
from .fixtures import make_app_client
from .utils import assert_footer_links, inner_html
import copy
import json

View file

@ -158,7 +158,7 @@ def test_datasette_error_if_string_not_list(tmpdir):
# https://github.com/simonw/datasette/issues/1985
db_path = str(tmpdir / "data.db")
with pytest.raises(ValueError):
ds = Datasette(db_path)
Datasette(db_path)
@pytest.mark.asyncio

View file

@ -2,7 +2,7 @@ import collections
from datasette.app import Datasette
from datasette.cli import cli
from datasette.default_permissions import restrictions_allow_action
from .fixtures import app_client, assert_permissions_checked, make_app_client
from .fixtures import assert_permissions_checked, make_app_client
from click.testing import CliRunner
from bs4 import BeautifulSoup as Soup
import copy
@ -1481,7 +1481,6 @@ async def test_actor_restrictions_view_instance_only(perms_ds):
assert response.status_code == 200
# But no databases should be visible (no view-database permission)
data = response.json()
# The instance is visible but databases list should be empty or minimal
# Actually, let's check via allowed_resources
page = await perms_ds.allowed_resources("view-database", actor)

View file

@ -1172,8 +1172,6 @@ async def test_hook_filters_from_request(ds_client):
@pytest.mark.asyncio
@pytest.mark.parametrize("extra_metadata", (False, True))
async def test_hook_register_actions(extra_metadata):
from datasette.permissions import Action
from datasette.resources import DatabaseResource, InstanceResource
ds = Datasette(
config=(
@ -1527,7 +1525,7 @@ async def test_hook_register_events():
@pytest.mark.asyncio
async def test_hook_register_actions():
async def test_hook_register_actions_view_collection():
datasette = Datasette(memory=True, plugins_dir=PLUGINS_DIR)
await datasette.invoke_startup()
# Check that the custom action from my_plugin.py is registered
@ -1545,7 +1543,7 @@ async def test_hook_register_actions_with_custom_resources():
- A parent-level action (DocumentCollectionResource)
- A child-level action (DocumentResource)
"""
from datasette.permissions import Resource, Action
from datasette.permissions import Resource
# Define custom Resource classes
class DocumentCollectionResource(Resource):

View file

@ -182,8 +182,8 @@ async def test_also_requires_with_restrictions():
"""
ds = Datasette()
await ds.invoke_startup()
db1 = ds.add_memory_database("db1_also_requires")
db2 = ds.add_memory_database("db2_also_requires")
ds.add_memory_database("db1_also_requires")
ds.add_memory_database("db2_also_requires")
await ds._refresh_schemas()
# Actor restricted to only db1_also_requires for view-database

View file

@ -1,4 +1,3 @@
import asyncio
import pytest
import pytest_asyncio
from datasette.app import Datasette

View file

@ -1,13 +1,6 @@
from datasette.utils import detect_json1
from datasette.utils.sqlite import sqlite_version
from .fixtures import ( # noqa
app_client,
app_client_with_trace,
app_client_returned_rows_matches_page_size,
generate_compound_rows,
generate_sortable_rows,
make_app_client,
)
from .fixtures import generate_compound_rows, generate_sortable_rows, make_app_client
import json
import pytest
import urllib

View file

@ -1,10 +1,6 @@
from datasette.app import Datasette
from bs4 import BeautifulSoup as Soup
from .fixtures import ( # noqa
app_client,
make_app_client,
app_client_with_dot,
)
from .fixtures import make_app_client
import pathlib
import pytest
import urllib.parse
@ -1263,7 +1259,7 @@ async def test_foreign_key_labels_obey_permissions(config):
"insert or replace into b (id, name, a_id) values (1, 'world', 1)"
)
# Anonymous user can see table b but not table a
blah = await ds.client.get("/foreign_key_labels.json")
await ds.client.get("/foreign_key_labels.json")
anon_a = await ds.client.get("/foreign_key_labels/a.json?_labels=on")
assert anon_a.status_code == 403
anon_b = await ds.client.get("/foreign_key_labels/b.json?_labels=on")