Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions taxonomy/applicator/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -1317,6 +1317,10 @@ def build_plan(
f"change {index}: field {field_name!r} is not an "
"ADT tag field"
)
# Work around https://github.com/JelleZijlstra/pycroscope/issues/520.
field = cast( # type: ignore[redundant-cast]
ADTField[Any], field
)
if field_key in planned_values:
raise RecommendationError(
f"change {index}: remove_raw must be the first change "
Expand Down
2 changes: 1 addition & 1 deletion taxonomy/applicator/location.py
Original file line number Diff line number Diff line change
Expand Up @@ -1481,7 +1481,7 @@ def add_virtual_models(plan: RecommendationPlan, builder: ProposalBuilder) -> No
change.field, change.new_value, expected_label=change.new_label
),
)
tags = tuple(proposal.tags or ())
tags = tuple(proposal.tags)
tags = tuple(tag for tag in tags if tag not in row.remove_tags)
tags = (*tags, *(tag for tag in row.add_tags if tag not in tags))
proposal.tags = tuple(sorted(set(tags))) # type: ignore[assignment]
Expand Down
8 changes: 6 additions & 2 deletions taxonomy/applicator/taxon.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
"""Atomic Taxon/base-Name recommendations for manifest-created classifications."""

import enum
from collections import Counter
from collections.abc import Callable, Iterable, Mapping
from dataclasses import dataclass
from typing import Any
from typing import Any, TypeVar

from taxonomy.applicator import generic
from taxonomy.applicator.proposals import ProposalBuilder
Expand Down Expand Up @@ -72,7 +73,10 @@ def _string(data: Mapping[str, Any], key: str, line: int) -> str:
return value


def _enum(data: Mapping[str, Any], key: str, cls: type[Any], line: int) -> Any:
EnumT = TypeVar("EnumT", bound=enum.Enum)


def _enum(data: Mapping[str, Any], key: str, cls: type[EnumT], line: int) -> EnumT:
value = _string(data, key, line)
try:
return cls[value]
Expand Down
27 changes: 17 additions & 10 deletions taxonomy/applicator/test_article.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import copy
import hashlib
from collections.abc import Mapping
from dataclasses import dataclass
from pathlib import Path
from types import SimpleNamespace
from typing import Any, cast
Expand All @@ -14,6 +15,12 @@
from taxonomy.db.models.person import VirtualPerson


@dataclass(frozen=True)
class _Options:
new_path: Path
library_path: Path


def _pdf_bytes() -> bytes:
return b"%PDF-1.4\nminimal test fixture\n%%EOF\n"

