Skip to content

Commit 0741a32

Browse files
authored
fix: isolate malformed metadata source failures (#120)
1 parent 8eee004 commit 0741a32

2 files changed

Lines changed: 71 additions & 2 deletions

File tree

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
"""Tests for isolating expected metadata-source failures."""
2+
3+
from __future__ import annotations
4+
5+
import asyncio
6+
7+
import pytest
8+
9+
import wenxian.from_identifier as identifier_module
10+
from wenxian.reference import Reference
11+
12+
13+
def test_sync_doi_keeps_good_result_when_one_source_has_bad_payload(monkeypatch):
14+
"""Test one malformed source does not discard usable DOI metadata."""
15+
monkeypatch.setattr(
16+
identifier_module.Pubmed,
17+
"from_doi",
18+
lambda self, doi: Reference(title="Usable", journal="Journal"),
19+
)
20+
monkeypatch.setattr(
21+
identifier_module.Crossref,
22+
"from_doi",
23+
lambda self, doi: (_ for _ in ()).throw(KeyError("message")),
24+
)
25+
monkeypatch.setattr(identifier_module.Arxiv, "from_doi", lambda self, doi: None)
26+
monkeypatch.setattr(identifier_module.Chemrxiv, "from_doi", lambda self, doi: None)
27+
monkeypatch.setattr(
28+
identifier_module.Semanticscholar, "from_doi", lambda self, doi: None
29+
)
30+
31+
assert identifier_module.from_doi("10.1234/example") == Reference(
32+
title="Usable", journal="Journal"
33+
)
34+
35+
36+
def test_async_doi_keeps_good_result_when_one_source_has_bad_payload(monkeypatch):
37+
"""Test async aggregation also isolates malformed source payloads."""
38+
39+
async def good(self, doi):
40+
return Reference(title="Usable", journal="Journal")
41+
42+
async def bad(self, doi):
43+
raise TypeError("malformed payload")
44+
45+
async def missing(self, doi):
46+
return None
47+
48+
monkeypatch.setattr(identifier_module.Pubmed, "async_from_doi", good)
49+
monkeypatch.setattr(identifier_module.Crossref, "async_from_doi", bad)
50+
monkeypatch.setattr(identifier_module.Arxiv, "async_from_doi", missing)
51+
monkeypatch.setattr(identifier_module.Chemrxiv, "async_from_doi", missing)
52+
monkeypatch.setattr(identifier_module.Semanticscholar, "async_from_doi", missing)
53+
54+
assert asyncio.run(
55+
identifier_module.async_from_doi("10.1234/example")
56+
) == Reference(title="Usable", journal="Journal")
57+
58+
59+
def test_native_programming_error_still_escapes(monkeypatch):
60+
"""Test unexpected programming errors are not swallowed on native Python."""
61+
monkeypatch.setattr(identifier_module.sys, "platform", "linux")
62+
63+
def broken(identifier):
64+
raise RuntimeError("bug")
65+
66+
with pytest.raises(RuntimeError, match="bug"):
67+
identifier_module._fetch_safely("test", broken, "id")

wenxian/from_identifier.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from concurrent.futures import ThreadPoolExecutor
88
from difflib import SequenceMatcher
99
from typing import TYPE_CHECKING, TypeVar
10+
from xml.etree.ElementTree import ParseError
1011

1112
from requests.exceptions import RequestException
1213

@@ -25,6 +26,7 @@
2526
from collections.abc import Awaitable, Callable, Iterable
2627

2728
T = TypeVar("T")
29+
_SOURCE_DATA_ERRORS = (KeyError, IndexError, TypeError, ValueError, ParseError)
2830

2931

3032
def _title_similarity(title1: str, title2: str) -> float:
@@ -40,7 +42,7 @@ def _fetch_safely(
4042
"""Fetch from one source without aborting a fallback chain."""
4143
try:
4244
return fetcher(identifier)
43-
except (OSError, RequestException) as exc:
45+
except (OSError, RequestException, *_SOURCE_DATA_ERRORS) as exc:
4446
logger.warning("%s lookup failed for %s: %s", source, identifier, exc)
4547
return None
4648
except Exception as exc:
@@ -58,7 +60,7 @@ async def _async_fetch_safely(
5860
"""Fetch from one source asynchronously without aborting other sources."""
5961
try:
6062
return await fetcher(identifier)
61-
except (OSError, RequestException) as exc:
63+
except (OSError, RequestException, *_SOURCE_DATA_ERRORS) as exc:
6264
logger.warning("%s lookup failed for %s: %s", source, identifier, exc)
6365
return None
6466
except Exception as exc:

0 commit comments

Comments
 (0)