Coverage for src/glotter_core/settings.py: 100%

61 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2026-03-03 02:09 +0000

1"""Project settings""" 

2 

3from __future__ import annotations 

4 

5import os 

6from dataclasses import dataclass, field 

7from pathlib import Path 

8from typing import Any 

9from warnings import warn 

10 

11import yaml 

12 

13from .project import AcronymScheme, CoreProject 

14 

15 

16@dataclass(frozen=True, init=False) 

17class CoreSettings: 

18 """Global project settings 

19 

20 :raises: :exc:`ValueError` if invalid settings 

21 

22 :ivar str project_root: Root directory of project 

23 :ivar src source_root: Root directory for source files 

24 :ivar AcronymScheme acronym_scheme: Optional project acronym scheme. 

25 Default is :const:`AcronymScheme.two_letter_limit` 

26 :ivar projects: Dictionary whose key is the project name and whose value 

27 is the project information 

28 """ 

29 

30 project_root: str = "" 

31 acronym_scheme: AcronymScheme = AcronymScheme.two_letter_limit 

32 source_root: str = "" 

33 projects: dict[str | CoreProject] = field(default=dict) 

34 

35 def __init__(self) -> None: 

36 object.__setattr__(self, "project_root", str(Path.cwd())) 

37 parser = CoreSettingsParser(self.project_root) 

38 self._set_global_settings(parser.yml.get("settings", {})) 

39 self._set_projects(parser.yml.get("projects", {})) 

40 

41 def _set_global_settings(self, settings_item: Any) -> None: 

42 if not isinstance(settings_item, dict): 

43 raise ValueError("settings does not contain a dict") 

44 

45 acronym_scheme = settings_item.get("acronym_scheme", "two_letter_limit") 

46 try: 

47 object.__setattr__(self, "acronym_scheme", AcronymScheme[acronym_scheme]) 

48 except KeyError: 

49 raise ValueError(f'Unknown acronym scheme: "{acronym_scheme}"') 

50 

51 source_root = settings_item.get("source_root") or self.project_root 

52 object.__setattr__(self, "source_root", str(Path(source_root).resolve())) 

53 

54 def _set_projects(self, projects_item: dict[str, Any]) -> None: 

55 if not isinstance(projects_item, dict): 

56 raise ValueError("projects does not contain a dict") 

57 

58 projects = {name: CoreProject(project) for name, project in projects_item.items()} 

59 object.__setattr__(self, "projects", projects) 

60 

61 

62@dataclass(frozen=True, init=False) 

63class CoreSettingsParser: 

64 """Parse the settings file (``.glotter.yml``) 

65 

66 :param project_root: Root directory of project 

67 :raises: :exc:`ValueError` if setting file does not contain a dictionary 

68 

69 :ivar str project_root: Root directory of project 

70 :ivar str | None yml_path: Path to ``.glotter.yml`` file 

71 :ivar dict[str, Any] yml: Contents of ``.glotter.yml`` file 

72 """ 

73 

74 project_root: str 

75 yml_path: str | None = None 

76 yml: dict[str, Any] = field(default_factory=dict, repr=False) 

77 

78 def __init__(self, project_root): 

79 object.__setattr__(self, "project_root", project_root) 

80 object.__setattr__(self, "yml_path", self._locate_yml()) 

81 

82 yml = None 

83 if self.yml_path is not None: 

84 yml = self._parse_yml() 

85 else: 

86 object.__setattr__(self, "yml_path", self.project_root) 

87 warn(f'.glotter.yml not found in directory "{self.project_root}"') 

88 

89 if yml is None: 

90 yml = {} 

91 

92 if not isinstance(yml, dict): 

93 raise ValueError(".glotter.yml does not contain a dict") 

94 

95 object.__setattr__(self, "yml", yml) 

96 

97 def _parse_yml(self) -> Any: 

98 contents = Path(self.yml_path).read_text(encoding="utf-8") 

99 return yaml.safe_load(contents) 

100 

101 def _locate_yml(self) -> str | None: 

102 for root, _, files in os.walk(self.project_root): 

103 if ".glotter.yml" in files: 

104 return str((Path(root) / ".glotter.yml").resolve()) 

105 

106 return None 

107 

108 

109__all__ = ["CoreSettings", "CoreSettingsParser"]