Skip to content

Commit a6824d8

Browse files
committed
Merge pull request #9 from boegel/dump-formatting
move enhanced support for dumping easyconfig reformatted to format/one.py
2 parents f7d5ed2 + 6afadc0 commit a6824d8

File tree

11 files changed

+358
-276
lines changed

11 files changed

+358
-276
lines changed

easybuild/framework/easyconfig/easyconfig.py

Lines changed: 29 additions & 195 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@
4444
from vsc.utils.patterns import Singleton
4545

4646
import easybuild.tools.environment as env
47-
from easybuild.tools.autopep8 import fix_code
4847
from easybuild.tools.build_log import EasyBuildError
4948
from easybuild.tools.config import build_option, get_module_naming_scheme
5049
from easybuild.tools.filetools import decode_class_name, encode_class_name, read_file, write_file
@@ -56,7 +55,7 @@
5655
from easybuild.tools.systemtools import check_os_dependency
5756
from easybuild.tools.toolchain import DUMMY_TOOLCHAIN_NAME, DUMMY_TOOLCHAIN_VERSION
5857
from easybuild.tools.toolchain.utilities import get_toolchain
59-
from easybuild.tools.utilities import remove_unwanted_chars, quote_str
58+
from easybuild.tools.utilities import quote_py_str, quote_str, remove_unwanted_chars
6059
from easybuild.framework.easyconfig import MANDATORY
6160
from easybuild.framework.easyconfig.constants import EXTERNAL_MODULE_MARKER
6261
from easybuild.framework.easyconfig.default import DEFAULT_CONFIG
@@ -76,26 +75,12 @@
7675
# set of configure/build/install options that can be provided as lists for an iterated build
7776
ITERATE_OPTIONS = ['preconfigopts', 'configopts', 'prebuildopts', 'buildopts', 'preinstallopts', 'installopts']
7877

79-
# values for these keys will not be templated in dump()
80-
EXCLUDED_KEYS_REPLACE_TEMPLATES = ['easyblock', 'name', 'version', 'description', 'homepage', 'toolchain']
81-
82-
83-
# ordered groups of keys to obtain a nice looking easyconfig file
84-
GROUPED_PARAMS = [
85-
['easyblock'],
86-
['name', 'version', 'versionprefix', 'versionsuffix'],
87-
['homepage', 'description'],
88-
['toolchain', 'toolchainopts'],
89-
['sources', 'source_urls'],
90-
['patches'],
91-
['builddependencies', 'dependencies', 'hiddendependencies'],
92-
['osdependencies'],
93-
['preconfigopts', 'configopts'],
94-
['prebuildopts', 'buildopts'],
95-
['preinstallopts', 'installopts'],
96-
['parallel', 'maxparallel'],
97-
]
98-
LAST_PARAMS = ['sanity_check_paths', 'moduleclass']
78+
79+
try:
80+
import autopep8
81+
except ImportError as err:
82+
_log.warning("Failed to import autopep8, dumping easyconfigs with reformatting enabled will not work: %s", err)
83+
9984

