summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPeter Pentchev <roam@ringlet.net>2018-12-23 16:56:14 +0200
committerPeter Pentchev <roam@ringlet.net>2019-01-02 20:39:29 +0200
commit2f5e78512c7814578db992c0fe2ce060f9e95d00 (patch)
treebf47fb0fb5cd586cb3273600e720203b4cac59aa
parente632e229358105dd3010006f3fda2c0712230cf9 (diff)
Implement confget as a Python module.
Also implement the command-line interface in Python.
-rw-r--r--python/confget/__init__.py0
-rwxr-xr-xpython/confget/__main__.py297
-rw-r--r--python/confget/backend/__init__.py16
-rw-r--r--python/confget/backend/abstract.py29
-rw-r--r--python/confget/backend/http_get.py77
-rw-r--r--python/confget/backend/ini.py144
-rw-r--r--python/confget/defs.py23
-rw-r--r--python/confget/format.py164
-rw-r--r--python/tox.ini31
-rw-r--r--t/14-too-many.t7
10 files changed, 788 insertions, 0 deletions
diff --git a/python/confget/__init__.py b/python/confget/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/python/confget/__init__.py
diff --git a/python/confget/__main__.py b/python/confget/__main__.py
new file mode 100755
index 0000000..4064989
--- /dev/null
+++ b/python/confget/__main__.py
@@ -0,0 +1,297 @@
+# Copyright (c) 2018 Peter Pentchev
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+"""
+The command-line interface to the confget module: specify some parameters
+through command-line options, then display variable values or names.
+"""
+
+from __future__ import print_function
+
+import argparse
+import sys
+
+from typing import List, Optional
+
+from . import backend
+from . import defs
+from . import format as fmt
+
+
+_TYPING_USED = (defs, List, Optional)
+
+VERSION_STRING = '2.2.0'
+FEATURES = [
+ ('BASE', VERSION_STRING),
+]
+
+
+class MainConfig(fmt.FormatConfig):
+ # pylint: disable=too-few-public-methods,too-many-instance-attributes
+ """
+ Extend the format config class with some output settings.
+
+ Add the following settings:
+ - check_only (boolean): only check whether a variable is defined
+ - query_sections (boolean): only display the section names
+ """
+
+ def __init__(self, # type: MainConfig
+ check_only, # type: bool
+ filename, # type: Optional[str]
+ list_all, # type: bool
+ match_regex, # type: bool
+ match_var_names, # type: bool
+ match_var_values, # type: Optional[str]
+ name_prefix, # type: Optional[str]
+ query_sections, # type: bool
+ section, # type: str
+ section_override, # type: bool
+ section_specified, # type: bool
+ show_var_name, # type: bool
+ shell_escape, # type: bool
+ varnames # type: List[str]
+ ): # type: (...) -> None
+ # pylint: disable=too-many-arguments,too-many-locals
+ """ Store the specified configuration values. """
+ super(MainConfig, self).__init__(
+ filename=filename,
+ list_all=list_all,
+ match_regex=match_regex,
+ match_var_names=match_var_names,
+ match_var_values=match_var_values,
+ name_prefix=name_prefix,
+ section=section,
+ section_specified=section_specified,
+ section_override=section_override,
+ show_var_name=show_var_name,
+ shell_escape=shell_escape,
+ varnames=varnames,
+ )
+ self.check_only = check_only
+ self.query_sections = query_sections
+
+
+def version():
+ # type: () -> None
+ """
+ Display program version information.
+ """
+ print('confget ' + VERSION_STRING)
+
+
+def features(name):
+ # type: (Optional[str]) -> None
+ """
+ Display a list of the features supported by the program.
+ """
+ if name is None:
+ print(' '.join([
+ '{name}={version}'.format(name=item[0], version=item[1])
+ for item in FEATURES
+ ]))
+ else:
+ ver = dict(FEATURES).get(name, None)
+ if ver is None:
+ sys.exit(1)
+ print(ver)
+
+
+def output_check_only(cfg, data):
+ # type: (MainConfig, defs.ConfigData) -> None
+ """ Check whether the variable is present. """
+ if cfg.section not in data:
+ sys.exit(1)
+ elif cfg.varnames[0] not in data[cfg.section]:
+ sys.exit(1)
+ sys.exit(0)
+
+
+def output_vars(cfg, data):
+ # type: (MainConfig, defs.ConfigData) -> None
+ """ Output the variable values. """
+ for vitem in fmt.filter_vars(cfg, data):
+ print(vitem.output_full)
+
+
+def output_sections(data):
+ # type: (defs.ConfigData) -> None
+ """ Output the section names. """
+ for name in sorted(data.keys()):
+ if name != '':
+ print(name)
+
+
+def validate_options(args, backend_name):
+ # type: (argparse.Namespace, str) -> None
+ """
+ Make sure the command-line options are not used in an invalid combination.
+ """
+ query_sections = args.query == 'sections'
+
+ if args.list_all or query_sections:
+ if args.varnames:
+ sys.exit('Only a single query at a time, please!')
+ elif args.match_var_names:
+ if not args.varnames:
+ sys.exit('No patterns to match against')
+ elif args.check_only and len(args.varnames) > 1:
+ sys.exit('Only a single query at a time, please!')
+ elif not args.varnames:
+ sys.exit('No variables specified to query')
+
+ if query_sections and backend_name != 'ini':
+ sys.exit("The query for sections is only supported for "
+ "the 'ini' backend for the present")
+
+
+def check_option_conflicts(args):
+ # type: (argparse.Namespace) -> None
+ """ Make sure that the command-line options do not conflict. """
+ total = int(args.query is not None) + int(args.match_var_names) + \
+ int(args.list_all) + \
+ int(bool(args.varnames) and
+ not (args.match_var_names or args.query == 'feature'))
+ if total > 1:
+ sys.exit('Only a single query at a time, please!')
+
+
+def main():
+ # type: () -> None
+ """
+ The main program: parse arguments, do things.
+ """
+ parser = argparse.ArgumentParser(
+ prog='confget',
+ usage='''
+ confget [-t ini] -f filename [-s section] varname...
+ confget -V | -h | --help | --version
+ confget -q features''')
+ parser.add_argument('-c', action='store_true', dest='check_only',
+ help='check whether the variables are defined in '
+ 'the file')
+ parser.add_argument('-f', type=str, dest='filename',
+ help='specify the configuration file name')
+ parser.add_argument('-L', action='store_true', dest='match_var_names',
+ help='specify which variables to display')
+ parser.add_argument('-l', action='store_true', dest='list_all',
+ help='list all variables in the specified section')
+ parser.add_argument('-m', type=str, dest='match_var_values',
+ help='only display variables with values that match '
+ 'the specified pattern')
+ parser.add_argument('-N', action='store_true', dest='show_var_name',
+ help='always display the variable name')
+ parser.add_argument('-n', action='store_true', dest='hide_var_name',
+ help='never display the variable name')
+ parser.add_argument('-O', action='store_true', dest='section_override',
+ help='allow variables in the specified section to '
+ 'override those placed before any '
+ 'section definitions')
+ parser.add_argument('-p', type=str, dest='name_prefix',
+ help='display this string before the variable name')
+ parser.add_argument('-q', type=str, dest='query', choices=[
+ 'feature',
+ 'features',
+ 'sections'
+ ], help='query for a specific type of information, e.g. the list of '
+ 'sections defined in ' 'the configuration file')
+ parser.add_argument('-S', action='store_true', dest='shell_quote',
+ help='quote the values suitably for the Bourne shell')
+ parser.add_argument('-s', type=str, dest='section',
+ help='specify the configuration file section')
+ parser.add_argument('-t', type=str, default='ini', dest='backend',
+ help='specify the configuration file type')
+ parser.add_argument('-V', '--version', action='store_true',
+ help='display program version information and exit')
+ parser.add_argument('-x', action='store_true', dest='match_regex',
+ help='treat the match patterns as regular expressions')
+ parser.add_argument('varnames', nargs='*',
+ help='the variable names to query')
+
+ args = parser.parse_args()
+ if args.version:
+ version()
+ return
+
+ check_option_conflicts(args)
+
+ if args.query == 'features':
+ if args.varnames:
+ sys.exit('No arguments to -q features')
+ features(None)
+ return
+ if args.query == 'feature':
+ if len(args.varnames) != 1:
+ sys.exit('Only a single feature name expected')
+ features(args.varnames[0])
+ return
+
+ query_sections = args.query == 'sections'
+
+ cfg = MainConfig(
+ check_only=args.check_only,
+ filename=args.filename,
+ list_all=args.list_all,
+ match_regex=args.match_regex,
+ match_var_names=args.match_var_names,
+ match_var_values=args.match_var_values,
+ name_prefix=args.name_prefix,
+ query_sections=query_sections,
+ section=args.section if args.section is not None else '',
+ section_override=args.section_override,
+ section_specified=args.section is not None,
+ shell_escape=args.shell_quote,
+ show_var_name=args.show_var_name or
+ ((args.match_var_names or args.list_all or len(args.varnames) > 1) and
+ not args.hide_var_name),
+ varnames=args.varnames,
+ )
+
+ matched_backends = [name for name in sorted(backend.BACKENDS.keys())
+ if name.startswith(args.backend)]
+ if not matched_backends:
+ sys.exit('Unknown backend "{name}", use "list" for a list'
+ .format(name=args.backend))
+ elif len(matched_backends) > 1:
+ sys.exit('Ambiguous backend "{name}": {lst}'
+ .format(name=args.backend, lst=' '.join(matched_backends)))
+ back = backend.BACKENDS[matched_backends[0]]
+
+ validate_options(args, matched_backends[0])
+
+ try:
+ cfgp = back(cfg)
+ except Exception as exc: # pylint: disable=broad-except
+ sys.exit(str(exc))
+ data = cfgp.read_file()
+
+ if cfg.check_only:
+ output_check_only(cfg, data)
+ elif query_sections:
+ output_sections(data)
+ else:
+ output_vars(cfg, data)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/python/confget/backend/__init__.py b/python/confget/backend/__init__.py
new file mode 100644
index 0000000..25cdb75
--- /dev/null
+++ b/python/confget/backend/__init__.py
@@ -0,0 +1,16 @@
+"""
+Provide an interface to all the configuration format backends.
+"""
+
+from typing import Dict, Type
+
+from . import abstract, http_get, ini
+
+
+_TYPING_USED = (Dict, Type, abstract)
+
+
+BACKENDS = {
+ 'http_get': http_get.HTTPGetBackend,
+ 'ini': ini.INIBackend,
+} # type: Dict[str, Type[abstract.Backend]]
diff --git a/python/confget/backend/abstract.py b/python/confget/backend/abstract.py
new file mode 100644
index 0000000..af7c3e6
--- /dev/null
+++ b/python/confget/backend/abstract.py
@@ -0,0 +1,29 @@
+"""
+An abstract metaclass for confget backends.
+"""
+
+import abc
+
+from typing import Dict
+
+import six
+
+from .. import defs
+
+
+_TYPING_USED = (defs, Dict)
+
+
+@six.add_metaclass(abc.ABCMeta)
+class Backend(object):
+ # pylint: disable=too-few-public-methods
+ """ An abstract confget parser backend. """
+ def __init__(self, cfg):
+ # type: (Backend, defs.Config) -> None
+ self._cfg = cfg
+
+ @abc.abstractmethod
+ def read_file(self):
+ # type: (Backend) -> defs.ConfigData
+ """ Read and parse the configuration file, invoke the callbacks. """
+ raise NotImplementedError('Backend.read_file')
diff --git a/python/confget/backend/http_get.py b/python/confget/backend/http_get.py
new file mode 100644
index 0000000..de32c66
--- /dev/null
+++ b/python/confget/backend/http_get.py
@@ -0,0 +1,77 @@
+"""
+A confget backend for reading INI-style files.
+"""
+
+import os
+import re
+import urllib
+
+from typing import Dict, List
+
+from .. import defs
+
+from . import abstract
+
+
+_TYPING_USED = (defs, Dict, List)
+
+RE_ENTITY = re.compile(r'^ (?P<full> [a-zA-Z0-9_]+ ; )', re.X)
+
+
+class HTTPGetBackend(abstract.Backend):
+ # pylint: disable=too-few-public-methods
+ """ Parse INI-style configuration files. """
+
+ def __init__(self, cfg):
+ # type: (HTTPGetBackend, defs.Config) -> None
+ super(HTTPGetBackend, self).__init__(cfg)
+
+ if self._cfg.filename is not None:
+ raise ValueError('No config filename expected')
+
+ qname = 'QUERY_STRING' if self._cfg.section == '' \
+ else self._cfg.section
+ qval = os.environ.get(qname, None)
+ if qval is None:
+ raise ValueError('No "{qname}" variable in the environment'
+ .format(qname=qname))
+ self.query_string = qval
+
+ def read_file(self):
+ # type: (HTTPGetBackend) -> defs.ConfigData
+ unquote = getattr(urllib, 'parse', urllib).unquote
+
+ def split_by_amp(line):
+ # type: (str) -> List[str]
+ """ Split a line by "&" or "&amp;" tokens. """
+ if not line:
+ return []
+
+ start = end = 0
+ while True:
+ pos = line[start:].find('&')
+ if pos == -1:
+ return [line]
+ if line[pos + 1:].startswith('amp;'):
+ end = pos + 4
+ break
+ entity = RE_ENTITY.match(line[pos + 1:])
+ if entity is None:
+ end = pos
+ break
+ start = pos + len(entity.group('full'))
+
+ return [line[:pos]] + split_by_amp(line[end + 1:])
+
+ data = {} # type: Dict[str, str]
+ fragments = split_by_amp(self.query_string)
+ for varval in fragments:
+ fields = varval.split('=')
+ if len(fields) == 1:
+ fields.append('')
+ elif len(fields) != 2:
+ raise ValueError('Invalid query string component: "{varval}"'
+ .format(varval=varval))
+ data[unquote(fields[0])] = unquote(fields[1])
+
+ return {self._cfg.section: data}
diff --git a/python/confget/backend/ini.py b/python/confget/backend/ini.py
new file mode 100644
index 0000000..a409e85
--- /dev/null
+++ b/python/confget/backend/ini.py
@@ -0,0 +1,144 @@
+"""
+A confget backend for reading INI-style files.
+"""
+
+import re
+import sys
+
+from typing import Callable, Dict, Match, NamedTuple, Pattern
+
+from .. import defs
+
+from . import abstract
+
+
+_TYPING_USED = (defs, Dict)
+
+
+class INIBackend(abstract.Backend):
+ # pylint: disable=too-few-public-methods
+ """ Parse INI-style configuration files. """
+
+ def __init__(self, cfg, encoding='UTF-8'):
+ # type: (INIBackend, defs.Config, str) -> None
+ super(INIBackend, self).__init__(cfg)
+
+ if self._cfg.filename is None:
+ raise ValueError('No config filename specified')
+ elif self._cfg.filename == '-':
+ infile = sys.stdin
+ else:
+ infile = open(self._cfg.filename, mode='r')
+
+ reconfigure = getattr(infile, 'reconfigure', None)
+ if reconfigure is not None:
+ reconfigure(encoding=encoding)
+
+ self.infile = infile
+
+ def read_file(self):
+ # type: (INIBackend) -> defs.ConfigData
+ state = {
+ 'section': '',
+ 'name': '',
+ 'value': '',
+ 'cont': '',
+ 'found': '',
+ }
+ res = {'': {}} # type: defs.ConfigData
+
+ def handle_section(match, # type: Match[str]
+ state, # type: Dict[str, str]
+ cfg, # type: defs.Config
+ res # type: defs.ConfigData
+ ): # type: (...) -> None
+ """ Handle a section heading: store the name. """
+ state['section'] = match.group('name')
+ if state['section'] not in res:
+ res[state['section']] = {}
+ if not (cfg.section_specified or cfg.section or state['found']):
+ cfg.section = state['section']
+ state['found'] = state['section']
+
+ def handle_comment(_match, # type: Match[str]
+ _state, # type: Dict[str, str]
+ _cfg, # type: defs.Config
+ _res # type: defs.ConfigData
+ ): # type: (...) -> None
+ """ Handle a comment line: ignore it. """
+ pass # pylint: disable=unnecessary-pass
+
+ def handle_variable(match, # type: Match[str]
+ state, # type: Dict[str, str]
+ _cfg, # type: defs.Config
+ res # type: defs.ConfigData
+ ): # type: (...) -> None
+ """ Handle an assignment: store, check for a continuation. """
+ state['name'] = match.group('name')
+ state['value'] = match.group('value')
+ state['cont'] = match.group('cont')
+ state['found'] = state['name']
+ if not state['cont']:
+ res[state['section']][state['name']] = state['value']
+
+ MatcherType = NamedTuple('MatcherType', [
+ ('regex', Pattern[str]),
+ ('handle', Callable[[
+ Match[str],
+ Dict[str, str],
+ defs.Config,
+ defs.ConfigData,
+ ], None]),
+ ])
+ matches = [
+ MatcherType(
+ regex=re.compile(r'^ \s* (?: [#;] .* )? $', re.X),
+ handle=handle_comment,
+ ),
+ MatcherType(
+ regex=re.compile(r'''
+ ^
+ \s* \[ \s*
+ (?P<name> [^\]]+? )
+ \s* \] \s*
+ $''',
+ re.X),
+ handle=handle_section,
+ ),
+ MatcherType(
+ regex=re.compile(r'''
+ ^
+ \s*
+ (?P<name> \S+ )
+ \s* = \s*
+ (?P<value> .*? )
+ \s*
+ (?P<cont> [\\] )?
+ $''', re.X),
+ handle=handle_variable,
+ ),
+ ]
+
+ for line in self.infile.readlines():
+ line = line.rstrip('\r\n')
+ if state['cont']:
+ if line.endswith('\\'):
+ line, state['cont'] = line[:-1], line[-1]
+ else:
+ state['cont'] = ''
+ state['value'] += line
+ if not state['cont']:
+ res[state['section']][state['name']] = state['value']
+ continue
+
+ for data in matches:
+ match = data.regex.match(line)
+ if match is None:
+ continue
+ data.handle(match, state, self._cfg, res)
+ break
+ else:
+ raise ValueError('Unexpected line in {fname}: {line}'
+ .format(fname=self._cfg.filename, line=line))
+
+ return res
diff --git a/python/confget/defs.py b/python/confget/defs.py
new file mode 100644
index 0000000..d098e25
--- /dev/null
+++ b/python/confget/defs.py
@@ -0,0 +1,23 @@
+"""
+Common definitions for the confget configuration parsing library.
+"""
+
+from typing import Dict, List, Optional
+
+
+_TYPING_USED = (List, Optional)
+
+ConfigData = Dict[str, Dict[str, str]]
+
+
+class Config(object):
+ # pylint: disable=too-few-public-methods
+ """ Base class for the internal confget configuration. """
+
+ def __init__(self, filename, section, section_specified, varnames):
+ # type: (Config, Optional[str], str, bool, List[str]) -> None
+ """ Store the specified configuration values. """
+ self.filename = filename
+ self.section = section
+ self.section_specified = section_specified
+ self.varnames = varnames
diff --git a/python/confget/format.py b/python/confget/format.py
new file mode 100644
index 0000000..fc2a012
--- /dev/null
+++ b/python/confget/format.py
@@ -0,0 +1,164 @@
+"""
+Filter and format a subset of the configuration variables.
+"""
+
+import fnmatch
+import re
+
+from typing import Callable, Dict, Iterable, NamedTuple, List, Optional
+
+from . import defs
+
+
+_TYPING_USED = (Callable, Dict, Iterable, List, Optional)
+
+FormatOutput = NamedTuple('FormatOutput', [
+ ('name', str),
+ ('value', str),
+ ('output_name', str),
+ ('output_value', str),
+ ('output_full', str),
+])
+
+
+class FormatConfig(defs.Config):
+ # pylint: disable=too-few-public-methods,too-many-instance-attributes
+ """
+ Extend the config class with some output settings.
+
+ Add the following settings:
+ - list_all (boolean): list all variables, not just a subset
+ - match_regex (boolean): for match_var_names and match_var_values,
+ perform regular expression matches instead of filename pattern ones
+ - match_var_names (boolean): treat the variable names specified as
+ patterns and display all variables that match those
+ - match_var_values (string): display only the variables with values
+ that match this pattern
+ - name_prefix (string): when displaying variable names, prefix them
+ with this string
+ - show_var_name (boolean): display the variable names, not just
+ the values
+ - shell_escape (boolean): format the values in a manner suitable for
+ the Bourne shell
+ """
+
+ def __init__(self, # type: FormatConfig
+ filename, # type: Optional[str]
+ list_all, # type: bool
+ match_regex, # type: bool
+ match_var_names, # type: bool
+ match_var_values, # type: Optional[str]
+ name_prefix, # type: Optional[str]
+ section, # type: str
+ section_override, # type: bool
+ section_specified, # type: bool
+ show_var_name, # type: bool
+ shell_escape, # type: bool
+ varnames # type: List[str]
+ ): # type: (...) -> None
+ # pylint: disable=too-many-arguments
+ """ Store the specified configuration values. """
+ super(FormatConfig, self).__init__(
+ filename=filename,
+ section=section,
+ section_specified=section_specified,
+ varnames=varnames,
+ )
+ self.list_all = list_all
+ self.match_regex = match_regex
+ self.match_var_names = match_var_names
+ self.match_var_values = match_var_values
+ self.name_prefix = name_prefix
+ self.section_override = section_override
+ self.shell_escape = shell_escape
+ self.show_var_name = show_var_name
+
+
+def get_check_function(cfg, patterns):
+ # type: (FormatConfig, List[str]) -> Callable[[str], bool]
+ """
+ Get a function that determines whether a variable name should be
+ included in the displayed subset.
+ """
+ if cfg.match_regex:
+ re_vars = [re.compile(name) for name in patterns]
+
+ def check_re_vars(key):
+ # type: (str) -> bool
+ """ Check that the key matches any of the specified regexes. """
+ return any(rex.search(key) for rex in re_vars)
+
+ return check_re_vars
+
+ def check_fn_vars(key):
+ # type: (str) -> bool
+ """ Check that the key matches any of the specified patterns. """
+ return any(fnmatch.fnmatch(key, pattern)
+ for pattern in patterns)
+
+ return check_fn_vars
+
+
+def get_varnames(cfg, sect_data):
+ # type: (FormatConfig, Dict[str, str]) -> Iterable[str]
+ """ Get the variable names that match the configuration requirements. """
+ if cfg.list_all:
+ varnames = sect_data.keys() # type: Iterable[str]
+ elif cfg.match_var_names:
+ check_var = get_check_function(cfg, cfg.varnames)
+ varnames = [name for name in sect_data.keys() if check_var(name)]
+ else:
+ varnames = [name for name in cfg.varnames if name in sect_data]
+
+ if not cfg.match_var_values:
+ return varnames
+
+ check_value = get_check_function(cfg, [cfg.match_var_values])
+ return [name for name in varnames if check_value(sect_data[name])]
+
+
+def filter_vars(cfg, data):
+ # type: (FormatConfig, defs.ConfigData) -> Iterable[FormatOutput]
+ """
+ Filter the variables in the configuration file according to
+ the various criteria specified in the configuration.
+ Return an iterable of FormatOutput structures allowing the caller to
+ process the variable names and values in various ways.
+ """
+ if cfg.section_override:
+ sect_data = data['']
+ else:
+ sect_data = {}
+ if cfg.section in data:
+ sect_data.update(data[cfg.section])
+
+ varnames = get_varnames(cfg, sect_data)
+ res = [] # type: List[FormatOutput]
+ for name in sorted(varnames):
+ if cfg.name_prefix:
+ output_name = cfg.name_prefix + name
+ else:
+ output_name = name
+
+ value = sect_data[name]
+ if cfg.shell_escape:
+ output_value = "'{esc}'".format(
+ esc="'\"'\"'".join(value.split("'")))
+ else:
+ output_value = value
+
+ if cfg.show_var_name:
+ output_full = '{name}={value}'.format(name=output_name,
+ value=output_value)
+ else:
+ output_full = output_value
+
+ res.append(FormatOutput(
+ name=name,
+ value=value,
+ output_name=output_name,
+ output_value=output_value,
+ output_full=output_full,
+ ))
+
+ return res
diff --git a/python/tox.ini b/python/tox.ini
new file mode 100644
index 0000000..a33bf53
--- /dev/null
+++ b/python/tox.ini
@@ -0,0 +1,31 @@
+[tox]
+envlist = pep8,mypy2,mypy3,pylint
+skipsdist = True
+
+[testenv:pep8]
+basepython = python3
+deps =
+ flake8
+commands =
+ flake8 confget
+
+[testenv:mypy2]
+basepython = python3
+deps =
+ mypy
+commands =
+ mypy --strict --py2 confget
+
+[testenv:mypy3]
+basepython = python3
+deps =
+ mypy
+commands =
+ mypy --strict confget
+
+[testenv:pylint]
+basepython = python3
+deps =
+ pylint
+commands =
+ pylint --disable=useless-object-inheritance,duplicate-code confget
diff --git a/t/14-too-many.t b/t/14-too-many.t
index 5949fe3..5c1be63 100644
--- a/t/14-too-many.t
+++ b/t/14-too-many.t
@@ -56,6 +56,13 @@ for args in \
'-q sections key1' \
'-q features -q feature BASE' \
'-q features key1'; do
+ if [ "${args#-q sections -q}" != "$args" ] || [ "${args#-q features -q}" != "$args" ]; then
+ if [ "${CONFGET#*python}" != "$CONFGET" ]; then
+ echo "ok $idx $args - skipped, Python argparse"
+ idx="$((idx + 1))"
+ continue
+ fi
+ fi
v=`$CONFGET -f "$T2" $args 2>&1`
if expr "x$v" : 'x.*Only a single ' > /dev/null; then echo "ok $idx $args"; else echo "not ok $idx args $args v is $v"; fi
idx="$((idx + 1))"