-
Notifications
You must be signed in to change notification settings - Fork 221
Expand file tree
/
Copy pathparser.py
More file actions
285 lines (246 loc) · 11.5 KB
/
parser.py
File metadata and controls
285 lines (246 loc) · 11.5 KB
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
# #
# Copyright 2013-2024 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
# with support of Ghent University (http://ugent.be/hpc),
# the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
# Flemish Research Foundation (FWO) (http://www.fwo.be/en)
# and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
#
# https://github.com/easybuilders/easybuild
#
# EasyBuild is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation v2.
#
# EasyBuild is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with EasyBuild. If not, see <http://www.gnu.org/licenses/>.
# #
"""
This describes the easyconfig parser
The parser is format version aware
Authors:
* Stijn De Weirdt (Ghent University)
"""
import os
import re
from easybuild.base import fancylogger
from easybuild.framework.easyconfig.format.format import FORMAT_DEFAULT_VERSION
from easybuild.framework.easyconfig.format.format import get_format_version, get_format_version_classes
from easybuild.framework.easyconfig.types import PARAMETER_TYPES, check_type_of_param_value
from easybuild.tools.build_log import EasyBuildError
from easybuild.tools.filetools import read_file, write_file
# alternate easyconfig parameters, and their non-deprecated equivalents
ALTERNATE_PARAMETERS = {
# <new_param>: <equivalent_param>,
'build_deps': 'builddependencies',
'build_in_install_dir': 'buildininstalldir',
'build_opts': 'buildopts',
'build_stats': 'buildstats',
'clean_up_old_build': 'cleanupoldbuild',
'clean_up_old_install': 'cleanupoldinstall',
'configure_opts': 'configopts',
'deps': 'dependencies',
'doc_paths': 'docpaths',
'doc_urls': 'docurls',
'dont_create_install_dir': 'dontcreateinstalldir',
'exts_class_map': 'exts_classmap',
'exts_default_class': 'exts_default_class',
'exts_default_opts': 'exts_default_options',
'hidden_deps': 'hiddendependencies',
'include_modulepath_exts': 'include_modpath_extensions',
'install_opts': 'installopts',
'keep_previous_install': 'keeppreviousinstall',
'keep_symlinks': 'keepsymlinks',
'max_parallel': 'maxparallel',
'env_mod_aliases': 'modaliases',
'env_mod_alt_soft_name': 'modaltsoftname',
'modulepath_prepend_paths': 'moddependpaths',
'env_mod_extra_paths_append': 'modextrapaths_append',
'env_mod_extra_paths': 'modextrapaths',
'env_mod_extra_vars': 'modextravars',
'env_mod_load_msg': 'modloadmsg',
'env_mod_lua_footer': 'modluafooter',
'env_mod_tcl_footer': 'modtclfooter',
'env_mod_class': 'moduleclass',
'env_mod_depends_on': 'module_depends_on',
'env_mod_force_unload': 'moduleforceunload',
'env_mod_load_no_conflict': 'moduleloadnoconflict',
'env_mod_unload_msg': 'modunloadmsg',
'only_toolchain_mod_env': 'onlytcmod',
'os_deps': 'osdependencies',
'post_install_cmds': 'postinstallcmds',
'post_install_msgs': 'postinstallmsgs',
'post_install_patches': 'postinstallpatches',
'pre_build_opts': 'prebuildopts',
'pre_configure_opts': 'preconfigopts',
'pre_install_opts': 'preinstallopts',
'pre_test_opts': 'pretestopts',
'recursive_env_mod_unload': 'recursive_module_unload',
'run_test': 'runtest',
'sanity_check_cmds': 'sanity_check_commands',
'skip_fortran_mod_files_sanity_check': 'skip_mod_files_sanity_check',
'skip_steps': 'skipsteps',
'test_opts': 'testopts',
'toolchain_opts': 'toolchainopts',
'unpack_opts': 'unpack_options',
'version_prefix': 'versionprefix',
'version_suffix': 'versionsuffix',
}
# deprecated easyconfig parameters, and their replacements
DEPRECATED_PARAMETERS = {
# <old_param>: (<new_param>, <deprecation_version>),
}
# replaced easyconfig parameters, and their replacements
REPLACED_PARAMETERS = {
'license': 'license_file',
'makeopts': 'buildopts',
'premakeopts': 'prebuildopts',
}
_log = fancylogger.getLogger('easyconfig.parser', fname=False)
def fetch_parameters_from_easyconfig(rawtxt, params):
"""
Fetch (initial) parameter definition from the given easyconfig file contents.
:param rawtxt: contents of the easyconfig file
:param params: list of parameter names to fetch values for
"""
param_values = []
for param in params:
regex = re.compile(r"^\s*%s\s*(=|: )\s*(?P<param>\S.*?)\s*(#.*)?$" % param, re.M)
res = regex.search(rawtxt)
if res:
param_values.append(res.group('param').strip("'\""))
else:
param_values.append(None)
_log.debug("Obtained parameters value for %s: %s" % (params, param_values))
return param_values
class EasyConfigParser(object):
"""Read the easyconfig file, return a parsed config object
Can contain references to multiple version and toolchain/toolchain versions
"""
def __init__(self, filename=None, format_version=None, rawcontent=None,
auto_convert_value_types=True):
"""
Initialise the EasyConfigParser class
:param filename: path to easyconfig file to parse (superseded by rawcontent, if specified)
:param format_version: version of easyconfig file format, used to determine how to parse supplied easyconfig
:param rawcontent: raw content of easyconfig file to parse (preferred over easyconfig supplied via filename)
:param auto_convert_value_types: indicates whether types of easyconfig values should be automatically converted
in case they are wrong
"""
self.log = fancylogger.getLogger(self.__class__.__name__, fname=False)
self.rawcontent = None # the actual unparsed content
self.auto_convert = auto_convert_value_types
self.get_fn = None # read method and args
self.set_fn = None # write method and args
self.format_version = format_version
self._formatter = None
if rawcontent is not None:
self.rawcontent = rawcontent
self._set_formatter(filename)
elif filename is not None:
self._check_filename(filename)
self.process()
else:
raise EasyBuildError("Neither filename nor rawcontent provided to EasyConfigParser")
def process(self, filename=None):
"""Create an instance"""
self._read(filename=filename)
self._set_formatter(filename)
def check_values_types(self, cfg):
"""
Check types of easyconfig parameter values.
:param cfg: dictionary with easyconfig parameter values (result of get_config_dict())
"""
wrong_type_msgs = []
for key in cfg:
type_ok, newval = check_type_of_param_value(key, cfg[key], self.auto_convert)
if not type_ok:
wrong_type_msgs.append("value for '%s' should be of type '%s'" % (key, PARAMETER_TYPES[key].__name__))
elif newval != cfg[key]:
warning_msg = "Value for '%s' easyconfig parameter was converted from %s (type: %s) to %s (type: %s)"
self.log.warning(warning_msg, key, cfg[key], type(cfg[key]), newval, type(newval))
cfg[key] = newval
if wrong_type_msgs:
raise EasyBuildError("Type checking of easyconfig parameter values failed: %s", ', '.join(wrong_type_msgs))
else:
self.log.info("Type checking of easyconfig parameter values passed!")
def _check_filename(self, fn):
"""Perform sanity check on the filename, and set mechanism to set the content of the file"""
if os.path.isfile(fn):
self.get_fn = (read_file, (fn,))
self.set_fn = (write_file, (fn, self.rawcontent))
self.log.debug("Process filename %s with get function %s, set function %s" % (fn, self.get_fn, self.set_fn))
if self.get_fn is None:
raise EasyBuildError('Failed to determine get function for filename %s', fn)
if self.set_fn is None:
raise EasyBuildError('Failed to determine set function for filename %s', fn)
def _read(self, filename=None):
"""Read the easyconfig, dump content in self.rawcontent"""
if filename is not None:
self._check_filename(filename)
try:
self.rawcontent = self.get_fn[0](*self.get_fn[1])
except IOError as err:
raise EasyBuildError('Failed to obtain content with %s: %s', self.get_fn, err)
if not isinstance(self.rawcontent, str):
msg = 'rawcontent is not a string: type %s, content %s' % (type(self.rawcontent), self.rawcontent)
raise EasyBuildError("Unexpected result for raw content: %s", msg)
def _det_format_version(self):
"""Extract the format version from the raw content"""
if self.format_version is None:
self.format_version = get_format_version(self.rawcontent)
if self.format_version is None:
self.format_version = FORMAT_DEFAULT_VERSION
self.log.debug('No version found, using default %s' % self.format_version)
def _get_format_version_class(self):
"""Locate the class matching the version"""
if self.format_version is None:
self._det_format_version()
found_classes = get_format_version_classes(version=self.format_version)
if len(found_classes) == 1:
return found_classes[0]
elif not found_classes:
raise EasyBuildError('No format classes found matching version %s', self.format_version)
else:
raise EasyBuildError("More than one format class found matching version %s in %s",
self.format_version, found_classes)
def _set_formatter(self, filename):
"""Obtain instance of the formatter"""
if self._formatter is None:
klass = self._get_format_version_class()
self._formatter = klass()
self._formatter.parse(self.rawcontent)
def set_format_text(self):
"""Create the text for the formatter instance"""
# TODO create the data in self.rawcontent
raise NotImplementedError
def write(self, filename=None):
"""Write the easyconfig format instance, using content in self.rawcontent."""
if filename is not None:
self._check_filename(filename)
try:
self.set_fn[0](*self.set_fn[1])
except IOError as err:
raise EasyBuildError("Failed to process content with %s: %s", self.set_fn, err)
def set_specifications(self, specs):
"""Set specifications."""
self._formatter.set_specifications(specs)
def get_config_dict(self, validate=True):
"""Return parsed easyconfig as a dict."""
# allows to bypass the validation step, typically for testing
if validate:
self._formatter.validate()
cfg = self._formatter.get_config_dict()
self.check_values_types(cfg)
return cfg
def dump(self, ecfg, default_values, templ_const, templ_val, toolchain_hierarchy=None):
"""Dump easyconfig in format it was parsed from."""
return self._formatter.dump(ecfg, default_values, templ_const, templ_val,
toolchain_hierarchy=toolchain_hierarchy)