10085
_easyconfig_files_cache = {}
10186
_easyconfigs_cache = {}
@@ -197,6 +182,7 @@ def __init__(self, path, extra_options=None, build_specs=None, validate=True, hi
197182

198183
# parse easyconfig file
199184
self.build_specs = build_specs
185+
self.parser = EasyConfigParser(filename=self.path, rawcontent=self.rawtxt)
200186
self.parse()
201187

202188
# handle allowed system dependencies
@@ -260,13 +246,10 @@ def parse(self):
260246
self.log.debug("Obtained specs dict %s" % arg_specs)
261247

262248
self.log.info("Parsing easyconfig file %s with rawcontent: %s" % (self.path, self.rawtxt))
263-
parser = EasyConfigParser(filename=self.path, rawcontent=self.rawtxt)
264-
parser.set_specifications(arg_specs)
265-
local_vars = parser.get_config_dict()
249+
self.parser.set_specifications(arg_specs)
250+
local_vars = self.parser.get_config_dict()
266251
self.log.debug("Parsed easyconfig as a dictionary: %s" % local_vars)
267252

268-
self.comments = parser.get_comments()
269-
270253
# make sure all mandatory parameters are defined
271254
# this includes both generic mandatory parameters and software-specific parameters defined via extra_options
272255
missing_mandatory_keys = [key for key in self.mandatory if key not in local_vars]
@@ -492,12 +475,14 @@ def toolchain(self):
492475
self.log.debug("Initialized toolchain: %s (opts: %s)" % (tc_dict, self['toolchainopts']))
493476
return self._toolchain
494477

495-
def dump(self, fp, formatting=True):
478+
def dump(self, fp):
496479
"""
497480
Dump this easyconfig to file, with the given filename.
498481
"""
499482
orig_enable_templating = self.enable_templating
500-
self.enable_templating = False # templated values should be dumped unresolved
483+
484+
# templated values should be dumped unresolved
485+
self.enable_templating = False
501486

502487
# build dict of default values
503488
default_values = dict([(key, DEFAULT_CONFIG[key][0]) for key in DEFAULT_CONFIG])
@@ -510,125 +495,22 @@ def dump(self, fp, formatting=True):
510495
keys = sorted(self.template_values, key=lambda k: len(self.template_values[k]), reverse=True)
511496
templ_val = OrderedDict([(self.template_values[k], k) for k in keys if len(self.template_values[k]) > 2])
512497

513-
def add_key_and_comments(key, val):
514-
"""
515-
Add key + value and comments (if any) to txt to be dumped.
516-
"""
517-
if key in self.comments['inline']:
518-
ebtxt.append("%s = %s %s" % (key, val, self.comments['inline'][key]))
519-
else:
520-
if key in self.comments['above']:
521-
ebtxt.extend(self.comments['above'][key])
522-
ebtxt.append("%s = %s" % (key, val))
523-
524-
def include_defined_parameters(ebtxt, keyset):
525-
"""
526-
Internal function to include parameters in the dumped easyconfig file which have a non-default value.
527-
"""
528-
for group in keyset:
529-
printed = False
530-
for key in group:
531-
val = self[key]
532-
if val != default_values[key]:
533-
# dependency easyconfig parameters were parsed, so these need special care to 'unparse' them
534-
if key in ['builddependencies', 'dependencies', 'hiddendependencies']:
535-
dumped_deps = [self._dump_dependency(d) for d in val]
536-
val = dumped_deps
537-
else:
538-
val = quote_py_str(val)
539-
540-
ebtxt = self._add_key_and_comments(ebtxt, key, val, templ_const, templ_val, formatting)
541-
542-
printed_keys.append(key)
543-
printed = True
544-
if printed:
545-
ebtxt.append('')
546-
547-
ebtxt = []
548-
printed_keys = []
549-
550-
# add header comments
551-
ebtxt.extend(self.comments['header'])
552-
553-
# print easyconfig parameters ordered and in groups specified above
554-
include_defined_parameters(ebtxt, GROUPED_PARAMS)
555-
556-
# print other easyconfig parameters at the end
557-
keys_to_ignore = printed_keys + LAST_PARAMS
558-
for key in default_values:
559-
if key not in keys_to_ignore and self[key] != default_values[key]:
560-
ebtxt = self._add_key_and_comments(ebtxt, key, quote_py_str(self[key]), templ_const, templ_val, formatting)
561-
ebtxt.append('')
562-
563-
# print last parameters
564-
include_defined_parameters(ebtxt, [[k] for k in LAST_PARAMS])
565-
566-
write_file(fp, ('\n'.join(ebtxt)).strip()) # strip for newlines at the end
567-
568-
dumped_text = ('\n'.join(ebtxt))
569-
write_file(fp, (fix_code(dumped_text, options={'aggressive': 1, 'max_line_length':120})).strip())
570-
self.enable_templating = orig_enable_templating
498+
ectxt = self.parser.dump(self, default_values, templ_const, templ_val)
499+
self.log.debug("Dumped easyconfig: %s", ectxt)
571500

572-
def _add_key_and_comments(self, ebtxt, key, val, templ_const, templ_val, formatting):
573-
""" Add key, value pair and comments (if there are any) to the dump file (helper method for dump()) """
574-
if formatting:
575-
val = self._format(key, val, True, dict())
576-
else:
577-
val = str(val)
501+
if build_option('dump_autopep8'):
502+
autopep8_opts = {
503+
'aggressive': 1, # enable non-whitespace changes, but don't be too aggressive
504+
'max_line_length': 120,
505+
}
506+
self.log.info("Reformatting dumped easyconfig using autopep8 (options: %s)", autopep8_opts)
507+
print("Reformatting dumped easyconfig using autopep8 (options: %s)", autopep8_opts)
508+
ectxt = autopep8.fix_code(ectxt, options=autopep8_opts)
509+
self.log.debug("Dumped easyconfig after autopep8 reformatting: %s", ectxt)
578510

579-
# templates
580-
if key not in EXCLUDED_KEYS_REPLACE_TEMPLATES:
581-
new_val = to_template_str(val, templ_const, templ_val)
582-
if not r'%(' + key in new_val:
583-
val = new_val
511+
write_file(fp, ectxt.strip())
584512

585-
if key in self.comments['inline']:
586-
ebtxt.append("%s = %s%s" % (key, val, self.comments['inline'][key]))
587-
else:
588-
if key in self.comments['above']:
589-
ebtxt.extend(self.comments['above'][key])
590-
ebtxt.append("%s = %s" % (key, val))
591-
592-
return ebtxt
593-
594-
def _format(self, key, value, outer, comment):
595-
""" Returns string version of the value, including comments and newlines in lists, tuples and dicts """
596-
str_value = ''
597-
598-
for k, v in self.comments['list_value'].get(key, {}).items():
599-
if str(value) in k:
600-
comment[str(value)] = v
601-
602-
if outer:
603-
if isinstance(value, list):
604-
str_value += '[\n'
605-
for el in value:
606-
str_value += self._format(key, el, False, comment)
607-
str_value += ',' + comment.get(str(el), '') + '\n'
608-
str_value += ']'
609-
elif isinstance(value, tuple):
610-
str_value += '(\n'
611-
for el in value:
612-
str_value += self._format(key, el, False, comment)
613-
str_value += ',' + comment.get(str(el), '') + '\n'
614-
str_value += ')'
615-
elif isinstance(value, dict):
616-
str_value += '{\n'
617-
for k, v in value.items():
618-
str_value += quote_py_str(k) + ': ' + self._format(key, v, False, comment)
619-
str_value += ',' + comment.get(str(v), '') + '\n'
620-
str_value += '}'
621-
622-
value = str_value or str(value)
623-
624-
else:
625-
# dependencies are already dumped as strings, so they do not need to be quoted again
626-
if isinstance(value, basestring) and key not in ['builddependencies', 'dependencies', 'hiddendependencies']:
627-
value = quote_py_str(value)
628-
else:
629-
value = str(value)
630-
631-
return value
513+
self.enable_templating = orig_enable_templating
632514

633515
def _validate(self, attr, values): # private method
634516
"""
@@ -764,26 +646,6 @@ def _parse_dependency(self, dep, hidden=False):
764646

765647
return dependency
766648

767-
def _dump_dependency(self, dep):
768-
"""Dump parsed dependency in tuple format"""
769-
770-
if dep['external_module']:
771-
res = "(%s, EXTERNAL_MODULE)" % quote_py_str(dep['full_mod_name'])
772-
else:
773-
# mininal spec: (name, version)
774-
tup = (dep['name'], dep['version'])
775-
if dep['toolchain'] != self['toolchain']:
776-
if dep['dummy']:
777-
tup += (dep['versionsuffix'], True)
778-
else:
779-
tup += (dep['versionsuffix'], (dep['toolchain']['name'], dep['toolchain']['version']))
780-
781-
elif dep['versionsuffix']:
782-
tup += (dep['versionsuffix'],)
783-
784-
res = str(tup)
785-
return res
786-
787649
def generate_template_values(self):
788650
"""Try to generate all template values."""
789651
# TODO proper recursive code https://github.com/hpcugent/easybuild-framework/issues/474
@@ -1042,34 +904,6 @@ def resolve_template(value, tmpl_dict):
1042904
return value
1043905

1044906

1045-
def quote_py_str(val):
1046-
"""Version of quote_str specific for generating use in Python context (e.g., easyconfig parameters)."""
1047-
return quote_str(val, escape_newline=True, prefer_single_quotes=True)
1048-
1049-
1050-
def to_template_str(value, templ_const, templ_val):
1051-
"""
1052-
Insert template values where possible
1053-
- value is a string
1054-
- templ_const is a dictionary of template strings (constants)
1055-
- templ_val is an ordered dictionary of template strings specific for this easyconfig file
1056-
"""
1057-
old_value = None
1058-
while value != old_value:
1059-
old_value = value
1060-
# check for constant values
1061-
for tval, tname in templ_const.items():
1062-
value = re.sub(r'(^|\W)' + re.escape(tval) + r'(\W|$)', r'\1' + tname + r'\2', value)
1063-
1064-
for tval, tname in templ_val.items():
1065-
# only replace full words with templates: word to replace should be at the beginning of a line
1066-
# or be preceded by a non-alphanumeric (\W). It should end at the end of a line or be succeeded
1067-
# by another non-alphanumeric.
1068-
value = re.sub(r'(^|\W)' + re.escape(tval) + r'(\W|$)', r'\1%(' + tname + r')s\2', value)
1069-
1070-
return value
1071-
1072-
1073907
def process_easyconfig(path, build_specs=None, validate=True, parse_only=False, hidden=None):
1074908
"""
1075909
Process easyconfig, returning some information for each block
@@ -1088,7 +922,7 @@ def process_easyconfig(path, build_specs=None, validate=True, parse_only=False,
1088922
if build_specs is None:
1089923
cache_key = (path, validate, hidden, parse_only)
1090924
if cache_key in _easyconfigs_cache:
1091-
return copy.deepcopy(_easyconfigs_cache[cache_key])
925+
return [e.copy() for e in _easyconfigs_cache[cache_key]]
1092926

1093927
easyconfigs = []
1094928
for spec in blocks:
@@ -1147,7 +981,7 @@ def process_easyconfig(path, build_specs=None, validate=True, parse_only=False,
1147981
easyconfig['unresolved_deps'] = copy.deepcopy(easyconfig['dependencies'])
1148982

1149983
if cache_key is not None:
1150-
_easyconfigs_cache[cache_key] = copy.deepcopy(easyconfigs)
984+
_easyconfigs_cache[cache_key] = [e.copy() for e in easyconfigs]
1151985

1152986
return easyconfigs
1153987

easybuild/framework/easyconfig/format/format.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,36 @@
4141
from easybuild.tools.configobj import Section
4242

4343

44+
INDENT_4SPACES = ' ' * 4
45+
4446
# format is mandatory major.minor
4547
FORMAT_VERSION_KEYWORD = "EASYCONFIGFORMAT"
4648
FORMAT_VERSION_TEMPLATE = "%(major)s.%(minor)s"
4749
FORMAT_VERSION_HEADER_TEMPLATE = "# %s %s\n" % (FORMAT_VERSION_KEYWORD, FORMAT_VERSION_TEMPLATE) # must end in newline
4850
FORMAT_VERSION_REGEXP = re.compile(r'^#\s+%s\s*(?P<major>\d+)\.(?P<minor>\d+)\s*$' % FORMAT_VERSION_KEYWORD, re.M)
4951
FORMAT_DEFAULT_VERSION = EasyVersion('1.0')
5052

53+
# values for these keys will not be templated in dump()
54+
EXCLUDED_KEYS_REPLACE_TEMPLATES = ['easyblock', 'name', 'version', 'description', 'homepage', 'toolchain']
55+
56+
# ordered groups of keys to obtain a nice looking easyconfig file
57+
GROUPED_PARAMS = [
58+
['easyblock'],
59+
['name', 'version', 'versionprefix', 'versionsuffix'],
60+
['homepage', 'description'],
61+
['toolchain', 'toolchainopts'],
62+
['sources', 'source_urls'],
63+
['patches'],
64+
['builddependencies', 'dependencies', 'hiddendependencies'],
65+
['osdependencies'],
66+
['preconfigopts', 'configopts'],
67+
['prebuildopts', 'buildopts'],
68+
['preinstallopts', 'installopts'],
69+
['parallel', 'maxparallel'],
70+
]
71+
LAST_PARAMS = ['sanity_check_paths', 'moduleclass']
72+
73+
5174
_log = fancylogger.getLogger('easyconfig.format.format', fname=False)
5275

5376

@@ -580,6 +603,7 @@ def __init__(self):
580603
raise EasyBuildError('Invalid version number %s (incorrect length)', self.VERSION)
581604

582605
self.rawtext = None # text version of the easyconfig
606+
self.comments = {} # comments in easyconfig file
583607
self.header = None # easyconfig header (e.g., format version, license, ...)
584608
self.docstring = None # easyconfig docstring (e.g., author, maintainer, ...)
585609

@@ -602,10 +626,14 @@ def parse(self, txt, **kwargs):
602626
"""Parse the txt according to this format. This is highly version specific"""
603627
raise NotImplementedError
604628

605-
def dump(self):
629+
def dump(self, ecfg, default_values, templ_const, templ_val):
606630
"""Dump easyconfig according to this format. This is higly version specific"""
607631
raise NotImplementedError
608632

633+
def extract_comments(self, rawtxt):
634+
"""Extract comments from raw content."""
635+
raise NotImplementedError
636+
609637

610638
def get_format_version_classes(version=None):
611639
"""Return the (usable) subclasses from EasyConfigFormat that have a matching version."""

0 commit comments

Comments
 (0)