Manager of the list of required external features of the software.
Module sppas.core.preinstall
Class Features
Description
Constructor
__init__
Create a new Features instance.
A Features instance is a container for a list of features. It parses a '.ini' file to get each feature config. If a filename is not specified, the default one will be used.
Parameters
- req: (str)
- cmdos: (str)
- filename: (str) A custom configuration filename.
View Source
def __init__(self, req='', cmdos='', filename=None):
"""Create a new Features instance.
A Features instance is a container for a list of features.
It parses a '.ini' file to get each feature config. If a filename is
not specified, the default one will be used.
:param req: (str)
:param cmdos: (str)
:param filename: (str) A custom configuration filename.
"""
self.__req = req
self.__cmdos = cmdos
self.__features = list()
self.__filename = None
if filename is not None:
if os.path.exists(filename) and filename.endswith('.ini'):
self.__filename = filename
self.set_features()
Public functions
get_features_filename
Return the name of the file with the features descriptions.
View Source
def get_features_filename(self):
"""Return the name of the file with the features descriptions."""
if self.__filename is not None:
return self.__filename
return os.path.join(paths.etc, 'features.ini')
get_ids
Return the list of feature identifiers of the given type.
Parameters
- feat_type: (str) Feature type, or None to get all ids
Returns
- (list) Feature identifiers
View Source
def get_ids(self, feat_type=None):
"""Return the list of feature identifiers of the given type.
:param feat_type: (str) Feature type, or None to get all ids
:return: (list) Feature identifiers
"""
if feat_type is None:
return [f.get_id() for f in self.__features]
return [f.get_id() for f in self.__features if f.get_type() == feat_type]
feature_type
Return the feature type: deps, lang, annot.
Parameters
- fid: (str) Identifier of a feature
View Source
def feature_type(self, fid):
"""Return the feature type: deps, lang, annot.
:param fid: (str) Identifier of a feature
"""
for feat in self.__features:
if feat.get_id() == fid:
return feat.get_type()
logging.error('Unknown feature {}'.format(fid))
return None
enable
Return True if the feature is enabled and/or set it.
Parameters
- fid: (str) Identifier of a feature
- value: (bool or None) Enable of disable the feature.
View Source
def enable(self, fid, value=None):
"""Return True if the feature is enabled and/or set it.
:param fid: (str) Identifier of a feature
:param value: (bool or None) Enable of disable the feature.
"""
for feat in self.__features:
if feat.get_id() == fid:
if value is None:
return feat.get_enable()
return feat.set_enable(value)
logging.error('Unknown feature {}'.format(fid))
return False
available
Return True if the feature is available and/or set it.
Parameters
- fid: (str) Identifier of a feature
- value: (bool or None) Make the feature available or not.
View Source
def available(self, fid, value=None):
"""Return True if the feature is available and/or set it.
:param fid: (str) Identifier of a feature
:param value: (bool or None) Make the feature available or not.
"""
for feat in self.__features:
if feat.get_id() == fid:
if value is None:
return feat.get_available()
return feat.set_available(value)
logging.error('Unknown feature {}'.format(fid))
return False
brief
Return the brief description of the feature.
Parameters
- fid: (str) Identifier of a feature
View Source
def brief(self, fid):
"""Return the brief description of the feature.
:param fid: (str) Identifier of a feature
"""
for feat in self.__features:
if feat.get_id() == fid:
return feat.get_brief()
logging.error('Unknown feature {}'.format(fid))
return None
description
Return the description of the feature
Parameters
- fid: (str) Identifier of a feature
View Source
def description(self, fid):
"""Return the description of the feature
:param fid: (str) Identifier of a feature
"""
for feat in self.__features:
if feat.get_id() == fid:
return feat.get_desc()
logging.error('Unknown feature {}'.format(fid))
return None
packages
Return the dictionary of system dependencies of the feature.
Parameters
- fid: (str) Identifier of a feature
Returns
- (dict) key=package; value=version
View Source
def packages(self, fid):
"""Return the dictionary of system dependencies of the feature.
:param fid: (str) Identifier of a feature
:return: (dict) key=package; value=version
"""
for feat in self.__features:
if feat.get_id() == fid:
if isinstance(feat, DepsFeature) is True:
return feat.get_packages()
else:
logging.error('Feature {} is not a DepsFeature:No packages are defined.'.format(fid))
logging.error('Unknown feature {}'.format(fid))
return dict()
pypi
Return the dictionary of pip dependencies of the feature.
Parameters
- fid: (str) Identifier of a feature
Returns
- (dict) key=package; value=version
View Source
def pypi(self, fid):
"""Return the dictionary of pip dependencies of the feature.
:param fid: (str) Identifier of a feature
:return: (dict) key=package; value=version
"""
for feat in self.__features:
if feat.get_id() == fid:
if isinstance(feat, DepsFeature) is True:
pips = dict()
for key in feat.get_pypi_packages():
pips[key] = feat.get_pypi_package_version(key)
return pips
else:
logging.error('Feature {} is not a DepsFeature:No pypi is defined.'.format(fid))
logging.error('Unknown feature {}'.format(fid))
return dict()
pypi_alt
Return the dictionary of alternative pip dependencies of the feature.
Parameters
- fid: (str) Identifier of a feature
Returns
- (dict) key=package; value=version
View Source
def pypi_alt(self, fid):
"""Return the dictionary of alternative pip dependencies of the feature.
:param fid: (str) Identifier of a feature
:return: (dict) key=package; value=version
"""
for feat in self.__features:
if feat.get_id() == fid:
if isinstance(feat, DepsFeature) is True:
pips = dict()
for key in feat.get_pypi_alt_packages():
pips[key] = feat.get_pypi_alt_package_version(key)
return pips
else:
logging.error('Feature {} is not a DepsFeature:No alt pypi is defined.'.format(fid))
logging.error('Unknown feature {}'.format(fid))
return dict()
pypi_opt
Return the options of a pip dependency of the feature.
Parameters
- fid: (str) Identifier of a feature
- package_name: (str) Name of a pip package of the feature
Returns
- (str)
View Source
def pypi_opt(self, fid, package_name=''):
"""Return the options of a pip dependency of the feature.
:param fid: (str) Identifier of a feature
:param package_name: (str) Name of a pip package of the feature
:return: (str)
"""
for feat in self.__features:
if feat.get_id() == fid:
if isinstance(feat, DepsFeature) is True:
return feat.get_pip_options(package_name)
return ''
check_pip_deps
Check Python (pip) dependencies for all enabled 'deps' features.
This method checks each DepsFeature for the presence of required Python modules, as specified in the 'pip_test' field (supporting alternatives via '|'). The feature is set as available if at least one required module (per group) is importable. If no module is importable, the feature is set as unavailable and disabled.
Example
> > features = Features(filename="/path/to/features.ini")
> > features.check_pip_deps()
View Source
def check_pip_deps(self):
"""Check Python (pip) dependencies for all enabled 'deps' features.
This method checks each DepsFeature for the presence of required Python modules,
as specified in the 'pip_test' field (supporting alternatives via '|').
The feature is set as available if at least one required module (per group) is importable.
If no module is importable, the feature is set as unavailable and disabled.
:example:
> features = Features(filename="/path/to/features.ini")
> features.check_pip_deps()
"""
for fid in self.get_pip_test_deps():
feat = [f for f in self.__features if f.get_id() == fid][0]
if feat.get_enable() is False:
continue
pip_test = feat.get_pip_test()
modules_to_test = self._parse_pip_test(pip_test)
if modules_to_test is None:
continue
try:
DepsFeatureChecker.check_modules(modules_to_test)
feat.set_available(True)
except ImportError:
feat.set_available(False)
feat.set_enable(False)
logging.error(f"Feature '{fid}' disabled: missing Python dependency (see pip_test: '{pip_test}').")
get_pip_test_deps
Return the list of deps feature IDs with a non-empty pip_test field.
Returns
- (list) List of feature identifiers
View Source
def get_pip_test_deps(self):
"""Return the list of deps feature IDs with a non-empty pip_test field.
:return: (list) List of feature identifiers
"""
ids = []
for feat in self.__features:
if isinstance(feat, DepsFeature) is True:
pip_test = feat.get_pip_test()
if pip_test and pip_test.strip() != '':
ids.append(feat.get_id())
return ids
cmd
Return the command to execute for the feature.
Parameters
- fid: (str) Identifier of a feature
Returns
- (str)
View Source
def cmd(self, fid):
"""Return the command to execute for the feature.
:param fid: (str) Identifier of a feature
:return: (str)
"""
for feat in self.__features:
if feat.get_id() == fid:
if isinstance(feat, DepsFeature) is True:
return feat.get_cmd()
else:
logging.error('Feature {} is not a DepsFeature:No cmd is defined.'.format(fid))
logging.error('Unknown feature {}'.format(fid))
return str()
check_cmd_deps
Check system command availability for all enabled 'deps' features.
This method uses the 'cmd_test' field to verify whether the required system command is available in the environment. If not found, the feature is marked as unavailable and disabled.
View Source
def check_cmd_deps(self):
"""Check system command availability for all enabled 'deps' features.
This method uses the 'cmd_test' field to verify whether the required
system command is available in the environment. If not found,
the feature is marked as unavailable and disabled.
"""
for feat in self.__features:
if isinstance(feat, DepsFeature) is False:
continue
if feat.get_enable() is False:
continue
test_command = feat.get_cmd_test()
commands_to_test = self._parse_cmd_test(test_command)
if commands_to_test is None:
continue
available = True
for cmd in commands_to_test:
try:
DepsFeatureChecker((None, (cmd,))).check()
except RuntimeError:
available = False
break
feat.set_available(available)
if not available:
feat.set_enable(False)
logging.error(f"Feature '{feat.get_id()}' disabled: missing system command (see cmd_test: '{test_command}').")
check_feature_deps
Check the dependencies of the given 'deps' feature.
The feature is made available and enabled if both its modules can be imported and its commands can be executed. It is made unavailable and disabled if not. Its state is unchanged if it has nothing to be tested.
Parameters
- fid: (str) Identifier of a feature
Returns
- (bool) True if the dependencies of the feature are satisfied
View Source
def check_feature_deps(self, fid):
"""Check the dependencies of the given 'deps' feature.
The feature is made available and enabled if both its modules can be
imported and its commands can be executed. It is made unavailable and
disabled if not. Its state is unchanged if it has nothing to be tested.
:param fid: (str) Identifier of a feature
:return: (bool) True if the dependencies of the feature are satisfied
"""
for feat in self.__features:
if feat.get_id() == fid:
if isinstance(feat, DepsFeature) is False:
logging.error('Feature {} is not a DepsFeature:No dependency is defined.'.format(fid))
return False
logging.debug("Checking dependencies for feature '{}'".format(fid))
test_modules = self._parse_pip_test(feat.get_pip_test())
test_commands = self._parse_cmd_test(feat.get_cmd_test())
if test_commands is None and test_modules is None:
return True
try:
DepsFeatureChecker((test_modules, test_commands)).check()
except (ImportError, RuntimeError) as e:
feat.set_available(False)
feat.set_enable(False)
logging.info(f"Feature '{fid}' disabled: {str(e)}")
return False
feat.set_available(True)
feat.set_enable(True)
return True
logging.error('Unknown feature {}'.format(fid))
return False
check_deps
Check all dependencies for all enabled 'deps' features.
Check if both the imports and the commands are available and can be enabled for all features, or only the enabled ones.
Parameters
- test_all: (bool) If True, all dependencies are checked, not only the enabled ones
View Source
def check_deps(self, test_all=False):
"""Check all dependencies for all enabled 'deps' features.
Check if both the imports and the commands are available and can
be enabled for all features, or only the enabled ones.
:param test_all: (bool) If True, all dependencies are checked, not only the enabled ones
"""
for feat in self.__features:
if isinstance(feat, DepsFeature) is False:
continue
if test_all is False and feat.get_enable() is False:
continue
self.check_feature_deps(feat.get_id())
lang
Return the lang code of the linguistic resource to download.
Parameters
- fid: (str) Identifier of a feature
Returns
- (str)
View Source
def lang(self, fid):
"""Return the lang code of the linguistic resource to download.
:param fid: (str) Identifier of a feature
:return: (str)
"""
for feat in self.__features:
if feat.get_id() == fid:
if isinstance(feat, LangFeature) is True:
return feat.get_lang()
else:
logging.error('Feature {} is not a LangFeature:No lang is defined.'.format(fid))
logging.error('Unknown feature {}'.format(fid))
return str()
annot
Return the name the annotation resource to download.
Parameters
- fid: (str) Identifier of a feature
Returns
- (str)
View Source
def annot(self, fid):
"""Return the name the annotation resource to download.
:param fid: (str) Identifier of a feature
:return: (str)
"""
for feat in self.__features:
if feat.get_id() == fid:
if isinstance(feat, AnnotFeature) is True:
return feat.get_annot()
else:
logging.error('Feature {} is not an AnnotFeature:No annot is defined.'.format(fid))
logging.error('Unknown feature {}'.format(fid))
return str()
spin
Return the name of the spin-off resource to download.
Parameters
- fid: (str) Identifier of a feature
Returns
- (str)
View Source
def spin(self, fid):
"""Return the name of the spin-off resource to download.
:param fid: (str) Identifier of a feature
:return: (str)
"""
for feat in self.__features:
if feat.get_id() == fid:
if isinstance(feat, SpinOffFeature) is True:
return feat.get_spin()
else:
logging.error('Feature {} is not a SpinOffFeature:No spin is defined.'.format(fid))
logging.error('Unknown feature {}'.format(fid))
return str()
spin_url
Return the url of the spin-off resource.
Parameters
- fid: (str) Identifier of a feature
Returns
- (str)
View Source
def spin_url(self, fid):
"""Return the url of the spin-off resource.
:param fid: (str) Identifier of a feature
:return: (str)
"""
for feat in self.__features:
if feat.get_id() == fid:
if isinstance(feat, SpinOffFeature) is True:
return feat.get_spin_url()
else:
logging.error('Feature {} is not a SpinOffFeature:No spin is defined.'.format(fid))
logging.error('Unknown feature {}'.format(fid))
return str()
spin_version
Return the version of the spin-off resource.
Parameters
- fid: (str) Identifier of a feature
Returns
- (str)
View Source
def spin_version(self, fid):
"""Return the version of the spin-off resource.
:param fid: (str) Identifier of a feature
:return: (str)
"""
for feat in self.__features:
if feat.get_id() == fid:
if isinstance(feat, SpinOffFeature) is True:
return feat.get_spin_version()
else:
logging.error('Feature {} is not a SpinOffFeature:No spin is defined.'.format(fid))
logging.error('Unknown feature {}'.format(fid))
return str()
set_features
Browses the features.ini file and instantiate a Feature().
Only unix-based systems can have package requirements. If they don't, the corresponding req_ attribute is missing or empty or with "nil".
A feature is not available for a system, if none of the corresponding "cmd_" and "req_" and the "pip" attributes are defined.
View Source
def set_features(self):
"""Browses the features.ini file and instantiate a Feature().
Only unix-based systems can have package requirements. If they don't,
the corresponding req_ attribute is missing or empty or with "nil".
A feature is not available for a system, if none of the corresponding "cmd_"
and "req_" and the "pip" attributes are defined.
"""
self.__features = list()
features_parser = self.__init_features()
for fid in features_parser.sections():
try:
feature = self.__set_feature(fid, features_parser)
except cp.NoOptionError:
logging.error('Missing or wrong feature type for feature {}'.format(fid))
continue
try:
desc = features_parser.get(fid, 'brief')
feature.set_brief(desc)
except cp.NoOptionError:
pass
try:
desc = features_parser.get(fid, 'desc')
feature.set_desc(desc)
except cp.NoOptionError:
pass
try:
e = features_parser.getboolean(fid, 'enable')
feature.set_enable(e)
except cp.NoOptionError:
pass
self.__features.append(feature)
Private functions
_parse_pip_test
Parse a pip_test string into a tuple of tuples (AND/OR structure).
- If the string contains spaces, each space separates AND groups.
- Within each group, '|' separates OR alternatives.
- If there are no spaces but a '|', treat the whole string as one OR group.
- If there is only one module name, return a single group with one element.
Examples: 'a|b c' -> (('a', 'b'), ('c',)) 'a|b' -> (('a', 'b'),) 'a' -> (('a',),)
Parameters
- pipteststr: (str|None) The pip_test string from the feature.
Returns
- (tuple of tuple of str) or None if input is empty.
View Source
def _parse_pip_test(self, pip_test_str):
"""Parse a pip_test string into a tuple of tuples (AND/OR structure).
- If the string contains spaces, each space separates AND groups.
- Within each group, '|' separates OR alternatives.
- If there are no spaces but a '|', treat the whole string as one OR group.
- If there is only one module name, return a single group with one element.
Examples:
'a|b c' -> (('a', 'b'), ('c',))
'a|b' -> (('a', 'b'),)
'a' -> (('a',),)
:param pip_test_str: (str|None) The pip_test string from the feature.
:return: (tuple of tuple of str) or None if input is empty.
"""
if pip_test_str is None:
return None
pip_test_str = pip_test_str.strip()
if pip_test_str == '':
return None
if ' ' in pip_test_str:
and_groups = []
for group in pip_test_str.split():
or_group = tuple((m.strip() for m in group.split('|') if m.strip()))
and_groups.append(or_group)
return tuple(and_groups)
if '|' in pip_test_str:
or_group = tuple((m.strip() for m in pip_test_str.split('|') if m.strip()))
return (or_group,)
return ((pip_test_str,),)
_parse_cmd_test
Parse a cmd_test string into a tuple of commands.
Parse the cmd_test field and return a tuple of command strings. Commands are separated by a semicolon. Each command is stripped of spaces.
Example: "ffmpeg -h;ls" -> ('ffmpeg -h', 'ls')
Parameters
- cmdteststr: (str|None) The cmd_test string from the feature.
Returns
- (tuple of str) or None if input is empty.
View Source
def _parse_cmd_test(self, cmd_test_str):
"""Parse a cmd_test string into a tuple of commands.
Parse the cmd_test field and return a tuple of command strings.
Commands are separated by a semicolon. Each command is stripped of spaces.
Example:
"ffmpeg -h;ls" -> ('ffmpeg -h', 'ls')
:param cmd_test_str: (str|None) The cmd_test string from the feature.
:return: (tuple of str) or None if input is empty.
"""
if cmd_test_str is None:
return None
cmd_test_str = cmd_test_str.strip()
if cmd_test_str == '':
return None
return tuple((cmd.strip() for cmd in cmd_test_str.split(';') if cmd.strip() != ''))
Protected functions
__set_feature
View Source
def __set_feature(self, fid, parser):
feature = None
try:
ft = parser.get(fid, 'type')
if ft == 'deps':
feature = DepsFeature(fid)
self.__fill_deps_feature(feature, parser)
if ft == 'lang':
feature = LangFeature(fid)
self.__fill_lang_feature(feature)
if ft == 'annot':
feature = AnnotFeature(fid)
self.__fill_annot_feature(feature)
if ft == 'spin':
feature = SpinOffFeature(fid)
self.__fill_spin_feature(feature, parser)
except cp.NoOptionError:
pass
if feature is not None:
return feature
raise cp.NoOptionError
__fill_deps_feature
View Source
def __fill_deps_feature(self, feature, parser):
fid = feature.get_id()
try:
d = parser.get(fid, self.__req)
if len(d) > 0 and d.lower() != 'nil':
depend_packages = self.__parse_depend(d)
feature.set_packages(depend_packages)
except cp.NoOptionError:
pass
try:
d = parser.get(fid, 'pip')
if len(d) > 0 and d.lower() != 'nil':
depend_pypi = self.__parse_depend(d)
for key in depend_pypi:
feature.add_pypi(key, depend_pypi[key])
except cp.NoOptionError:
pass
try:
d = parser.get(fid, 'pip_alt')
if len(d) > 0 and d.lower() != 'nil':
depend_pypi = self.__parse_depend(d)
for key in depend_pypi:
feature.add_pypi_alt(key, depend_pypi[key])
except cp.NoOptionError:
pass
try:
opt = parser.get(fid, 'pip_opt')
if len(opt) > 0 and opt.lower() != 'nil':
feature.set_pip_options(self.__parse_pip_options(opt))
except cp.NoOptionError:
pass
try:
pip_test = parser.get(fid, 'pip_test')
if len(pip_test) > 0 and pip_test.lower() != 'nil':
feature.set_pip_test(pip_test)
except cp.NoOptionError:
pass
try:
cmd = parser.get(fid, self.__cmdos)
if len(cmd) > 0 and cmd != 'none' and (cmd != 'nil'):
feature.set_cmd(cmd)
except cp.NoOptionError:
pass
try:
cmd_test = parser.get(fid, 'cmd_test')
if len(cmd_test) > 0 and cmd_test.lower() not in ('none', 'nil'):
feature.set_cmd_test(cmd_test)
except cp.NoOptionError:
pass
if len(feature.get_cmd()) > 0 or len(feature.get_pypi_packages()) > 0 or len(feature.get_packages()) > 0:
feature.set_available(True)
if fid not in cfg.get_feature_ids():
logging.debug(' -->> newly available feature: {:s}'.format(fid))
__fill_lang_feature
View Source
def __fill_lang_feature(self, feature):
fid = feature.get_id()
feature.set_lang(fid)
feature.set_available(True)
__fill_annot_feature
View Source
def __fill_annot_feature(self, feature):
fid = feature.get_id()
feature.set_annot(fid)
feature.set_available(True)
if fid not in cfg.get_feature_ids():
cfg.set_feature(fid, False)
logging.debug(' ----->>> new available feature: {:s}'.format(fid))
__fill_spin_feature
View Source
def __fill_spin_feature(self, feature, parser):
fid = feature.get_id()
feature.set_spin(fid)
try:
url = parser.get(fid, 'url')
version = parser.get(fid, 'version')
feature.set_spin_url(url)
feature.set_spin_version(version)
except:
logging.error('Missing or invalid url or version for spin-off feature {}'.format(fid))
feature.set_available(False)
else:
feature.set_available(True)
__init_features
Return a parsed version of the features.ini file.
View Source
def __init_features(self):
"""Return a parsed version of the features.ini file.
"""
cfg_filename = self.get_features_filename()
if cfg_filename is None or os.path.exists(cfg_filename) is False:
raise IOError('Installation error: the file {filename} to configure the software is missing.'.format(filename=cfg_filename))
features_parser = cp.ConfigParser()
try:
features_parser.read(self.get_features_filename())
except cp.MissingSectionHeaderError:
raise IOError('Malformed features configuration file {}: missing section header.'.format(cfg_filename))
return features_parser
__parse_depend
Create a dictionary from the string given as an argument.
Parameters
- string_require: (string) The value of one of the req_*** key in one of the section of feature.ini.
Returns
- (dict)
View Source
@staticmethod
def __parse_depend(string_require):
"""Create a dictionary from the string given as an argument.
:param string_require: (string) The value of one of the req_*** key in one of the section of feature.ini.
:return: (dict)
"""
string_require = str(string_require)
dependencies = string_require.split(' ')
depend = dict()
for line in dependencies:
tab = line.split(':')
if len(tab) == 1:
depend[line] = ''
else:
depend[tab[0]] = tab[1]
return depend
__parse_pip_options
Create a dictionary from the pip_opt value given as an argument.
Each line is either the options of a given pip package, in the form "package:options", or the options of all the packages of the feature. The latter are stored with an empty string as key.
Parameters
- string_options: (string) The value of the pip_opt key in one of the section of features.ini.
Returns
- (dict) key=package name or an empty string; value=options
View Source
@staticmethod
def __parse_pip_options(string_options):
"""Create a dictionary from the pip_opt value given as an argument.
Each line is either the options of a given pip package, in the form
"package:options", or the options of all the packages of the feature.
The latter are stored with an empty string as key.
:param string_options: (string) The value of the pip_opt key in one of the section of features.ini.
:return: (dict) key=package name or an empty string; value=options
"""
string_options = str(string_options)
options = dict()
for line in string_options.split('\n'):
line = line.strip()
if len(line) == 0:
continue
tab = line.split(':', maxsplit=1)
if len(tab) == 2 and len(tab[0]) > 0 and (' ' not in tab[0]):
options[tab[0]] = tab[1].strip()
else:
options[''] = line
return options
Overloads
__str__
Print each Feature of the list.
View Source
def __str__(self):
"""Print each Feature of the list. """
for f in self.__features:
print(f.__str__())
__format__
View Source
def __format__(self, fmt):
return str(self).__format__(fmt)
__len__
Return the number of features.
View Source
def __len__(self):
"""Return the number of features."""
return len(self.__features)
__contains__
Value can be either a Feature or its identifier.
Parameters
- value
View Source
def __contains__(self, value):
"""Value can be either a Feature or its identifier."""
if isinstance(value, Feature):
return value in self.__features
else:
for f in self.__features:
if f.get_id() == value:
return True
return False
