Coverage for src/software_citation_sync/readme.py: 94%

33 statements  

« prev     ^ index     » next       coverage.py v7.14.3, created at 2026-06-27 17:58 +0000

1# SPDX-FileCopyrightText: 2026 Arcangelo Massari <github@a.arcangelomassari.com> 

2# 

3# SPDX-License-Identifier: ISC 

4 

5from __future__ import annotations 

6 

7import re 

8from pathlib import Path 

9 

10START_MARKER = "<!-- software-citation-action:start -->" 

11END_MARKER = "<!-- software-citation-action:end -->" 

12BIBTEX_REFERENCE_PATTERN = re.compile(r"[^0-9A-Za-z_.:-]+") 

13 

14 

15def bibtex_reference(title: str, version: str) -> str: 

16 return BIBTEX_REFERENCE_PATTERN.sub("-", f"{title}-{version}").strip("-") 

17 

18 

19def expected_readme_block(version: str, bibtex: str) -> str: 

20 return "\n".join( 

21 [ 

22 START_MARKER, 

23 f"To cite the latest version of this software ({version}), use this BibTeX entry:", 

24 "", 

25 "```bibtex", 

26 bibtex, 

27 "```", 

28 END_MARKER, 

29 ], 

30 ) 

31 

32 

33def readme_block_matches(path: Path, version: str, bibtex: str) -> bool: 

34 content = path.read_text(encoding="utf-8") 

35 return _current_block(content) == expected_readme_block(version, bibtex) 

36 

37 

38def write_readme_block(path: Path, version: str, bibtex: str) -> bool: 

39 content = path.read_text(encoding="utf-8") 

40 block = expected_readme_block(version, bibtex) 

41 current_block = _current_block(content) 

42 if current_block == block: 

43 return False 

44 if current_block is None: 

45 new_content = content.rstrip() + "\n\n" + block + "\n" 

46 else: 

47 new_content = content.replace(current_block, block) 

48 path.write_text(new_content, encoding="utf-8") 

49 return True 

50 

51 

52def _current_block(content: str) -> str | None: 

53 start = content.find(START_MARKER) 

54 end = content.find(END_MARKER) 

55 if start == -1 and end == -1: 

56 return None 

57 if start == -1 or end == -1 or end < start: 

58 msg = "README citation block markers are incomplete or out of order" 

59 raise ValueError(msg) 

60 return content[start : end + len(END_MARKER)]