Coverage for src/software_citation_sync/discovery.py: 48%

99 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 json 

8from pathlib import Path 

9from typing import cast 

10 

11import tomlkit 

12from tomlkit.items import Table 

13 

14from software_citation_sync.config import ProjectMetadata 

15 

16 

17def discover_version(root: Path) -> str | None: 

18 package_json = root / "package.json" 

19 pyproject_toml = root / "pyproject.toml" 

20 if package_json.exists(): 

21 data: object = json.loads(package_json.read_text(encoding="utf-8")) 

22 if not isinstance(data, dict): 

23 msg = "package.json must contain a JSON object" 

24 raise TypeError(msg) 

25 package = cast("dict[str, object]", data) 

26 version = package["version"] 

27 if not isinstance(version, str): 

28 msg = "package.json `version` must be a string" 

29 raise TypeError(msg) 

30 return str(version) 

31 if pyproject_toml.exists(): 

32 data = tomlkit.parse(pyproject_toml.read_text(encoding="utf-8")) 

33 project = data["project"] 

34 if not isinstance(project, Table): 

35 msg = "pyproject.toml `[project]` must be a table" 

36 raise TypeError(msg) 

37 version = project["version"] 

38 if not isinstance(version, str): 

39 msg = "pyproject.toml `project.version` must be a string" 

40 raise TypeError(msg) 

41 return str(version) 

42 return None 

43 

44 

45def discover_project_metadata(root: Path) -> ProjectMetadata | None: 

46 package_json = root / "package.json" 

47 pyproject_toml = root / "pyproject.toml" 

48 if package_json.exists(): 

49 return _discover_package_json_metadata(package_json) 

50 if pyproject_toml.exists(): 

51 return _discover_pyproject_metadata(pyproject_toml) 

52 return None 

53 

54 

55def _discover_package_json_metadata(path: Path) -> ProjectMetadata: 

56 data: object = json.loads(path.read_text(encoding="utf-8")) 

57 if not isinstance(data, dict): 

58 msg = "package.json must contain a JSON object" 

59 raise TypeError(msg) 

60 package = cast("dict[str, object]", data) 

61 title = package["name"] 

62 if not isinstance(title, str): 

63 msg = "package.json `name` must be a string" 

64 raise TypeError(msg) 

65 return ProjectMetadata(title=str(title), authors=_package_json_authors(package)) 

66 

67 

68def _discover_pyproject_metadata(path: Path) -> ProjectMetadata: 

69 data = tomlkit.parse(path.read_text(encoding="utf-8")) 

70 project = data["project"] 

71 if not isinstance(project, Table): 

72 msg = "pyproject.toml `[project]` must be a table" 

73 raise TypeError(msg) 

74 title = project["name"] 

75 if not isinstance(title, str): 

76 msg = "pyproject.toml `project.name` must be a string" 

77 raise TypeError(msg) 

78 return ProjectMetadata(title=str(title), authors=_pyproject_authors(project)) 

79 

80 

81def _package_json_authors(package: dict[str, object]) -> tuple[str, ...]: 

82 if "author" in package: 

83 return (_package_json_author_name(package["author"]),) 

84 contributors = package["contributors"] 

85 if not isinstance(contributors, list): 

86 msg = "package.json `contributors` must be an array" 

87 raise TypeError(msg) 

88 return tuple(_package_json_author_name(contributor) for contributor in contributors) 

89 

90 

91def _package_json_author_name(author: object) -> str: 

92 if isinstance(author, str): 

93 return str(author) 

94 if not isinstance(author, dict): 

95 msg = "package.json authors must be strings or objects" 

96 raise TypeError(msg) 

97 author_data = cast("dict[str, object]", author) 

98 name = author_data["name"] 

99 if not isinstance(name, str): 

100 msg = "package.json author `name` must be a string" 

101 raise TypeError(msg) 

102 return str(name) 

103 

104 

105def _pyproject_authors(project: Table) -> tuple[str, ...]: 

106 authors = project["authors"] 

107 if not isinstance(authors, list): 

108 msg = "pyproject.toml `project.authors` must be an array" 

109 raise TypeError(msg) 

110 return tuple(_pyproject_author_name(author) for author in authors) 

111 

112 

113def _pyproject_author_name(author: object) -> str: 

114 if not isinstance(author, dict): 

115 msg = "pyproject.toml authors must be tables" 

116 raise TypeError(msg) 

117 author_data = cast("dict[str, object]", author) 

118 name = author_data["name"] 

119 if not isinstance(name, str): 

120 msg = "pyproject.toml author `name` must be a string" 

121 raise TypeError(msg) 

122 return str(name)