Skip to content

Commit

Permalink
Add generic *_plugin Jinja2 tests (#2285)
Browse files Browse the repository at this point in the history
  • Loading branch information
bartfeenstra authored Jan 29, 2025
1 parent 9cda18f commit 7076200
Show file tree
Hide file tree
Showing 9 changed files with 163 additions and 31 deletions.
2 changes: 1 addition & 1 deletion betty/assets/templates/head.html.j2
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
{{ page_resource.dump_linked_data(project) | tojson }}
</script>
{% endif %}
{% if page_resource is entity and page_resource is persistent_entity_id %}
{% if page_resource is entity_plugin and page_resource is persistent_entity_id %}
<link rel="alternate" href="{{ page_resource | localized_url(media_type='application/json') }}" hreflang="und" type="application/json">
{% endif %}
{% endif %}
Expand Down
91 changes: 90 additions & 1 deletion betty/jinja2/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,45 @@

from __future__ import annotations

from typing import Any, TYPE_CHECKING, Self
from typing import Any, TYPE_CHECKING, Self, Generic, TypeVar

from typing_extensions import override

from betty.ancestry.event_type import EventType
from betty.ancestry.event_type.event_types import (
StartOfLifeEventType,
EndOfLifeEventType,
)
from betty.ancestry.gender import Gender
from betty.ancestry.has_file_references import HasFileReferences
from betty.ancestry.link import HasLinks
from betty.ancestry.place_type import PlaceType
from betty.ancestry.presence_role import PresenceRole
from betty.ancestry.presence_role.presence_roles import Subject, Witness
from betty.copyright_notice import CopyrightNotice
from betty.date import DateRange
from betty.factory import IndependentFactory
from betty.json.linked_data import LinkedDataDumpable
from betty.license import License
from betty.model import (
Entity,
UserFacingEntity,
ENTITY_TYPE_REPOSITORY,
persistent_id,
)
from betty.plugin import Plugin
from betty.privacy import is_private, is_public
from betty.typing import internal
from betty.warnings import deprecated

if TYPE_CHECKING:
from betty.machine_name import MachineName
from collections.abc import Mapping, Callable
from betty.ancestry.event import Event
from betty.plugin import PluginIdToTypeMapping

_PluginT = TypeVar("_PluginT", bound=Plugin)


def test_linked_data_dumpable(value: Any) -> bool:
"""
Expand All @@ -41,6 +51,34 @@ def test_linked_data_dumpable(value: Any) -> bool:
return isinstance(value, LinkedDataDumpable)


class PluginTester(Generic[_PluginT]):
"""
Provides tests for a specific plugin type.
"""

def __init__(self, plugin_type: type[_PluginT], plugin_type_name: str):
self._plugin_type = plugin_type
self._plugin_type_name = plugin_type_name

def tests(self) -> Mapping[str, Callable[..., bool]]:
"""
Get the available tests, keyed by test name.
"""
return {f"{self._plugin_type_name}_plugin": self}

def __call__(
self, value: Any, plugin_identifier: MachineName | None = None
) -> bool:
"""
:param entity_type_id: If given, additionally ensure the value is an entity of this type.
"""
if not isinstance(value, self._plugin_type):
return False
if plugin_identifier is not None and value.plugin_id() != plugin_identifier:
return False
return True


class TestEntity(IndependentFactory):
"""
Test if a value is an entity.
Expand All @@ -54,6 +92,9 @@ def __init__(self, entity_type_id_to_type_mapping: PluginIdToTypeMapping[Entity]
async def new(cls) -> Self:
return cls(await ENTITY_TYPE_REPOSITORY.mapping())

@deprecated(
"This test has been deprecated since Betty 0.4.5, and will be removed in Betty 0.5. Instead use the `entity_plugin` test."
)
def __call__(
self, value: Any, entity_type_identifier: MachineName | None = None
) -> bool:
Expand Down Expand Up @@ -88,13 +129,19 @@ def test_has_file_references(value: Any) -> bool:
return isinstance(value, HasFileReferences)


@deprecated(
"This test has been deprecated since Betty 0.4.5, and will be removed in Betty 0.5. Instead use the `presence_role_plugin` test."
)
def test_subject_role(value: Any) -> bool:
"""
Test if a presence role is that of Subject.
"""
return isinstance(value, Subject)


@deprecated(
"This test has been deprecated since Betty 0.4.5, and will be removed in Betty 0.5. Instead use the `presence_role_plugin` test."
)
def test_witness_role(value: Any) -> bool:
"""
Test if a presence role is that of Witness.
Expand Down Expand Up @@ -142,4 +189,46 @@ async def tests() -> Mapping[str, Callable[..., bool]]:
"subject_role": test_subject_role,
"user_facing_entity": test_user_facing_entity,
"witness_role": test_witness_role,
**(
PluginTester(
CopyrightNotice, # type: ignore[type-abstract]
"copyright_notice",
)
).tests(),
**(
PluginTester(
Entity, # type: ignore[type-abstract]
"entity",
)
).tests(),
**(
PluginTester(
EventType, # type: ignore[type-abstract]
"event_type",
)
).tests(),
**(
PluginTester(
Gender, # type: ignore[type-abstract]
"gender",
)
).tests(),
**(
PluginTester(
License, # type: ignore[type-abstract]
"license",
)
).tests(),
**(
PluginTester(
PlaceType, # type: ignore[type-abstract]
"place_type",
)
).tests(),
**(
PluginTester(
PresenceRole, # type: ignore[type-abstract]
"presence_role",
)
).tests(),
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
</div>

<div class="featured-entity-feature">
{% set places = place.walk_enclosees | map(attribute='enclosee') | select('entity', 'place') | list %}
{% set places = place.walk_enclosees | map(attribute='enclosee') | select('entity_plugin', 'place') | list %}
{% if place.coordinates %}
{% set places = places + [place] %}
{% endif %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
{%- if event.description -%}
{% set formatted_event = formatted_event + ' (' + event.description | localize + ')' | safe %}
{%- endif -%}
{%- set subjects = event.presences | select('public') | selectattr('role', 'subject_role') | map(attribute='person') | select('public') | list -%}
{%- set subjects = event.presences | select('public') | selectattr('role', 'presence_role_plugin', 'subject') | map(attribute='person') | select('public') | list -%}
{%- set non_context_subjects = subjects | reject('eq', person_context) | list -%}
{%- set formatted_subjects = non_context_subjects | map(person_label) | join(', ') %}
{%- if non_context_subjects | length == 0 -%}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
{%- if alternative_names -%}
<span class="aka">{%- trans names=alternative_names | map(_embedded_person_name_label if embedded else _person_name_label) | list | join(', ') -%}Also known as {{ names }}{%- endtrans -%}</span>
{%- endif -%}
{% set events = person.presences | selectattr('role', 'subject_role') | map(attribute='event') | reject('none') | select('public') | list %}
{% set events = person.presences | selectattr('role', 'presence_role_plugin', 'subject') | map(attribute='event') | reject('none') | select('public') | list %}
{% set start_of_life_events = events | select('start_of_life_event') | list %}
{%- if start_of_life_events | length -%}
{% set start_of_life_event = start_of_life_events | first %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

{% set ns = namespace(witnesses=[]) %}
{% for presence in event.presences | select('public') %}
{% if presence.role is witness_role %}
{% if presence.role is presence_role_plugin('witness') %}
{% set ns.witnesses = ns.witnesses + [presence.person] %}
{% endif %}
{% endfor %}
Expand All @@ -35,7 +35,7 @@

{% set ns = namespace(attendees=[]) %}
{% for presence in event.presences | select('public') %}
{% if not presence.role is subject_role and not presence.role is witness_role %}
{% if not presence.role is presence_role_plugin('subject') and not presence.role is presence_role_plugin('witness') %}
{% set ns.attendees = ns.attendees + [presence.person] %}
{% endif %}
{% endfor %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
{% block page_content %}
{% include 'entity/meta--place.html.j2' %}

{% set places = place.walk_enclosees | map(attribute='enclosee') | select('entity', 'place') | list %}
{% set places = place.walk_enclosees | map(attribute='enclosee') | select('entity_plugin', 'place') | list %}
{% if place.coordinates %}
{% set places = places + [place] %}
{% endif %}
Expand Down
79 changes: 58 additions & 21 deletions betty/tests/jinja2/test_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,51 @@
Unknown as UnknownPresenceRole,
)
from betty.date import DateRange, Date
from betty.jinja2.test import PluginTester
from betty.json.linked_data import LinkedDataDumpableJsonLdObject
from betty.test_utils.ancestry.event_type import DummyEventType
from betty.test_utils.jinja2 import TemplateStringTestBase
from betty.test_utils.model import DummyUserFacingEntity
from betty.test_utils.plugin import DummyPlugin
from betty.tests.ancestry.test___init__ import DummyHasFileReferences
from betty.tests.ancestry.test_link import DummyHasLinks
from betty.warnings import BettyDeprecationWarning

if TYPE_CHECKING:
from betty.machine_name import MachineName
from betty.model import Entity


class DummyPluginOne(DummyPlugin):
pass


class DummyPluginTwo(DummyPlugin):
pass


class TestPluginTester(TemplateStringTestBase):
def test_tests(self):
sut = PluginTester(DummyPlugin, "dummy_plugin")
assert "dummy_plugin_plugin" in sut.tests()

@pytest.mark.parametrize(
("expected", "plugin_identifier", "data"),
[
(True, None, DummyPluginOne()),
(True, DummyPluginOne.plugin_id(), DummyPluginOne()),
(False, DummyPluginOne.plugin_id(), DummyPluginTwo()),
(False, None, None),
(False, None, object()),
],
)
async def test___call__(
self, expected: bool, plugin_identifier: MachineName | None, data: Any
) -> None:
sut = PluginTester(DummyPlugin, "dummy_plugin")
assert sut(data, plugin_identifier) == expected


class TestTestEntity(TemplateStringTestBase):
@pytest.mark.parametrize(
("expected", "entity_type_identifier", "data"),
Expand Down Expand Up @@ -66,13 +100,14 @@ async def test___call__(
else f'"{entity_type_identifier.plugin_id()}"'
)
template = f"{{% if data is entity({entity_type_identifier_arg}) %}}true{{% else %}}false{{% endif %}}"
async with self.assert_template_string(
template=template,
data={
"data": data,
},
) as (actual, _):
assert actual == expected
with pytest.warns(BettyDeprecationWarning):
async with self.assert_template_string(
template=template,
data={
"data": data,
},
) as (actual, _):
assert actual == expected


class TestTestSubjectRole(TemplateStringTestBase):
Expand All @@ -87,13 +122,14 @@ class TestTestSubjectRole(TemplateStringTestBase):
)
async def test(self, expected: str, data: Any) -> None:
template = "{% if data is subject_role %}true{% else %}false{% endif %}"
async with self.assert_template_string(
template=template,
data={
"data": data,
},
) as (actual, _):
assert actual == expected
with pytest.warns(BettyDeprecationWarning):
async with self.assert_template_string(
template=template,
data={
"data": data,
},
) as (actual, _):
assert actual == expected


class TestTestWitnessRole(TemplateStringTestBase):
Expand All @@ -108,13 +144,14 @@ class TestTestWitnessRole(TemplateStringTestBase):
)
async def test(self, expected: str, data: Any) -> None:
template = "{% if data is witness_role %}true{% else %}false{% endif %}"
async with self.assert_template_string(
template=template,
data={
"data": data,
},
) as (actual, _):
assert actual == expected
with pytest.warns(BettyDeprecationWarning):
async with self.assert_template_string(
template=template,
data={
"data": data,
},
) as (actual, _):
assert actual == expected


class TestTestDateRange(TemplateStringTestBase):
Expand Down
10 changes: 8 additions & 2 deletions documentation/usage/templating/tests.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,20 @@ Built-in tests
--------------
In addition to Jinja2's built-in tests, Betty provides the following:

- :py:func:`copyright_notice_plugin <betty.jinja2.test.PluginTester.__call__>`
- :py:func:`date_range <betty.jinja2.test.test_date_range>`
- :py:func:`end_of_life_event <betty.jinja2.test.test_end_of_life_event>`
- :py:func:`entity <betty.jinja2.test.TestEntity>`
- :py:func:`entity <betty.jinja2.test.TestEntity.__call__>`
- :py:func:`entity_plugin <betty.jinja2.test.PluginTester.__call__>`
- :py:func:`event_type_plugin <betty.jinja2.test.PluginTester.__call__>`
- :py:func:`gender_plugin <betty.jinja2.test.PluginTester.__call__>`
- :py:func:`has_file_references <betty.jinja2.test.test_has_file_references>`
- :py:func:`persistent_id <betty.model.persistent_id>`
- :py:func:`has_links <betty.jinja2.test.test_has_links>`
- :py:func:`license_plugin <betty.jinja2.test.PluginTester.__call__>`
- :py:func:`linked_data_dumpable <betty.jinja2.test.test_linked_data_dumpable>`
- :py:func:`persistent_entity_id <betty.model.persistent_id>`
- :py:func:`place_type_plugin <betty.jinja2.test.PluginTester.__call__>`
- :py:func:`presence_role_plugin <betty.jinja2.test.PluginTester.__call__>`
- :py:func:`private <betty.privacy.is_private>`
- :py:func:`public <betty.privacy.is_public>`
- :py:func:`start_of_life_event <betty.jinja2.test.test_start_of_life_event>`
Expand Down

0 comments on commit 7076200

Please sign in to comment.