mirror of
https://github.com/kuhyx/signal-bot.git
synced 2026-11-10 21:37:03 +01:00
!pułapka (and !trap, !sidła, !wnyk, !potrzask, 26 spellings in all) posts a photograph of the literal device -- a bear trap, a mousetrap, a snare, a fish weir -- captioned with what Commons says it is. The pool is 2218 Commons page ids, harvested by tool/gen_traps.py from a curated seed list and committed. Page ids and not filenames on purpose: a filename carries photographers' and sitters' names, and one of those colliding with a label somebody mints from the chat would wedge check_personal_data.py permanently, over data nobody can edit by hand. _repo_vocabulary() subtracts aliases.json and nothing else, so it would not have caught it. An integer cannot carry a name. Nothing is cached. !papież caches because 267 portraits are a closed set a group works through; a uniform draw from 2218 warms a cache essentially never, so this fetches per call the way !kot and !pies do. What the harvest measured, and now enforces: - five plausible seed categories do not exist (Snares, Man traps, Hunting traps, Wasp traps, Animal trapping). A missing category is an empty answer, not an error, so verify_seeds() asks Commons before harvesting and fails loudly naming all of them. - depth three from Category:Fish traps reaches Alfred Sisley's net paintings. Depth one, from seeds picked by hand. - "<thing> in art" categories are paintings: Mousetraps in art put a Robert Campin altarpiece in the pool. Pruned by pattern, since Commons mints a new one whenever somebody needs it. Booby traps, the Wolfsangel, camera traps and animals dying in traps are pruned by name. - Commons rate-limits hard, so a partial harvest must not be committed silently: 429 backs off, and a short pool refuses to write at all. Descriptions are HTML and sometimes say nothing, so the tags come out, the Fisheries records' trailing catalogue list is cut, a description naming only the country falls back to the filename, and the file's own page is always linked. !łapka is not an alias: it resolves to !papa at distance two, which test_nearest.py fails on, and rightly -- the suggestion would be a coin toss. signal_bot/commons.py is the imageinfo query both Commons commands now share; portraits.py keeps its behaviour and its tests unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01416TegJ3sa8xL5qGGgrygP
124 lines
4.6 KiB
Python
124 lines
4.6 KiB
Python
# Copyright (c) 2026 Krzysztof Rudnicki. MIT licence.
|
|
"""The imageinfo query both Commons commands send."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from signal_bot.commons import (
|
|
CAPTION_LIMIT,
|
|
SUFFIXES,
|
|
THUMB_PATH,
|
|
caption_for,
|
|
imageinfo_params,
|
|
page_of,
|
|
)
|
|
from signal_bot.http_client import ImageFetchError
|
|
|
|
|
|
def test_a_file_can_be_named_by_title() -> None:
|
|
params = imageinfo_params(640, title="File:1868 Mausefalle.jpg")
|
|
assert params["titles"] == "File:1868 Mausefalle.jpg"
|
|
assert "pageids" not in params
|
|
assert params["iiurlwidth"] == "640"
|
|
assert params["prop"] == "imageinfo"
|
|
|
|
|
|
def test_a_file_can_be_named_by_page_id() -> None:
|
|
# The page id arrives as an int and has to reach Commons as a string;
|
|
# sending the int would raise inside httpx rather than here.
|
|
params = imageinfo_params(480, page_id=111419226)
|
|
assert params["pageids"] == "111419226"
|
|
assert "titles" not in params
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("title", "page_id"),
|
|
[(None, None), ("File:x.jpg", 1)],
|
|
)
|
|
def test_naming_a_file_neither_way_or_both_ways_is_refused(
|
|
title: str | None, page_id: int | None
|
|
) -> None:
|
|
# Preferring one silently would make a caller's mistake look like a 404,
|
|
# which is the one failure shape that already exists downstream.
|
|
with pytest.raises(ValueError, match="exactly one"):
|
|
imageinfo_params(640, title=title, page_id=page_id)
|
|
|
|
|
|
def test_the_thumbnail_path_is_where_commons_actually_puts_it() -> None:
|
|
# Pinned because both callers walk it and neither can see it fail until
|
|
# a real fetch returns something unwalkable.
|
|
assert THUMB_PATH == ("query", "pages", 0, "imageinfo", 0, "thumburl")
|
|
|
|
|
|
def test_the_suffix_allowlist_is_what_signal_can_render() -> None:
|
|
assert ".jpg" in SUFFIXES
|
|
assert ".svg" not in SUFFIXES
|
|
|
|
|
|
def _page(
|
|
description: str | None = None, *, title: str = "File:Molchreuse.JPG", url: str = ""
|
|
) -> dict[str, object]:
|
|
"""One imageinfo page, shaped the way Commons shapes it."""
|
|
info: dict[str, object] = {"thumburl": "https://cdn/x.jpg"}
|
|
if url:
|
|
info["descriptionurl"] = url
|
|
if description is not None:
|
|
info["extmetadata"] = {"ImageDescription": {"value": description}}
|
|
return {"title": title, "imageinfo": [info]}
|
|
|
|
|
|
def test_asking_for_a_description_widens_the_query() -> None:
|
|
plain = imageinfo_params(640, page_id=1)
|
|
described = imageinfo_params(640, page_id=1, describe=True)
|
|
assert plain["iiprop"] == "url"
|
|
assert "iiextmetadatafilter" not in plain
|
|
assert described["iiprop"] == "url|extmetadata"
|
|
assert described["iiextmetadatafilter"] == "ImageDescription"
|
|
|
|
|
|
def test_the_caption_strips_markup_and_links_the_page() -> None:
|
|
page = _page("Eine <b>Molchreuse</b> & ein Korb", url="https://commons/File:M.JPG")
|
|
assert caption_for(page) == "Eine Molchreuse & ein Korb\nhttps://commons/File:M.JPG"
|
|
|
|
|
|
def test_the_catalogue_list_is_cut_and_the_sentence_kept() -> None:
|
|
# The Fisheries records append their filing fields as an HTML list.
|
|
page = _page("Round Fyke (Bollreuse). Prussia\n<ul><li>Subject: Fishing nets</li></ul>")
|
|
assert caption_for(page) == "Round Fyke (Bollreuse). Prussia"
|
|
|
|
|
|
@pytest.mark.parametrize("description", [None, "", "<ul><li>Subject: only filing</li></ul>"])
|
|
def test_a_description_that_says_nothing_falls_back_to_the_title(description: str | None) -> None:
|
|
assert caption_for(_page(description)) == "Molchreuse"
|
|
|
|
|
|
def test_a_long_description_is_cut_rather_than_burying_the_photo() -> None:
|
|
page = _page("word " * 200)
|
|
caption = caption_for(page)
|
|
assert caption.endswith("…")
|
|
# rstrip() before the ellipsis means the exact length depends on where
|
|
# the cut landed; the ceiling is what matters.
|
|
assert len(caption) <= CAPTION_LIMIT + 1
|
|
|
|
|
|
def test_a_page_commons_could_not_find_is_an_image_fetch_error() -> None:
|
|
# A deleted or renumbered page id comes back as {"missing": true}.
|
|
for document in ({"query": {"pages": [{"missing": True}]}}, {"query": {"pages": []}}, {}, []):
|
|
with pytest.raises(ImageFetchError, match="named no image"):
|
|
page_of(document)
|
|
|
|
|
|
def test_the_page_comes_back_when_commons_did_find_it() -> None:
|
|
assert page_of({"query": {"pages": [_page("x")]}})["title"] == "File:Molchreuse.JPG"
|
|
|
|
|
|
def test_a_template_that_is_not_a_description_falls_back_to_the_title() -> None:
|
|
# Measured on a Maltese fish trap: the filename said more than Commons'
|
|
# own description field did.
|
|
page = _page(
|
|
"This photo has been taken in the country: Malta",
|
|
title="File:Hand woven fish trap.jpg",
|
|
)
|
|
assert caption_for(page) == "Hand woven fish trap"
|