diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..235ad58 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,12 @@ +{ + "[python]": { + "diffEditor.ignoreTrimWhitespace": false, + "editor.insertSpaces": true, + "editor.rulers": [ 51, 61, 76 ], + "editor.tabSize": 4, + }, + "editor.insertSpaces": false, + "editor.renderWhitespace": "all", + "editor.tabSize": 2, + "files.trimTrailingWhitespace": true, +} diff --git a/CHANGES.md b/CHANGES.md index e1c36c3..90e51d1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,39 +1,48 @@ # **CLASP.Python** Changes + ## 0.8.8 - 24th August 2020 * ~ fixing defect in option default value processing + ## 0.8.7 - 24th August 2020 * ~ fixing defect in option value processing + ## 0.8.6 - 17th August 2020 * ~ ``show_usage()`` now issues a warning message to stderr for duplicate aliases (consistent with behaviour of (CLASP.Ruby)[https://github.com/synesissoftware/CLASP.Ruby]) + ## 0.8.5 - 13th August 2020 * ~ fixing compatibility with Python 3's removal of ``long`` + ## 0.8.4 - 12th August 2020 * allows ``OptionSpecification``'s ``value_type`` to be ``bool`` * added ``InvalidBooleanException`` exception class + ## 0.8.3 - 12th August 2020 * allows ``OptionSpecification``'s ``value_type`` to be ``long`` + ## 0.8.2 - 12th August 2020 * allows ``OptionSpecification``'s ``value_type`` to be ``str`` + ## 0.8.1 - 11th August 2020 * can now compare equal ``Flag`` instances with ``FlagSpecification`` instances and ``Option`` instances with ``OptionSpecification`` instances * significantly expanded unit-tests + ## 0.8.0 - 6th August 2020 * added ``on_multiple`` keyword argument to ``OptionSpecification`` @@ -42,55 +51,68 @@ * added ``ParsingException`` exception class * added examples/multiple_options.py (and examples/multiple_options.md) + ## 0.7.1 - 4th August 2020 * ~ various tidyings + + ## 0.7.0 - 22nd August 2019 * + added sections, via ``clasp.section()`` * + added examples/sections.py (and examples/section.md) + ## 0.6.1 - 22nd August 2019 * ~ fixed defect in ``get_first_unused_flag_or_option()`` introduced in 0.6.0 + ## 0.6.0 - 19th August 2019 * ~ ``get_first_unused_flag()``, ``get_first_unused_option()``, ``get_first_unused_flag_or_option()`` all now take an optional ``id`` parameter, which may be a flag/option or a string, that constrains the search + ## 0.5.3 - 16th July 2019 * ~ increased flexibility of dealing with ``flags_and_options`` and ``values_string`` + ## 0.5.2 - 16th July 2019 * ~ fixed ``lookup_flag()``/``lookup_option()`` such that can now be passed specifications as well as strings for id * ~ compatibility for StringIO for Python 2/3 * ~ updated examples to use non-deprecated constructs + ## 0.5.1 - 12th June 2019 * ~ improving flexibility of handling of options to ``show_usage()`` + ## 0.5.0 - 25th April 2019 * + added ``OptionArgument.given_value`` attribute, which is always the string that is given during parsing * + now supports 'value_type' named parameter in option specifications, whose value can be ``None`` (default), ``float``, or ``int``. For ``float`` and ``int`` a successful parse of the option value means that the ``OptionArgument.value`` attribute will be a ``float`` or ``int`` converted from the given value + ## 0.4.2 - 25th April 2019 * ~ ensuring that all flag/option arguments receive the correct (underlying) flag/option specifications + ## 0.4.1 - 25th April 2019 * ~ fixed type in exception message in ``option()`` + ## 0.4.0 - 17th April 2019 * ~ changed *Alias classes to *Specification * ~ clasp.Arguments aliases attribute is now changed to specifications, and a [DEPRECATED] #aliases added + ## 0.3.1 - 17th April 2019 * ~ fixed defect whereby sys.argv[0] was not used correctly to determine program name diff --git a/examples/test_usage.py b/examples/test_usage.py index 2629be7..28e517d 100755 --- a/examples/test_usage.py +++ b/examples/test_usage.py @@ -25,12 +25,12 @@ args = clasp.parse(sys.argv, specifications) -print '*' * 50 + "\n" -print "usage:\n" +print('*' * 50 + "\n") +print("usage:\n") clasp.show_usage(args, version = [ 1, 2, 3 ], stream = sys.stdout, program_name = 'myprog', version_prefix = 'v', info_lines=INFO_LINES) -print '*' * 50 + "\n" -print "version:\n" +print('*' * 50 + "\n") +print("version:\n") clasp.show_version(args, version = [ 1, 2, 3 ], stream = sys.stdout, program_name = 'myprog', version_prefix = 'v') print diff --git a/examples/typed_options.md b/examples/typed_options.md index 2b204d1..298528c 100644 --- a/examples/typed_options.md +++ b/examples/typed_options.md @@ -59,7 +59,7 @@ if args.flag_is_specified('--version'): opt_length = args.lookup_option('--length') if opt_length: - print("You specified length with the value: %d (of type %s). The string that was passed is available in the 'given_value' attribute, which is '%s' (of type %s)\n" % (opt_length.value, type(opt_length.value), opt_length.given_value, type(opt_length.given_value))) + print("You specified length with the value: %d (of type `%s`). The string that was passed is available in the `given_value` attribute, which is '%s' (of type `%s`)\n" % (opt_length.value, type(opt_length.value), opt_length.given_value, type(opt_length.given_value))) else: sys.stderr.write("try specifying the '--length' option; use --help for usage\n") diff --git a/examples/typed_options.py b/examples/typed_options.py index 95c7ef4..d239c1b 100755 --- a/examples/typed_options.py +++ b/examples/typed_options.py @@ -50,7 +50,7 @@ opt_length = args.lookup_option('--length') if opt_length: - print("You specified length with the value: %d (of type %s). The string that was passed is available in the 'given_value' attribute, which is '%s' (of type %s)\n" % (opt_length.value, type(opt_length.value), opt_length.given_value, type(opt_length.given_value))) + print("You specified length with the value: %d (of type `%s`). The string that was passed is available in the `given_value` attribute, which is '%s' (of type `%s`)\n" % (opt_length.value, type(opt_length.value), opt_length.given_value, type(opt_length.given_value))) else: sys.stderr.write("try specifying the '--length' option; use --help for usage\n") diff --git a/pyclasp/__init__.py b/pyclasp/__init__.py index 5a62dd3..641777a 100644 --- a/pyclasp/__init__.py +++ b/pyclasp/__init__.py @@ -28,7 +28,9 @@ import sys def parse(argv = None, specifications = None): - """Obtains an instance of clasp.Arguments, representing all the command-line arguments present in argv (or sys.argv)""" + """ + Obtains an instance of `clasp.Arguments`, representing all the command-line arguments present in `argv` (or `sys.argv`) + """ if argv is None: @@ -37,7 +39,9 @@ def parse(argv = None, specifications = None): return Arguments(argv, specifications) def get_program_name(argv = None): - """Obtains/infers the program name from the given array, or from sys.argv""" + """ + Obtains/infers the program name from the given array, or from `sys.argv` + """ return Arguments.get_program_name(argv) diff --git a/pyclasp/arguments.py b/pyclasp/arguments.py index 852831d..d712942 100644 --- a/pyclasp/arguments.py +++ b/pyclasp/arguments.py @@ -12,19 +12,25 @@ import re import sys + class Arguments: - """Represents a parsed command-line, separated into program-name, flags, options, and values""" + """ + Represents a parsed command-line, separated into program-name, flags, options, and values + """ + def __init__(self, argv, specifications = None): - """Initialises an instance from the given argv and specifications sequences. Users should instead use clasp.parse()""" + """ + Initialises an instance from the given `argv` and `specifications` sequences. Users should instead use `clasp.parse()` + """ if not isinstance(argv, ( list, tuple )): - raise TypeError("'argv' argument must be an instance of 'list' or 'tuple'") + raise TypeError("`argv` argument must be an instance of `list` or `tuple`") if specifications and not isinstance(specifications, ( list, tuple )): - raise TypeError("'specifications' argument must be None or an instance of 'list' or 'tuple'") + raise TypeError("`specifications` argument must be `None` or an instance of `list` or `tuple`") self.argv = argv @@ -33,40 +39,69 @@ def __init__(self, argv, specifications = None): flags, options, values = Arguments._parse(argv, self.specifications) self.program_name = Arguments.get_program_name(argv) - """The program name""" + """ + str + The program name + """ self.flags = tuple(flags) - """The parsed flags""" + """ + tuple + The parsed flags + """ self.options = tuple(options) - """The parsed options""" + """ + tuple + The parsed options + """ self.values = tuple(values) - """The parsed values""" + """ + tuple + The parsed values + """ + def aliases(self): - "[DEPRECATED] instead use 'specifications'""" + """ + [DEPRECATED] instead use `specifications` + """ return self.specifications def flagIsSpecified(self, id): - """[DEPRECATED] Use flag_is_specified()""" + """ + [DEPRECATED] Use `flag_is_specified()` + """ return self.flag_is_specified(id) + def flag_is_specified(self, id): - """Returns true if the given flag (name, or instance) has been specified; false otherwise""" + """ + Returns + ------- + bool + `True` if the given flag (name, or instance) has been specified; `False` otherwise + """ return None != self.lookup_flag(id); + def lookupFlag(self, id): - """[DEPRECATED] Use lookup_flag()""" + """ + [DEPRECATED] Use `lookup_flag()` + """ return self.lookup_flag(id) + def lookup_flag(self, id): - """Looks and returns the identified flag from the instance's flags; returns None if not found""" + """ + Looks and returns the identified flag from the instance's flags; returns `None` if not found + """ name = None @@ -93,13 +128,19 @@ def lookup_flag(self, id): return None + def lookupOption(self, id): - """[DEPRECATED] Use lookup_option()""" + """ + [DEPRECATED] Use `lookup_option()` + """ return self.lookup_option(id) + def lookup_option(self, id): - """Looks and returns the identified option from the instance's options; returns None if not found""" + """ + Looks and returns the identified option from the instance's options; returns `None` if not found + """ name = None @@ -126,13 +167,18 @@ def lookup_option(self, id): return None + def getFirstUnusedFlag(self): - """[DEPRECATED] Use get_first_unused_flag()""" + """ + [DEPRECATED] Use `get_first_unused_flag()` + """ return self.get_first_unused_flag() + def get_first_unused_flag(self, id=None): - """Looks and returns the first unused flag from the instance's flags; returns None if not found + """ + Looks and returns the first unused flag from the instance's flags; returns `None` if not found If the argument `id` is specified, only matching unused flags will be obtained """ @@ -149,13 +195,18 @@ def get_first_unused_flag(self, id=None): return None + def getFirstUnusedOption(self): - """[DEPRECATED] Use get_first_unused_option()""" + """ + [DEPRECATED] Use `get_first_unused_option()` + """ return self.get_first_unused_option() + def get_first_unused_option(self, id=None): - """Looks and returns the first unused option from the instance's options; returns None if not found + """ + Looks and returns the first unused option from the instance's options; returns `None` if not found If the argument `id` is specified, only matching unused options will be obtained """ @@ -174,19 +225,31 @@ def get_first_unused_option(self, id=None): def getFirstUnusedFlagOrOption(self): - """[DEPRECATED] Use get_first_unused_flag_or_option()""" + """ + [DEPRECATED] Use `get_first_unused_flag_or_option()` + """ return self.get_first_unused_flag_or_option() + def get_first_unused(self, id=None): - """shorthand for get_first_unused_flag_or_option()""" + """ + Shorthand for `get_first_unused_flag_or_option()` + """ return self.get_first_unused_flag_or_option(id) + def get_first_unused_flag_or_option(self, id=None): - """Obtains a reference to the first unused flag or option, or None if all no unused are found + """ + Obtains a reference to the first unused flag or option, or `None` if all no unused are found If the argument `id` is specified, only matching unused flags/options will be obtained + + Returns + ------- + FlagArgument, OptionArgument, None + the first unused flag or option, if any """ flag = self.get_first_unused_flag(id) @@ -209,6 +272,7 @@ def get_first_unused_flag_or_option(self, id=None): return option + @staticmethod def _id_matches(flag_or_option, id): @@ -229,9 +293,17 @@ def _id_matches(flag_or_option, id): return name == flag_or_option.name + @staticmethod def get_program_name(argv=None): - """Obtains/infers the program name from the given array, or from sys.argv""" + """ + Obtains/infers the program name from the given array, or from `sys.argv` + + Returns + ------- + str + the program name + """ if argv is None: @@ -257,6 +329,7 @@ def _select_specification(item, specifications): return None + @staticmethod def _select_specifications(item, specifications): @@ -306,6 +379,7 @@ def _select_specifications(item, specifications): return None + @staticmethod def _add_flag(flags, flag, spec): @@ -332,6 +406,7 @@ def _add_flag(flags, flag, spec): flags.append(flag) + @staticmethod def _add_option(options, option, spec): @@ -374,6 +449,7 @@ def _add_option(options, option, spec): options.append(option) + @staticmethod def _parse(argv, specifications): diff --git a/pyclasp/cli.py b/pyclasp/cli.py index 789af32..772d39c 100644 --- a/pyclasp/cli.py +++ b/pyclasp/cli.py @@ -11,6 +11,7 @@ import re import sys + def _ensure_single_space_prefix(s): if s: @@ -23,6 +24,7 @@ def _ensure_single_space_prefix(s): return s + def _generate_version_string(argv, options): program_name = _get_program_name(argv, options) @@ -67,7 +69,9 @@ def _generate_version_string(argv, options): def show_usage(specifications, **kwargs): - """Displays program usage from the given specifications (or arguments), according to the given options""" + """ + Displays program usage from the given `specifications` (or arguments), according to the given options + """ argv = sys.argv @@ -81,17 +85,17 @@ def show_usage(specifications, **kwargs): if specifications == None: - raise TypeError("'specifications' may not be None") + raise TypeError("`specifications` may not be `None`") elif isinstance(specifications, (list, tuple, )): for index, a in enumerate(specifications): if not isinstance(a, (Specification, )): - raise TypeError("every element in 'specifications' must be an instance of clasp.Specification: the element at index %d is of type '%s'" % (index, type(a).__name__)) + raise TypeError("every element in `specifications` must be an instance of `clasp.Specification`: the element at index %d is of type `%s`" % (index, type(a).__name__)) else: - raise TypeError("'specifications' must be a list or a tuple") + raise TypeError("`specifications` must be a `list` or a `tuple`") alias_dups = {} for spec in specifications: @@ -222,9 +226,10 @@ def show_usage(specifications, **kwargs): sys.exit(exit_code) - def show_version(specifications, **kwargs): - """Displays program version from the given specifications (or arguments), according to the given options""" + """ + Displays program version from the given `specifications` (or arguments), according to the given options + """ argv = sys.argv @@ -238,17 +243,17 @@ def show_version(specifications, **kwargs): if specifications == None: - raise TypeError("'specifications' may not be None") + raise TypeError("`specifications` may not be `None`") elif isinstance(specifications, (list, tuple, )): for index, a in enumerate(specifications): if not isinstance(a, (Specification, )): - raise TypeError("every element in 'specifications' must be an instance of clasp.Specification: the element at index %d is of type '%s'" % (index, type(a).__name__)) + raise TypeError("every element in `specifications` must be an instance of `clasp.Specification`: the element at index %d is of type `%s`" % (index, type(a).__name__)) else: - raise TypeError("'specifications' must be a list or a tuple") + raise TypeError("`specifications` must be a `list` or a `tuple`") # options: # diff --git a/pyclasp/exceptions.py b/pyclasp/exceptions.py index 5e6bf1c..8ede2a8 100644 --- a/pyclasp/exceptions.py +++ b/pyclasp/exceptions.py @@ -1,74 +1,120 @@ class CLASPException(RuntimeError): - """Root exception for CLASP""" + """ + Root exception for CLASP + """ pass + class ParsingException(CLASPException): - """Root exception for parsing""" + """ + Root exception for parsing + """ pass + class ValueParsingException(ParsingException): - """Root exception for value parsing""" + """ + Root exception for value parsing + """ pass + class MissingValueException(ValueParsingException): - """Used to indicate that no value is specified for an option (and its specification, if any, does not have a default)""" + """ + Used to indicate that no value is specified for an option (and its specification, if any, does not have a default) + """ pass + class InvalidValueException(ValueParsingException): - """Root exception for invalid values""" + """ + Root exception for invalid values + """ pass + class InvalidBooleanException(InvalidValueException): - """The given value could not be recognised as a (properly-formatted) boolean""" + """ + The given value could not be recognised as a (properly-formatted) boolean + """ pass + class InvalidNumberException(InvalidValueException): - """The given value could not be recognised as a (properly-formatted) number""" + """ + The given value could not be recognised as a (properly-formatted) number + """ pass + class InvalidIntegerException(InvalidNumberException): - """The given value could not be recognised as a (properly-formatted) integer""" + """ + The given value could not be recognised as a (properly-formatted) integer + """ pass + class IntegerOutOfRangeException(InvalidValueException): - """The given value as a valid integer but is out of range""" + """ + The given value as a valid integer but is out of range + """ pass + class DuplicateFlagSpecified(ParsingException): - """Used to indicate that a duplicate flag is specified""" + """ + Used to indicate that a duplicate flag is specified + """ + def __init__(self, existing, current, spec): self.existing = existing - """The existing flag that is duplicated""" + """ + The existing flag that is duplicated + """ self.current = current - """The new flag that is a duplicate""" + """ + The new flag that is a duplicate + """ self.specification = spec - """The specification of the flag, if any""" + """ + The specification of the flag, if any + """ + class DuplicateOptionSpecified(ParsingException): - """Used to indicate that a duplicate option is specified""" + """ + Used to indicate that a duplicate option is specified + """ + def __init__(self, existing, current, spec): self.existing = existing - """The existing option that is duplicated""" + """ + The existing option that is duplicated + """ self.current = current - """The new option that is a duplicate""" + """ + The new option that is a duplicate + """ self.specification = spec - """The specification of the option, if any""" + """ + The specification of the option, if any + """ diff --git a/pyclasp/flag_argument.py b/pyclasp/flag_argument.py index ed62f14..e423f3d 100644 --- a/pyclasp/flag_argument.py +++ b/pyclasp/flag_argument.py @@ -2,6 +2,10 @@ from .flag_specification import FlagSpecification class FlagArgument(object): + """ + TBC + """ + def __init__(self, arg, given_index, given_name, resolved_name, argument_specification, given_hyphens, given_label, extras): @@ -14,7 +18,7 @@ def __init__(self, arg, given_index, given_name, resolved_name, argument_specifi pass else: - raise TypeError("'argument_specification' must be instance of type '%s'; '%s' (%s) given" % (FlagSpecification, type(argument_specification), argument_specification)) + raise TypeError("`argument_specification` must be instance of type `%s`; `%s` (%s) given" % (FlagSpecification, type(argument_specification), argument_specification)) self.arg_ = arg self.given_index = given_index @@ -30,20 +34,26 @@ def __init__(self, arg, given_index, given_name, resolved_name, argument_specifi 'used' : False } + def use(self): self.private_fields['used'] = True + def used(self): return self.private_fields['used'] + def __str__(self): return self.name + def __eq__(self, other): - """Yields True if other is a string that is the same as 'name', or a FlagArgument or a FlagSpecification that has the same 'name'""" + """ + Yields `True` if `other` is a string that is the same as `#name`, or a `FlagArgument` or a `FlagSpecification` that has the same `#name` + """ if isinstance(other, FlagArgument): @@ -59,8 +69,11 @@ def __eq__(self, other): return False + def __ne__(self, other): - """Yields False if other is not a FlagArgument or has a different 'name'""" + """ + Yields `False` if `other` is not a `FlagArgument` or has a different `#name` + """ return not self.__eq__(other) diff --git a/pyclasp/flag_specification.py b/pyclasp/flag_specification.py index 6eb6cbd..8397629 100644 --- a/pyclasp/flag_specification.py +++ b/pyclasp/flag_specification.py @@ -2,11 +2,16 @@ from .specification import Specification class FlagSpecification(Specification): + """ + TBC + """ + def __init__(self, name, aliases, help, extras): super(FlagSpecification, self).__init__(name, aliases, help, extras) + def __str__(self): return "<%s.%s: name=%s; help=%s; aliases=%s; extras=%s>" %\ @@ -14,7 +19,9 @@ def __str__(self): def flag(name, **kwargs): - """Creates a flag specification from the given parameters""" + """ + Creates a flag specification from the given parameters + """ aliases = None help = None @@ -39,10 +46,11 @@ def flag(name, **kwargs): extras = v else: - raise TypeError("'flag' method does not recognise the '%s' keyword argument" % (n, )) + raise TypeError("`flag` method does not recognise the `%s` keyword argument" % (n, )) return FlagSpecification(name, aliases, help, extras) + _HELP_FLAG = FlagSpecification('--help', None, 'Shows usage and terminates', None) _VERSION_FLAG = FlagSpecification('--version', None, 'Shows version and terminates', None) @@ -51,6 +59,7 @@ def HelpFlag(): return _HELP_FLAG + def VersionFlag(): return _VERSION_FLAG diff --git a/pyclasp/option_argument.py b/pyclasp/option_argument.py index 7443e9c..0fe57fb 100644 --- a/pyclasp/option_argument.py +++ b/pyclasp/option_argument.py @@ -18,6 +18,7 @@ "1", ) + def _parse_to_bool(v): s = str(v) @@ -34,7 +35,12 @@ def _parse_to_bool(v): return None + class OptionArgument(object): + """ + TBC + """ + def __init__(self, arg, given_index, given_name, resolved_name, argument_specification, given_hyphens, given_label, value, extras): @@ -47,7 +53,7 @@ def __init__(self, arg, given_index, given_name, resolved_name, argument_specifi pass else: - raise TypeError("'argument_specification' must be instance of type '%s'; '%s' (%s) given" % (OptionSpecification, type(argument_specification), argument_specification)) + raise TypeError("`argument_specification` must be instance of type `%s`; `%s` (%s) given" % (OptionSpecification, type(argument_specification), argument_specification)) self.arg_ = arg self.given_index = given_index @@ -64,6 +70,7 @@ def __init__(self, arg, given_index, given_name, resolved_name, argument_specifi 'used' : False } + def _set_value(self, value, from_ctor=False): given_value = value @@ -150,10 +157,12 @@ def use(self): self.private_fields['used'] = True + def used(self): return self.private_fields['used'] + def __str__(self): if isinstance(self.value, (bool, )): @@ -165,13 +174,17 @@ def __str__(self): return "%s=%s" % (self.name, v) + def __repr__(self): return "<%s.%s: given_index=%s; given_name=%s; given_value=%s; given_hyphens=%s, given_label=%s, extras=%s; argument_specification=%s >" %\ (self.__module__, self.__class__.__name__, self.given_index, self.given_name, self.given_value, self.given_hyphens, self.given_label, self.extras, self.argument_specification, ) + def __eq__(self, other): - """Yields True if other is a string that is the same as 'name', or a OptionArgument or a OptionSpecification that has the same 'name'""" + """ + Yields `True` if `other` is a string that is the same as `#name`, or a `OptionArgument` or a `OptionSpecification` that has the same `#name` + """ if isinstance(other, OptionArgument): @@ -187,8 +200,11 @@ def __eq__(self, other): return False + def __ne__(self, other): - """Yields False if other is not a OptionArgument or has a different 'name'""" + """ + Yields `False` if `other` is not an `OptionArgument` or has a different `#name` + """ return not self.__eq__(other) diff --git a/pyclasp/option_specification.py b/pyclasp/option_specification.py index 13cabf3..ef64617 100644 --- a/pyclasp/option_specification.py +++ b/pyclasp/option_specification.py @@ -5,6 +5,10 @@ from .util import _SUPPORT_long class OptionSpecification(Specification): + """ + TBC + """ + if _SUPPORT_long: @@ -13,6 +17,7 @@ class OptionSpecification(Specification): _VALID_VALUE_TYPES = (bool, float, int, str, ) + def __init__(self, name, aliases, help, extras, values_range, default_value, is_required, require_message, value_type, on_multiple): super(OptionSpecification, self).__init__(name, aliases, help, extras) @@ -24,6 +29,7 @@ def __init__(self, name, aliases, help, extras, values_range, default_value, is_ self.value_type = value_type self.on_multiple = on_multiple + def __str__(self): return "<%s.%s: name=%s; help=%s; aliases=%s; extras=%s, default_value=%s, value_type=%s, values_range=%s, on_multiple=%s, is_required=%s, require_message=%s >" %\ @@ -31,7 +37,9 @@ def __str__(self): def option(name, **kwargs): - """Creates an option specification from the given parameters""" + """ + Creates an option specification from the given parameters + """ aliases = None help = None @@ -81,11 +89,11 @@ def option(name, **kwargs): if v not in _MULTIPLE_ACTION_OPTION_ALLOWED: - raise TypeError("'option' method keyword argument 'on_multiple' must be one of %s (in any case); '%s' given" % (_MULTIPLE_ACTION_OPTION_ALLOWED, v)) + raise TypeError("`option` method keyword argument `on_multiple` must be one of %s (in any case); '%s' given" % (_MULTIPLE_ACTION_OPTION_ALLOWED, v)) else: - raise TypeError("'option' method keyword argument 'on_multiple' must be 'None' or a string; '%s' (%s) given" % (v, type(v))) + raise TypeError("`option` method keyword argument `on_multiple` must be `None` or a string; '%s' (%s) given" % (v, type(v))) on_multiple = v elif 'require_message' == n: @@ -103,11 +111,10 @@ def option(name, **kwargs): pass else: - raise TypeError("'option' method supports 'value_type' only for 'None' and the types %s; '%s' (%s) given" % ([t.__name__ for t in OptionSpecification._VALID_VALUE_TYPES], value_type, type(value_type))) + raise TypeError("`option` method supports `value_type` only for `None` and the types %s; `%s` (%s) given" % ([t.__name__ for t in OptionSpecification._VALID_VALUE_TYPES], value_type, type(value_type))) else: - raise TypeError("'option' method does not recognise the '%s' keyword argument" % (n, )) + raise TypeError("`option` method does not recognise the `%s` keyword argument" % (n, )) return OptionSpecification(name, aliases, help, extras, values_range, default_value, is_required, require_message, value_type, on_multiple) - diff --git a/pyclasp/section_specification.py b/pyclasp/section_specification.py index 65a0b42..e02e3f4 100644 --- a/pyclasp/section_specification.py +++ b/pyclasp/section_specification.py @@ -2,11 +2,16 @@ from .specification import Specification class SectionSpecification(Specification): + """ + TBC + """ + def __init__(self, name, extras): super(SectionSpecification, self).__init__(name, None, None, extras) + def __str__(self): return "<%s.%s: name=%s; help=%s; aliases=%s; extras=%s>" %\ @@ -14,7 +19,9 @@ def __str__(self): def section(name, **kwargs): - """Creates a section specification from the given parameters""" + """ + Creates a section specification from the given parameters + """ extras = None @@ -28,7 +35,7 @@ def section(name, **kwargs): extras = v else: - raise TypeError("'section' method does not recognise the '%s' keyword argument" % (n, )) + raise TypeError("`section` method does not recognise the `%s` keyword argument" % (n, )) return SectionSpecification(name, extras) diff --git a/pyclasp/specification.py b/pyclasp/specification.py index 49e3445..c8bce27 100644 --- a/pyclasp/specification.py +++ b/pyclasp/specification.py @@ -1,18 +1,22 @@ class Specification(object): + """ + TBC + """ + def __init__(self, name, aliases, help, extras): if not isinstance(name, str): - raise TypeError("'name' must be of type 'str'") + raise TypeError("`name` must be of type `str`") if aliases: if not isinstance(aliases, ( list, tuple )): - raise TypeError("'aliases' must be None or an instance of 'list' or 'tuple'") + raise TypeError("`aliases` must be `None` or an instance of `list` or `tuple`") else: aliases = () @@ -22,14 +26,14 @@ def __init__(self, name, aliases, help, extras): if not isinstance(help, ( str, )): - raise TypeError("'help' must be an instance of 'str'") + raise TypeError("`help` must be an instance of `str`") if extras: if not isinstance(extras, ( dict, )): - raise TypeError("'extras must be None or an instance of 'dict'") + raise TypeError("`extras` must be `None` or an instance of `dict`") else: @@ -43,6 +47,10 @@ def __init__(self, name, aliases, help, extras): def specification(name, **kwargs): + """ + TBC + """ + aliases = None help = None @@ -64,7 +72,7 @@ def specification(name, **kwargs): extras = v else: - raise TypeError("'specification' method does not recognise the '%s' keyword argument" % (n, )) + raise TypeError("`specification` method does not recognise the `%s` keyword argument" % (n, )) return Specification(name, aliases, help, extras) diff --git a/pyclasp/util.py b/pyclasp/util.py index a4b3040..7cfc370 100644 --- a/pyclasp/util.py +++ b/pyclasp/util.py @@ -33,6 +33,7 @@ _MULTIPLE_FLAG_ACTION_DEFAULT = _MULTIPLE_ACTION_REPLACE _MULTIPLE_OPTION_ACTION_DEFAULT = _MULTIPLE_ACTION_ALLOW + def _dict_get_N(d, *keys, **kwargs): default = None @@ -58,6 +59,7 @@ def _dict_get_N(d, *keys, **kwargs): return default + def _get_program_name(argv, options): program_name = _dict_get_N(options, 'program_name', 'program-name') @@ -70,6 +72,7 @@ def _get_program_name(argv, options): return program_name + def _global_multiple_flags_action(): a = os.environ.get('CLASP_MULTIPLE_FLAG_ACTION') diff --git a/run_all_unit_tests.sh b/run_all_unit_tests.sh index 3af4fa2..138d680 100755 --- a/run_all_unit_tests.sh +++ b/run_all_unit_tests.sh @@ -1,30 +1,179 @@ -#!/bin/bash +#! /bin/bash -############################################################################# -# File: run_all_unit_tests.sh +# ######################################################################## # +# File: run_all_unit_tests.sh # -# Purpose: Executes the unit-tests regardless of calling directory +# Purpose: Executes the unit-tests of a Python project regardless of +# calling directory # -# Created: 13th February 2019 -# Updated: 6th August 2020 +# Created: 13th February 2019 +# Updated: 17th August 2024 # -# Author: Matthew Wilson +# Copyright (c) Matthew Wilson, 2019-2024 +# All rights reserved # -############################################################################# +# Redistribution and use in Source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * 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. +# +# * Neither the names of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT HOLDER 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. +# +# ######################################################################## # + + +# constants + +Source="${BASH_SOURCE[0]}" +while [ -h "$Source" ]; do + + Dir="$(cd -P "$(dirname "$Source")" && pwd)" + Source="$(readlink "$Source")" + [[ $Source != /* ]] && Source="$Dir/$Source" +done +Dir="$(cd -P "$( dirname "$Source" )" && pwd)" + +PossiblePythonCommands=(python3 python python2) + + +PythonCommandPath= + + + +# regular command-line handling + + +while [[ $# -gt 0 ]] +do + + #echo "\$1=$1" + + case "$1" in + + --python-cmd-path|-p) + + shift + + PythonCommandPath=$1 + ;; + --help) + + echo "USAGE: $Source { | --help | [ --python ] }" + echo + echo "flags/options:" + echo + echo " --help" + echo " shows this help and terminates" + echo + echo " -p " + echo " --python-cmd-path " + echo " specifies explicitly the path of the Python command to be executed (rather than discover it)" + echo + + exit + ;; + *) + + >&2 echo "unrecognised argument; use --help for usage" + + exit 1 + ;; + esac -source="${BASH_SOURCE[0]}" -while [ -h "$source" ]; do - dir="$(cd -P "$(dirname "$source")" && pwd)" - source="$(readlink "$source")" - [[ $source != /* ]] && source="$dir/$source" + shift done -dir="$( cd -P "$( dirname "$source" )" && pwd )" -python_cmd=python +# validate / discover python executable path -# This will operate recursively as long as each subdirectory of $dir/tests +if [ "x_$PythonCommandPath" != "x_" ]; then + + # check the given command + + if ! which "$PythonCommandPath" > /dev/null ; then + + >&2 echo "given python-cmd-path '$PythonCommandPath' is not executable" + + exit + fi +else + + # try and find a suitable command + + if [ "x_$PythonCommandPath" = "x_" ]; then + + if [ "y_$PYTHON_COMMAND_PATH" != "y_" ]; then + + if which "$PYTHON_COMMAND_PATH" > /dev/null ; then + + PythonCommandPath=$PYTHON_COMMAND_PATH + fi + fi + fi + + if [ "x_$PythonCommandPath" = "x_" ]; then + + if [ "y_$PYTHON_CMD_PATH" != "y_" ]; then + + if which "$PYTHON_CMD_PATH" > /dev/null ; then + + PythonCommandPath=$PYTHON_CMD_PATH + fi + fi + fi + + if [ "x_$PythonCommandPath" = "x_" ]; then + + for p in "${PossiblePythonCommands[@]}" + do + + if which "$p" > /dev/null ; then + + PythonCommandPath=$p + + echo "found validation python command '$p'" + + break + fi + done + fi + + if [ "x_$PythonCommandPath" = "x_" ]; then + + >&2 echo "no valid python command path discovered" + + exit + fi +fi + + +# executing tests + + +# This will operate recursively as long as each subdirectory of $Dir/tests # contains an __init__.py file (which may be empty) -PYTHONPATH=$dir:$PYTHONPATH $python_cmd -m unittest discover $dir/tests +"$PythonCommandPath" -m unittest discover "$Dir/tests" + +# ############################## end of file ############################# #