Coverage for gws-app/gws/spec/generator/manifest.py: 58%

48 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-05 13:35 +0200

1"""Reader for application manifest files (``MANIFEST.json``).""" 

2 

3import json 

4import os 

5 

6 

7class Error(Exception): 

8 """Raised when a manifest cannot be parsed.""" 

9 

10 pass 

11 

12 

13def from_path(path): 

14 """Read a manifest file. 

15 

16 Args: 

17 path: Path to the manifest file. 

18 

19 Returns: 

20 A dict with the manifest keys; missing keys get defaults. 

21 

22 Raises: 

23 ``Error``: If the manifest is not valid JSON or a value is invalid. 

24 """ 

25 

26 with open(path, 'rt', encoding='utf8') as fp: 

27 return from_text(fp.read(), path) 

28 

29 

30def from_text(text, path): 

31 """Parse a manifest from text. 

32 

33 Lines starting with ``//`` or ``#`` are ignored. Relative plugin and 

34 ``tsConfig`` paths are resolved against the directory of ``path``. 

35 

36 Args: 

37 text: Manifest JSON text. 

38 path: Path to the manifest file. 

39 

40 Returns: 

41 A dict with the manifest keys; missing keys get defaults. 

42 

43 Raises: 

44 ``Error``: If the text is not valid JSON or a value is invalid. 

45 """ 

46 

47 lines = [] 

48 for s in text.split('\n'): 

49 # we allow // or # comments in json 

50 if s.strip().startswith(('//', '#')): 

51 s = '' 

52 lines.append(s) 

53 

54 try: 

55 dct = json.loads('\n'.join(lines)) 

56 except Exception as exc: 

57 raise Error('invalid json') from exc 

58 

59 return _parse(dct, path) 

60 

61 

62## 

63 

64def _version(val, dct, path): 

65 p = [int(s) for s in val.split('.')] 

66 return '.'.join(str(s) for s in p) 

67 

68 

69def _plugins(val, dct, path): 

70 plugins = [] 

71 

72 for p in val: 

73 plugin_path = _relpath(p['path'], dct, path) 

74 plugin_name = p.get('name') or os.path.basename(plugin_path) 

75 plugins.append({'name': plugin_name, 'path': plugin_path}) 

76 

77 return plugins 

78 

79 

80def _relpath(val, dct, path): 

81 basedir = os.path.dirname(path) 

82 return val if os.path.isabs(val) else os.path.abspath(os.path.join(basedir, val)) 

83 

84 

85def _strlist(val, dct, path): 

86 return [str(s) for s in val] 

87 

88 

89def _str(val, dct, path): 

90 return str(val) 

91 

92 

93def _bool(val, dct, path): 

94 return bool(val) 

95 

96 

97_KEYS = [ 

98 ('uid', _str, None), 

99 ('release', _version, None), 

100 ('locales', _strlist, []), 

101 ('plugins', _plugins, []), 

102 ('tsConfig', _relpath, ''), 

103 ('excludePlugins', _strlist, []), 

104 ('withFallbackConfig', _bool, False), 

105 ('withStrictConfig', _bool, False), 

106] 

107 

108 

109def _parse(dct, path): 

110 res = {} 

111 

112 for key, fn, default in _KEYS: 

113 if key not in dct: 

114 res[key] = default 

115 else: 

116 try: 

117 res[key] = fn(dct[key], dct, path) 

118 except Exception as exc: 

119 raise Error(f'invalid value for key {key!r} in {path!r}') from exc 

120 

121 return res