From c0a1c21422d864c92bab2898eeeaa4623eb44b1a Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Fri, 31 Oct 2025 14:47:26 +0000 Subject: [PATCH 1/3] Add tests for depicts.utils --- tests/test_utils.py | 109 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 tests/test_utils.py diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..2ec7136 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,109 @@ +"""Unit tests for the helpers in ``depicts.utils``. + +These tests cover common paths and a few edge cases to ensure +stable behavior across refactors. +""" + +from depicts import utils +from flask import Flask +import pytest + + +def test_ordinal() -> None: + """Convert integers to the expected English ordinals.""" + assert utils.ordinal(1) == "1st" + assert utils.ordinal(2) == "2nd" + assert utils.ordinal(3) == "3rd" + assert utils.ordinal(4) == "4th" + + +def test_chunk_basic() -> None: + """Chunk an iterable into fixed-size tuples with a remainder.""" + data = list(range(10)) + chunks = list(utils.chunk(data, 3)) + assert chunks == [(0, 1, 2), (3, 4, 5), (6, 7, 8), (9,)] + + +def test_drop_start_success_and_failure() -> None: + """Drop a matching prefix and assert on missing prefix.""" + assert utils.drop_start("Category:Painting", "Category:") == "Painting" + with pytest.raises(AssertionError): + utils.drop_start("Painting", "Category:") + + +def test_drop_category_ns() -> None: + """Remove the "Category:" namespace from a string.""" + assert utils.drop_category_ns("Category:Portraits") == "Portraits" + + +def test_parse_sitelink() -> None: + """Decode sitelink by removing prefix, unquoting, and de-underscoring.""" + start = "https://en.wikipedia.org/wiki/" + s = "https://en.wikipedia.org/wiki/Hello_World%21" + assert utils.parse_sitelink(s, start) == "Hello World!" + + +def test_word_contains_letter() -> None: + """Detect whether a token contains at least one letter.""" + assert utils.word_contains_letter("abc123") is True + assert utils.word_contains_letter("12345") is False + assert utils.word_contains_letter("!!!") is False + + +def test_also_singular_main_plural_and_singular() -> None: + """Return both plural and singular when appropriate.""" + # plural should include both plural and singular + assert set(utils.also_singular_main("Dogs")) == {"Dogs", "Dog"} + # singular should return as-is + assert utils.also_singular_main("Dog") == ["Dog"] + + +def test_also_singular_gender_and_skip_names() -> None: + """Add gender-derived forms and honor the skip list.""" + # Adds gender-derived singulars + names = utils.also_singular("Women") + assert "Women" in names + assert "woman" in names + # Skips configured names + assert utils.also_singular("National Gallery") == [] + + +def test_wiki_url_capitalization_and_ns() -> None: + """Build proper MediaWiki URLs with capitalization and namespace.""" + # lowercase first letter should be capitalized, spaces become underscores + url = utils.wiki_url("example page", site="enwiki") + expected = "https://en.wikipedia.org/wiki/Example_page" + assert url == expected + # namespace prefix when provided + url_cat = utils.wiki_url("Portraits", site="commons", ns="Category") + expected_cat = "https://commons.wikimedia.org/wiki/Category:Portraits" + assert url_cat == expected_cat + + +def test_get_int_arg_present_and_missing() -> None: + """Parse integer query args and ignore missing/non-integer values.""" + app = Flask(__name__) + with app.test_request_context("/?page=10&foo=bar"): + assert utils.get_int_arg("page") == 10 + assert utils.get_int_arg("foo") is None + assert utils.get_int_arg("missing") is None + + +def test_format_time_precisions() -> None: + """Format time strings for multiple precision levels.""" + # Full date + assert utils.format_time("+1965-04-23T00:00:00Z", 11) == "23 April 1965" + # Month precision + assert utils.format_time("+1965-04-00T00:00:00Z", 10) == "April 1965" + # Year precision + assert utils.format_time("+1965-00-00T00:00:00Z", 9) == "1965" + # Decade precision + assert utils.format_time("+1960-00-00T00:00:00Z", 8) == "1960s" + # Century precision + assert utils.format_time("+1965-00-00T00:00:00Z", 7) == "20th century" + # Millennium precision + assert utils.format_time("+2001-00-00T00:00:00Z", 6) == "3rd millennium" + # Unparseable falls back to input + assert utils.format_time("not-a-date", 9) == "not-a-date" + # BC year with unknown month/day should still return year for precision 9 + assert utils.format_time("-0123-00-00T00:00:00Z", 9) == "-123" From 2d78c2814d73128949dd4a5b272eae5cd243660e Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Fri, 31 Oct 2025 14:53:49 +0000 Subject: [PATCH 2/3] Add .gitignore file. --- .gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bee8a64 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__ From dcb0849d2615f0b3d94b168c67307152003e016e Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Fri, 31 Oct 2025 14:54:16 +0000 Subject: [PATCH 3/3] Add pager tests. --- tests/test_pager.py | 74 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 tests/test_pager.py diff --git a/tests/test_pager.py b/tests/test_pager.py new file mode 100644 index 0000000..18be310 --- /dev/null +++ b/tests/test_pager.py @@ -0,0 +1,74 @@ +"""Unit tests for pagination helpers in ``depicts.pager``. + +Covers page counting, slicing, page iteration, URL building, and Jinja init. +""" + +from __future__ import annotations + +from depicts.pager import Pagination, init_pager, url_for_other_page +from flask import Flask + + +def _make_app() -> Flask: + """Create a minimal Flask app with a paging route.""" + app = Flask(__name__) + + @app.get("/items/") + def items(category_id: int) -> str: # noqa: ARG001 - used via request + # Return a URL for a different page while preserving args. + return url_for_other_page(5) + + return app + + +def test_pagination_basic_properties() -> None: + """Compute total pages and prev/next boundaries.""" + p = Pagination(page=1, per_page=10, total_count=95) + assert p.pages == 10 + assert p.has_prev is False + assert p.has_next is True + + p2 = Pagination(page=10, per_page=10, total_count=95) + assert p2.pages == 10 + assert p2.has_prev is True + assert p2.has_next is False + + +def test_pagination_slice() -> None: + """Return the correct slice for a page window.""" + items = list(range(25)) + p = Pagination(page=2, per_page=10, total_count=len(items)) + assert p.slice(items) == list(range(10, 20)) + + +def test_iter_pages_first_middle_last() -> None: + """Iterate pages with ellipses represented by ``None`` when skipping.""" + p = Pagination(page=1, per_page=10, total_count=100) + pages = list(p.iter_pages()) + # First page shows early pages, ellipsis, then tail pages + assert pages == [1, 2, 3, 4, 5, 6, None, 9, 10] + + mid = Pagination(page=5, per_page=10, total_count=100) + # In the middle, defaults show all pages without gaps + assert list(mid.iter_pages()) == list(range(1, 11)) + + last = Pagination(page=10, per_page=10, total_count=100) + # Near the end, elide the early middle (3), keep a window around current + assert list(last.iter_pages()) == [1, 2, None, 4, 5, 6, 7, 8, 9, 10] + + +def test_url_for_other_page_preserves_args_and_view_args() -> None: + """Build a URL for another page preserving args.""" + app = _make_app() + with app.test_client() as client: + resp = client.get("/items/42?q=abc&page=3") + assert resp.status_code == 200 + # The route returns the URL generated for page=5 + assert resp.get_data(as_text=True) == "/items/42?q=abc&page=5" + + +def test_init_pager_registers_jinja_helper() -> None: + """Register the helper in the Jinja environment globals.""" + app = Flask(__name__) + init_pager(app) + assert app.jinja_env.globals["url_for_other_page"] is url_for_other_page