From 897d0a843cf4e4e0db7808e3a3671141e89d8994 Mon Sep 17 00:00:00 2001 From: Gunnar Kreitz Date: Thu, 15 May 2025 09:11:19 +0200 Subject: [PATCH 1/5] Fix loading a problem with empty problem.yaml and with no statements --- problemtools/metadata.py | 7 ++++++- tests/test_metadata.py | 9 +++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/problemtools/metadata.py b/problemtools/metadata.py index a02b341c..a02cc288 100644 --- a/problemtools/metadata.py +++ b/problemtools/metadata.py @@ -224,7 +224,12 @@ def from_legacy(cls: Type[Self], legacy: MetadataLegacy, names_from_statements: metadata = legacy.model_dump() metadata['type'] = [metadata['type']] # Support for *ancient* problems where names_from_statements is empty - metadata['name'] = names_from_statements if names_from_statements else {'': metadata['name']} + if names_from_statements: + metadata['name'] = names_from_statements + elif metadata['name']: + metadata['name'] = {'': metadata['name']} + else: + metadata['name'] = {} metadata['version'] = None def parse_author_field(author: str) -> list[Person]: diff --git a/tests/test_metadata.py b/tests/test_metadata.py index 13c65f85..71f2491e 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -8,13 +8,18 @@ def test_parse_empty_legacy(): - m = metadata.parse_metadata(formatversion.get_format_data_by_name(formatversion.VERSION_LEGACY), {}, {'en': 'Hello World!'}) + m = metadata.parse_metadata(formatversion.get_format_data_by_name(formatversion.VERSION_LEGACY), {}, {}) # Just check off a few random things - assert m.name['en'] == 'Hello World!' + assert not m.name assert not m.source assert not m.credits.authors +def test_parse_legacy_with_problem_names(): + m = metadata.parse_metadata(formatversion.get_format_data_by_name(formatversion.VERSION_LEGACY), {}, {'en': 'Hello World!'}) + assert m.name['en'] == 'Hello World!' + + def test_parse_empty_2023_fails(): with pytest.raises(ValidationError): metadata.parse_metadata(formatversion.get_format_data_by_name(formatversion.VERSION_2023_07), {}, {'en': 'Hello World!'}) From c197977f5847c181781f9bc55da41cb33c642a3b Mon Sep 17 00:00:00 2001 From: Gunnar Kreitz Date: Thu, 15 May 2025 13:50:19 +0200 Subject: [PATCH 2/5] Add utility method to load problem metadata, including names from statements when needed --- problemtools/metadata.py | 33 ++++++++++++++++++++++++---- problemtools/statement_util.py | 39 ++++++++++++++++++++++++++++++++-- tests/test_metadata.py | 19 ++++++++++++++++- 3 files changed, 84 insertions(+), 7 deletions(-) diff --git a/problemtools/metadata.py b/problemtools/metadata.py index a02cc288..ec91d8c8 100644 --- a/problemtools/metadata.py +++ b/problemtools/metadata.py @@ -3,13 +3,16 @@ import re from dataclasses import dataclass, field from enum import StrEnum +from pathlib import Path from typing import Any, Literal, Self, Type, Union from uuid import UUID from pydantic import BaseModel, ConfigDict, Field +import yaml from . import config from . import formatversion +from . import statement_util class ProblemType(StrEnum): @@ -189,7 +192,7 @@ class Metadata(BaseModel): """ problem_format_version: str - type: list[str] + type: list[ProblemType] name: dict[str, str] uuid: UUID | None version: str | None @@ -227,7 +230,7 @@ def from_legacy(cls: Type[Self], legacy: MetadataLegacy, names_from_statements: if names_from_statements: metadata['name'] = names_from_statements elif metadata['name']: - metadata['name'] = {'': metadata['name']} + metadata['name'] = {'en': metadata['name']} else: metadata['name'] = {} metadata['version'] = None @@ -306,7 +309,9 @@ def parse_person(person: str | Person) -> Person: def parse_metadata( - version: formatversion.FormatData, problem_yaml_data: dict[str, Any], names_from_statements: dict[str, str] + version: formatversion.FormatData, + problem_yaml_data: dict[str, Any], + names_from_statements: dict[str, str] | None = None, ) -> Metadata: """ Parses a data structure from problem.yaml into a Metadata model @@ -323,8 +328,28 @@ def parse_metadata( if version.name == formatversion.VERSION_LEGACY: legacy_model = MetadataLegacy.model_validate(data) - return Metadata.from_legacy(legacy_model, names_from_statements) + return Metadata.from_legacy(legacy_model, names_from_statements or {}) else: assert version.name == formatversion.VERSION_2023_07 model_2023_07 = Metadata2023_07.model_validate(data) return Metadata.from_2023_07(model_2023_07) + + +def load_metadata(problem_root: Path) -> tuple[Metadata, dict]: + """ + Loads metadata from a problem directory. + + Returns Metadata as well as the raw parsed yaml. The latter is likely only of use to verifyproblem. + Leaks exceptions, which is a bit of a mess. Unclear how to best deal with error handling. + """ + with (problem_root / 'problem.yaml').open() as f: + data = yaml.safe_load(f) + if data is None: # Loading empty yaml returns None + data = {} + + version = formatversion.get_format_data_by_name(data.get('problem_format_version', formatversion.VERSION_LEGACY)) + if version.name == formatversion.VERSION_LEGACY: + names_from_statements = statement_util.load_names_from_statements(problem_root, version) + else: + names_from_statements = None + return parse_metadata(version, data, names_from_statements), data diff --git a/problemtools/statement_util.py b/problemtools/statement_util.py index e7f8513b..a90e545c 100644 --- a/problemtools/statement_util.py +++ b/problemtools/statement_util.py @@ -1,11 +1,12 @@ -import os -from typing import Optional, List, Tuple +import collections import html import json +import os import re import subprocess import tempfile from pathlib import Path +from typing import Optional, List, Tuple from . import formatversion from . import verifyproblem @@ -14,6 +15,40 @@ FOOTNOTES_STRINGS = ['
', '