partis-pyproj 0.1.4__py3-none-any.whl → 0.1.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {partis_pyproj-0.1.4.data → partis_pyproj-0.1.6.data}/purelib/partis/pyproj/__init__.py +9 -1
- {partis_pyproj-0.1.4.data → partis_pyproj-0.1.6.data}/purelib/partis/pyproj/_legacy_setup.py +11 -11
- {partis_pyproj-0.1.4.data → partis_pyproj-0.1.6.data}/purelib/partis/pyproj/_nonprintable.py +4 -3
- {partis_pyproj-0.1.4.data → partis_pyproj-0.1.6.data}/purelib/partis/pyproj/backend.py +44 -37
- partis_pyproj-0.1.6.data/purelib/partis/pyproj/builder/builder.py +351 -0
- {partis_pyproj-0.1.4.data → partis_pyproj-0.1.6.data}/purelib/partis/pyproj/builder/cargo.py +2 -2
- {partis_pyproj-0.1.4.data → partis_pyproj-0.1.6.data}/purelib/partis/pyproj/builder/cmake.py +9 -15
- {partis_pyproj-0.1.4.data → partis_pyproj-0.1.6.data}/purelib/partis/pyproj/builder/meson.py +5 -13
- partis_pyproj-0.1.6.data/purelib/partis/pyproj/builder/process.py +42 -0
- {partis_pyproj-0.1.4.data → partis_pyproj-0.1.6.data}/purelib/partis/pyproj/dist_file/__init__.py +1 -1
- {partis_pyproj-0.1.4.data → partis_pyproj-0.1.6.data}/purelib/partis/pyproj/dist_file/dist_base.py +75 -86
- {partis_pyproj-0.1.4.data → partis_pyproj-0.1.6.data}/purelib/partis/pyproj/dist_file/dist_binary.py +6 -24
- {partis_pyproj-0.1.4.data → partis_pyproj-0.1.6.data}/purelib/partis/pyproj/dist_file/dist_copy.py +7 -18
- {partis_pyproj-0.1.4.data → partis_pyproj-0.1.6.data}/purelib/partis/pyproj/dist_file/dist_source.py +4 -21
- {partis_pyproj-0.1.4.data → partis_pyproj-0.1.6.data}/purelib/partis/pyproj/dist_file/dist_targz.py +5 -12
- {partis_pyproj-0.1.4.data → partis_pyproj-0.1.6.data}/purelib/partis/pyproj/dist_file/dist_zip.py +5 -14
- {partis_pyproj-0.1.4.data → partis_pyproj-0.1.6.data}/purelib/partis/pyproj/file.py +2 -1
- {partis_pyproj-0.1.4.data → partis_pyproj-0.1.6.data}/purelib/partis/pyproj/legacy.py +3 -2
- {partis_pyproj-0.1.4.data → partis_pyproj-0.1.6.data}/purelib/partis/pyproj/load_module.py +7 -6
- {partis_pyproj-0.1.4.data → partis_pyproj-0.1.6.data}/purelib/partis/pyproj/norms.py +35 -31
- {partis_pyproj-0.1.4.data → partis_pyproj-0.1.6.data}/purelib/partis/pyproj/path/__init__.py +2 -1
- {partis_pyproj-0.1.4.data → partis_pyproj-0.1.6.data}/purelib/partis/pyproj/path/match.py +42 -35
- {partis_pyproj-0.1.4.data → partis_pyproj-0.1.6.data}/purelib/partis/pyproj/path/pattern.py +60 -54
- partis_pyproj-0.1.6.data/purelib/partis/pyproj/path/utils.py +94 -0
- {partis_pyproj-0.1.4.data → partis_pyproj-0.1.6.data}/purelib/partis/pyproj/pep.py +36 -35
- {partis_pyproj-0.1.4.data → partis_pyproj-0.1.6.data}/purelib/partis/pyproj/pkginfo.py +7 -16
- {partis_pyproj-0.1.4.data → partis_pyproj-0.1.6.data}/purelib/partis/pyproj/pptoml.py +125 -120
- {partis_pyproj-0.1.4.data → partis_pyproj-0.1.6.data}/purelib/partis/pyproj/pyproj.py +47 -36
- partis_pyproj-0.1.6.data/purelib/partis/pyproj/template.py +229 -0
- {partis_pyproj-0.1.4.data → partis_pyproj-0.1.6.data}/purelib/partis/pyproj/validate.py +273 -268
- partis_pyproj-0.1.6.dist-info/METADATA +500 -0
- partis_pyproj-0.1.6.dist-info/RECORD +37 -0
- partis_pyproj-0.1.4.data/purelib/partis/pyproj/builder/builder.py +0 -267
- partis_pyproj-0.1.4.data/purelib/partis/pyproj/builder/process.py +0 -75
- partis_pyproj-0.1.4.data/purelib/partis/pyproj/path/utils.py +0 -40
- partis_pyproj-0.1.4.dist-info/METADATA +0 -51
- partis_pyproj-0.1.4.dist-info/RECORD +0 -36
- {partis_pyproj-0.1.4.data → partis_pyproj-0.1.6.data}/purelib/partis/pyproj/builder/__init__.py +0 -0
- {partis_pyproj-0.1.4.dist-info → partis_pyproj-0.1.6.dist-info}/LICENSE.txt +0 -0
- {partis_pyproj-0.1.4.dist-info → partis_pyproj-0.1.6.dist-info}/WHEEL +0 -0
- {partis_pyproj-0.1.4.dist-info → partis_pyproj-0.1.6.dist-info}/entry_points.txt +0 -0
- {partis_pyproj-0.1.4.dist-info → partis_pyproj-0.1.6.dist-info}/top_level.txt +0 -0
@@ -1,8 +1,11 @@
|
|
1
|
+
from __future__ import annotations
|
1
2
|
import os
|
2
3
|
import os.path as osp
|
3
4
|
import sys
|
4
5
|
import shutil
|
5
|
-
import
|
6
|
+
from logging import (
|
7
|
+
getLogger,
|
8
|
+
Logger)
|
6
9
|
import tempfile
|
7
10
|
from copy import copy, deepcopy
|
8
11
|
from collections.abc import (
|
@@ -27,7 +30,6 @@ except ImportError:
|
|
27
30
|
from .pkginfo import (
|
28
31
|
PkgInfoReq,
|
29
32
|
PkgInfo )
|
30
|
-
|
31
33
|
from .validate import (
|
32
34
|
ValidationWarning,
|
33
35
|
ValidationError,
|
@@ -38,17 +40,16 @@ from .validate import (
|
|
38
40
|
valid,
|
39
41
|
restrict,
|
40
42
|
mapget )
|
41
|
-
|
42
43
|
from .norms import (
|
43
44
|
scalar_list,
|
44
45
|
norm_bool,
|
45
46
|
norm_path_to_os,
|
46
47
|
norm_path )
|
47
|
-
|
48
48
|
from .pep import (
|
49
49
|
purelib_compat_tags,
|
50
50
|
platlib_compat_tags )
|
51
|
-
|
51
|
+
from .path import (
|
52
|
+
resolve)
|
52
53
|
from .load_module import (
|
53
54
|
EntryPointError,
|
54
55
|
EntryPoint,
|
@@ -68,7 +69,7 @@ from .builder import (
|
|
68
69
|
from .dist_file import (
|
69
70
|
dist_copy )
|
70
71
|
|
71
|
-
|
72
|
+
#===============================================================================
|
72
73
|
class PyProjBase:
|
73
74
|
"""Minimal build system for a Python project
|
74
75
|
|
@@ -77,21 +78,21 @@ class PyProjBase:
|
|
77
78
|
|
78
79
|
Parameters
|
79
80
|
----------
|
80
|
-
root :
|
81
|
+
root :
|
81
82
|
Path to the root project directory containing 'pyproject.toml'.
|
82
|
-
logger :
|
83
|
+
logger :
|
83
84
|
Parent logger to use when processing project.
|
84
85
|
|
85
86
|
"""
|
86
87
|
#-----------------------------------------------------------------------------
|
87
88
|
def __init__( self, *,
|
88
|
-
root,
|
89
|
-
config_settings = None,
|
90
|
-
logger = None ):
|
89
|
+
root: Path,
|
90
|
+
config_settings: dict|None = None,
|
91
|
+
logger: Logger|None = None ):
|
91
92
|
|
92
|
-
self.logger = logger or
|
93
|
+
self.logger = logger or getLogger( __name__ )
|
93
94
|
|
94
|
-
self.root = Path(root)
|
95
|
+
self.root = resolve(Path(root))
|
95
96
|
|
96
97
|
self.pptoml_file = self.root / 'pyproject.toml'
|
97
98
|
|
@@ -117,6 +118,7 @@ class PyProjBase:
|
|
117
118
|
|
118
119
|
#...........................................................................
|
119
120
|
# construct a validator from the tool.pyproj.config table
|
121
|
+
# NOTE: only really used in the event that "config_settings" are passed to the backend
|
120
122
|
config_default = dict()
|
121
123
|
|
122
124
|
for k,v in self.pyproj.config.items():
|
@@ -129,14 +131,12 @@ class PyProjBase:
|
|
129
131
|
else:
|
130
132
|
config_default[k] = valid(v, type(v))
|
131
133
|
|
132
|
-
class
|
133
|
-
|
134
|
-
|
134
|
+
class valid_config_settings(valid_dict):
|
135
|
+
allow_keys = list()
|
136
|
+
default = config_default
|
135
137
|
|
136
138
|
with validating( key = 'config_settings' ):
|
137
|
-
_config_settings =
|
138
|
-
|
139
|
-
self.pyproj.config = _config_settings
|
139
|
+
self._config_settings = valid_config_settings(config_settings or dict())
|
140
140
|
|
141
141
|
#...........................................................................
|
142
142
|
self.build_backend = mapget( self.pptoml,
|
@@ -196,10 +196,14 @@ class PyProjBase:
|
|
196
196
|
|
197
197
|
#-----------------------------------------------------------------------------
|
198
198
|
@property
|
199
|
-
def
|
200
|
-
"""
|
199
|
+
def config_settings(self):
|
200
|
+
"""Config settings passed to backend, or defaults from ``pyproj.config``
|
201
201
|
"""
|
202
|
-
return self.
|
202
|
+
return self._config_settings
|
203
|
+
|
204
|
+
#-----------------------------------------------------------------------------
|
205
|
+
# alias for backward compatibility
|
206
|
+
config = config_settings
|
203
207
|
|
204
208
|
#-----------------------------------------------------------------------------
|
205
209
|
@property
|
@@ -218,6 +222,9 @@ class PyProjBase:
|
|
218
222
|
These are no longer restricted to meson, but this attribute kept for backward
|
219
223
|
compatability.
|
220
224
|
|
225
|
+
Inplace changes to the returned object are not propagated back to the target
|
226
|
+
configuration.
|
227
|
+
|
221
228
|
"""
|
222
229
|
targets = self._pptoml.tool.pyproj.targets
|
223
230
|
|
@@ -227,6 +234,7 @@ class PyProjBase:
|
|
227
234
|
meson = dict(targets[0])
|
228
235
|
meson.pop('entry')
|
229
236
|
meson.pop('work_dir')
|
237
|
+
meson.pop('env')
|
230
238
|
meson['compile'] = meson.pop('enabled')
|
231
239
|
return pyproj_meson(meson)
|
232
240
|
|
@@ -297,9 +305,9 @@ class PyProjBase:
|
|
297
305
|
dynamic = project.dynamic
|
298
306
|
|
299
307
|
self.prep_entrypoint(
|
300
|
-
name =
|
308
|
+
name = "tool.pyproj.prep",
|
301
309
|
obj = self.pyproj,
|
302
|
-
logger = self.logger.getChild(
|
310
|
+
logger = self.logger.getChild("prep") )
|
303
311
|
|
304
312
|
# NOTE: check that any dynamic meta-data is defined after prep
|
305
313
|
for k in dynamic:
|
@@ -327,9 +335,9 @@ class PyProjBase:
|
|
327
335
|
"""
|
328
336
|
|
329
337
|
self.prep_entrypoint(
|
330
|
-
name =
|
338
|
+
name = "tool.pyproj.dist.prep",
|
331
339
|
obj = self.dist,
|
332
|
-
logger = self.logger.getChild(
|
340
|
+
logger = self.logger.getChild("dist.prep") )
|
333
341
|
|
334
342
|
|
335
343
|
#-----------------------------------------------------------------------------
|
@@ -338,9 +346,9 @@ class PyProjBase:
|
|
338
346
|
"""
|
339
347
|
|
340
348
|
self.prep_entrypoint(
|
341
|
-
name =
|
349
|
+
name = "tool.pyproj.dist.source.prep",
|
342
350
|
obj = self.dist.source,
|
343
|
-
logger = self.logger.getChild(
|
351
|
+
logger = self.logger.getChild("dist.source.prep") )
|
344
352
|
|
345
353
|
#-----------------------------------------------------------------------------
|
346
354
|
def dist_source_copy( self, *, dist ):
|
@@ -352,7 +360,7 @@ class PyProjBase:
|
|
352
360
|
Builder used to write out source distribution files
|
353
361
|
"""
|
354
362
|
|
355
|
-
with validating( key =
|
363
|
+
with validating( key = 'tool.pyproj.dist.source'):
|
356
364
|
dist_copy(
|
357
365
|
base_path = dist.named_dirs['root'],
|
358
366
|
include = self.source.copy,
|
@@ -362,9 +370,9 @@ class PyProjBase:
|
|
362
370
|
logger = self.logger )
|
363
371
|
|
364
372
|
if self.add_legacy_setup:
|
365
|
-
with validating(
|
373
|
+
with validating(key = 'add_legacy_setup'):
|
366
374
|
|
367
|
-
self.logger.info(
|
375
|
+
self.logger.info("generating legacy 'setup.py'")
|
368
376
|
legacy_setup_content( self, dist )
|
369
377
|
|
370
378
|
#-----------------------------------------------------------------------------
|
@@ -372,16 +380,19 @@ class PyProjBase:
|
|
372
380
|
"""Prepares project files for a binary distribution
|
373
381
|
"""
|
374
382
|
|
375
|
-
|
383
|
+
builder = Builder(
|
376
384
|
pyproj = self,
|
377
385
|
root = self.root,
|
378
386
|
targets = self.targets,
|
379
|
-
logger = self.logger.getChild(
|
387
|
+
logger = self.logger.getChild("targets"))
|
388
|
+
|
389
|
+
with builder:
|
390
|
+
builder.build_targets()
|
380
391
|
|
381
392
|
self.prep_entrypoint(
|
382
|
-
name =
|
393
|
+
name = "tool.pyproj.dist.binary.prep",
|
383
394
|
obj = self.binary,
|
384
|
-
logger = self.logger.getChild(
|
395
|
+
logger = self.logger.getChild("dist.binary.prep") )
|
385
396
|
|
386
397
|
self.logger.debug(f"Compatibility tags after dist.binary.prep: {self.binary.compat_tags}")
|
387
398
|
|
@@ -396,7 +407,7 @@ class PyProjBase:
|
|
396
407
|
"""
|
397
408
|
|
398
409
|
|
399
|
-
with validating(
|
410
|
+
with validating(key = 'tool.pyproj.dist.binary'):
|
400
411
|
ignore = self.dist.ignore + self.dist.binary.ignore
|
401
412
|
|
402
413
|
dist_copy(
|
@@ -0,0 +1,229 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
import re
|
3
|
+
from copy import copy
|
4
|
+
from pathlib import Path
|
5
|
+
from collections.abc import (
|
6
|
+
Sequence,
|
7
|
+
Mapping)
|
8
|
+
# from string import Template
|
9
|
+
|
10
|
+
from .validate import (
|
11
|
+
ValidationError,
|
12
|
+
FileOutsideRootError)
|
13
|
+
from .path import (
|
14
|
+
subdir,
|
15
|
+
resolve)
|
16
|
+
|
17
|
+
namespace_sep = re.compile(r"[\.\[\]]")
|
18
|
+
|
19
|
+
_idpattern = re.compile(r"""(?:
|
20
|
+
# references a template variable
|
21
|
+
[A-Z_][A-Z0-9_]* # Python identifier
|
22
|
+
(?:
|
23
|
+
\.[A-Z_][A-Z0-9_]* # attribute access by Python identifier
|
24
|
+
|
|
25
|
+
\[-?[0-9]+\])* # integer index (potentially negative)
|
26
|
+
|
|
27
|
+
\/ # forward slash separate for building path
|
28
|
+
|
|
29
|
+
(?<=\/)\.\. # double-dot (parent directory), must follow a slash
|
30
|
+
|
|
31
|
+
'[A-Z0-9\-_\.]+' # string literal, quotes to be removed
|
32
|
+
)+
|
33
|
+
""",
|
34
|
+
re.IGNORECASE|re.VERBOSE)
|
35
|
+
|
36
|
+
_group_pattern = re.compile(
|
37
|
+
# NOTE: handles escaped '$' and missing closing brace
|
38
|
+
r"\$(?:(?P<escaped>\$)|{ *(?P<braced>[^\s\}]+) *(?:}|(?P<unterminated>$|[^}])))",
|
39
|
+
re.IGNORECASE)
|
40
|
+
|
41
|
+
#===============================================================================
|
42
|
+
class TemplateError(ValidationError):
|
43
|
+
...
|
44
|
+
|
45
|
+
#===============================================================================
|
46
|
+
class NamespaceError(ValidationError):
|
47
|
+
...
|
48
|
+
|
49
|
+
#===============================================================================
|
50
|
+
class Template:
|
51
|
+
r"""Template support nested mappings and paths using :class:`Namespace`
|
52
|
+
"""
|
53
|
+
|
54
|
+
#-----------------------------------------------------------------------------
|
55
|
+
def __init__(self, template):
|
56
|
+
self.template = template
|
57
|
+
|
58
|
+
#-----------------------------------------------------------------------------
|
59
|
+
def substitute(self, namespace: Mapping = None, /, **kwargs):
|
60
|
+
if namespace is None:
|
61
|
+
namespace = kwargs
|
62
|
+
|
63
|
+
elif kwargs:
|
64
|
+
raise TypeError("Cannot use both namespace and kwargs")
|
65
|
+
|
66
|
+
if not isinstance(namespace, Namespace):
|
67
|
+
namespace = Namespace(namespace)
|
68
|
+
|
69
|
+
def _handler(m):
|
70
|
+
if m.group('escaped'):
|
71
|
+
return '$'
|
72
|
+
|
73
|
+
if m.group('unterminated') is not None:
|
74
|
+
raise TemplateError(f"Unterminated template substitution: {m.group()}")
|
75
|
+
|
76
|
+
name = m.group('braced').strip()
|
77
|
+
|
78
|
+
if not _idpattern.fullmatch(name):
|
79
|
+
raise TemplateError(f"Invalid template substitution: {name}")
|
80
|
+
|
81
|
+
return str(namespace[name])
|
82
|
+
|
83
|
+
return _group_pattern.sub(_handler, self.template)
|
84
|
+
|
85
|
+
#===============================================================================
|
86
|
+
class Namespace(Mapping):
|
87
|
+
r"""Namespace (potentially nested) mapping for using with :class:`Template`
|
88
|
+
|
89
|
+
Parameters
|
90
|
+
----------
|
91
|
+
data:
|
92
|
+
Mapping for names to values. Note that changes to the namespace will also
|
93
|
+
change the data. Making a shallow copy of the namespace also make a shallow
|
94
|
+
copy of the data.
|
95
|
+
root:
|
96
|
+
If given, absolute path to project root, used to resolve relative paths and ensure
|
97
|
+
any derived paths are within this parent directory.
|
98
|
+
"""
|
99
|
+
__slots__ = ['data', 'root']
|
100
|
+
|
101
|
+
#-----------------------------------------------------------------------------
|
102
|
+
def __init__(self, data: Mapping, *, root: Path = None):
|
103
|
+
self.data = data
|
104
|
+
self.root = root
|
105
|
+
|
106
|
+
#-----------------------------------------------------------------------------
|
107
|
+
def __iter__(self):
|
108
|
+
return iter(self.data)
|
109
|
+
|
110
|
+
#-----------------------------------------------------------------------------
|
111
|
+
def __len__(self):
|
112
|
+
return len(self.data)
|
113
|
+
|
114
|
+
#-----------------------------------------------------------------------------
|
115
|
+
def __setitem__(self, name, value):
|
116
|
+
self.data[name] = value
|
117
|
+
|
118
|
+
#-----------------------------------------------------------------------------
|
119
|
+
def __getitem__(self, key):
|
120
|
+
raw_segments = key.split('/')
|
121
|
+
segments = []
|
122
|
+
|
123
|
+
for name in raw_segments:
|
124
|
+
if len(name) == 0 or name == '..':
|
125
|
+
# empty segment
|
126
|
+
segments.append(name)
|
127
|
+
|
128
|
+
elif name.startswith("'"):
|
129
|
+
# string literal, remove quotes
|
130
|
+
segments.append(name[1:-1])
|
131
|
+
|
132
|
+
else:
|
133
|
+
# variable name lookup
|
134
|
+
segments.append(self.lookup(name))
|
135
|
+
|
136
|
+
if len(segments) == 1:
|
137
|
+
return segments[0]
|
138
|
+
|
139
|
+
if self.root is None:
|
140
|
+
out = Path(*segments)
|
141
|
+
|
142
|
+
else:
|
143
|
+
root = self.root
|
144
|
+
out = type(root)(*segments)
|
145
|
+
|
146
|
+
if not out.is_absolute():
|
147
|
+
out = root/out
|
148
|
+
|
149
|
+
if isinstance(root, Path):
|
150
|
+
# NOTE: ignored if root is a pure path
|
151
|
+
out = resolve(out)
|
152
|
+
|
153
|
+
if not subdir(root, out, check = False):
|
154
|
+
raise FileOutsideRootError(
|
155
|
+
f"Must be within project root directory:"
|
156
|
+
f"\n file = \"{out}\"\n root = \"{root}\"")
|
157
|
+
|
158
|
+
return out
|
159
|
+
|
160
|
+
#-----------------------------------------------------------------------------
|
161
|
+
def __copy__(self):
|
162
|
+
cls = type(self)
|
163
|
+
obj = cls.__new__(cls)
|
164
|
+
obj.data = copy(self.data)
|
165
|
+
obj.root = self.root
|
166
|
+
return obj
|
167
|
+
|
168
|
+
#-----------------------------------------------------------------------------
|
169
|
+
def lookup(self, name):
|
170
|
+
parts = namespace_sep.split(name)
|
171
|
+
data = self.data
|
172
|
+
|
173
|
+
try:
|
174
|
+
cur = []
|
175
|
+
|
176
|
+
for k in parts:
|
177
|
+
if k:
|
178
|
+
if isinstance(data, Mapping):
|
179
|
+
data = data[k]
|
180
|
+
elif not isinstance(data, (str,bytes)) and isinstance(data, Sequence):
|
181
|
+
i = int(k)
|
182
|
+
data = data[i]
|
183
|
+
else:
|
184
|
+
raise NamespaceError(f"Expected mapping or sequence for '{k}': {type(data).__name__}")
|
185
|
+
|
186
|
+
cur.append(k)
|
187
|
+
|
188
|
+
except (KeyError,TypeError,IndexError) as e:
|
189
|
+
raise NamespaceError(f"Invalid key '{k}' of name '{name}': {str(e)}") from None
|
190
|
+
|
191
|
+
return data
|
192
|
+
|
193
|
+
#===============================================================================
|
194
|
+
def template_substitute(
|
195
|
+
value: bool|int|str|Path|Mapping|Sequence,
|
196
|
+
namespace: Mapping):
|
197
|
+
r"""Recursively performs template substitution based on type of value
|
198
|
+
"""
|
199
|
+
|
200
|
+
if not isinstance(namespace, Namespace):
|
201
|
+
namespace = Namespace(namespace)
|
202
|
+
|
203
|
+
if isinstance(value, (bool,int)):
|
204
|
+
# just handles case where definitely not a template
|
205
|
+
return value
|
206
|
+
|
207
|
+
cls = type(value)
|
208
|
+
|
209
|
+
if isinstance(value, str):
|
210
|
+
return cls(Template(value).substitute(namespace))
|
211
|
+
|
212
|
+
if isinstance(value, Path):
|
213
|
+
return cls(*(
|
214
|
+
Template(v).substitute(namespace)
|
215
|
+
for v in value.parts))
|
216
|
+
|
217
|
+
if isinstance(value, Mapping):
|
218
|
+
return cls({
|
219
|
+
k: template_substitute(v, namespace)
|
220
|
+
for k,v in value.items()})
|
221
|
+
|
222
|
+
if isinstance(value, Sequence):
|
223
|
+
return cls([
|
224
|
+
template_substitute(v, namespace)
|
225
|
+
for v in value])
|
226
|
+
|
227
|
+
|
228
|
+
raise TypeError(f"Unknown template value type: {value}")
|
229
|
+
|