/
__init__.py
531 lines (405 loc) · 21.4 KB
/
__init__.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
#
# Copyright (C) OpenCyphal Development Team <opencyphal.org>
# Copyright Amazon.com Inc. or its affiliates.
# SPDX-License-Identifier: MIT
#
"""
Language-specific support in nunavut.
This package contains modules that provide specific support for generating
source for various languages using templates.
"""
import functools
import logging
import pathlib
import typing
from ._config import LanguageConfig
from ._language import Language, LanguageClassLoader
logger = logging.getLogger(__name__)
class UnsupportedLanguageError(ValueError):
"""
Error type raised if an unsupported language type is used.
"""
class LanguageContextBuilder:
"""
Used to instantiate new :class:`LanguageContext` objects.
The simplest invocation will always work by using the :data:`LanguageContextBuilder.DEFAULT_TARGET_LANGUAGE`
constant:
.. code-block:: python
from nunavut.lang import LanguageContextBuilder
default_language_context = LanguageContextBuilder().create()
assert LanguageContextBuilder.DEFAULT_TARGET_LANGUAGE == default_language_context.get_target_language().name
Typically a target language is specified at minimum. Also see constants on :class:`nunavut.lang.Language` for
well-known options that the builder can override:
.. code-block:: python
from nunavut.lang import LanguageContextBuilder
customized_language_context = (
LanguageContextBuilder()
.set_target_language("c")
.set_target_language_configuration_override(Language.WKCV_DEFINITION_FILE_EXTENSION, ".h")
.create()
)
assert customized_language_context.get_target_language().extension == ".h"
:param include_experimental_languages: If set then languages that are not fully supported will be allowed otherwise
any experimental languages will be missing and errors will be raised as if the language specified was unknown.
"""
DEFAULT_TARGET_LANGUAGE = "c" #: The target language used for new contexts if none is specified.
def __init__(self, include_experimental_languages: bool = False):
self._target_language_name: typing.Optional[str] = None
self._target_language_config: typing.Dict[str, str] = {}
self._ln_loader = LanguageClassLoader()
self._include_experimental_languages = include_experimental_languages
def get_supported_language_names(self) -> typing.Iterable[str]:
"""
Get a list of target languages supported by Nunavut.
:return: An iterable of strings which are languages with special
support within Nunavut templates.
"""
return [LanguageClassLoader.to_language_name(s) for s in self._ln_loader.config.sections()]
@property
def config(self) -> LanguageConfig:
"""
The configuration object that will be used to create the language context.
"""
return self._ln_loader.config
# +-----------------------------------------------------------------------+
# | BUILDER SYNTAX
# +-----------------------------------------------------------------------+
def set_target_language_configuration_override(self, key: str, value: typing.Any) -> "LanguageContextBuilder":
"""
Stores a key and value to override in the configuration for a language target when a LanguageContext is crated.
These overrides are always set under the language section of the target language.
.. invisible-code-block: python
from nunavut.lang import LanguageContextBuilder, Language, LanguageClassLoader
.. code-block:: python
builder = LanguageContextBuilder().set_target_language("c")
default_c_file_extension = builder.config.get_config_value(
LanguageClassLoader.to_language_module_name("c"),
Language.WKCV_DEFINITION_FILE_EXTENSION)
assert default_c_file_extension == ".h"
We can now try to override the file extension for a future "C" target language object:
.. code-block:: python
builder.set_target_language_configuration_override(Language.WKCV_DEFINITION_FILE_EXTENSION, ".foo")
...but that value will not be overridden until you create the target language:
.. code-block:: python
default_c_file_extension = builder.config.get_config_value(
LanguageClassLoader.to_language_module_name("c"),
Language.WKCV_DEFINITION_FILE_EXTENSION)
assert default_c_file_extension == ".h"
_ = builder.create()
overridden_c_file_extension = builder.config.get_config_value(
LanguageClassLoader.to_language_module_name("c"),
Language.WKCV_DEFINITION_FILE_EXTENSION)
assert overridden_c_file_extension == ".foo"
Note that the config is scoped by the builder but is then inherited by the language objects created by the
builder:
.. code-block:: python
one = (
LanguageContextBuilder()
.set_target_language("c")
.set_target_language_configuration_override("foo", 1)
)
two = (
LanguageContextBuilder()
.set_target_language("c")
.set_target_language_configuration_override("foo", 2)
)
# Here we see that the second override of "foo" does not affect the first because they
# are in different builders.
assert (
one.create().get_target_language().get_config_value("foo")
!=
two.create().get_target_language().get_config_value("foo")
)
"""
if value is not None:
self._target_language_config[key] = value
return self
def set_target_language_extension(
self, target_language_extension: typing.Optional[str]
) -> "LanguageContextBuilder":
"""
Helper method for setting the target language file extension (since this is a common override).
Calling this method is the same as doing:
.. invisible-code-block: python
from nunavut.lang import LanguageContextBuilder, Language, LanguageClassLoader
.. code-block:: python
LanguageContextBuilder().set_target_language_configuration_override(
Language.WKCV_DEFINITION_FILE_EXTENSION,
".h")
"""
return self.set_target_language_configuration_override(
Language.WKCV_DEFINITION_FILE_EXTENSION, target_language_extension
)
def set_target_language(self, target_language: typing.Optional[str]) -> "LanguageContextBuilder":
"""
Set the language name to target. This can be either the name of the language, as defined by Nunavut, or
it can be the language package name.
.. invisible-code-block: python
from nunavut.lang import LanguageContextBuilder, LanguageClassLoader
.. code-block:: python
assert LanguageContextBuilder().set_target_language("c").create().get_target_language().name == "c"
assert (
LanguageContextBuilder()
.set_target_language(LanguageClassLoader.to_language_module_name("c"))
.create()
.get_target_language().name == "c"
)
Also note that, if the language name is None, the default name will be assigned internally:
.. code-block:: python
target_language = LanguageContextBuilder().set_target_language(None).create().get_target_language()
assert target_language.name == LanguageContextBuilder.DEFAULT_TARGET_LANGUAGE
"""
if target_language is None:
self._target_language_name = self.DEFAULT_TARGET_LANGUAGE
else:
self._target_language_name = LanguageClassLoader.to_language_name(target_language)
return self
def set_additional_config_files(
self, additional_config_files: typing.List[pathlib.Path]
) -> "LanguageContextBuilder":
"""
Deprecated. Use :func:`add_config_files` instead.
"""
logger.warning("set_additional_config_files is deprecated. Use add_config_files instead.")
return self.add_config_files(*additional_config_files)
def add_config_files(self, *additional_config_files: pathlib.Path) -> "LanguageContextBuilder":
"""
A list of paths to additional yaml files to load as configuration.
These will override any values found in the :file:`nunavut.lang.properties.yaml` file and files
appearing later in this list will override value found in earlier entries.
.. invisible-code-block: python
import pathlib
import yaml
import textwrap
from nunavut.lang import LanguageContextBuilder, Language, LanguageClassLoader
overrides_file = gen_paths_for_module.out_dir / pathlib.Path("overrides1.yaml")
overrides_data = {LanguageClassLoader.to_language_module_name("c"):
{Language.WKCV_DEFINITION_FILE_EXTENSION: ".foo"}
}
with open(overrides_file, "w", encoding="utf-8") as overrides_handle:
yaml.dump(overrides_data, overrides_handle)
.. code-block:: python
target_language_w_overrides = (
LanguageContextBuilder()
.set_target_language("c")
.add_config_files(overrides_file)
.create()
.get_target_language()
)
target_language_no_overrides = (
LanguageContextBuilder()
.set_target_language("c")
.create()
.get_target_language()
)
assert target_language_w_overrides.extension == ".foo"
assert target_language_no_overrides.extension == ".h"
Overrides are applied as unions. For example, given this override data:
.. code-block:: python
overrides_data = '''
nunavut.lang.c:
extension: .foo
non-standard: bar
'''
...the standard "extension" property will be overridden and the "non-standard" property will be added.
.. invisible-code-block: python
second_overrides_file = gen_paths_for_module.out_dir / pathlib.Path("overrides2.yaml")
with open(second_overrides_file, "w", encoding="utf-8") as overrides_handle:
overrides_handle.write(textwrap.dedent(overrides_data))
.. code-block:: python
target_language_w_overrides = (
LanguageContextBuilder()
.set_target_language("c")
.add_config_files(second_overrides_file)
.create()
.get_target_language()
)
assert ".foo" == target_language_w_overrides.extension
assert "bar" == target_language_w_overrides.get_config_value("non-standard")
.. invisible-code-block: python
from nunavut import DefaultValue
# verification of issue #329 fix
with_default = {"enable_serialization_asserts" : DefaultValue(False) }
without_default = {"enable_serialization_asserts" : False }
# verification of issue #329 fix
overrides_data = '''
nunavut.lang.c:
options:
enable_serialization_asserts: true
'''
issue_329_overrides = gen_paths_for_module.out_dir / pathlib.Path("overrides329.yaml")
with open(issue_329_overrides, "w", encoding="utf-8") as overrides_handle:
overrides_handle.write(textwrap.dedent(overrides_data))
target_language_329_no_file_overrides = (
LanguageContextBuilder()
.set_target_language("c")
.set_target_language_configuration_override(Language.WKCV_LANGUAGE_OPTIONS, with_default)
.create()
.get_target_language()
)
target_language_329_file_override = (
LanguageContextBuilder()
.set_target_language("c")
.add_config_files(issue_329_overrides)
.set_target_language_configuration_override(Language.WKCV_LANGUAGE_OPTIONS, with_default)
.create()
.get_target_language()
)
target_language_329_file_override_overridden = (
LanguageContextBuilder()
.set_target_language("c")
.add_config_files(issue_329_overrides)
.set_target_language_configuration_override(Language.WKCV_LANGUAGE_OPTIONS, without_default)
.create()
.get_target_language()
)
# default from command line
assert not target_language_329_no_file_overrides.get_option("enable_serialization_asserts")
# default from command line overridden by file
assert target_language_329_file_override.get_option("enable_serialization_asserts")
# command-line overrides file
assert not target_language_329_file_override_overridden.get_option("enable_serialization_asserts")
"""
for additional_path in additional_config_files:
with open(str(additional_path), "r", encoding="utf-8") as additional_file:
self.config.update_from_yaml_file(additional_file)
return self
def create(self) -> "LanguageContext":
"""
Applies all pending configuration overrides to the internal :class:`LanguageConfig` object and instantiates
a :class:`LanguageContext` object.
"""
# First find the target language to use...
target_language_name = self._resolve_target_language(self._target_language_name)
# Now update the configuration for the target language with everything we stored in this
# builder instance...
self.config.update_section(
LanguageClassLoader.to_language_module_name(target_language_name), self._target_language_config
)
# Create the target language instance...
target_language = self._new_language_w_experimental_handling(target_language_name)
# and finally, build the LanguageContext.
return LanguageContext(
self._ln_loader.config, target_language, functools.partial(self._new_language_map, target_language)
)
# +-----------------------------------------------------------------------+
# | PRIVATE
# +-----------------------------------------------------------------------+
def _new_language_w_experimental_handling(self, language_name: str) -> Language:
try:
language = self._ln_loader.new_language(language_name)
except ImportError as e:
logger.debug("Import Error %s when trying to load language %s", e, language_name)
raise KeyError(f"language {language_name} is not a supported language") from e
if not (language.stable_support or self._include_experimental_languages):
raise UnsupportedLanguageError(
f"{language_name} support is only experimental, but experimental language support is not enabled"
)
return language
def _new_language_map(self, target_language: Language) -> typing.Dict[str, Language]:
"""
Build a map of all supported languages.
:param target_language: The target language is included in the returned map but must be build
by another method.
"""
languages: typing.Dict[str, Language] = {target_language.name: target_language}
for language_name in set(self.get_supported_language_names()) - set((target_language.name,)):
try:
languages[language_name] = self._new_language_w_experimental_handling(language_name)
except UnsupportedLanguageError:
pass
return languages
def _resolve_target_language(self, explicit_value: typing.Optional[str]) -> str:
if explicit_value is not None:
return explicit_value
inferred_target_language_name: typing.Optional[str] = None
target_extension = self._target_language_config.get(Language.WKCV_DEFINITION_FILE_EXTENSION, None)
if target_extension is not None:
for language_config_section_name, language_config_section in self.config.sections().items():
if language_config_section.get("extension", None) == target_extension:
inferred_target_language_name = LanguageClassLoader.to_language_name(language_config_section_name)
break
if inferred_target_language_name is None:
inferred_target_language_name = self.DEFAULT_TARGET_LANGUAGE
logger.info(
"No target language specified and none could be inferred. Using default language, %s",
self.DEFAULT_TARGET_LANGUAGE,
)
else:
logging.info(
'Inferring target language %s based on extension "%s".',
inferred_target_language_name,
target_extension,
)
return inferred_target_language_name
class LanguageContext:
"""
Context object containing the current target language and all supported :class:`nunavut.lang.Language` objects.
:param language_configuration: The configuration for all languages as defined by the properties.yaml schema.
:param target_language: The target language.
:param supported_language_builder: factory closure that will create :class:`nunavut.lang.Language` objects for
all supported languages when :func:`LanguageContext.get_target_languages`
is first called.
"""
def __init__(
self,
language_configuration: LanguageConfig,
target_language: Language,
supported_language_builder: typing.Callable[[], typing.Dict[str, Language]],
):
self._config = language_configuration
self._target_language = target_language
self._all_supported_languages: typing.Optional[typing.Dict[str, Language]] = None
self._all_supported_languages_builder = supported_language_builder
def get_language(self, key_or_module_name: str) -> Language:
"""
Get a :class:`nunavut.lang.Language` object for a given language identifier.
:param str key_or_module_name: Either one of the Nunavut mnemonics for a supported language or
the ``__name__`` of one of the ``nunavut.lang.[language]`` python modules.
:return: A :class:`nunavut.lang.Language` object cached by this context.
:rtype: nunavut.lang.Language
"""
if key_or_module_name is None or len(key_or_module_name) == 0:
raise ValueError("key argument is required.")
key = LanguageClassLoader.to_language_name(key_or_module_name)
return self.get_supported_languages()[key]
def get_target_language(self) -> Language:
"""
Returns the target language for code generation.
"""
return self._target_language
def filter_id_for_target(self, instance: typing.Any, id_type: str = "any") -> str:
"""
A filter that will transform a given string or pydsdl identifier into a valid identifier in the target language.
:param any instance: Any object or data that either has a name property or can be converted to a string.
:param str id_type: A type of identifier. This is different for each language.
Use 'any' to apply stropping rules for all identifier types to the instance.
:return: A token that is a valid identifier in the target language, is not a reserved keyword, and is
transformed in a deterministic manner based on the provided instance.
"""
return self._target_language.filter_id(instance, id_type)
def get_supported_languages(self) -> typing.Dict[str, Language]:
"""
Returns a collection of available language support objects.
.. invisible-code-block: python
from nunavut.lang import LanguageContextBuilder
lctx = LanguageContextBuilder().create()
default_language = None
for language_name, language in lctx.get_supported_languages().items():
if language_name == LanguageContextBuilder.DEFAULT_TARGET_LANGUAGE:
default_language = language
break
assert default_language is not None
assert default_language.name == LanguageContextBuilder.DEFAULT_TARGET_LANGUAGE
assert len(lctx.get_supported_languages()) > 1
"""
if self._all_supported_languages is None:
self._all_supported_languages = self._all_supported_languages_builder()
return self._all_supported_languages
@property
def config(self) -> LanguageConfig:
"""
Returns the :class:`nunavut.lang.LanguageConfig` object that contains the configuration for all
supported languages. This is the same object that is used to instantiate the :class:`nunavut.lang.Language`
"""
return self._config