diff --git a/pyproject.toml b/pyproject.toml index a756819..8e6db30 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,8 @@ dependencies = [ "pypdf>=3.17.0", "lxml>=4.9.0", "matplotlib>=3.5.0", + "citeproc-py>=0.9.0", + "citeproc-py-styles>=0.1.5", ] [dependency-groups] diff --git a/src/lit_agent/identifiers/api.py b/src/lit_agent/identifiers/api.py index ca1537d..6c22421 100644 --- a/src/lit_agent/identifiers/api.py +++ b/src/lit_agent/identifiers/api.py @@ -611,17 +611,22 @@ def render_bibliography_to_strings( } try: - entries = list(resolution_result.citations.values()) - style_obj = CitationStylesStyle(style, validate=False, locale=locale) + entries = _prepare_citeproc_entries(resolution_result.citations.values()) + style_name = _resolve_csl_style(style) + style_obj = CitationStylesStyle(style_name, validate=False, locale=locale) source = CiteProcJSON(entries) bibliography = CitationStylesBibliography(style_obj, source, formatter.plain) - for item in source.items: - citation = Citation([CitationItem(item.id)]) + for item_id in source: + citation = Citation([CitationItem(item_id)]) bibliography.register(citation) rendered = [str(entry) for entry in bibliography.bibliography()] - return rendered, {"renderer": "citeproc-py", "style": style, "locale": locale} + return rendered, { + "renderer": "citeproc", + "style": style, + "locale": locale, + } except Exception as exc: # pragma: no cover - defensive return _render_compact(resolution_result), { "renderer": "fallback", @@ -678,6 +683,45 @@ def _extract_year(citation: Dict[str, Any]) -> Optional[int]: return None +def _prepare_citeproc_entries( + citations: Iterable[Dict[str, Any]], +) -> List[Dict[str, Any]]: + """Normalize CSL entries to avoid citeproc parsing errors.""" + + prepared: List[Dict[str, Any]] = [] + for index, citation in enumerate(citations, start=1): + normalized = dict(citation) + + citation_id = normalized.get("id") + normalized["id"] = ( + str(citation_id) if citation_id not in [None, ""] else str(index) + ) + + if not normalized.get("type"): + normalized["type"] = "article-journal" + + # Remove internal fields citeproc does not understand + normalized.pop("resolution", None) + + # Ensure author list exists + if normalized.get("author") is None: + normalized["author"] = [] + + prepared.append(normalized) + + return prepared + + +def _resolve_csl_style(style: str) -> str: + """Map friendly style aliases to canonical CSL identifiers.""" + + aliases = { + "chicago": "chicago-author-date", + } + normalized = style.lower() + return aliases.get(normalized, normalized) + + def _import_citeproc(): """Import citeproc modules, isolated for easier testing.""" diff --git a/tests/integration/test_agent_integration.py b/tests/integration/test_agent_integration.py index 291ca0f..69ca05b 100644 --- a/tests/integration/test_agent_integration.py +++ b/tests/integration/test_agent_integration.py @@ -7,8 +7,6 @@ import pytest # noqa: E402 import os # noqa: E402 -import warnings # noqa: E402 -from unittest.mock import Mock, patch # noqa: E402 from lit_agent.agent_connection import ( # noqa: E402 create_agent_from_env, @@ -16,6 +14,12 @@ AnthropicAgent, ) +ALLOW_INTEGRATION_FALLBACK = os.getenv("ALLOW_INTEGRATION_FALLBACK", "").lower() in { + "1", + "true", + "yes", +} + @pytest.mark.integration class TestOpenAIIntegration: @@ -26,91 +30,46 @@ def test_openai_hello_world_query(self): api_key = os.getenv("OPENAI_API_KEY") if not api_key: - # Fallback to mock with warning - warnings.warn( - "OPENAI_API_KEY not found - falling back to mock test. " - "Set OPENAI_API_KEY environment variable for real integration testing.", - UserWarning, + if ALLOW_INTEGRATION_FALLBACK: + pytest.skip("OPENAI_API_KEY not set (allowed fallback)") + pytest.fail( + "OPENAI_API_KEY not found; set it or ALLOW_INTEGRATION_FALLBACK=1 to skip" ) - # Mock test - with patch("litellm.completion") as mock_completion: - mock_response = Mock() - mock_response.choices = [Mock()] - mock_response.choices[0].message = Mock() - mock_response.choices[0].message.content = ( - 'Here\'s a simple "Hello, World!" program in Python:\n\n' - '```python\nprint("Hello, World!")\n```\n\n' - 'This program uses the print() function to display the text "Hello, World!" to the console. ' - "It's typically the first program beginners learn when starting with Python." - ) - mock_completion.return_value = mock_response - - agent = OpenAIAgent("mock-key") - prompt = "Write a hello world program in Python. Please provide a brief answer in 2-3 sentences." - - response = agent.query(prompt) - - print("\n--- OpenAI Hello World Response (MOCK) ---") - print(response) - print("--- End Response ---\n") - - # Verify mock response - assert isinstance(response, str) - assert len(response.strip()) > 0 - assert "print" in response.lower() and "hello" in response.lower() - else: - # Real API test - agent = OpenAIAgent(api_key) - prompt = "Write a hello world program in Python. Please provide a brief answer in 2-3 sentences." - - response = agent.query(prompt) - - print("\n--- OpenAI Hello World Response (REAL API) ---") - print(response) - print("--- End Response ---\n") - - # Verify we got a meaningful response - assert isinstance(response, str) - assert len(response.strip()) > 0 - assert "print" in response.lower() or "hello" in response.lower() + # Real API test + agent = OpenAIAgent(api_key) + prompt = "Write a hello world program in Python. Please provide a brief answer in 2-3 sentences." + + response = agent.query(prompt) + + print("\n--- OpenAI Hello World Response (REAL API) ---") + print(response) + print("--- End Response ---\n") + + # Verify we got a meaningful response + assert isinstance(response, str) + assert len(response.strip()) > 0 + assert "print" in response.lower() or "hello" in response.lower() def test_openai_agent_from_env(self): """Test creating OpenAI agent from environment variables.""" api_key = os.getenv("OPENAI_API_KEY") if not api_key: - # Fallback to mock with warning - warnings.warn( - "OPENAI_API_KEY not found - falling back to mock test for factory function.", - UserWarning, + if ALLOW_INTEGRATION_FALLBACK: + pytest.skip("OPENAI_API_KEY not set (allowed fallback)") + pytest.fail( + "OPENAI_API_KEY not found; set it or ALLOW_INTEGRATION_FALLBACK=1 to skip" ) - # Mock the environment and API call - with patch.dict("os.environ", {"OPENAI_API_KEY": "mock-key"}): - with patch("litellm.completion") as mock_completion: - mock_response = Mock() - mock_response.choices = [Mock()] - mock_response.choices[0].message = Mock() - mock_response.choices[0].message.content = "Hello!" - mock_completion.return_value = mock_response - - agent = create_agent_from_env("openai") - assert isinstance(agent, OpenAIAgent) - - # Test a simple query - response = agent.query("Say hello in one word.") - assert isinstance(response, str) - assert len(response.strip()) > 0 - else: - # Real API test - agent = create_agent_from_env("openai") - assert isinstance(agent, OpenAIAgent) + # Real API test + agent = create_agent_from_env("openai") + assert isinstance(agent, OpenAIAgent) - # Test a simple query - response = agent.query("Say hello in one word.") - assert isinstance(response, str) - assert len(response.strip()) > 0 + # Test a simple query + response = agent.query("Say hello in one word.") + assert isinstance(response, str) + assert len(response.strip()) > 0 @pytest.mark.integration @@ -122,101 +81,54 @@ def test_anthropic_hello_world_query(self): api_key = os.getenv("ANTHROPIC_API_KEY") if not api_key: - # Fallback to mock with warning - warnings.warn( - "ANTHROPIC_API_KEY not found - falling back to mock test. " - "Set ANTHROPIC_API_KEY environment variable for real integration testing.", - UserWarning, + if ALLOW_INTEGRATION_FALLBACK: + pytest.skip("ANTHROPIC_API_KEY not set (allowed fallback)") + pytest.fail( + "ANTHROPIC_API_KEY not found; set it or ALLOW_INTEGRATION_FALLBACK=1 to skip" ) - # Mock test - with patch("litellm.completion") as mock_completion: - mock_response = Mock() - mock_response.choices = [Mock()] - mock_response.choices[0].message = Mock() - mock_response.choices[0].message.content = ( - "The first recorded use of 'Hello, World!' was by Brian Kernighan in 1972. " - "It appeared in 'The C Programming Language' book as a simple example program. " - "The program simply prints 'hello, world' to demonstrate basic syntax." - ) - mock_completion.return_value = mock_response - - agent = AnthropicAgent("mock-key") - prompt = ( - "What is the first recorded use of Hello World to demonstrate " - "a programming language. Please provide a brief answer in 2-3 sentences." - ) - - response = agent.query(prompt) - - print("\n--- Anthropic Hello World Response (MOCK) ---") - print(response) - print("--- End Response ---\n") - - # Verify mock response - assert isinstance(response, str) - assert len(response.strip()) > 0 - assert "hello" in response.lower() - else: - # Real API test - agent = AnthropicAgent(api_key) - prompt = ( - "What is the first recorded use of Hello World to demonstrate " - "a programming language. Please provide a brief answer in 2-3 sentences." - ) - - response = agent.query(prompt) - - # Print the response for verification - print("\n--- Anthropic Hello World Response (REAL API) ---") - print(response) - print("--- End Response ---\n") - - # Verify we got a meaningful response - assert isinstance(response, str) - assert len(response.strip()) > 0 - assert ( - "hello" in response.lower() - or "kernighan" in response.lower() - or "programming" in response.lower() - ) + # Real API test + agent = AnthropicAgent(api_key) + prompt = ( + "What is the first recorded use of Hello World to demonstrate " + "a programming language. Please provide a brief answer in 2-3 sentences." + ) + + response = agent.query(prompt) + + # Print the response for verification + print("\n--- Anthropic Hello World Response (REAL API) ---") + print(response) + print("--- End Response ---\n") + + # Verify we got a meaningful response + assert isinstance(response, str) + assert len(response.strip()) > 0 + assert ( + "hello" in response.lower() + or "kernighan" in response.lower() + or "programming" in response.lower() + ) def test_anthropic_agent_from_env(self): """Test creating Anthropic agent from environment variables.""" api_key = os.getenv("ANTHROPIC_API_KEY") if not api_key: - # Fallback to mock with warning - warnings.warn( - "ANTHROPIC_API_KEY not found - falling back to mock test for factory function.", - UserWarning, + if ALLOW_INTEGRATION_FALLBACK: + pytest.skip("ANTHROPIC_API_KEY not set (allowed fallback)") + pytest.fail( + "ANTHROPIC_API_KEY not found; set it or ALLOW_INTEGRATION_FALLBACK=1 to skip" ) - # Mock the environment and API call - with patch.dict("os.environ", {"ANTHROPIC_API_KEY": "mock-key"}): - with patch("litellm.completion") as mock_completion: - mock_response = Mock() - mock_response.choices = [Mock()] - mock_response.choices[0].message = Mock() - mock_response.choices[0].message.content = "Hello!" - mock_completion.return_value = mock_response - - agent = create_agent_from_env("anthropic") - assert isinstance(agent, AnthropicAgent) - - # Test a simple query - response = agent.query("Say hello in one word.") - assert isinstance(response, str) - assert len(response.strip()) > 0 - else: - # Real API test - agent = create_agent_from_env("anthropic") - assert isinstance(agent, AnthropicAgent) + # Real API test + agent = create_agent_from_env("anthropic") + assert isinstance(agent, AnthropicAgent) - # Test a simple query - response = agent.query("Say hello in one word.") - assert isinstance(response, str) - assert len(response.strip()) > 0 + # Test a simple query + response = agent.query("Say hello in one word.") + assert isinstance(response, str) + assert len(response.strip()) > 0 @pytest.mark.integration @@ -229,43 +141,19 @@ def test_both_agents_available(self): anthropic_available = bool(os.getenv("ANTHROPIC_API_KEY")) if not (openai_available or anthropic_available): - # No real API keys, use mocks with warning - warnings.warn( - "No API keys found for OpenAI or Anthropic - falling back to mock tests. " - "Set OPENAI_API_KEY or ANTHROPIC_API_KEY for real integration testing.", - UserWarning, + if ALLOW_INTEGRATION_FALLBACK: + pytest.skip("No API keys found (allowed fallback)") + pytest.fail( + "No API keys found for OpenAI or Anthropic; set keys or ALLOW_INTEGRATION_FALLBACK=1 to skip" ) - with patch.dict( - "os.environ", - { - "OPENAI_API_KEY": "mock-openai-key", - "ANTHROPIC_API_KEY": "mock-anthropic-key", - }, - ): - with patch("litellm.completion") as mock_completion: - mock_response = Mock() - mock_response.choices = [Mock()] - mock_response.choices[0].message = Mock() - mock_response.choices[0].message.content = "Hello!" - mock_completion.return_value = mock_response - - # Test both agents with mocks - openai_agent = create_agent_from_env("openai") - response = openai_agent.query("Hello") - assert isinstance(response, str) - - anthropic_agent = create_agent_from_env("anthropic") - response = anthropic_agent.query("Hello") - assert isinstance(response, str) - else: - # Real API tests - if openai_available: - agent = create_agent_from_env("openai") - response = agent.query("Hello") - assert isinstance(response, str) - - if anthropic_available: - agent = create_agent_from_env("anthropic") - response = agent.query("Hello") - assert isinstance(response, str) + # Real API tests + if openai_available: + agent = create_agent_from_env("openai") + response = agent.query("Hello") + assert isinstance(response, str) + + if anthropic_available: + agent = create_agent_from_env("anthropic") + response = agent.query("Hello") + assert isinstance(response, str) diff --git a/tests/integration/test_identifier_integration.py b/tests/integration/test_identifier_integration.py index a6207a1..d67010a 100644 --- a/tests/integration/test_identifier_integration.py +++ b/tests/integration/test_identifier_integration.py @@ -1,8 +1,18 @@ import importlib.util +import os import warnings +from unittest.mock import patch +from dotenv import load_dotenv import pytest -from unittest.mock import patch + +load_dotenv() + +ALLOW_INTEGRATION_FALLBACK = os.getenv("ALLOW_INTEGRATION_FALLBACK", "").lower() in { + "1", + "true", + "yes", +} from lit_agent.identifiers import ( extract_identifiers_from_bibliography, @@ -59,24 +69,33 @@ def test_ncbi_api_validation_with_known_identifiers(self, sample_known_identifie assert confidence >= 0.9 except Exception as e: - warnings.warn( - f"NCBI API validation failed for {id_type_str} {value}: {e}" - ) - print("--- NCBI API Validation Results (FAILED - using mock) ---") - print( - f"{id_type_str.upper()} {value}: API call failed, using format validation" - ) - - # Fall back to format validation test - from lit_agent.identifiers.validators import FormatValidator - - format_validator = FormatValidator() - assert format_validator.validate_identifier(id_type, value) + if ALLOW_INTEGRATION_FALLBACK: + warnings.warn( + f"NCBI API validation failed for {id_type_str} {value}: {e}" + ) + print("--- NCBI API Validation Results (FAILED - using mock) ---") + print( + f"{id_type_str.upper()} {value}: API call failed, using format validation" + ) + + # Fall back to format validation test + from lit_agent.identifiers.validators import FormatValidator + + format_validator = FormatValidator() + assert format_validator.validate_identifier(id_type, value) + else: + pytest.fail( + f"NCBI API validation failed for {id_type_str} {value}: {e}" + ) def test_metapub_validation_with_known_identifiers(self, sample_known_identifiers): """Test metapub validation with known valid identifiers.""" if importlib.util.find_spec("metapub") is None: - pytest.skip("metapub not available") + if ALLOW_INTEGRATION_FALLBACK: + pytest.skip("metapub not available (allowed fallback)") + pytest.fail( + "metapub not available; install dependency or set ALLOW_INTEGRATION_FALLBACK=1" + ) validator = MetapubValidator() @@ -97,9 +116,14 @@ def test_metapub_validation_with_known_identifiers(self, sample_known_identifier assert confidence >= 0.9 except Exception as e: - warnings.warn( - f"Metapub validation failed for {id_type_str} {value}: {e}" - ) + if ALLOW_INTEGRATION_FALLBACK: + warnings.warn( + f"Metapub validation failed for {id_type_str} {value}: {e}" + ) + else: + pytest.fail( + f"Metapub validation failed for {id_type_str} {value}: {e}" + ) def test_extraction_with_api_validation(self, sample_urls_with_identifiers): """Test extraction with real API validation when available.""" @@ -132,20 +156,23 @@ def test_extraction_with_api_validation(self, sample_urls_with_identifiers): ) except Exception as e: - warnings.warn(f"API validation test failed: {e}") - print("--- Extraction with API Validation (FAILED - using mock) ---") - - # Fall back to extraction without API validation - result = extract_identifiers_from_bibliography( - sample_urls_with_identifiers, - use_api_validation=False, - use_metapub_validation=False, - ) + if ALLOW_INTEGRATION_FALLBACK: + warnings.warn(f"API validation test failed: {e}") + print("--- Extraction with API Validation (FAILED - using mock) ---") + + # Fall back to extraction without API validation + result = extract_identifiers_from_bibliography( + sample_urls_with_identifiers, + use_api_validation=False, + use_metapub_validation=False, + ) - assert len(result.identifiers) > 0 - print( - f"Fallback extraction successful: {len(result.identifiers)} identifiers" - ) + assert len(result.identifiers) > 0 + print( + f"Fallback extraction successful: {len(result.identifiers)} identifiers" + ) + else: + pytest.fail(f"API validation test failed: {e}") def test_single_url_extraction_with_validation(self): """Test single URL extraction with validation.""" @@ -172,14 +199,17 @@ def test_single_url_extraction_with_validation(self): assert identifier.value == "37674083" except Exception as e: - warnings.warn(f"Single URL validation test failed: {e}") - print("--- Single URL Extraction (FAILED - using mock) ---") - - # Fall back without validation - identifiers = extract_identifiers_from_url(test_url) - assert len(identifiers) > 0 - assert identifiers[0].type == IdentifierType.PMID - assert identifiers[0].value == "37674083" + if ALLOW_INTEGRATION_FALLBACK: + warnings.warn(f"Single URL validation test failed: {e}") + print("--- Single URL Extraction (FAILED - using mock) ---") + + # Fall back without validation + identifiers = extract_identifiers_from_url(test_url) + assert len(identifiers) > 0 + assert identifiers[0].type == IdentifierType.PMID + assert identifiers[0].value == "37674083" + else: + pytest.fail(f"Single URL validation test failed: {e}") def test_validation_function_with_api(self, sample_known_identifiers): """Test standalone validation function with API.""" @@ -200,9 +230,14 @@ def test_validation_function_with_api(self, sample_known_identifiers): assert result["value"] == value except Exception as e: - warnings.warn( - f"Standalone validation failed for {id_type_str} {value}: {e}" - ) + if ALLOW_INTEGRATION_FALLBACK: + warnings.warn( + f"Standalone validation failed for {id_type_str} {value}: {e}" + ) + else: + pytest.fail( + f"Standalone validation failed for {id_type_str} {value}: {e}" + ) @pytest.mark.integration @@ -235,8 +270,11 @@ def test_rate_limiting_behavior(self): assert elapsed > 0 except Exception as e: - warnings.warn(f"Rate limiting test failed: {e}") - print("--- Rate Limiting Test (FAILED - API unavailable) ---") + if ALLOW_INTEGRATION_FALLBACK: + warnings.warn(f"Rate limiting test failed: {e}") + print("--- Rate Limiting Test (FAILED - API unavailable) ---") + else: + pytest.fail(f"Rate limiting test failed: {e}") def test_api_timeout_handling(self): """Test API timeout handling.""" diff --git a/tests/unit/test_rendering.py b/tests/unit/test_rendering.py index b74dba4..3375da4 100644 --- a/tests/unit/test_rendering.py +++ b/tests/unit/test_rendering.py @@ -41,3 +41,189 @@ def raise_import_error(): assert rendered[0].startswith("[1] Doe Example Paper 2024 10.1234/example") assert meta["renderer"] == "fallback" assert meta["style"] == "vancouver" + + +@pytest.mark.unit +def test_render_bibliography_vancouver_style(): + """Test proper bibliography rendering with vancouver style.""" + result = CitationResolutionResult( + citations={ + "1": { + "id": "1", + "title": "Example Paper on Glioblastoma", + "author": [{"family": "Doe", "given": "John"}], + "issued": {"date-parts": [[2024]]}, + "container-title": "Nature", + "DOI": "10.1234/example", + "resolution": {"method": "doi"}, + "URL": "https://example.com", + }, + }, + stats={"total": 1, "resolved": 1}, + failures=[], + ) + + rendered, meta = render_bibliography_to_strings(result, style="vancouver") + + # Should use citeproc renderer with citeproc-py-styles installed + assert meta["renderer"] == "citeproc", ( + "Expected citeproc renderer but got fallback. " + "Ensure citeproc-py-styles is installed: pip install citeproc-py-styles" + ) + assert meta["style"] == "vancouver" + assert meta["locale"] == "en-US" + + # Bibliography should be properly formatted and non-empty + assert len(rendered) == 1, "Should have one bibliography entry" + assert rendered[0], "Bibliography entry should not be empty" + assert "Doe" in rendered[0], "Author name should appear in citation" + assert "2024" in rendered[0], "Publication year should appear" + + +@pytest.mark.unit +def test_render_bibliography_apa_style(): + """Test bibliography rendering with APA style.""" + result = CitationResolutionResult( + citations={ + "1": { + "id": "1", + "title": "Machine Learning in Cancer Research", + "author": [ + {"family": "Smith", "given": "Jane"}, + {"family": "Johnson", "given": "Bob"}, + ], + "issued": {"date-parts": [[2023]]}, + "container-title": "Cell", + "DOI": "10.5678/test", + "resolution": {"method": "doi"}, + }, + }, + stats={"total": 1, "resolved": 1}, + failures=[], + ) + + rendered, meta = render_bibliography_to_strings(result, style="apa") + + assert meta["renderer"] == "citeproc", ( + "Expected citeproc renderer. Ensure citeproc-py-styles is installed." + ) + assert meta["style"] == "apa" + assert len(rendered) == 1 + assert "Smith" in rendered[0] + assert "2023" in rendered[0] + + +@pytest.mark.unit +def test_render_bibliography_chicago_style(): + """Test bibliography rendering with Chicago style.""" + result = CitationResolutionResult( + citations={ + "1": { + "id": "1", + "title": "The Role of Inflammation in Disease", + "author": [{"family": "Brown", "given": "Alice"}], + "issued": {"date-parts": [[2022]]}, + "container-title": "Science", + "DOI": "10.9999/chicago-test", + "resolution": {"method": "doi"}, + }, + }, + stats={"total": 1, "resolved": 1}, + failures=[], + ) + + rendered, meta = render_bibliography_to_strings(result, style="chicago") + + assert meta["renderer"] == "citeproc" + assert meta["style"] == "chicago" + assert len(rendered) == 1 + assert rendered[0] # Non-empty + + +@pytest.mark.unit +def test_render_bibliography_multiple_citations(): + """Test rendering multiple citations in correct order.""" + result = CitationResolutionResult( + citations={ + "1": { + "id": "1", + "title": "First Paper", + "author": [{"family": "Alpha", "given": "A"}], + "issued": {"date-parts": [[2021]]}, + "DOI": "10.1111/first", + }, + "2": { + "id": "2", + "title": "Second Paper", + "author": [{"family": "Beta", "given": "B"}], + "issued": {"date-parts": [[2022]]}, + "DOI": "10.2222/second", + }, + "3": { + "id": "3", + "title": "Third Paper", + "author": [{"family": "Gamma", "given": "C"}], + "issued": {"date-parts": [[2023]]}, + "DOI": "10.3333/third", + }, + }, + stats={"total": 3, "resolved": 3}, + failures=[], + ) + + rendered, meta = render_bibliography_to_strings(result, style="vancouver") + + assert meta["renderer"] == "citeproc" + assert len(rendered) == 3, "Should have three bibliography entries" + + # Check all entries are non-empty + for entry in rendered: + assert entry, "Each bibliography entry should be non-empty" + + # Check authors appear in their respective entries + assert "Alpha" in rendered[0] + assert "Beta" in rendered[1] + assert "Gamma" in rendered[2] + + +@pytest.mark.unit +def test_render_bibliography_empty_citations(): + """Test rendering with no citations.""" + result = CitationResolutionResult( + citations={}, + stats={"total": 0, "resolved": 0}, + failures=[], + ) + + rendered, meta = render_bibliography_to_strings(result, style="vancouver") + + # Even with no citations, should use citeproc if available + assert meta["renderer"] in ["citeproc", "fallback"] + assert meta["style"] == "vancouver" + assert len(rendered) == 0, "Should have no bibliography entries" + + +@pytest.mark.unit +def test_render_bibliography_with_locale(): + """Test bibliography rendering with different locale.""" + result = CitationResolutionResult( + citations={ + "1": { + "id": "1", + "title": "Example Paper", + "author": [{"family": "Doe", "given": "John"}], + "issued": {"date-parts": [[2024]]}, + "DOI": "10.1234/example", + }, + }, + stats={"total": 1, "resolved": 1}, + failures=[], + ) + + rendered, meta = render_bibliography_to_strings( + result, style="vancouver", locale="en-GB" + ) + + assert meta["renderer"] == "citeproc" + assert meta["locale"] == "en-GB" + assert len(rendered) == 1 diff --git a/uv.lock b/uv.lock index 8180473..1c2dde4 100644 --- a/uv.lock +++ b/uv.lock @@ -397,6 +397,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/33/16/82b65366ff45eec94785ca562908f5f55ab83b4e1d31ce3e00f77d6a4120/citeproc_py-0.9.0-py3-none-any.whl", hash = "sha256:4737a2adbbb8ba13e2abd6c24c29095b7d0c36722e7d377f7b046323bfc4d4f9", size = 182713, upload-time = "2025-08-25T19:47:13.39Z" }, ] +[[package]] +name = "citeproc-py-styles" +version = "0.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lxml" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/5f/f404e956c24ea487563eb898e5a34cf4a399cd3f29e0a62abfd85d8e38c5/citeproc_py_styles-0.1.5.tar.gz", hash = "sha256:20a82e22b4eb814039449ba83be8c142a385e6f6f17f5b70b1b33b3fd1f8e619", size = 3691118, upload-time = "2025-07-17T19:22:07.555Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/f4/46806ef35525b9025561f1a05cc28f7f88a19ee4e03e4d005f7da3dd46e3/citeproc_py_styles-0.1.5-py2.py3-none-any.whl", hash = "sha256:2d15558d82fc447daf49a0861f01b51ee9dbc0d2f1872ac87fb6545867620433", size = 11939977, upload-time = "2025-07-17T19:22:05.68Z" }, +] + [[package]] name = "click" version = "8.1.8" @@ -4679,6 +4692,8 @@ version = "0.1.0" source = { editable = "." } dependencies = [ { name = "beautifulsoup4" }, + { name = "citeproc-py" }, + { name = "citeproc-py-styles" }, { name = "litellm" }, { name = "lxml" }, { name = "matplotlib", version = "3.9.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, @@ -4705,6 +4720,8 @@ dev = [ [package.metadata] requires-dist = [ { name = "beautifulsoup4", specifier = ">=4.12.0" }, + { name = "citeproc-py", specifier = ">=0.9.0" }, + { name = "citeproc-py-styles", specifier = ">=0.1.5" }, { name = "litellm", specifier = ">=1.79.1" }, { name = "lxml", specifier = ">=4.9.0" }, { name = "matplotlib", specifier = ">=3.5.0" },