Coverage for collectra/types/texts.py: 77.19%

57 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-16 09:56 +0000

1from dataclasses import dataclass, field 

2from difflib import SequenceMatcher 

3from io import StringIO 

4from pathlib import Path 

5 

6from markdown import Markdown as MDown 

7 

8from .base import Artefact 

9 

10__all__ = ["Text"] 

11 

12 

13def unmark_element(element, stream=None): 

14 if stream is None: 

15 stream = StringIO() 

16 if element.text: 

17 stream.write(element.text) 

18 for sub in element: 

19 unmark_element(sub, stream) 

20 if element.tail: 

21 stream.write(element.tail) 

22 return stream.getvalue() 

23 

24 

25# patching Markdown 

26MDown.output_formats["plain"] = unmark_element 

27__md = MDown(output_format="plain") 

28__md.stripTopLevelTags = False 

29 

30 

31def unmark(text): 

32 return __md.convert(text) 

33 

34 

35@dataclass 

36class Text(Artefact): 

37 

38 data: str | Path | None = field(default=None) 

39 path: str | Path | None = field(default=None) 

40 

41 def __post_init__(self): 

42 super().__post_init__() 

43 if self.path is not None and self.data is not None: 

44 raise ValueError("Text accepts either data or path, not both") 

45 # Continue accepting legacy file references in data. 

46 if self.path is None and self.data: 

47 try: 

48 if Path(self.data).is_file(): 

49 self.path, self.data = Path(self.data), None 

50 except (OSError, ValueError): 

51 pass 

52 if self.path is not None: 

53 self.path = Path(self.path) 

54 self.data = self.path.read_text(encoding="utf-8") 

55 elif self.data is None: 

56 self.data = "" 

57 

58 def serialize(self) -> dict: 

59 serialized = super().serialize() 

60 if self.path is not None: 

61 serialized.pop("data", None) 

62 serialized["path"] = str(self.path) 

63 else: 

64 serialized.pop("path", None) 

65 return serialized 

66 

67 def __call__(self) -> str: 

68 return str(self.data) 

69 

70 def display(self, context) -> dict: 

71 return context.text(self) 

72 

73 def evaluate(self, gold: "Text") -> float: 

74 if not isinstance(gold, Text): 

75 raise ValueError("Reference data must be an instance of Text.") 

76 ratio = SequenceMatcher(None, str(self.data), str(gold.data)).ratio() 

77 return ratio