A => .gitignore +4 -0
@@ 1,4 @@
+**/__pycache__
+**/*.pyc
+tests/generated_syntax/*
+
A => Makefile +31 -0
@@ 1,31 @@
+
+unittest = unittest --color
+unittest_discover = unittest --color --working-directory .
+# If you don't have my unittest wrapper installed...
+#unittest = python -m unittest
+#unittest_discover = python -m unittest discover tests --top-level-directory . --start-directory
+
+clean:
+ rm -rf **/__pycache__ **/*.pyc
+
+bootstrap:
+ ./gap/bootstrap.py > gap/cli.py
+
+test:
+ mkdir -p tests/generated_syntax
+ touch tests/__init__.py tests/generated_syntax/__init__.py
+ python -m py_compile gap/*.py
+ $(unittest_discover) tests
+ python -m py_compile tests/generated_syntax/*.py
+ $(unittest) tests/generated_syntax_tests.py
+
+unittest:
+ $(unittest_discover) tests --verbose
+ $(unittest) tests/generated_syntax_tests.py --verbose
+
+install:
+ pipx install --spec . gap
+
+uninstall:
+ pipx uninstall gap
+
A => README.md +66 -0
@@ 1,66 @@
+
+# generated argument parser
+
+A package that uses external configuration files to generate a static, standalone parser module.
+
+
+## To-Do
+
+Add parsers aside from `toml`, like `json` and `configparser`.
+
+Allow import of third-party libraries, like `toml`, to fail-just remove that format from the valid list.
+
+
+## Workflow
+
+Given a configuration like:
+
+```toml
+# Quickly define '--help', '-h', and '-x'
+[help]
+number = 0
+alternatives = ['h', 'x']
+
+# Immediately raise an error if too few values given.
+[range]
+number = 2
+
+# Take variable numbers of values, only raising an error at the minimum.
+# Will greedily consume arguments up to the maximum.
+[files]
+minimum = 1
+maximum = 9
+```
+
+You could include the following in your build process:
+
+```shell
+python -m gap my-project-cli.conf > cli.py
+```
+
+Your argument parser then is accessible in Python like:
+
+```python
+import sys, cli
+
+options, positionals = cli.main(sys.argv[1:])
+
+if "help" in options.keys():
+ print_help_message()
+
+# It's just a built-in dict, so use all the usual methods as you please.
+for file in options.get("files", []):
+ read_input(file)
+else:
+ for argument in positionals:
+ read_input(file)
+```
+
+
+## Licensing
+
+This package is licensed under GPL.
+
+I claim absolutely no license over any code generated by this package.
+
+
A => gap/__init__.py +0 -0
A => gap/__main__.py +126 -0
@@ 1,126 @@
+#!/usr/bin/env python3
+
+import sys
+
+from . import generator
+from . import cli
+
+def help_message():
+ message = (
+ "Usage: gap [OPTIONS] INPUT\n"
+ )
+ sys.stdout.write(message)
+
+def version_message():
+ message = (
+ "gap 1.0.0\n"
+ )
+ sys.stdout.write(message)
+
+def usage_message():
+ message = (
+ "Usage: gap [OPTIONS] INPUT\n"
+ )
+ sys.stderr.write(message)
+
+def format_message(input_format):
+ message = (
+ f'gap: Invalid input format "{input_format}"\n'
+ "Use one of: toml\n"
+ )
+ sys.stderr.write(message)
+
+def file_message(filename):
+ message = (
+ f'gap: Cannot access file "{filename}"\n'
+ )
+ sys.stderr.write(message)
+
+def main():
+ config, positionals = cli.main(sys.argv[1:])
+
+ # Check for --help
+ if "help" in config.keys():
+ help_message()
+ sys.exit(0)
+ elif "version" in config.keys():
+ version_message()
+ sys.exit(0)
+
+ # Check for too few arguments
+ if len(positionals) == 0:
+ usage_message()
+ sys.exit(1)
+ else:
+ filename = positionals[0]
+
+ # Check for valid format, then import corresponding parser
+ if "format" in config.keys():
+ input_format = config["format"]
+ if input_format == "toml":
+ from . import toml_parser as parser
+ else:
+ format_message(input_format)
+ sys.exit(0)
+ else:
+ from . import toml_parser as parser
+
+ # Set verbosity level
+ verbose = False
+ if "verbose" in config.keys():
+ verbose = True
+ if "quiet" in config.keys():
+ verbose = False
+ if "output" in config.keys():
+ output_filename = config["output"]
+ else:
+ output_filename = None
+
+ # Set generator options
+ attached_values = generator.DEFAULT_ATTACHED_VALUES
+ if "attached-values" in config.keys():
+ attached_values = True
+ elif "no-attached-values" in config.keys():
+ attached_values = False
+
+ debug_mode = generator.DEFAULT_ATTACHED_VALUES
+ if "debug-mode" in config.keys():
+ debug_mode = True
+ elif "no-debug-mode" in config.keys():
+ debug_mode = False
+
+ executable = generator.DEFAULT_ATTACHED_VALUES
+ if "executable" in config.keys():
+ executable = True
+ elif "no-executable" in config.keys():
+ executable = False
+
+ raise_on_overfull = generator.DEFAULT_ATTACHED_VALUES
+ if "raise-on-overfull" in config.keys():
+ raise_on_overfull = True
+ elif "no-raise-on-overfull" in config.keys():
+ raise_on_overfull = False
+
+ # Parse input file and generate output
+ data = parser.data_from_file(filename)
+ options = generator.Options._from_dict(data, expand_alternatives=True)
+ options.attached_values(attached_values)
+ options.debug_mode(debug_mode)
+ options.executable(executable)
+ options.raise_on_overfull(raise_on_overfull)
+ syntax = options.build_syntax()
+
+ # Write/print and return
+ if output_filename is not None:
+ try:
+ with open(output_filename, 'w') as f:
+ f.write(syntax)
+ except OSError:
+ file_message(output_filename)
+ else:
+ sys.stdout.write(syntax)
+ sys.exit(0)
+
+if __name__ == "__main__":
+ main()
+
A => gap/bootstrap.py +63 -0
@@ 1,63 @@
+#!/usr/bin/env python3
+
+import toml
+
+import generator
+
+GAP_TOML = """
+
+[format]
+number = 1
+alternatives = ['f']
+
+[output]
+number = 1
+alternatives = ['o']
+
+[help]
+number = 0
+alternatives = ['h', 'x']
+
+[version]
+number = 0
+alternatives = ['V']
+
+[verbose]
+number = 0
+alternatives = ['v']
+
+[quiet]
+number = 0
+alternatives = ['q']
+
+[attached-values]
+number = 0
+
+[no-attached-values]
+number = 0
+
+[debug-mode]
+number = 0
+
+[no-debug-mode]
+number = 0
+
+[executable]
+number = 0
+
+[no-executable]
+number = 0
+
+[raise-on-overfull]
+number = 0
+
+[no-raise-on-overfull]
+number = 0
+
+"""
+
+if __name__ == "__main__":
+ data = toml.loads(GAP_TOML)
+ syntax = generator.Options._from_dict(data, expand_alternatives=True).build_syntax()
+ print(syntax)
+
A => gap/cli.py +204 -0
@@ 1,204 @@
+#!/usr/bin/env python3
+
+import re
+
+def main(arguments):
+ config=dict()
+ positional=[]
+ pattern=re.compile(r"(?:-(?:f|o|h|x|V|v|q)|--(?:format|output|help|version|verbose|quiet|attached-values|no-attached-values|debug-mode|no-debug-mode|executable|no-executable|raise-on-overfull|no-raise-on-overfull))(?:=.*)?$")
+ consuming,needing,wanting=None,0,0
+ attached_value=None
+ while len(arguments) and arguments[0]!="--":
+ if consuming is not None:
+ if config[consuming] is None:
+ config[consuming]=arguments.pop(0)
+ else:
+ config[consuming].append(arguments.pop(0))
+ needing-=1
+ wanting-=1
+ if wanting==0:
+ consuming,needing,wanting=None,0,0
+ elif pattern.match(arguments[0]):
+ option = arguments.pop(0).lstrip('-')
+ if '=' in option:
+ option,attached_value=option.split('=',1)
+ if option=="format":
+ if attached_value is not None:
+ config["format"]=attached_value
+ attached_value=None
+ consuming,needing,wanting=None,0,0
+ else:
+ config["format"]=None
+ consuming,needing,wanting="format",1,1
+ elif option=="output":
+ if attached_value is not None:
+ config["output"]=attached_value
+ attached_value=None
+ consuming,needing,wanting=None,0,0
+ else:
+ config["output"]=None
+ consuming,needing,wanting="output",1,1
+ elif option=="help":
+ if attached_value is not None:
+ message=(
+ 'unexpected value while parsing "help"'
+ ' (expected 0 values)'
+ )
+ raise ValueError(message) from None
+ config["help"]=True
+ elif option=="version":
+ if attached_value is not None:
+ message=(
+ 'unexpected value while parsing "version"'
+ ' (expected 0 values)'
+ )
+ raise ValueError(message) from None
+ config["version"]=True
+ elif option=="verbose":
+ if attached_value is not None:
+ message=(
+ 'unexpected value while parsing "verbose"'
+ ' (expected 0 values)'
+ )
+ raise ValueError(message) from None
+ config["verbose"]=True
+ elif option=="quiet":
+ if attached_value is not None:
+ message=(
+ 'unexpected value while parsing "quiet"'
+ ' (expected 0 values)'
+ )
+ raise ValueError(message) from None
+ config["quiet"]=True
+ elif option=="attached-values":
+ if attached_value is not None:
+ message=(
+ 'unexpected value while parsing "attached-values"'
+ ' (expected 0 values)'
+ )
+ raise ValueError(message) from None
+ config["attached-values"]=True
+ elif option=="no-attached-values":
+ if attached_value is not None:
+ message=(
+ 'unexpected value while parsing "no-attached-values"'
+ ' (expected 0 values)'
+ )
+ raise ValueError(message) from None
+ config["no-attached-values"]=True
+ elif option=="debug-mode":
+ if attached_value is not None:
+ message=(
+ 'unexpected value while parsing "debug-mode"'
+ ' (expected 0 values)'
+ )
+ raise ValueError(message) from None
+ config["debug-mode"]=True
+ elif option=="no-debug-mode":
+ if attached_value is not None:
+ message=(
+ 'unexpected value while parsing "no-debug-mode"'
+ ' (expected 0 values)'
+ )
+ raise ValueError(message) from None
+ config["no-debug-mode"]=True
+ elif option=="executable":
+ if attached_value is not None:
+ message=(
+ 'unexpected value while parsing "executable"'
+ ' (expected 0 values)'
+ )
+ raise ValueError(message) from None
+ config["executable"]=True
+ elif option=="no-executable":
+ if attached_value is not None:
+ message=(
+ 'unexpected value while parsing "no-executable"'
+ ' (expected 0 values)'
+ )
+ raise ValueError(message) from None
+ config["no-executable"]=True
+ elif option=="raise-on-overfull":
+ if attached_value is not None:
+ message=(
+ 'unexpected value while parsing "raise-on-overfull"'
+ ' (expected 0 values)'
+ )
+ raise ValueError(message) from None
+ config["raise-on-overfull"]=True
+ elif option=="no-raise-on-overfull":
+ if attached_value is not None:
+ message=(
+ 'unexpected value while parsing "no-raise-on-overfull"'
+ ' (expected 0 values)'
+ )
+ raise ValueError(message) from None
+ config["no-raise-on-overfull"]=True
+ elif option=="f":
+ if attached_value is not None:
+ config["format"]=attached_value
+ attached_value=None
+ consuming,needing,wanting=None,0,0
+ else:
+ config["format"]=None
+ consuming,needing,wanting="format",1,1
+ elif option=="o":
+ if attached_value is not None:
+ config["output"]=attached_value
+ attached_value=None
+ consuming,needing,wanting=None,0,0
+ else:
+ config["output"]=None
+ consuming,needing,wanting="output",1,1
+ elif option=="h":
+ if attached_value is not None:
+ message=(
+ 'unexpected value while parsing "help"'
+ ' (expected 0 values)'
+ )
+ raise ValueError(message) from None
+ config["help"]=True
+ elif option=="x":
+ if attached_value is not None:
+ message=(
+ 'unexpected value while parsing "help"'
+ ' (expected 0 values)'
+ )
+ raise ValueError(message) from None
+ config["help"]=True
+ elif option=="V":
+ if attached_value is not None:
+ message=(
+ 'unexpected value while parsing "version"'
+ ' (expected 0 values)'
+ )
+ raise ValueError(message) from None
+ config["version"]=True
+ elif option=="v":
+ if attached_value is not None:
+ message=(
+ 'unexpected value while parsing "verbose"'
+ ' (expected 0 values)'
+ )
+ raise ValueError(message) from None
+ config["verbose"]=True
+ elif option=="q":
+ if attached_value is not None:
+ message=(
+ 'unexpected value while parsing "quiet"'
+ ' (expected 0 values)'
+ )
+ raise ValueError(message) from None
+ config["quiet"]=True
+ else:
+ positional.append(arguments.pop(0))
+ if needing>0:
+ message=(
+ f'unexpected end while parsing "{consuming}"'
+ f' (expected {needing} values)'
+ )
+ raise ValueError(message) from None
+ for argument in arguments[1:]:
+ positional.append(argument)
+ return config,positional
+
A => gap/generator.py +531 -0
@@ 1,531 @@
+#!/usr/bin/env python3
+
+from pprint import pformat
+from typing import Dict,List
+
+DEFAULT_ARG_NUM = 1 # assumed number of arguments for an option
+DEFAULT_ATTACHED_VALUES = True # if True, parser splits attached values (i.e. --verbosity=1)
+DEFAULT_DEBUG_MODE = False # if True, parser generates with logging calls
+DEFAULT_EXECUTABLE = False # if True, parser generates with executable main procedure
+DEFAULT_RAISE_ON_OVERFULL = True # if True, parser complains about flags with attached values (i.e. --quiet=true)
+
+def update_no_collide(
+ d1: Dict,
+ d2: Dict,
+) -> Dict:
+ """Update d1 with the keys and values in d2, and raise KeyError instead of
+ overwriting a key.
+ """
+ for key, val in d2.items():
+ if key in d1.keys():
+ message = "cannot overwrite key '{0}'".format(key)
+ raise KeyError(message) from None
+ d1[key] = val
+ return d1
+
+def indent(
+ number: int,
+ lines: List[str],
+) -> List[str]:
+ if number > 0:
+ buffer = []
+ for line in lines:
+ buffer.append(("\t" * number) + line)
+ return buffer
+ else:
+ return lines
+
+class Options(object):
+ def __init__(
+ self,
+ options: Dict[str, 'Option'] = {},
+ ) -> None:
+ self.options = options
+ self._attached_values = DEFAULT_ATTACHED_VALUES
+ self._debug_mode = DEFAULT_DEBUG_MODE
+ self._executable = DEFAULT_EXECUTABLE
+
+ def __len__(self):
+ return len(self.options)
+
+ def __str__(self):
+ return pformat(self._as_dict())
+
+ def __eq__(self, other):
+ if not isinstance(other, Options):
+ return NotImplemented
+ elif len(self) != len(other):
+ return False
+ for key in self.options.keys():
+ if key not in other.options.keys():
+ return False
+ elif self.options[key] != other.options[key]:
+ return False
+ return True
+
+ def _as_dict(self):
+ return {name: opt._as_dict() for name, opt in self.options.items()}
+
+ @classmethod
+ def _from_dict(
+ cls,
+ data: Dict[str,Dict],
+ *,
+ expand_alternatives: bool = False,
+ ) -> 'Options':
+ options = cls(
+ {name: Option._from_dict(name,option_data)
+ for name, option_data in data.items()}
+ )
+ if expand_alternatives:
+ options.expand_alternatives()
+ return options
+
+ @classmethod
+ def _from_list_object(
+ cls,
+ data: List['Option'],
+ *,
+ expand_alternatives: bool = False,
+ ) -> 'Options':
+ options = cls(
+ {option.name: option for option in data}
+ )
+ if expand_alternatives:
+ options.expand_alternatives()
+ return options
+
+ def expand_alternatives(self) -> None:
+ orig_options = [key for key in self.options.keys()]
+ for name in orig_options:
+ new_options: Dict[str, 'Option'] = {}
+ for alt in self.options[name]._alternatives:
+ new_options[alt] = self.options[name].make_alternate(alt)
+ self.options = update_no_collide(self.options, new_options)
+
+ def attached_values(
+ self,
+ on: bool,
+ ) -> None:
+ self._attached_values = on
+ for name in self.options.keys():
+ self.options[name]._attached_values = on
+
+ def raise_on_overfull(
+ self,
+ on: bool,
+ ) -> None:
+ for name in self.options.keys():
+ self.options[name]._raise_on_overfull = on
+
+ def executable(
+ self,
+ on: bool,
+ ) -> None:
+ self._executable = on
+
+ def debug_mode(
+ self,
+ on: bool,
+ ) -> None:
+ self._debug_mode = on
+
+ def build_syntax(self):
+ # Accessible Variables
+ # ====================
+ # option parsed option name
+ # pattern pattern for option names ('(?:a|b|c)?=.*')
+ # attached_value if a value came attached to an argument ('--a=b'),
+ # then it is stored here
+ # config dictionary of options -> values, where value is...
+ # Optional[bool] if flag option,
+ # Optional[str] if singleton option,
+ # Optional[List[str]] if multi-value option, or
+ # Optional[List[str]] if range of values option
+ # positional list of positional arguments
+ # consuming name of option that the next argument *must* be
+ # stored into
+ # needing number of arguments that *must* be stored into the
+ # consuming option
+ # wanting number of arguments that will be stored into the
+ # consuming option, unless '--' is received
+
+ # build regex and body of parser syntax by looping over options
+ flag_single, flag_double = [], []
+ body = []
+ for option in self.options.values():
+ if len(option.name) == 1:
+ flag_single.append(option.name)
+ else:
+ flag_double.append(option.name)
+ body.extend(indent(3, option.build_syntax()))
+ body[0] = body[0].replace("elif", "if", 1)
+ pattern = r"(?:"
+ pattern += r"-(?:" + '|'.join(flag_single) + r")"
+ pattern += r"|"
+ pattern += r"--(?:" + '|'.join(flag_double) + r")"
+ pattern += r")"
+ if self._attached_values:
+ pattern += r"(?:=.*)?"
+ pattern += r"$"
+
+ # (mostly) static head/tail
+ head = [
+ """#!/usr/bin/env python3\n""",
+ """import re\n""",
+ """def main(arguments):""",
+ """\tconfig=dict()""",
+ """\tpositional=[]""",
+ """\tpattern=re.compile(r"{0}")""".format(pattern),
+ """\tconsuming,needing,wanting=None,0,0""",
+ ]
+ if self._attached_values: head.extend([
+ """\tattached_value=None""",
+ ])
+ if self._debug_mode: head.extend([
+ """\tdef log(*values): pass\n""",
+ """\tif "--debug-gap-behavior" in arguments:""",
+ """\t\tdef log(*values): print(*values)""",
+ ])
+ head.extend([
+ """\twhile len(arguments) and arguments[0]!="--":""",
+ ])
+ if self._debug_mode: head.extend([
+ """\t\tif arguments[0]=="--debug-gap-behavior":""",
+ """\t\t\targuments.pop(0)""",
+ """\t\t\tcontinue""",
+ """\t\tlog(f'processing {arguments[0]}...')""",
+ ])
+ head.extend([
+ """\t\tif consuming is not None:""",
+ ])
+ if self._debug_mode: head.extend([
+ """\t\t\tlog(f'option {consuming} is consuming')""",
+ ])
+ head.extend([
+ """\t\t\tif config[consuming] is None:""",
+ """\t\t\t\tconfig[consuming]=arguments.pop(0)""",
+ ])
+ if self._debug_mode: head.extend([
+ """\t\t\t\tlog(f'option {consuming} = {config[consuming]}')""",
+ ])
+ head.extend([
+ """\t\t\telse:""",
+ """\t\t\t\tconfig[consuming].append(arguments.pop(0))""",
+ ])
+ if self._debug_mode: head.extend([
+ """\t\t\t\tlog(f'option {consuming} = {config[consuming]}')""",
+ ])
+ head.extend([
+ """\t\t\tneeding-=1""",
+ """\t\t\twanting-=1""",
+ """\t\t\tif wanting==0:""",
+ ])
+ if self._debug_mode: head.extend([
+ """\t\t\t\tlog(f'option {consuming} is no longer consuming')""",
+ ])
+ head.extend([
+ """\t\t\t\tconsuming,needing,wanting=None,0,0""",
+ """\t\telif pattern.match(arguments[0]):""",
+ ])
+ if self._debug_mode: head.extend([
+ """\t\t\tlog(f'{arguments[0]} matched an option')""",
+ ])
+ head.extend([
+ """\t\t\toption = arguments.pop(0).lstrip('-')""",
+ ])
+ if self._attached_values: head.extend([
+ """\t\t\tif '=' in option:""",
+ ])
+ if self._attached_values and self._debug_mode: head.extend([
+ """\t\t\t\tlog(f'{option} has an attached value')""",
+ ])
+ if self._attached_values: head.extend([
+ """\t\t\t\toption,attached_value=option.split('=',1)""",
+ ])
+ if self._debug_mode: head.extend([
+ """\t\t\tlog(f'{option} is an option')""",
+ ])
+
+ tail = [
+ """\t\telse:""",
+ """\t\t\tpositional.append(arguments.pop(0))""",
+ """\tif needing>0:""",
+ """\t\tmessage=(""",
+ """\t\t\tf'unexpected end while parsing "{consuming}"'""",
+ """\t\t\tf' (expected {needing} values)'""",
+ """\t\t)""",
+ """\t\traise ValueError(message) from None""",
+ """\tfor argument in arguments[1:]:""",
+ ]
+ if self._debug_mode: tail.extend([
+ """\t\tif argument=="--debug-gap-behavior":""",
+ """\t\t\tcontinue""",
+ ])
+ tail.extend([
+ """\t\tpositional.append(argument)""",
+ """\treturn config,positional\n""",
+ ])
+ if self._executable: tail.extend([
+ """if __name__=="__main__":""",
+ """\timport sys""",
+ """\tcfg,pos = main(sys.argv[1:])""",
+ """\tcfg = {k:v for k,v in cfg.items() if v is not None}""",
+ """\tif len(cfg):""",
+ """\t\tprint("Options:")""",
+ """\t\tfor k,v in cfg.items():""",
+ """\t\t\tprint(f"{k:20} = {v}")""",
+ """\tif len(pos):""",
+ """\t\tprint("Positional arguments:", ", ".join(pos))\n""",
+ ])
+
+ # combine and return
+ whole = head + body + tail
+ return '\n'.join(whole)
+
+class Option(object):
+ def __init__(
+ self,
+ name: str,
+ *,
+ alternatives: List[str] = [],
+ canon_name: str = "",
+ minimum: int = DEFAULT_ARG_NUM,
+ maximum: int = DEFAULT_ARG_NUM,
+ ) -> None:
+ # core attributes
+ self.name = name
+ self.canon_name = name
+ if canon_name != "":
+ self.canon_name = canon_name
+ self.min = minimum
+ self.max = maximum
+
+ # check min and max
+ if self.min > self.max:
+ message = (
+ "expected maximum greater than minimum (got {0} and {1})"
+ ).format(self.min, self.max)
+ raise ValueError(message) from None
+
+ # hidden attributes
+ self._alternatives = alternatives
+ self._raise_on_overfull = DEFAULT_RAISE_ON_OVERFULL
+ self._attached_values = DEFAULT_ATTACHED_VALUES
+
+ def __eq__(self, other):
+ if not isinstance(other, Option):
+ print(" -> ???")
+ return NotImplemented
+ elif self.canon_name != other.canon_name:
+ print(" -> 'canon_name' differs in one")
+ return False
+ elif self.min != other.min:
+ print(" -> 'min' differs in one")
+ return False
+ elif self.max != other.max:
+ print(" -> 'max' differs in one")
+ return False
+ return True
+
+ def _as_dict(self):
+ return {
+ "name": self.name,
+ "canon_name": self.canon_name,
+ "minimum": self.min,
+ "maximum": self.max,
+ }
+
+ @classmethod
+ def _from_dict(cls, name, data):
+ option = cls(
+ name,
+ alternatives=data.get("alternatives", []),
+ canon_name=name,
+ minimum=data.get("minimum", DEFAULT_ARG_NUM),
+ maximum=data.get("maximum", DEFAULT_ARG_NUM),
+ )
+ if "number" in data.keys():
+ option.max = data["number"]
+ option.min = data["number"]
+ return option
+
+ def make_alternate(
+ self,
+ name: str,
+ ) -> 'Option':
+ return Option(
+ name,
+ canon_name=self.canon_name,
+ minimum=self.min,
+ maximum=self.max,
+ )
+
+ def build_syntax(self) -> List[str]:
+ buffer = ['elif option=="{0}":'.format(self.name)]
+ if self.min == self.max == 0:
+ buffer.extend(indent(1, self._build_syntax_flag()))
+ elif self.min == self.max == 1:
+ buffer.extend(indent(1, self._build_syntax_singleton()))
+ elif self.min == 0 and self.max == 1:
+ buffer.extend(indent(1, self._build_syntax_optional_singleton()))
+ elif self.min <= self.max:
+ buffer.extend(indent(1, self._build_syntax_multivalue()))
+ else:
+ message = (
+ "cannot parse option '{0}' with {1} to {2} values"
+ ).format(self.name, self.min, self.max)
+ raise ValueError(message) from None
+ return buffer
+
+ def _build_syntax_flag(self) -> List[str]:
+ #Generate syntax to consume a flag (Optional[bool])
+ cnn = self.canon_name
+ if self._raise_on_overfull and self._attached_values:
+ return [
+ """if attached_value is not None:""",
+ """\tmessage=(""",
+ """\t\t'unexpected value while parsing "{0}"'""".format(cnn),
+ """\t\t' (expected 0 values)'""",
+ """\t)""",
+ """\traise ValueError(message) from None""",
+ """config["{0}"]=True""".format(cnn),
+ ]
+ else:
+ return [
+ """config["{0}"]=True""".format(self.canon_name),
+ ]
+
+ def _build_syntax_singleton(self) -> List[str]:
+ #Generate syntax to consume an option (Optional[str])
+ cnn = self.canon_name
+ if self._attached_values:
+ return [
+ """if attached_value is not None:""",
+ """\tconfig["{0}"]=attached_value""".format(cnn),
+ """\tattached_value=None""",
+ """\tconsuming,needing,wanting=None,0,0""",
+ """else:""",
+ """\tconfig["{0}"]=None""".format(cnn),
+ """\tconsuming,needing,wanting="{0}",1,1""".format(cnn),
+ ]
+ else:
+ return [
+ """config["{0}"]=None""".format(cnn),
+ """consuming,needing,wanting="{0}",1,1""".format(cnn),
+ ]
+
+ def _build_syntax_optional_singleton(self) -> List[str]:
+ #Generate syntax to consume a 0 or 1 value option (Optional[List[str]])
+ cnn = self.canon_name
+ if self._attached_values:
+ return [
+ """if attached_value is not None:""",
+ """\tconfig["{0}"]=[attached_value]""".format(cnn),
+ """\tconsuming,needing,wanting=None,0,0""",
+ """\tattached_value=None""",
+ """else:""",
+ """\tconfig["{0}"]=[]""".format(cnn),
+ """\tconsuming,needing,wanting="{0}",1,1""".format(cnn),
+ ]
+ else:
+ return [
+ """config["{0}"]=[]""".format(cnn),
+ """consuming,needing,wanting="{0}",1,1""".format(cnn),
+ ]
+
+
+ def _build_syntax_multivalue(self) -> List[str]:
+ #Generate syntax to consume a multi-value option (Optional[List[str]])
+ cnn = self.canon_name
+ etc = (cnn, self.min-1, self.max-1, )
+ if self._attached_values:
+ return [
+ """if attached_value is not None:""",
+ """\tconfig["{0}"]=[attached_value]""".format(cnn),
+ """\tconsuming,needing,wanting="{0}",{1},{2}""".format(*etc),
+ """\tattached_value=None""",
+ """else:""",
+ """\tconfig["{0}"]=[]""".format(cnn),
+ """\tconsuming,needing,wanting="{0}",{1},{2}""".format(*etc),
+ ]
+ else:
+ return [
+ """config["{0}"]=[]""".format(cnn),
+ """consuming,needing,wanting="{0}",{1},{2}""".format(*etc),
+ ]
+
+def test_expand_alternatives():
+ a = Option("a", minimum=0, maximum=1, alternatives=["ab", "abc"])
+ ab = Option("ab", canon_name="a", minimum=0, maximum=1)
+ abc = Option("abc", canon_name="a", minimum=0, maximum=1)
+ a._alternatives = ["ab", "abc"]
+
+ x = Option("x", minimum=2)
+ y = Option("y", minimum=3)
+ z = Option("z", minimum=4)
+
+ compacted = Options({"a": a, "x": x, "y": y, "z": z})
+ expanded = Options({"a": a, "ab": ab, "abc": abc, "x": x, "y": y, "z": z})
+
+ compacted.expand_alternatives()
+
+ #print(compacted)
+ #print(expanded)
+ assert compacted == expanded
+
+def test_expand_alternatives_bad():
+ a = Option("a", minimum=0, maximum=1, alternatives=["ab", "abc"])
+ ab = Option("ab", canon_name="a", minimum=0, maximum=1)
+
+ x = Option("x", minimum=2)
+ y = Option("y", minimum=3)
+ z = Option("z", minimum=4)
+
+ options = Options({"a": a, "ab": ab, "x": x, "y": y, "z": z})
+
+ try:
+ options.expand_alternatives()
+ raise AssertionFailure
+ except KeyError:
+ pass
+
+def test_build_syntax_no_attached_values():
+ a = Option("alpha", minimum=0, maximum=0, alternatives=['a'])
+ b = Option("beta", alternatives=['b'])
+ g = Option("gamma", minimum=0, alternatives=['y'])
+ d = Option("delta", minimum=1, maximum=4, alternatives=['d'])
+ options = Options({"alpha": a, "beta": b, "gamma": g, "delta": d})
+ options.expand_alternatives()
+ options.attached_values(False)
+ print(options.build_syntax())
+
+def test_build_syntax_executable():
+ a = Option("alpha", minimum=0, maximum=0, alternatives=['a'])
+ b = Option("beta", alternatives=['b'])
+ g = Option("gamma", minimum=0, alternatives=['y'])
+ d = Option("delta", minimum=1, maximum=4, alternatives=['d'])
+ options = Options({"alpha": a, "beta": b, "gamma": g, "delta": d})
+ options.expand_alternatives()
+ options.executable(True)
+ print(options.build_syntax())
+
+def test_build_syntax_executable_debug_mode():
+ a = Option("alpha", minimum=0, maximum=0, alternatives=['a'])
+ b = Option("beta", alternatives=['b'])
+ g = Option("gamma", minimum=0, alternatives=['y'])
+ d = Option("delta", minimum=1, maximum=4, alternatives=['d'])
+ options = Options({"alpha": a, "beta": b, "gamma": g, "delta": d})
+ options.expand_alternatives()
+ options.executable(True)
+ options.debug_mode(True)
+ print(options.build_syntax())
+
+#if __name__=="__main__":
+ #test_expand_alternatives()
+ #test_expand_alternatives_bad()
+ #test_build_syntax()
+ #test_build_syntax_no_attached_values()
+ #test_build_syntax_executable()
+ #test_build_syntax_executable_debug_mode()
+
A => gap/toml_parser.py +21 -0
@@ 1,21 @@
+#!/usr/bin/env python3
+
+from typing import Dict
+
+import toml
+
+def data_from_file(filename: str) -> Dict:
+ try:
+ with open(filename, 'r') as f:
+ data = toml.load(f)
+ except OSError:
+ message = 'file "{0}" cannot be found'.format(filename)
+ raise FileNotFoundError(message) from None
+ except toml.TomlDecodeError:
+ message = 'file "{0}" is invalid TOML'.format(filename)
+ raise ValueError(message) from None
+ return data
+
+if __name__=="__main__":
+ pass
+
A => requirements.txt +3 -0
@@ 1,3 @@
+setuptools>=47.1.1
+toml>=0.10.1
+
A => setup.py +24 -0
@@ 1,24 @@
+#!/usr/bin/env python3
+
+from setuptools import setup
+
+long_description = None
+with open('README.md', encoding='utf-8') as f:
+ long_description = f.read()
+
+setup(
+ name="gap",
+ packages=["gap"],
+ version="1.0.0",
+ license="GPL",
+ description="Generated argument parser",
+ long_description=long_description,
+ long_description_content_type='text/markdown',
+ author="Dominic Ricottone",
+ author_email="me@dominic-ricottone.com",
+ url="git.dominic-ricottone.com/gap",
+ entry_points={"console_scripts": ["gap = gap.__main__:main"]},
+ install_requires=["toml>=0.10.1"],
+ python_requires=">=3.6",
+)
+
A => tests/__init__.py +0 -0
A => tests/generated_syntax_tests.py +326 -0
@@ 1,326 @@
+#!/usr/bin/env python3
+
+import unittest
+
+import tests.generated_syntax.syntax0 as syntax0
+import tests.generated_syntax.syntax1 as syntax1
+import tests.generated_syntax.syntax2 as syntax2
+import tests.generated_syntax.syntax3 as syntax3
+import tests.generated_syntax.syntax4 as syntax4
+import tests.generated_syntax.syntax5 as syntax5
+import tests.generated_syntax.syntax6 as syntax6
+import tests.generated_syntax.syntax7 as syntax7
+import tests.generated_syntax.syntax8 as syntax8
+import tests.generated_syntax.syntax9 as syntax9
+import tests.generated_syntax.syntax10 as syntax10
+import tests.generated_syntax.syntax11 as syntax11
+import tests.generated_syntax.syntax12 as syntax12
+import tests.generated_syntax.syntax13 as syntax13
+import tests.generated_syntax.syntax14 as syntax14
+import tests.generated_syntax.syntax15 as syntax15
+
+# itertools.product([True,False], repeat=4) ->
+# True, True, True, True
+# True, True, True, False
+# True, True, False, True
+# True, True, False, False
+# True, False, True, True
+# True, False, True, False
+# True, False, False, True
+# True, False, False, False
+# False, True, True, True
+# False, True, True, False
+# False, True, False, True
+# False, True, False, False
+# False, False, True, True
+# False, False, True, False
+# False, False, False, True
+# False, False, False, False
+# Order: attached_values, raise_on_overfull, executable, debug_mode
+
+class Test_GeneratedSyntax(unittest.TestCase):
+ def read(self, filename):
+ try:
+ with open(filename, 'r') as f:
+ return f.readlines()
+ except OSError:
+ self.skipTest("cannot read file '{0}'".format(filename))
+
+ def test_attached_values(self):
+ # The below tests shoudl parse apart "-b=1" and set beta to "1".
+ args = ["-b=1"]
+ cfg, pos = syntax0.main(args)
+ self.assertEqual(pos, [])
+ self.assertEqual(cfg, {"beta": "1"})
+
+ args = ["-b=1"]
+ cfg, pos = syntax1.main(args)
+ self.assertEqual(pos, [])
+ self.assertEqual(cfg, {"beta": "1"})
+
+ args = ["-b=1"]
+ cfg, pos = syntax2.main(args)
+ self.assertEqual(pos, [])
+ self.assertEqual(cfg, {"beta": "1"})
+
+ args = ["-b=1"]
+ cfg, pos = syntax3.main(args)
+ self.assertEqual(pos, [])
+ self.assertEqual(cfg, {"beta": "1"})
+
+ args = ["-b=1"]
+ cfg, pos = syntax4.main(args)
+ self.assertEqual(pos, [])
+ self.assertEqual(cfg, {"beta": "1"})
+
+ args = ["-b=1"]
+ cfg, pos = syntax5.main(args)
+ self.assertEqual(pos, [])
+ self.assertEqual(cfg, {"beta": "1"})
+
+ args = ["-b=1"]
+ cfg, pos = syntax6.main(args)
+ self.assertEqual(pos, [])
+ self.assertEqual(cfg, {"beta": "1"})
+
+ args = ["-b=1"]
+ cfg, pos = syntax7.main(args)
+ self.assertEqual(pos, [])
+ self.assertEqual(cfg, {"beta": "1"})
+
+ # The below tests should set "-b=1" into positionals.
+ args = ["-b=1"]
+ cfg, pos = syntax8.main(args)
+ self.assertEqual(pos, ["-b=1"])
+ self.assertEqual(cfg, {})
+
+ args = ["-b=1"]
+ cfg, pos = syntax9.main(args)
+ self.assertEqual(pos, ["-b=1"])
+ self.assertEqual(cfg, {})
+
+ args = ["-b=1"]
+ cfg, pos = syntax10.main(args)
+ self.assertEqual(pos, ["-b=1"])
+ self.assertEqual(cfg, {})
+
+ args = ["-b=1"]
+ cfg, pos = syntax11.main(args)
+ self.assertEqual(pos, ["-b=1"])
+ self.assertEqual(cfg, {})
+
+ args = ["-b=1"]
+ cfg, pos = syntax12.main(args)
+ self.assertEqual(pos, ["-b=1"])
+ self.assertEqual(cfg, {})
+
+ args = ["-b=1"]
+ cfg, pos = syntax13.main(args)
+ self.assertEqual(pos, ["-b=1"])
+ self.assertEqual(cfg, {})
+
+ args = ["-b=1"]
+ cfg, pos = syntax14.main(args)
+ self.assertEqual(pos, ["-b=1"])
+ self.assertEqual(cfg, {})
+
+ args = ["-b=1"]
+ cfg, pos = syntax15.main(args)
+ self.assertEqual(pos, ["-b=1"])
+ self.assertEqual(cfg, {})
+
+ def test_attached_values_raise_on_overfull(self):
+ # The below tests should raise on finding the "=1".
+ args = ["-a=1"]
+ with self.assertRaises(ValueError):
+ cfg, pos = syntax0.main(args)
+
+ args = ["-a=1"]
+ with self.assertRaises(ValueError):
+ cfg, pos = syntax1.main(args)
+
+ args = ["-a=1"]
+ with self.assertRaises(ValueError):
+ cfg, pos = syntax2.main(args)
+
+ args = ["-a=1"]
+ with self.assertRaises(ValueError):
+ cfg, pos = syntax3.main(args)
+
+ # The below tests should set alpha to True and ignore the "=1".
+ args = ["-a=1"]
+ cfg, pos = syntax4.main(args)
+ self.assertEqual(cfg, {"alpha": True})
+ self.assertEqual(pos, [])
+
+ args = ["-a=1"]
+ cfg, pos = syntax5.main(args)
+ self.assertEqual(cfg, {"alpha": True})
+ self.assertEqual(pos, [])
+
+ args = ["-a=1"]
+ cfg, pos = syntax6.main(args)
+ self.assertEqual(cfg, {"alpha": True})
+ self.assertEqual(pos, [])
+
+ args = ["-a=1"]
+ cfg, pos = syntax7.main(args)
+ self.assertEqual(cfg, {"alpha": True})
+ self.assertEqual(pos, [])
+
+ # The below tests should set "-a=1" into positionals.
+ # raise_on_overfull only matters for options that take 0 values but
+ # have an attached value. Therefore, this option is only effective
+ # if attached_values is also True.
+ args = ["-a=1"]
+ cfg, pos = syntax8.main(args)
+ self.assertEqual(cfg, {})
+ self.assertEqual(pos, ["-a=1"])
+
+ args = ["-a=1"]
+ cfg, pos = syntax9.main(args)
+ self.assertEqual(cfg, {})
+ self.assertEqual(pos, ["-a=1"])
+
+ args = ["-a=1"]
+ cfg, pos = syntax10.main(args)
+ self.assertEqual(cfg, {})
+ self.assertEqual(pos, ["-a=1"])
+
+ args = ["-a=1"]
+ cfg, pos = syntax11.main(args)
+ self.assertEqual(cfg, {})
+ self.assertEqual(pos, ["-a=1"])
+
+ args = ["-a=1"]
+ cfg, pos = syntax12.main(args)
+ self.assertEqual(cfg, {})
+ self.assertEqual(pos, ["-a=1"])
+
+ args = ["-a=1"]
+ cfg, pos = syntax13.main(args)
+ self.assertEqual(cfg, {})
+ self.assertEqual(pos, ["-a=1"])
+
+ args = ["-a=1"]
+ cfg, pos = syntax14.main(args)
+ self.assertEqual(cfg, {})
+ self.assertEqual(pos, ["-a=1"])
+
+ args = ["-a=1"]
+ cfg, pos = syntax15.main(args)
+ self.assertEqual(cfg, {})
+ self.assertEqual(pos, ["-a=1"])
+
+ def test_executable(self):
+ for fn in enumerate([
+ "tests/generated_syntax/syntax0.py",
+ "tests/generated_syntax/syntax1.py",
+ "tests/generated_syntax/syntax2.py",
+ "tests/generated_syntax/syntax3.py",
+ "tests/generated_syntax/syntax4.py",
+ "tests/generated_syntax/syntax5.py",
+ "tests/generated_syntax/syntax6.py",
+ "tests/generated_syntax/syntax7.py",
+ "tests/generated_syntax/syntax8.py",
+ "tests/generated_syntax/syntax9.py",
+ "tests/generated_syntax/syntax10.py",
+ "tests/generated_syntax/syntax11.py",
+ "tests/generated_syntax/syntax12.py",
+ "tests/generated_syntax/syntax13.py",
+ "tests/generated_syntax/syntax14.py",
+ "tests/generated_syntax/syntax15.py",
+ ]):
+ with self.subTest(fn=fn):
+ number, filename = fn
+ lines = self.read(filename)
+ # These files should have a line 'if __name__=="__main__":'
+ if number in (0,1,4,5,8,9,12,13,):
+ self.assertIn('if __name__=="__main__":\n', lines)
+
+ def test_debugmode_catch_option(self):
+ # The below tests should pull "--debug-gap-behavior" from the arguments
+ args = ["--", "--debug-gap-behavior"]
+ cfg, pos = syntax0.main(args)
+ self.assertEqual(cfg, {})
+ self.assertEqual(pos, [])
+
+ args = ["--", "--debug-gap-behavior"]
+ cfg, pos = syntax2.main(args)
+ self.assertEqual(cfg, {})
+ self.assertEqual(pos, [])
+
+ args = ["--", "--debug-gap-behavior"]
+ cfg, pos = syntax4.main(args)
+ self.assertEqual(cfg, {})
+ self.assertEqual(pos, [])
+
+ args = ["--", "--debug-gap-behavior"]
+ cfg, pos = syntax6.main(args)
+ self.assertEqual(cfg, {})
+ self.assertEqual(pos, [])
+
+ args = ["--", "--debug-gap-behavior"]
+ cfg, pos = syntax8.main(args)
+ self.assertEqual(cfg, {})
+ self.assertEqual(pos, [])
+
+ args = ["--", "--debug-gap-behavior"]
+ cfg, pos = syntax10.main(args)
+ self.assertEqual(cfg, {})
+ self.assertEqual(pos, [])
+
+ args = ["--", "--debug-gap-behavior"]
+ cfg, pos = syntax12.main(args)
+ self.assertEqual(cfg, {})
+ self.assertEqual(pos, [])
+
+ args = ["--", "--debug-gap-behavior"]
+ cfg, pos = syntax14.main(args)
+ self.assertEqual(cfg, {})
+ self.assertEqual(pos, [])
+
+ # The below tests should set "--debug-gap-behavior" into positionals
+ args = ["--", "--debug-gap-behavior"]
+ cfg, pos = syntax1.main(args)
+ self.assertEqual(cfg, {})
+ self.assertEqual(pos, ["--debug-gap-behavior"])
+
+ args = ["--", "--debug-gap-behavior"]
+ cfg, pos = syntax3.main(args)
+ self.assertEqual(cfg, {})
+ self.assertEqual(pos, ["--debug-gap-behavior"])
+
+ args = ["--", "--debug-gap-behavior"]
+ cfg, pos = syntax5.main(args)
+ self.assertEqual(cfg, {})
+ self.assertEqual(pos, ["--debug-gap-behavior"])
+
+ args = ["--", "--debug-gap-behavior"]
+ cfg, pos = syntax7.main(args)
+ self.assertEqual(cfg, {})
+ self.assertEqual(pos, ["--debug-gap-behavior"])
+
+ args = ["--", "--debug-gap-behavior"]
+ cfg, pos = syntax9.main(args)
+ self.assertEqual(cfg, {})
+ self.assertEqual(pos, ["--debug-gap-behavior"])
+
+ args = ["--", "--debug-gap-behavior"]
+ cfg, pos = syntax11.main(args)
+ self.assertEqual(cfg, {})
+ self.assertEqual(pos, ["--debug-gap-behavior"])
+
+ args = ["--", "--debug-gap-behavior"]
+ cfg, pos = syntax13.main(args)
+ self.assertEqual(cfg, {})
+ self.assertEqual(pos, ["--debug-gap-behavior"])
+
+ args = ["--", "--debug-gap-behavior"]
+ cfg, pos = syntax15.main(args)
+ self.assertEqual(cfg, {})
+ self.assertEqual(pos, ["--debug-gap-behavior"])
+
+if __name__ == "__main__":
+ unittest.main()
+
A => tests/test_generator.py +141 -0
@@ 1,141 @@
+#!/usr/bin/env python3
+
+import itertools
+import unittest
+
+from gap import generator
+
+class Test_Option(unittest.TestCase):
+ def test_object(self):
+ a = generator.Option("alpha", minimum=0, maximum=0, alternatives=['a'])
+ b = generator.Option("beta", alternatives=['b'])
+ y = generator.Option("gamma", minimum=0, alternatives=['y'])
+ d = generator.Option("delta", minimum=1, maximum=4, alternatives=['d'])
+
+ self.assertEqual(a.min, 0)
+ self.assertEqual(a.max, 0)
+ self.assertEqual(a._alternatives, ['a'])
+ self.assertEqual(a.canon_name, "alpha")
+
+ self.assertEqual(b.min, 1)
+ self.assertEqual(b.max, 1)
+ self.assertEqual(b._alternatives, ['b'])
+ self.assertEqual(b.canon_name, "beta")
+
+ self.assertEqual(y.min, 0)
+ self.assertEqual(y.max, 1)
+ self.assertEqual(y._alternatives, ['y'])
+ self.assertEqual(y.canon_name, "gamma")
+
+ self.assertEqual(d.min, 1)
+ self.assertEqual(d.max, 4)
+ self.assertEqual(d._alternatives, ['d'])
+ self.assertEqual(d.canon_name, "delta")
+
+ def test_bad_object(self):
+ with self.assertRaises(ValueError):
+ z = generator.Option("zeta", minimum=1, maximum=0, alternatives=['z'])
+
+class Test_Options(unittest.TestCase):
+ def test_object(self):
+ a = generator.Option("alpha", minimum=0, maximum=0, alternatives=['a'])
+ b = generator.Option("beta", alternatives=['b'])
+ y = generator.Option("gamma", minimum=0, alternatives=['y'])
+ d = generator.Option("delta", minimum=1, maximum=4, alternatives=['d'])
+
+ options = generator.Options._from_list_object([a,b,y,d])
+
+ self.assertEqual(options.options["alpha"].min, 0)
+ self.assertEqual(options.options["alpha"].max, 0)
+ self.assertEqual(options.options["alpha"]._alternatives, ['a'])
+ self.assertEqual(options.options["alpha"].canon_name, "alpha")
+
+ self.assertEqual(options.options["beta"].min, 1)
+ self.assertEqual(options.options["beta"].max, 1)
+ self.assertEqual(options.options["beta"]._alternatives, ['b'])
+ self.assertEqual(options.options["beta"].canon_name, "beta")
+
+ self.assertEqual(options.options["gamma"].min, 0)
+ self.assertEqual(options.options["gamma"].max, 1)
+ self.assertEqual(options.options["gamma"]._alternatives, ['y'])
+ self.assertEqual(options.options["gamma"].canon_name, "gamma")
+
+ self.assertEqual(options.options["delta"].min, 1)
+ self.assertEqual(options.options["delta"].max, 4)
+ self.assertEqual(options.options["delta"]._alternatives, ['d'])
+ self.assertEqual(options.options["delta"].canon_name, "delta")
+
+ def test_expand_alternatives(self):
+ a = generator.Option("alpha", minimum=0, maximum=0, alternatives=['a'])
+ b = generator.Option("beta", alternatives=['b'])
+ y = generator.Option("gamma", minimum=0, alternatives=['y'])
+ d = generator.Option("delta", minimum=1, maximum=4, alternatives=['d'])
+
+ options = generator.Options._from_list_object(
+ [a,b,y,d],
+ expand_alternatives=True,
+ )
+
+ self.assertEqual(options.options["a"].min, 0)
+ self.assertEqual(options.options["a"].max, 0)
+ self.assertEqual(options.options["a"].canon_name, "alpha")
+
+ self.assertEqual(options.options["b"].min, 1)
+ self.assertEqual(options.options["b"].max, 1)
+ self.assertEqual(options.options["b"].canon_name, "beta")
+
+ self.assertEqual(options.options["y"].min, 0)
+ self.assertEqual(options.options["y"].max, 1)
+ self.assertEqual(options.options["y"].canon_name, "gamma")
+
+ self.assertEqual(options.options["d"].min, 1)
+ self.assertEqual(options.options["d"].max, 4)
+ self.assertEqual(options.options["d"].canon_name, "delta")
+
+ def test_bad_expand_alternatives(self):
+ a1 = generator.Option("a1", minimum=0, maximum=0, alternatives=['a'])
+ a2 = generator.Option("a2", minimum=1, maximum=1, alternatives=['a'])
+
+ with self.assertRaises(KeyError):
+ options = generator.Options._from_list_object(
+ [a1,a2],
+ expand_alternatives=True,
+ )
+
+class Test_GeneratedCode(unittest.TestCase):
+ def try_write(self, filename, syntax):
+ try:
+ with open(filename, 'w') as f:
+ f.write(syntax)
+ except OSError:
+ self.skipTest("cannot write to file '{0}'".format(filename))
+ def make_options(self):
+ a = generator.Option("alpha", minimum=0, maximum=0, alternatives=['a'])
+ b = generator.Option("beta", alternatives=['b'])
+ y = generator.Option("gamma", minimum=0, alternatives=['y'])
+ d = generator.Option("delta", minimum=1, maximum=4, alternatives=['d'])
+ options = generator.Options._from_list_object(
+ [a,b,y,d],
+ expand_alternatives=True,
+ )
+ return options
+
+ def test_generate(self):
+ b = [True,False]
+ for permutation in enumerate(itertools.product(b, repeat=4)):
+ with self.subTest(permutation=permutation):
+ options = self.make_options()
+ number, settings = permutation
+
+ options.attached_values(settings[0])
+ options.raise_on_overfull(settings[1])
+ options.executable(settings[2])
+ options.debug_mode(settings[3])
+
+ syntax = options.build_syntax()
+ filename = "tests/generated_syntax/syntax{0}.py".format(number)
+ self.try_write(filename, syntax)
+
+if __name__ == "__main__":
+ unittest.main()
+