Commits

larry committed 843090b

Created fresh repository, hooray!

Comments (0)

Files changed (3)

+__pycache__
+#!/usr/bin/env python3
+#
+# dryparse
+#
+# A don't-repeat-yourself argument parser.
+#
+#
+# Copyright 2012 Larry Hastings
+#
+# This software is provided 'as-is', without any express or implied
+# warranty. In no event will the authors be held liable for any damages
+# arising from the use of this software.
+#
+# Permission is granted to anyone to use this software for any purpose,
+# including commercial applications, and to alter it and redistribute it
+# freely, subject to the following restrictions:
+#
+# 1. The origin of this software must not be misrepresented; you must not
+# claim that you wrote the original software. If you use this software
+# in a product, an acknowledgment in the product documentation would be
+# appreciated but is not required.
+#
+# 2. Altered source versions must be plainly marked as such, and must not be
+# misrepresented as being the original software.
+#
+# 3. This notice may not be removed or altered from any source
+# distribution.
+#
+#
+# Terminology:
+#
+#   % ./program.py --debug foo --xyz 123
+#                  ^       ^   ^     ^
+#                  |       |   |     |
+#                  |       |   |     argument
+#                  |       |   |
+#                  |       |   option
+#                  |       |
+#                  |       command
+#                  |
+#                  global option
+#
+# A "command" is the first thing on the command-line
+# (after the program) that doesn't start with a dash.
+#
+# "Options" are things that start with '-'.
+# Single-character options always use a single dash,
+# and can be grouped together ('-a -b -c' and '-abc'
+# are equivalent).  Multiple-character options must
+# use a double dash and must be specified separately.
+#
+# An option can itself take an argument.  This can be of
+# the form '-n 3' or '-n=3' or '-n3'.  You can mash a
+# single-letter option that takes an option up with single-letter
+# options that don't, e.g. '-abcn5' (where a, b, and c are booleans
+# and n=5).
+#
+# There are two kinds of options: "global options", which apply
+# to the program itself / to all commands, and "command options"
+# (also just called "options") which are specific to each command.
+# Options specified on the command-line before the command are
+# global options; options specified on the command-line after the
+# command are command options.
+#
+# "Arguments" are things that don't start with a dash that come
+# after the command.  They are implicitly positional.
+#
+#
+# You don't have to use commands if you don't want them.
+# In that case:
+#   * All options are global options.
+#   * Everything on the command-line that doesn't start with a
+#     dash is an argument (a "global argument").
+#
+#
+# Rules:
+#   * Each option can be specified no more than once.
+#
+#
+# the idea:
+# the command parser reads the annotation of each argument
+# which is an iterable containing... stuff:
+#	* if the item is a string starting with a dash,
+#     it's a whitespace-separated list of command-line
+#     dashed arguments that map to this argument
+#	    * special case: if it contains the string '--' (or '-')
+#         that means use the name of the variable itself
+#		  e.g. foo(abc='--') means it maps to option --abc
+#       * if unspecified, this is a positional parameter
+#   * if it's a string not starting with a dash,
+#     it's documentation
+#   * if it's a callable, the option must take a value,
+#     and this is used to cast the string argument to the value
+#     (if you specify no callable, the option is a boolean
+#     and is toggled when it's specified)
+#
+# the default value of the argument is honored
+# if it's unspecified, the option must be specified
+#
+# dashed arguments are automatically sorted to the front
+# (but you don't have to declare them first in your function;
+#  any order is fine)
+#
+# Testing:
+#    % python3 -m unittest dryparse
+#
+# todo:
+#  * unittest for global options
+#  * when called as a module (python3 -m dryparse <x>) it decorates
+#    the callables so that for all arguments it tries eval, and if
+#    that works it uses the result, otherwise it fails over to str
+#  * --version?
+#  * -h / --help ?
+#  * in usage: square brackets around optional options/arguments [-h]
+#  * in usage: angle brackets around positional arguments
+#  * python ideas:
+#      a typeerror should have an optional iterable of base classes of
+#      types it accepts
+#
+
+
+import collections
+import inspect
+import shlex
+import sys
+import textwrap
+import types
+import unittest
+
+__all__ = []
+def all(fn):
+	__all__.append(fn)
+	return fn
+
+class Unspecified:
+	def __repr__(self):
+		return '<Unspecified>'
+
+unspecified = Unspecified()
+
+@all
+class DryArgument:
+	def __init__(self, name, default, annotations):
+		self.name = name
+		self.default = default
+		self.type = None
+		self.doc = None
+		self.options = set()
+		for a in annotations or ():
+			if callable(a):
+				assert self.type is None
+				self.type = a
+				continue
+			if isinstance(a, str):
+				if a.startswith('-'):
+					# options
+					for option in a.split():
+						option = option.lstrip('-')
+						if not option:
+							option = name
+						assert option not in self.options
+						self.options.add(option)
+				else:
+					assert self.doc is None
+					self.doc = a
+				continue
+			assert None, "unknown annotation for " + name
+
+		if self.type is None:
+			if self.options:
+				# if they didn't specify a value for the option,
+				# and they didn't annotate with a type
+				# and it's a option (not a positional argument)
+				# then its default should be False
+				self.type = bool
+				if self.default is unspecified:
+					self.default = False
+			else:
+				self.type = str
+		if self.doc is None:
+			self.doc = ''
+
+@all
+class DryCommand:
+
+	def __repr__(self):
+		return '<DryCommand ' + self.name + '>'
+
+	def __init__(self, name, callable):
+		global unspecified
+		self.name = name
+		self.callable = callable
+		self.options = {}
+		self.all_arguments = []
+		self.arguments = []
+		self.doc = callable.__doc__ or ''
+		self.star_args = None
+
+		i = inspect.getfullargspec(callable)
+		if isinstance(callable, types.MethodType):
+			i.args.pop(0)
+
+		# i.defaults is kind of silly
+		# convert it to a straight-up tuple
+		# with unspecified for unspecified fields
+		if i.defaults is None:
+			defaults = (unspecified,) * len(i.args)
+		else:
+			defaults = ((unspecified,) * (len(i.args) - len(i.defaults))) + i.defaults
+
+		for name, default in zip(i.args, defaults):
+			argument = DryArgument(name, default, i.annotations.get(name))
+			self.all_arguments.append(argument)
+			if argument.options:
+				for option in argument.options:
+					assert option not in self.options
+					self.options[option] = argument
+			else:
+				self.arguments.append(argument)
+
+		if i.varargs:
+			self.star_args = DryArgument(i.varargs, None, i.annotations.get(i.varargs))
+
+	def _usage(self, *, error=None):
+		output = []
+		def print(*a):
+			s = " ".join([str(x) for x in a])
+			output.append(s)
+		lines = []
+		first_line = ''
+		if self.doc:
+			lines = [x.rstrip() for x in textwrap.dedent(self.doc.expandtabs()).split('\n')]
+			while lines and not lines[0]:
+				del lines[0]
+			if lines:
+				first_line = lines[0]
+				del lines[0]
+				while lines and not lines[0]:
+					del lines[0]
+				while lines and not lines[-1]:
+					lines.pop()
+
+		if error:
+			print("Error:", error)
+			print()
+		print(self.name + ":", first_line)
+		if lines:
+			print()
+			for line in lines:
+				print(" ", line)
+
+		print()
+		options = {}
+		longest_option = longest_positional = 0
+		for argument in self.all_arguments:
+			if argument.options:
+				dashed_options = [ '-' + ('-' if len(option) > 1 else '') + option for option in argument.options]
+				all_options = ', '.join(sorted(dashed_options))
+				longest_option = max(longest_option, len(all_options))
+				options[all_options] = argument
+			else:
+				longest_positional = max(longest_positional, len(argument.name))
+		if abs(longest_positional - longest_option) < 3:
+			longest_positional = longest_option = max(longest_positional, longest_option)
+		if options:
+			print("Options:")
+			for all_options in sorted(options):
+				argument = options[all_options]
+				print(" ", all_options.ljust(longest_option), "", argument.doc)
+			print()
+		if self.arguments:
+			print("Arguments:")
+			for argument in self.arguments:
+				print(" ", argument.name.ljust(longest_positional), "", argument.doc)
+			if self.star_args:
+				print(" ", '[' + self.star_args.name + '...]' )
+
+		print()
+		return '\n'.join(output)
+
+	def usage(self, *, error=None):
+		print(self._usage(error=error))
+
+	def __call__(self, argv, return_arguments=False):
+		seen = set()
+		needs_value = None
+
+		for a in self.all_arguments:
+			a.value = a.default
+
+		def analyze_option(option):
+			argument = self.options[option]
+			return argument, argument.type is not bool
+
+		def handle_option(option, value):
+			nonlocal self
+			nonlocal needs_value
+			argument, isnt_bool = analyze_option(option)
+			if isnt_bool:
+				if value is unspecified:
+					needs_value = argument
+				else:
+					argument.value = argument.type(value)
+			else:
+				argument.value = not argument.value
+			seen.add(option)
+
+		arguments = []
+		all_positional = False
+		while argv:
+			a = argv.pop(0)
+			if all_positional:
+				arguments.append(a)
+				continue
+			if needs_value:
+				needs_value.value = needs_value.type(a)
+				needs_value = None
+				continue
+			if a == '--':
+				all_positional = True
+				continue
+			if a.startswith('-'):
+				if '=' in a:
+					a, _, value = a.partition('=')
+					a = a.strip()
+					value = value.strip()
+					assert a.startswith('--') or (len(a) == 2), "string " + repr(a) + " isn't double-dash nor -x"
+				else:
+					value = unspecified
+				if a.startswith('--'):
+					option = a[2:]
+					if option not in self.options:
+						return self.usage(error="Unknown option " + a)
+					handle_option(option, value)
+				else:
+					single_letters = []
+					for c in a[1:]:
+						if c not in self.options:
+							return self.usage(error="Unknown option -" + c)
+						arg, isnt_bool = analyze_option(c)
+						single_letters.append([c, unspecified])
+						if isnt_bool:
+							break
+					remaining = a[len(single_letters)+1:]
+					if remaining or (value is not unspecified):
+						remaining_str = "".join(remaining)
+						if value is not unspecified:
+							if remaining_str:
+								remaining_str += '=' + value
+							else:
+								remaining_str = value
+						single_letters[-1][1] = remaining_str
+					for c, value in single_letters:
+						handle_option(c, value)
+				continue
+			arguments.append(a)
+
+		star_args = []		
+		if not return_arguments:
+			i = -1
+			for i, (argument, a) in enumerate(zip(self.arguments, arguments)):
+				t = argument.type or str
+				argument.value = t(a)
+			if self.star_args:
+				t = self.star_args.type or str
+				star_args = [t(x) for x in arguments[i + 1:]]
+
+		# print()
+		# print("argv", argv)
+		# for i, a in enumerate(self.all_arguments):
+		# 	print("argument[", i, '] =', a, "name", a.name, "type", a.type, "value", a.value)
+		final_arguments = [a.value for a in self.all_arguments]
+		if star_args:
+			final_arguments.extend(star_args)
+		needed = len(list(filter(lambda x: x == unspecified, final_arguments)))
+		if needed:
+			specified = len(arguments)
+			return self.usage(error=" ".join(("Not enough arguments;", str(specified + needed), "required, but only", str(specified), "specified.")))
+		assert unspecified not in final_arguments
+		return_value = self.callable(*final_arguments)
+		if return_arguments:
+			return arguments
+		return return_value
+
+@all
+def ignore(callable):
+	callable.__dryparse_use__ = False
+	return callable
+
+@all
+def command(callable):
+	callable.__dryparse_use__ = True
+	return callable
+
+@all
+class DryParse:
+
+	def __init__(self, namespace=None):
+		self.commands = {}
+		self.doc = ""
+		self.commands['help'] = self.help
+		self.global_handler = None
+		self.update(namespace)
+
+	def set_global(self, handler):
+		self.global_handler = DryCommand('', handler)
+
+	def add_raw(self, o, name=None):
+		assert callable(o)
+		name = name or o.__name__
+		self.commands[name] = o
+
+	def add(self, o, name=None):
+		assert callable(o)
+		name = name or o.__name__
+		self.commands[name] = DryCommand(name, o)
+
+	def __setitem__(self, name, value):
+		self.add(name, value)
+
+	def test(self, o):
+		if not callable(o):
+			return False
+
+		# if it's been explicitly labeled, obey that
+		value = getattr(o, '__dryparse_use__', None)
+		if value is not None:
+			return value
+
+		try:
+			signature = inspect.signature(o)
+		except ValueError:
+			return False
+
+		ra = signature.return_annotation
+		#		* no if its return annotation is false
+		if ra is False:
+			return False
+		#		* yes if it has a return annotation of '-'
+		if ra is '-':
+			return True
+
+		#		* yes if it has any argument annotations that look like our stuff
+		for argument in signature.parameters.values():
+			a = argument.annotation
+			if isinstance(a, collections.Iterable) and not isinstance(a, str):
+				for value in a:
+					if isinstance(value, str) or callable(value):
+						return True
+
+		#		* no if it starts and ends with '__'
+		name = o.__name__
+		if name.startswith('__') and name.endswith('__'):
+			return False
+
+		return True
+
+
+	def update(self, o, test=None):
+		if o is None:
+			return
+		if not test:
+			test = self.test
+
+		if isinstance(o, collections.Mapping):
+			for name, value in o.items():
+				if self.test(value):
+					self.add(value, name=name)
+			return
+
+		if isinstance(o, collections.Iterable):
+			for value in o:
+				if test(value):
+					self.add(value)
+			return
+
+		for name in dir(o):
+			value = getattr(o, name)
+			if test(value):
+				self.add(value, name=name)
+
+	def usage(self, *, argv=(), error=None):
+		print(self._usage(argv=argv, error=error))
+
+	def _usage(self, *, argv=(), error=None):
+		output = []
+		def print(*a):
+			output.append(' '.join(str(x) for x in a))
+
+		assert not (argv and error)
+		if error:
+			print("Error:", error)
+			print()
+		if argv:
+			command = argv[0]
+			if command not in self.commands:
+				return self.usage(error="Unknown command " + repr(command))
+			if command == 'help':
+				return self.usage()
+			return self.commands[command]._usage()
+		if self.doc:
+			print(self.doc)
+			print()
+		print("usage:", sys.argv[0], " [global-options] command [options] [arguments]")
+		print()
+		print("Supported commands:")
+		commands = sorted(self.commands)
+		longest = len('help')
+		for name in commands:
+			longest = max(longest, len(name))
+		for name in commands:
+			if name == 'help':
+				print(" ", "help".ljust(longest), " help on command usage")
+				continue
+			command = self.commands[name]
+			first_line = command.doc.strip().split('\n')[0].strip()
+			print(" ", name.ljust(longest), "", first_line)
+
+		print()
+		return '\n'.join(output)
+
+	def help(self, argv):
+		self.usage(argv=argv)
+
+	def main(self, argv=None):
+		return_value = None
+		if argv == None:
+			argv = sys.argv[1:]
+
+		if self.global_handler:
+			return_value = self.global_handler(argv, return_arguments=bool(self.commands))
+			if self.commands:
+				argv = return_value
+			else:
+				return return_value
+
+		if not self.commands:
+			return return_value
+
+		if not argv:
+			return self.usage(error="no command specified")
+
+		command = argv.pop(0)
+
+		if command not in self.commands:
+			if command == "help":
+				return self.usage(argv=argv)
+			return self.usage(error="command " + repr(command) + " not recognized")
+		return self.commands[command](argv)
+
+class UnitTests(unittest.TestCase):
+
+	def setUp(self):
+		class Commands:
+			pass
+		self.C = Commands
+		self.c = Commands()
+		self.dp = DryParse()
+
+	def add(self, callable):
+		name = callable.__name__
+		setattr(self.C, name, callable)
+		self.dp.add(getattr(self.c, name))
+
+	def main(self, cmdline):
+		self.dp.main(shlex.split(cmdline))
+
+	def test_basics(self):
+		c = self.c
+		dp = self.dp
+		self.assertTrue('command1' not in dp._usage())
+		self.assertTrue('command2' not in dp._usage())
+		def command1(self, a, b):
+			self.a = a
+			self.b = b
+		self.add(command1)
+		self.assertTrue('command1'     in dp._usage())
+		self.assertTrue('command2' not in dp._usage())
+		def command2(self, a, b):
+			self.a = a
+			self.b = b
+		self.add(command2)
+		self.assertTrue('command1'     in dp._usage())
+		self.assertTrue('command2'     in dp._usage())
+		c.a = c.b = 0
+		self.main('command1 22 33')
+		self.assertEqual(c.a, '22')
+		self.assertEqual(c.b, '33')
+
+	def test_function_docs(self):
+		def command(self, a):
+			"box turtle"
+			pass
+		self.add(command)
+		self.assertTrue('box turtle' in self.dp._usage())
+
+	def test_argument_docs(self):
+		def command3(self, a:('lagomorph',)):
+			pass
+		self.add(command3)
+		self.assertTrue('lagomorph' in self.dp._usage(argv=['command3']))
+
+	def test_callable_annotations(self):
+		def cast_fn(s):
+			return 12345
+		def command(self, a:(cast_fn,)):
+			self.value = a
+		self.add(command)
+		self.main('command abc')
+		self.assertTrue(self.c.value == 12345)
+
+	def test_long_dashed_annotations(self):
+		self.debug = self.a = None
+		def command(self, a, debug:('-',)):
+			self.debug = debug
+			self.a = a
+		self.add(command)
+		self.main('command abc')
+		self.assertEqual(self.c.a, 'abc')
+		self.assertEqual(self.c.debug, False)
+		self.main('command --debug def')
+		self.assertEqual(self.c.a, 'def')
+		self.assertEqual(self.c.debug, True)
+		self.main('command ghi')
+		self.assertEqual(self.c.a, 'ghi')
+		self.assertEqual(self.c.debug, False)
+
+	def test_short_dashed_annotations(self):
+		self.a = self.n = self.q = None
+		def command(self, a, n:('-',), q:('-',)):
+			self.a = a
+			self.n = n
+			self.q = q
+		self.add(command)
+		self.main('command abc')
+		self.assertEqual(self.c.a, 'abc')
+		self.assertEqual(self.c.n, False)
+		self.assertEqual(self.c.q, False)
+		self.main('command -q ducks')
+		self.assertEqual(self.c.a, 'ducks')
+		self.assertEqual(self.c.n, False)
+		self.assertEqual(self.c.q, True)
+		self.main('command -n garofalo')
+		self.assertEqual(self.c.a, 'garofalo')
+		self.assertEqual(self.c.n, True)
+		self.assertEqual(self.c.q, False)
+		self.main('command -nq sassy')
+		self.assertEqual(self.c.a, 'sassy')
+		self.assertEqual(self.c.n, True)
+		self.assertEqual(self.c.q, True)
+		self.main('command -qn boots')
+		self.assertEqual(self.c.a, 'boots')
+		self.assertEqual(self.c.n, True)
+		self.assertEqual(self.c.q, True)
+
+	def test_short_dashed_with_value(self):
+		self.a = self.n = self.q = None
+		def command(self, a, n:('-', int)=0, q:('-', int)=0):
+			self.a = a
+			self.n = n
+			self.q = q
+		self.add(command)
+
+		self.main('command abc')
+		self.assertEqual(self.c.a, 'abc')
+		self.assertEqual(self.c.n, 0)
+		self.assertEqual(self.c.q, 0)
+
+		self.main('command -n=5 abc')
+		self.assertEqual(self.c.a, 'abc')
+		self.assertEqual(self.c.n, 5)
+		self.assertEqual(self.c.q, 0)
+
+		self.main('command -n 5 abc')
+		self.assertEqual(self.c.a, 'abc')
+		self.assertEqual(self.c.n, 5)
+		self.assertEqual(self.c.q, 0)
+
+		self.main('command -n5 abc')
+		self.assertEqual(self.c.a, 'abc')
+		self.assertEqual(self.c.n, 5)
+		self.assertEqual(self.c.q, 0)
+
+		self.main('command -q=3 abc')
+		self.assertEqual(self.c.a, 'abc')
+		self.assertEqual(self.c.n, 0)
+		self.assertEqual(self.c.q, 3)
+
+		self.main('command -q 3 abc')
+		self.assertEqual(self.c.a, 'abc')
+		self.assertEqual(self.c.n, 0)
+		self.assertEqual(self.c.q, 3)
+
+		self.main('command -q 3 -n=5 abc')
+		self.assertEqual(self.c.a, 'abc')
+		self.assertEqual(self.c.n, 5)
+		self.assertEqual(self.c.q, 3)
+
+		def command2(self, a:('-',), b:('-',), n:('-', int)=0):
+			self.a = a
+			self.b = b
+			self.n = n
+		self.add(command2)
+		self.main("command2 -ban5")
+		self.assertEqual(self.c.a, True)
+		self.assertEqual(self.c.b, True)
+		self.assertEqual(self.c.n, 5)
+
+	def test_long_dashed_with_value(self):
+		self.a = self.n = self.q = None
+		def command(self, a, debug:('-', int)=0, quiet:('-', int)=0):
+			self.a = a
+			self.debug = debug
+			self.quiet = quiet
+		self.add(command)
+
+		self.main('command abc')
+		self.assertEqual(self.c.a, 'abc')
+		self.assertEqual(self.c.debug, 0)
+		self.assertEqual(self.c.quiet, 0)
+
+		self.main('command --debug=5 abc')
+		self.assertEqual(self.c.a, 'abc')
+		self.assertEqual(self.c.debug, 5)
+		self.assertEqual(self.c.quiet, 0)
+
+		self.main('command --debug 5 abc')
+		self.assertEqual(self.c.a, 'abc')
+		self.assertEqual(self.c.debug, 5)
+		self.assertEqual(self.c.quiet, 0)
+
+		self.main('command --quiet=3 abc')
+		self.assertEqual(self.c.a, 'abc')
+		self.assertEqual(self.c.debug, 0)
+		self.assertEqual(self.c.quiet, 3)
+
+		self.main('command --quiet 3 abc')
+		self.assertEqual(self.c.a, 'abc')
+		self.assertEqual(self.c.debug, 0)
+		self.assertEqual(self.c.quiet, 3)
+
+		self.main('command --quiet 3 --debug=5 abc')
+		self.assertEqual(self.c.a, 'abc')
+		self.assertEqual(self.c.debug, 5)
+		self.assertEqual(self.c.quiet, 3)
+
+	def test_multiple_options_as_boolean(self):
+		def command(self, quiet:('- -q --shut-up -x',)):
+			self.quiet = quiet
+		self.add(command)
+		self.main('command')
+		self.assertEqual(self.c.quiet, False)
+		self.main('command -q')
+		self.assertEqual(self.c.quiet, True)
+		self.main('command --quiet')
+		self.assertEqual(self.c.quiet, True)
+		self.main('command --shut-up')
+		self.assertEqual(self.c.quiet, True)
+		self.main('command -x')
+		self.assertEqual(self.c.quiet, True)
+		self.main('command')
+		self.assertEqual(self.c.quiet, False)
+
+	def test_multiple_options_as_type(self):
+		def command(self, value:('- -v --thingy -y',str)='unset'):
+			self.value = value
+		self.add(command)
+
+		self.main('command')
+		self.assertEqual(self.c.value, 'unset')
+
+		self.main('command -v 123')
+		self.assertEqual(self.c.value, '123')
+		self.main('command --value 456')
+		self.assertEqual(self.c.value, '456')
+		self.main('command --thingy 789')
+		self.assertEqual(self.c.value, '789')
+		self.main('command -y 101112')
+		self.assertEqual(self.c.value, '101112')
+
+		self.main('command -v=123')
+		self.assertEqual(self.c.value, '123')
+		self.main('command --value=456')
+		self.assertEqual(self.c.value, '456')
+		self.main('command --thingy=789')
+		self.assertEqual(self.c.value, '789')
+		self.main('command -y=101112')
+		self.assertEqual(self.c.value, '101112')
+
+		self.main('command')
+		self.assertEqual(self.c.value, 'unset')
+
+	def test_double_dash(self):
+		"""
+		command -- -a -b -c
+
+		-- means "all remaining arguments are positional parameters"
+		"""
+		def command(self, q:('-',), debug:('-',), a, b='optional', c='empty'):
+			self.a = a
+			self.b = b
+			self.c = c
+			self.q = q
+			self.debug = debug
+
+		self.add(command)
+
+		self.main('command 1 2 3')
+		self.assertEqual(self.c.a, '1')
+		self.assertEqual(self.c.b, '2')
+		self.assertEqual(self.c.c, '3')
+		self.assertEqual(self.c.q, False)
+		self.assertEqual(self.c.debug, False)
+
+		self.main('command -q --debug 1')
+		self.assertEqual(self.c.a, '1')
+		self.assertEqual(self.c.b, 'optional')
+		self.assertEqual(self.c.c, 'empty')
+		self.assertEqual(self.c.q, True)
+		self.assertEqual(self.c.debug, True)
+
+		self.main('command -- -q --debug 1')
+		self.assertEqual(self.c.a, '-q')
+		self.assertEqual(self.c.b, '--debug')
+		self.assertEqual(self.c.c, '1')
+		self.assertEqual(self.c.q, False)
+		self.assertEqual(self.c.debug, False)
+
+	def test_mixing_positional_arguments_and_options(self):
+		self.c.a = self.c.n = self.c.q = None
+		def command(self, a:(int,), debug:('-',)=0, quiet:('-',)=0):
+			self.a = a
+			self.debug = debug
+			self.quiet = quiet
+		self.add(command)
+
+		self.main('command --debug 1')
+		self.assertEqual(self.c.a, 1)
+		self.assertEqual(self.c.debug, True)
+		self.assertEqual(self.c.quiet, False)
+
+		self.main('command 2 --debug')
+		self.assertEqual(self.c.a, 2)
+		self.assertEqual(self.c.debug, True)
+		self.assertEqual(self.c.quiet, False)
+
+		self.main('command --quiet 3 --debug')
+		self.assertEqual(self.c.a, 3)
+		self.assertEqual(self.c.debug, True)
+		self.assertEqual(self.c.quiet, True)
+
+		self.main('command 4 --quiet')
+		self.assertEqual(self.c.a, 4)
+		self.assertEqual(self.c.debug, False)
+		self.assertEqual(self.c.quiet, True)
+
+	def test_star_args(self):
+		self.c.a = self.c.n = self.c.q = None
+		def command(self, a:(int,), *args):
+			self.a = a
+			self.args = args
+		self.add(command)
+
+		self.main('command 4')
+		self.assertEqual(self.c.a, 4)
+		self.assertEqual(self.c.args, ())
+
+		self.main('command 5 a b c')
+		self.assertEqual(self.c.a, 5)
+		self.assertEqual(self.c.args, ('a', 'b', 'c'))
+
+		def command2(self, a:(int,), *args:(int,)):
+			self.a = a
+			self.args = args
+		self.add(command2)
+
+		self.main('command2 5 6 7 8 9')
+		self.assertEqual(self.c.a, 5)
+		self.assertEqual(self.c.args, (6, 7, 8, 9))
+
+
+def eval_or_str(s):
+	try:
+		return eval(s, {}, {})
+	except (NameError, SyntaxError):
+		return s
+
+if __name__ == "__main__":
+	sys.exit("no module functionality yet!")
+#!/usr/bin/env python3
+
+import dryparse
+import sys
+
+
+class Calculator:
+
+	def sum(self, a:('First addend.', int),
+				  b:('Second addend.', int),
+				  *args:('Additional addends.', int)):
+		"""
+		Adds two or more numbers together.
+		"""
+		result = a + b + sum(args)
+		print(result)
+
+	def divide(self, numerator:(int,),
+					 denominator:(int,),
+					 integer:('- -i', 'Use integer division.')):
+		"""
+		Divides one number into another.
+		"""
+		if integer:
+			result = numerator // denominator
+		else:
+			result = numerator / denominator
+		print(result)
+
+if __name__ == '__main__':
+	c = Calculator()
+	dp = dryparse.DryParse()
+	dp.update(c)
+	sys.exit(dp.main())