Coverage for gws-app/gws/spec/runtime.py: 64%
159 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-24 12:46 +0200
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-24 12:46 +0200
1"""Validate values according to specs"""
3from typing import Optional
5import re
7import gws
8import gws.lib.jsonx
9import gws.lib.dynimport
11from . import core, reader
12from .generator import main as generator_main
14Error = core.Error
15ReadError = core.ReadError
16GeneratorError = core.GeneratorError
17LoadError = core.LoadError
20def create(manifest_path: str = '', read_cache=False, write_cache=False) -> 'Object':
21 """Create a new Runtime object by generating or loading the specs.
23 Args:
24 manifest_path: Optional path to the application manifest. If not provided, the generator will try to find it automatically.
25 read_cache: If True, try to read the specs from a cache file.
26 write_cache: If True, write the generated specs to a cache file.
27 """
29 sd = _get_specs(manifest_path, read_cache, write_cache)
30 return Object(sd)
33def _get_specs(manifest_path: str = '', read_cache=False, write_cache=False) -> core.SpecData:
34 cache_path = gws.u.ensure_dir(gws.c.SPEC_DIR) + '/spec_' + gws.u.to_uid(manifest_path or '') + '.json'
36 if read_cache and gws.u.is_file(cache_path):
37 try:
38 specs = generator_main.from_path(cache_path)
39 gws.log.debug(f'spec.create: loaded from {cache_path!r}')
40 return specs
41 except gws.lib.jsonx.Error:
42 gws.log.exception(f'spec.create: load failed')
44 if manifest_path:
45 gws.log.debug(f'spec.create: using manifest {manifest_path!r}...')
47 gws.debug.time_start('SPEC GENERATOR')
48 specs = generator_main.generate(manifest_path=manifest_path)
49 gws.debug.time_end()
51 if write_cache:
52 try:
53 generator_main.to_path(cache_path, specs)
54 gws.log.debug(f'spec.create: stored to {cache_path!r}')
55 except gws.lib.jsonx.Error:
56 gws.log.exception(f'spec.create: store failed')
58 return specs
61##
64class Object(gws.SpecRuntime):
65 def __init__(self, sd: core.SpecData):
66 self.sd = sd
67 self.manifest = gws.ApplicationManifest(sd.meta['manifest'])
68 self.manifestPath = sd.meta['manifestPath']
69 self.version = sd.meta['version']
71 self.serverTypes = sd.serverTypes
72 self.serverTypesDict = {}
73 self.rawCommands = set()
75 for typ in self.serverTypes:
76 self.serverTypesDict[typ.uid] = typ
77 if typ.extName:
78 self.serverTypesDict[typ.extName] = typ
79 if typ.extName.startswith(core.v.EXT_COMMAND_PREFIX + str(gws.CommandCategory.raw) + '.'):
80 self.rawCommands.add(typ.extName.split('.')[-1])
82 self.strings = sd.strings
83 self.chunks = sd.chunks
85 self.appBundlePaths = []
86 for chunk in self.chunks:
87 path = chunk.bundleDir + '/' + gws.c.JS_BUNDLE
88 if path not in self.appBundlePaths:
89 self.appBundlePaths.append(path)
91 self._descCache = {}
93 def __getstate__(self):
94 self._descCache = {}
95 return vars(self)
97 def get_type(self, key):
98 return self.serverTypesDict.get(key)
100 def read(self, value, type_name, path='', options=None):
101 r = reader.Reader(self, path, options)
102 return r.read(value, type_name)
104 def object_descriptor(self, name):
105 if name in self._descCache:
106 return self._descCache[name]
108 typ = self.get_type(name)
109 if not typ:
110 return
112 self._descCache[name] = gws.ExtObjectDescriptor(
113 extName=typ.extName,
114 extType=typ.extName.split('.').pop(),
115 ident=typ.ident,
116 modName=typ.modName,
117 modPath=typ.modPath,
118 classPtr=None,
119 )
121 return self._descCache[name]
123 def register_object(self, classref, ext_type, cls):
124 _, _, ext_name = self.parse_classref(classref)
125 if not ext_name:
126 raise Error(f'invalid class reference {classref!r}')
127 ext_name += '.' + ext_type
128 setattr(cls, 'extName', ext_name)
129 setattr(cls, 'extType', ext_type)
130 self._descCache[ext_name] = gws.ExtObjectDescriptor(
131 extName=ext_name,
132 extType=ext_type,
133 ident=cls.__name__,
134 modName='',
135 modPath='',
136 classPtr=cls,
137 )
139 def get_class(self, classref, ext_type=None):
140 cls, real_name, ext_name = self.parse_classref(classref)
141 if cls:
142 return cls
144 desc = None
145 if real_name:
146 desc = self.object_descriptor(real_name)
147 elif ext_name:
148 desc = self.object_descriptor(ext_name + '.' + (ext_type or core.v.DEFAULT_VARIANT_TAG))
149 if not desc:
150 return
152 if not desc.classPtr:
153 try:
154 mod = gws.lib.dynimport.import_from_path(desc.modPath, gws.c.APP_DIR)
155 except gws.lib.dynimport.Error as exc:
156 raise LoadError(f'cannot load class {classref!r} from {desc.modPath!r}') from exc
157 desc.classPtr = getattr(mod, desc.ident)
158 setattr(desc.classPtr, 'extName', desc.extName)
159 setattr(desc.classPtr, 'extType', desc.extType)
161 return desc.classPtr
163 def command_descriptor(self, command_category, command_name):
164 if command_name in self.rawCommands and command_category != gws.CommandCategory.raw:
165 command_category = gws.CommandCategory.raw
167 name = core.v.EXT_COMMAND_PREFIX + str(command_category) + '.' + command_name
169 if name in self._descCache:
170 return self._descCache[name]
172 typ = self.get_type(name)
174 if not typ:
175 return
177 return gws.ExtCommandDescriptor(
178 extName=typ.extName,
179 extType=typ.extName.split('.').pop(),
180 extCommandCategory=command_category,
181 tArg=typ.tArg,
182 tOwner=typ.tOwner,
183 owner=self.object_descriptor(typ.tOwner),
184 methodName=typ.ident,
185 )
187 def cli_commands(self, lang='en'):
188 strings = self.strings.get(lang) or self.strings['en']
189 cmds = []
191 for typ in self.serverTypes:
192 if not typ.extName.startswith(core.v.EXT_COMMAND_CLI_PREFIX):
193 continue
195 # e.g "gws.ext.command.cli.serverStart" -> [server, start]
196 m = re.search(r'\.([a-z]+)(\w+)$', typ.extName)
197 if not m:
198 continue
199 cmd1 = m.group(1)
200 cmd2 = m.group(2).lower()
202 args = []
203 arg_typ = self.get_type(typ.tArg)
204 if arg_typ:
205 for name, prop_type_uid in arg_typ.tProperties.items():
206 prop_typ = self.get_type(prop_type_uid)
207 if not prop_typ:
208 continue
209 args.append(
210 gws.Data(
211 name=name,
212 type=prop_typ.tValue,
213 doc=strings.get(prop_type_uid) or self.strings['en'].get(prop_type_uid) or '',
214 defaultValue=prop_typ.defaultValue,
215 hasDefault=prop_typ.hasDefault,
216 )
217 )
219 entry = gws.Data(
220 cmd1=cmd1,
221 cmd2=cmd2,
222 doc=strings.get(typ.uid) or self.strings['en'].get(typ.uid) or '',
223 args=sorted(args, key=lambda a: a.name),
224 )
225 cmds.append(entry)
227 return sorted(cmds, key=lambda c: (c.cmd1, c.cmd2))
229 def parse_classref(self, classref: gws.ClassRef) -> tuple[Optional[type], str, str]:
230 ext_name = gws.ext.name_for(classref)
231 if ext_name:
232 return None, '', ext_name
234 if isinstance(classref, str):
235 return None, classref, ''
237 if isinstance(classref, type):
238 return classref, '', ''
240 raise Error(f'invalid class reference {classref!r}')
242 def get_config_types(self, lang):
243 strs = self.strings.get(lang) or self.strings['en']
244 ts = []
246 for typ in self.serverTypes:
247 if not typ.isConfig:
248 continue
249 d = dict(vars(typ))
250 d['doc'] = strs.get(typ.uid) or ''
251 d['title'] = strs.get(typ.uid + '._title') or ''
252 if 'enumDocs' in d:
253 for k in d['enumDocs']:
254 key = typ.uid + '.' + k
255 if key in strs:
256 d['enumDocs'][k] = strs[key]
257 ts.append(d)
259 return ts