Expand Down Expand Up @@ -138,7 +145,7 @@ def _build_volume_and_chapter(tmp_path: Path) -> recommendations.RecommendationP
)
return recommendations.build_plan(
rows,
options=SimpleNamespace(new_path=new_path, library_path=library_path),
options=_Options(new_path=new_path, library_path=library_path),
get_article=lambda _name: None,
articles_with_doi=lambda _doi: (),
is_catalog_folder=lambda _path: True,
Expand All @@ -157,7 +164,7 @@ def _build(tmp_path: Path) -> recommendations.RecommendationPlan:
row = recommendations.parse_recommendation(_row(pdf), 1)
return recommendations.build_plan(
(row,),
options=SimpleNamespace(new_path=new_path, library_path=library_path),
options=_Options(new_path=new_path, library_path=library_path),
get_article=lambda _name: None,
articles_with_doi=lambda _doi: (),
is_catalog_folder=lambda _path: True,
Expand Down Expand Up @@ -216,7 +223,7 @@ def test_build_plan_canonicalizes_tags_and_infers_jstor(tmp_path: Path) -> None:

plan = recommendations.build_plan(
(row,),
options=SimpleNamespace(new_path=new_path, library_path=library_path),
options=_Options(new_path=new_path, library_path=library_path),
get_article=lambda _name: None,
articles_with_doi=lambda _doi: (),
is_catalog_folder=lambda _path: True,
Expand Down Expand Up @@ -280,7 +287,7 @@ def test_existing_person_author_is_guarded_and_reused_virtually(tmp_path: Path)
)
plan = recommendations.build_plan(
(recommendations.parse_recommendation(row_data, 1),),
options=SimpleNamespace(new_path=new_path, library_path=library_path),
options=_Options(new_path=new_path, library_path=library_path),
get_article=lambda _name: None,
get_person_by_id=lambda _id: person,
articles_with_doi=lambda _doi: (),
Expand Down Expand Up @@ -328,7 +335,7 @@ def test_existing_person_author_rejects_changed_family_name(tmp_path: Path) -> N
):
recommendations.build_plan(
(recommendations.parse_recommendation(row_data, 1),),
options=SimpleNamespace(new_path=new_path, library_path=library_path),
options=_Options(new_path=new_path, library_path=library_path),
get_article=lambda _name: None,
get_person_by_id=lambda _id: person,
articles_with_doi=lambda _doi: (),
Expand All @@ -350,7 +357,7 @@ def test_build_plan_rejects_changed_staged_file(tmp_path: Path) -> None:
with pytest.raises(recommendations.RecommendationError, match="size changed"):
recommendations.build_plan(
(row,),
options=SimpleNamespace(new_path=new_path, library_path=library_path),
options=_Options(new_path=new_path, library_path=library_path),
get_article=lambda _name: None,
articles_with_doi=lambda _doi: (),
is_catalog_folder=lambda _path: True,
Expand Down Expand Up @@ -424,7 +431,7 @@ def test_inline_citation_group_is_planned_and_created(tmp_path: Path) -> None:
recommendations.parse_recommendation(row_data, 1),
recommendations.parse_recommendation(second_row_data, 2),
),
options=SimpleNamespace(new_path=new_path, library_path=library_path),
options=_Options(new_path=new_path, library_path=library_path),
get_article=lambda _name: None,
articles_with_doi=lambda _doi: (),
is_catalog_folder=lambda _path: True,
Expand Down Expand Up @@ -494,7 +501,7 @@ def test_completed_exact_state_is_idempotent(tmp_path: Path) -> None:

plan = recommendations.build_plan(
(recommendations.parse_recommendation(_row(pdf), 1),),
options=SimpleNamespace(new_path=new_path, library_path=library_path),
options=_Options(new_path=new_path, library_path=library_path),
get_article=lambda _name: existing,
articles_with_doi=lambda _doi: (existing,),
is_catalog_folder=lambda _path: True,
Expand Down Expand Up @@ -578,7 +585,7 @@ def test_build_plan_rejects_forward_planned_parent_reference(tmp_path: Path) ->
with pytest.raises(recommendations.RecommendationError, match="must be an earlier"):
recommendations.build_plan(
rows,
options=SimpleNamespace(new_path=new_path, library_path=library_path),
options=_Options(new_path=new_path, library_path=library_path),
get_article=lambda _name: None,
articles_with_doi=lambda _doi: (),
is_catalog_folder=lambda _path: True,
Expand Down Expand Up @@ -610,7 +617,7 @@ def test_build_plan_orders_forward_typed_parent_reference(tmp_path: Path) -> Non

plan = recommendations.build_plan(
rows,
options=SimpleNamespace(new_path=new_path, library_path=library_path),
options=_Options(new_path=new_path, library_path=library_path),
get_article=lambda _name: None,
articles_with_doi=lambda _doi: (),
is_catalog_folder=lambda _path: True,
Expand Down
2 changes: 2 additions & 0 deletions taxonomy/applicator/test_generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ def create_object(
model: type[BaseModel], values: Mapping[str, object]
) -> BaseModel:
obj = model.virtual(**values)
assert isinstance(obj, Location)
created.append(obj)
return obj

Expand Down Expand Up @@ -118,6 +119,7 @@ def create_object(
model: type[BaseModel], values: Mapping[str, object]
) -> BaseModel:
obj = model.virtual(**values)
assert isinstance(obj, Location)
created.append(obj)
return obj

Expand Down
8 changes: 6 additions & 2 deletions taxonomy/db/models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1530,13 +1530,17 @@ def serialize(self, value: Sequence[ADTT]) -> str | None:
def prepare_virtual(
self, value: Sequence[ADTT]
) -> tuple[str | None, Sequence[ADTT]]:
raw_value = self.serialize(value)
# Work around https://github.com/JelleZijlstra/pycroscope/issues/521.
raw_value = self.serialize(value) # static analysis: ignore[incompatible_call]
if isinstance(value, str):
return raw_value, self.deserialize(raw_value)
return raw_value, tuple(value or ())

def validate_persistent(self, value: Sequence[ADTT]) -> None:
super().validate_persistent(value)
# Work around https://github.com/JelleZijlstra/pycroscope/issues/521.
super().validate_persistent(
value # static analysis: ignore[incompatible_argument]
)

def iter_models(item: Any) -> Iterable[Model]:
if isinstance(item, Model):
Expand Down
5 changes: 3 additions & 2 deletions taxonomy/db/models/classification_entry/lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,13 +94,13 @@ def check_tags(ce: ClassificationEntry, cfg: LintConfig) -> Iterable[LintResult]
yield "unnecessary CorrectedName tag"
if counts[ClassificationEntryTag.ReferencedUsage] > 1:
yield "multiple ReferencedUsage tags"
if counts[ClassificationEntryTag.AuxiliaryName] > 1:
if counts[type(ClassificationEntryTag.AuxiliaryName)] > 1:
yield "multiple AuxiliaryName tags"
if counts[ClassificationEntryTag.VerbatimParent] > 1:
yield "multiple VerbatimParent tags"
if counts[ClassificationEntryTag.Materialize] > 1:
yield "multiple Materialize tags"
if counts[ClassificationEntryTag.OriginalCitation] > 1:
if counts[type(ClassificationEntryTag.OriginalCitation)] > 1:
yield "multiple OriginalCitation tags"
base_name_author_tags = list(
ce.get_tags(ce.tags, ClassificationEntryTag.MaterializeBaseNameAuthor)
Expand Down Expand Up @@ -2146,6 +2146,7 @@ def check_page(ce: ClassificationEntry, cfg: LintConfig) -> Iterable[LintResult]
new_page = yield from models.name.page.check_page(
ce.page, get_raw_page_regex=ce.article.get_raw_page_regex
)
assert new_page is not None
if new_page != ce.page:
fixes = [field_fix(ce, "page", new_page)]
for tag in ce.get_tags(ce.tags, ClassificationEntryTag.PageLink):
Expand Down
15 changes: 15 additions & 0 deletions taxonomy/db/models/classification_entry/test_lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -765,6 +765,21 @@ def test_mapped_entry_removes_orphaned_base_name_instructions() -> None:
assert ce.tags == (retained,)


def test_check_tags_counts_parameterless_tags() -> None:
ce = _make_ce(parent=None, auxiliary=False)

for tag, expected in (
(ClassificationEntryTag.AuxiliaryName, "multiple AuxiliaryName tags"),
(ClassificationEntryTag.OriginalCitation, "multiple OriginalCitation tags"),
):
ce.tags = (tag, tag) # type: ignore[assignment]

assert any(
expected in str(issue)
for issue in check_tags(ce, LintConfig(autofix=False, interactive=False))
)


def test_etymology_detail_is_transferred_to_mapped_name() -> None:
taxon = Taxon.virtual(valid_name="Endodonta", rank=Rank.genus, age=AgeClass.extant)
name = Name.virtual(
Expand Down
6 changes: 3 additions & 3 deletions taxonomy/db/models/name/lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -4092,6 +4092,8 @@ def check_required_fields(nam: Name, cfg: LintConfig) -> Iterable[LintResult]:
if nam.verbatim_citation and not nam.citation_group:
yield "has verbatim citation but no citation group"
if has_accessible_original_citation(nam):
citation = nam.original_citation
assert citation is not None
if (
nam.page_described is None
and not (
Expand All @@ -4108,7 +4110,6 @@ def check_required_fields(nam: Name, cfg: LintConfig) -> Iterable[LintResult]:
yield "has original citation but no original_rank"
if nam.author_tags is None:
message = "has original citation but no author_tags"
citation = nam.original_citation
if citation.issupplement() and citation.parent is not None:
authors = citation.parent.author_tags
else:
Expand All @@ -4119,7 +4120,7 @@ def check_required_fields(nam: Name, cfg: LintConfig) -> Iterable[LintResult]:
yield field_issue(message, nam, "author_tags", authors)
if nam.year is None:
message = "has original citation but no year"
yield field_issue(message, nam, "year", nam.original_citation.year)
yield field_issue(message, nam, "year", citation.year)
if (
nam.name_complex is None
and nam.group is Group.genus
Expand Down Expand Up @@ -5508,7 +5509,6 @@ def check_structured_verbatim_citation_fields(
yield replace_tag_issue(
msg, nam, tag, new_tag, field="type_tags"
)
tag = new_tag
end_page = candidate
except ValueError:
pass
Expand Down
13 changes: 10 additions & 3 deletions taxonomy/db/models/name/test_lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from taxonomy.db import coordinate_lint, models
from taxonomy.db.constants import (
AgeClass,
ArticleType,
Group,
NamingConvention,
NomenclatureStatus,
Expand All @@ -20,6 +21,7 @@
)
from taxonomy.db.models.base import LintConfig
from taxonomy.db.models.location import Location
from taxonomy.db.models.person import AuthorTag

from .lint import (
_create_name_variant_issue,
Expand Down Expand Up @@ -62,8 +64,13 @@ def test_redirect_name_issue_changes_only_redirect_fields() -> None:


def test_take_over_name_issue_uses_explicit_fields_and_tag_removal() -> None:
citation = SimpleNamespace(
parent=None, author_tags=("Author",), year="1900", issupplement=lambda: False
author = AuthorTag.Author(person=models.Person.virtual(family_name="Author"))
citation = models.Article.virtual(
name="citation",
parent=None,
author_tags=(author,),
year="1900",
type=ArticleType.JOURNAL,
)
ce = SimpleNamespace(article=citation, page="12", name="Original name")
page_link = TypeTag.AuthorityPageLink(
Expand Down Expand Up @@ -91,7 +98,7 @@ def test_take_over_name_issue_uses_explicit_fields_and_tag_removal() -> None:
assert name.original_citation is citation
assert name.page_described == "12"
assert name.original_name == "Original name"
assert name.author_tags == ("Author",)
assert name.author_tags == (author,)
assert name.year == "1900"
assert name.type_tags == ()

Expand Down
4 changes: 2 additions & 2 deletions taxonomy/db/models/test_base.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import sqlite3
from collections.abc import Iterator
from collections.abc import Generator
from contextlib import contextmanager
from types import SimpleNamespace

Expand Down Expand Up @@ -59,7 +59,7 @@ def test_lint_all_uses_read_only_context_without_autofix(
events: list[str] = []

@contextmanager
def readonly() -> Iterator[None]:
def readonly() -> Generator[None]:
events.append("enter")
try:
yield
Expand Down
4 changes: 2 additions & 2 deletions taxonomy/db/models/test_lint_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,7 @@ def test_duplicate_finder_can_return_structured_fix() -> None:
def duplicate_key(_obj: FakeObject) -> str:
return "same"

issues = list(duplicate_key.linter(second, LintConfig(autofix=False)))
issues = list(duplicate_key.linter(cast(Any, second), LintConfig(autofix=False)))

assert len(issues) == 1
assert isinstance(issues[0], LintIssue)
Expand Down Expand Up @@ -475,7 +475,7 @@ def test_legacy_autofix_branches_are_explicitly_allowlisted() -> None:
parent, (ast.FunctionDef, ast.AsyncFunctionDef)
):
parent = parents.get(parent)
assert parent is not None
assert isinstance(parent, (ast.FunctionDef, ast.AsyncFunctionDef))
branch = (str(path.relative_to(models_dir)), parent.name)
if branch in {
("base.py", "_process_lint_results"),
Expand Down
7 changes: 5 additions & 2 deletions taxonomy/db/nomenclature_book.py
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,7 @@ def get_type_specimen_text(name: Name) -> tuple[str, list[str]]:


def get_row(taxon: Taxon, name: Name, taxon_to_ces: TaxonToCEs) -> Row:
todos = []
todos: list[str] = []
order = taxon.get_derived_field("order")
family = taxon.get_derived_field("family")
interpreted_tl = name.get_type_tag(TypeTag.InterpretedTypeLocality)
Expand All @@ -411,7 +411,10 @@ def get_row(taxon: Taxon, name: Name, taxon_to_ces: TaxonToCEs) -> Row:
todos.append(
f"Base name is not valid (status: {name.nomenclature_status.name})"
)
todos += check_full_expected_base_name.linter(taxon, LintConfig())
todos.extend(
str(issue)
for issue in check_full_expected_base_name.linter(taxon, LintConfig())
)

nomenclature_text = ""
if nomen_novum_for := name.get_tag_target(NameTag.NomenNovumFor):
Expand Down
Loading