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
« 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
5from __future__ import annotations
7import json
8from pathlib import Path
9from typing import cast
11import tomlkit
12from tomlkit.items import Table
14from software_citation_sync.config import ProjectMetadata
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
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
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))
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))
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)
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)
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)
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)