Hi - refactoring Option creation - making a factory
Robert Collins <[email protected]> Tue, 25 May 2004 11:33:06 +1000
| Newsgroups | gmane.comp.python.optik.user |
|---|---|
| Message-ID | <1085448786.3150.7.camel@localhost> |
A few folk over the arch world were looking at build a drop in module to
extend optik - so that a user can do something like:
import optparse
import arch.optparse
parser=optparse.OptionParser()
parser.add_option("-a", type="arch-version", action="store", \
dest="version")
without the bogosity of diddling with the internals of 'Option'.
We've done a few test cases, and refactored the creation of Option to
use a factory - which we've called OptionTypes to reflect it's registry
behaviour.
I believe we have managed to change this without breaking backwards
compatability. Attached is the current patch we are working on.
With this patch, the documented Best Practice for new style Option
creation is:
from arch.optparse import VersionOption
parser.add_option(VersionOption("-a", action="store", dest="version"))
which doesn't affect the registry at all.
Before this is ready for inclusion in MAIN, I plan to:
*) add alias classes for all the included options so that this can be
used across the board - i.e.
parser.add_option(ComplexOption("-c", action="store", dest="c"))
*) Make OptionTypes instance based, not class based for factory
creation, so that subclassing is not needed to get custom option sets.
This will also allow a make_option that doesn't take a default type -
but see below for more thoughts.
The point of emailing this patch is to get feedback on our approach.
Oh, and there is one thing in the current code that I haven't altered,
but that I think should be:
the make_option=Option entry in _ _init__.py is bogus - here's why:
It's not any easier than parser.add_option, and to have a fallback type
(The login in Option that allows creation of an option when no type is
provided means that the factory() in OptionTypes must allow for that
too) means it is equally prone to mistakes by the user.. The make_option
idiom, when made into a factory will require a fallback type (in the
instance once I complete the tasks above).
I think a better approach would be to
a) nuke make_option in favor of using OptionTypes.factory() via the
OptionParser.add_option("-c" ... ) factory.
b) encourage the direct creation of options (If you know you want a
string option - hey, just use "StringOption".)
How widespread is the make_option idiom - is it just in the test case,
or do you think folk will be using it? (Note that it's not documented in
the doco, so I'm hopeful it will be unknown).
Anyway, thats it for now.
Cheers,
Rob
--
GPG key available at: <http://www.robertcollins.net/keys.txt>.
current-diff.patch
(text/x-patch, 17.2 KB)
* added directories
{arch}/optik/optik--refactoring
{arch}/optik/optik--refactoring/optik--refactoring--0
{arch}/optik/optik--refactoring/optik--refactoring--0/[email protected]
{arch}/optik/optik--refactoring/optik--refactoring--0/[email protected]/patch-log
{arch}/optik/optik--refactoring/optik--refactoring--0/[email protected]
{arch}/optik/optik--refactoring/optik--refactoring--0/[email protected]/patch-log
* added files
.arch-inventory
lib/.arch-ids/option_types.py.id
lib/option_types.py
test/CleanOption.py
test/CleanOption2.py
test/CleanOptionTest.py
test/TraditionalOption.py
test/__init__.py
{arch}/optik/optik--fromcvs/optik--fromcvs--0/[email protected]/patch-log/patch-1
{arch}/optik/optik--refactoring/optik--refactoring--0/[email protected]/patch-log/base-0
{arch}/optik/optik--refactoring/optik--refactoring--0/[email protected]/patch-log/patch-1
{arch}/optik/optik--refactoring/optik--refactoring--0/[email protected]/patch-log/patch-10
{arch}/optik/optik--refactoring/optik--refactoring--0/[email protected]/patch-log/patch-11
{arch}/optik/optik--refactoring/optik--refactoring--0/[email protected]/patch-log/patch-2
{arch}/optik/optik--refactoring/optik--refactoring--0/[email protected]/patch-log/patch-3
{arch}/optik/optik--refactoring/optik--refactoring--0/[email protected]/patch-log/patch-4
{arch}/optik/optik--refactoring/optik--refactoring--0/[email protected]/patch-log/patch-5
{arch}/optik/optik--refactoring/optik--refactoring--0/[email protected]/patch-log/patch-6
{arch}/optik/optik--refactoring/optik--refactoring--0/[email protected]/patch-log/patch-7
{arch}/optik/optik--refactoring/optik--refactoring--0/[email protected]/patch-log/patch-8
{arch}/optik/optik--refactoring/optik--refactoring--0/[email protected]/patch-log/patch-9
{arch}/optik/optik--refactoring/optik--refactoring--0/[email protected]/patch-log/base-0
{arch}/optik/optik--refactoring/optik--refactoring--0/[email protected]/patch-log/patch-1
{arch}/optik/optik--refactoring/optik--refactoring--0/[email protected]/patch-log/patch-10
{arch}/optik/optik--refactoring/optik--refactoring--0/[email protected]/patch-log/patch-11
{arch}/optik/optik--refactoring/optik--refactoring--0/[email protected]/patch-log/patch-12
{arch}/optik/optik--refactoring/optik--refactoring--0/[email protected]/patch-log/patch-13
{arch}/optik/optik--refactoring/optik--refactoring--0/[email protected]/patch-log/patch-14
{arch}/optik/optik--refactoring/optik--refactoring--0/[email protected]/patch-log/patch-15
{arch}/optik/optik--refactoring/optik--refactoring--0/[email protected]/patch-log/patch-16
{arch}/optik/optik--refactoring/optik--refactoring--0/[email protected]/patch-log/patch-17
{arch}/optik/optik--refactoring/optik--refactoring--0/[email protected]/patch-log/patch-18
{arch}/optik/optik--refactoring/optik--refactoring--0/[email protected]/patch-log/patch-2
{arch}/optik/optik--refactoring/optik--refactoring--0/[email protected]/patch-log/patch-3
{arch}/optik/optik--refactoring/optik--refactoring--0/[email protected]/patch-log/patch-4
{arch}/optik/optik--refactoring/optik--refactoring--0/[email protected]/patch-log/patch-5
{arch}/optik/optik--refactoring/optik--refactoring--0/[email protected]/patch-log/patch-6
{arch}/optik/optik--refactoring/optik--refactoring--0/[email protected]/patch-log/patch-7
{arch}/optik/optik--refactoring/optik--refactoring--0/[email protected]/patch-log/patch-8
{arch}/optik/optik--refactoring/optik--refactoring--0/[email protected]/patch-log/patch-9
* modified files
--- orig/examples/required_2.py
+++ mod/examples/required_2.py
@@ -8,7 +8,10 @@
import optik
class Option (optik.Option):
- ATTRS = optik.Option.ATTRS + ['required']
+ def __init__(self, *args, **kwargs):
+ self.required=kwargs.pop('required', None)
+ optik.Option.__init__(self,*args, **kwargs)
+ self._check_required()
def _check_required (self):
if self.required and not self.takes_value():
@@ -16,9 +19,6 @@
"required flag set for option that doesn't take a value",
self)
- # Make sure _check_required() is called from the constructor!
- CHECK_METHODS = optik.Option.CHECK_METHODS + [_check_required]
-
def process (self, opt, value, values, parser):
optik.Option.process(self, opt, value, values, parser)
parser.option_seen[self] = 1
--- orig/extending.txt
+++ mod/extending.txt
@@ -14,29 +14,25 @@
Adding new types
----------------
-To add new types, you need to define your own subclass of Optik's Option
-class. This class has a couple of attributes that define Optik's types:
-TYPES and TYPE_CHECKER.
-
-TYPES is a tuple of type names; in your subclass, simply define a new
-tuple TYPES that builds on the standard one.
-
-TYPE_CHECKER is a dictionary mapping type names to type-checking
-functions. A type-checking function has the following signature::
-
- def check_foo (option : Option, opt : string, value : string)
- -> foo
-
-You can name it whatever you like, and make it return any type you
-like. The value returned by a type-checking function will wind up in
-the OptionValues instance returned by OptionParser.parse_args(), or be
-passed to callbacks as the 'value' parameter.
-
-Your type-checking function should raise OptionValueError if it
-encounters any problems. OptionValueError takes a single string
-argument, which is passed as-is to OptionParser's error() method, which
-in turn prepends the program name and the string "error:" and prints
-everything to stderr before terminating the process.
+The was an API (Now deprecated) where you override Option.TYPES and
+Option.TYPE_CHECKER. This is now not needed, and recommended against.
+To add a new type to the list of types automatically created, use
+OptionTypes.add_type("name", YourType)
+
+To add new types, you need to define your own subclass (YourType above)
+of Optik's Option class. You need to override check_value
+(self, opt, value) in your subclass.
+
+Your check_value method can return any type you like. The value returned
+by a type-checking function will wind up in the OptionValues instance
+returned by OptionParser.parse_args(), or be passed to callbacks as the
+'value' parameter.
+
+Your check_value method should raise OptionValueError if it encounters
+any problems. OptionValueError takes a single string argument, which is
+passed as-is to OptionParser's error() method, which in turn prepends the
+program name and the string "error:" and prints everything to stderr before
+terminating the process.
Here's a silly example that demonstrates adding a "complex" option type
to parse Python-style complex numbers on the command line. (This is
@@ -46,44 +42,51 @@
First, the necessary imports::
from copy import copy
- from optik import Option, OptionValueError
+ from optik import Option, OptionValueError, OptionTypes
-You need to define your type-checker first, since it's referred to later
-(in the TYPE_CHECKER class attribute of your Option subclass)::
+Secondly we define your type::
- def check_complex (option, opt, value):
- try:
- return complex(value)
- except ValueError:
- raise OptionValueError(
- "option %s: invalid complex value: %r" % (opt, value))
-
-Finally, the Option subclass::
-
- class MyOption (Option):
- TYPES = Option.TYPES + ("complex",)
- TYPE_CHECKER = copy(Option.TYPE_CHECKER)
- TYPE_CHECKER["complex"] = check_complex
-
-(If we didn't make a copy() of Option.TYPE_CHECKER, we would end up
-modifying the TYPE_CHECKER attribute of Optik's Option class. This
-being Python, nothing stops you from doing that except good manners and
-common sense.)
+class ComplexOption (Option):
+ def check_value (self, opt, value):
+ try:
+ return complex(value)
+ except ValueError:
+ raise OptionValueError(
+ "option %s: invalid complex value: %r" % (opt, value))
+
+Lastly, we (optionally, see below) associate "complex" with the
+ComplexOption type; we can do this two ways. We can make the type
+globally available::
+ OptionTypes.add_type("complex", ComplexOption)
+or we can create our own set of Types:
+ MyOptionTypes=copy(OptionTypes)
+ MyOptionTypes.add_type("complex", ComplexOption)
That's it! Now you can write a script that uses the new option type
just like any other Optik-based script, except you have to instruct your
-OptionParser to use MyOption instead of Option::
+OptionParser to use MyOptionTypes instead of Option (assuming you did not
+take the 'easy' way and make the type global)::
- parser = OptionParser(option_class=MyOption)
+ parser = OptionParser(option_class=MyOptionTypes)
parser.add_option("-c", action="store", type="complex", dest="c")
Alternately, you can build your own option list and pass it to
OptionParser; if you don't use add_option() in the above way,
you don't need to tell OptionParser which option class to use::
- option_list = [MyOption("-c", action="store", type="complex", dest="c")]
+ option_list = [ComplexOption("-c", action="store", dest="c")]
parser = OptionParser(option_list=option_list)
+If you want to avoid all global type lists and building an option
+list, you can also simply use your type as an argument to add_option:
+ parser = OptionParser()
+ parser.add_option(ComplexOption("-c", action="store", dest="c"))
+
+If you want to avoid all global type lists and building an option
+list, you can also simply use your type as an argument to add_option:
+ parser = OptionParser()
+ parser.add_option(ComplexOption("-c", action="store", dest="c"))
+
Adding new actions
------------------
--- orig/lib/__init__.py
+++ mod/lib/__init__.py
@@ -17,6 +17,7 @@
# Re-import these for convenience
from optik.option import Option
+from optik.option_types import OptionTypes
from optik.option_parser import *
from optik.help import *
from optik.errors import *
@@ -32,4 +33,7 @@
# preferred way to instantiate Options is indirectly, via make_option(),
# which will become a factory function when there are many Option
# classes.
+# NOTE: this should not be exposed to end users, as it isn't a factory,
+# and the only user - the test suite - isn't compatible with a
+# factory that has a default..
make_option = Option
--- orig/lib/option.py
+++ mod/lib/option.py
@@ -13,6 +13,7 @@
import sys
import types
from optik.errors import OptionError, OptionValueError
+from optik.option_types import OptionTypes
__all__ = ['Option']
@@ -22,29 +23,6 @@
except NameError:
(True, False) = (1, 0)
-_builtin_cvt = { "int" : (int, "integer"),
- "long" : (long, "long integer"),
- "float" : (float, "floating-point"),
- "complex" : (complex, "complex") }
-
-def check_builtin (option, opt, value):
- (cvt, what) = _builtin_cvt[option.type]
- try:
- return cvt(value)
- except ValueError:
- raise OptionValueError(
- #"%s: invalid %s argument %r" % (opt, what, value))
- "option %s: invalid %s value: %r" % (opt, what, value))
-
-def check_choice(option, opt, value):
- if value in option.choices:
- return value
- else:
- choices = ", ".join(map(repr, option.choices))
- raise OptionValueError(
- "option %s: invalid choice: %r (choose from %s)"
- % (opt, value, choices))
-
# Not supplying a default is different from a default of None,
# so we need an explicit "not supplied" value.
NO_DEFAULT = "NO"+"DEFAULT"
@@ -113,32 +91,9 @@
"append",
"callback")
- # The set of known types for option parsers. Again, listed here for
- # constructor argument validation.
- TYPES = ("string", "int", "long", "float", "complex", "choice")
-
- # Dictionary of argument checking functions, which convert and
- # validate option arguments according to the option type.
- #
- # Signature of checking functions is:
- # check(option : Option, opt : string, value : string) -> any
- # where
- # option is the Option instance calling the checker
- # opt is the actual option seen on the command-line
- # (eg. "-a", "--file")
- # value is the option argument seen on the command-line
- #
- # The return value should be in the appropriate Python type
- # for option.type -- eg. an integer if option.type == "int".
- #
- # If no checker is defined for a type, arguments will be
- # unchecked and remain strings.
- TYPE_CHECKER = { "int" : check_builtin,
- "long" : check_builtin,
- "float" : check_builtin,
- "complex" : check_builtin,
- "choice" : check_choice,
- }
+ # Legacy direct-internal modification API.
+ TYPES = OptionTypes.TYPES
+ TYPE_CHECKER = OptionTypes.TYPE_CHECKER
# CHECK_METHODS is a list of unbound method objects; they are called
@@ -239,7 +194,7 @@
# No type given? "string" is the most sensible default.
self.type = "string"
else:
- if self.type not in self.TYPES:
+ if self.type not in self.TYPES and not OptionTypes.valid_type(self):
raise OptionError("invalid option type: %r" % self.type, self)
if self.action not in self.TYPED_ACTIONS:
raise OptionError(
--- orig/lib/option_parser.py
+++ mod/lib/option_parser.py
@@ -15,6 +15,7 @@
from optik.option import Option, NO_DEFAULT
from optik.help import IndentedHelpFormatter
from optik.errors import OptionConflictError, OptionValueError, BadOptionError
+from optik.option_types import OptionTypes
__all__ = ['SUPPRESS_HELP', 'SUPPRESS_USAGE',
'Values', 'OptionContainer', 'OptionGroup', 'OptionParser']
@@ -184,7 +185,7 @@
add_option(opt_str, ..., kwarg=val, ...)
"""
if type(args[0]) is types.StringType:
- option = self.option_class(*args, **kwargs)
+ option = OptionTypes.factory(self.option_class, *args, **kwargs)
elif len(args) == 1 and not kwargs:
option = args[0]
if not isinstance(option, Option):
--- orig/merge
+++ mod/merge
@@ -124,7 +124,8 @@
# last because it depends on all the others.
modules = [("errors", "class OptikError"),
("help", "class HelpFormatter"),
- ("option", "_builtin_cvt"),
+ ("option_types", "_builtin_cvt"),
+ ("option", "# Not supplying"),
("option_parser", "SUPPRESS_HELP")]
if options.compatible:
modules[2] = ("option", "# Do the right thing with boolean values")
--- orig/test/test_optik.py
+++ mod/test/test_optik.py
@@ -17,13 +17,19 @@
from pprint import pprint
from test import test_support
-test_dir = os.path.dirname(sys.argv[0])
-build_dir = os.path.join(test_dir, os.pardir, "build", "lib")
-if os.path.isdir(build_dir):
- sys.path.insert(0, build_dir)
-else:
- sys.exit("you must run 'python setup.py build' before "
- "running the test suite")
+def tryBuildDir(a_dir):
+ """try adding '${a_dir}/../build/lib' to the path"""
+ build_dir = os.path.join(a_dir, os.pardir, "build", "lib")
+ if os.path.isdir(build_dir):
+ sys.path.insert(0, build_dir)
+ return True
+ else:
+ return False
+
+if not tryBuildDir(os.path.dirname(sys.argv[0])):
+ if not tryBuildDir(os.path.abspath(os.path.curdir)):
+ sys.exit("you must run 'python setup.py build' before "
+ "running the test suite")
from optik import make_option, Option, IndentedHelpFormatter, \
TitledHelpFormatter, OptionParser, OptionContainer, OptionGroup, \
@@ -1263,4 +1269,8 @@
test_support.run_suite(suite())
if __name__ == '__main__':
+ #TODO don't use unittest.main, rather test_main? Anyway, fix this to
+ # invoke both tests, but the main one first, and CleanOptionTest later
+ import CleanOptionTest
+ test_support.run_suite(CleanOptionTest.parserTestSuite)
unittest.main()
--- orig/{arch}/=tagging-method
+++ mod/{arch}/=tagging-method
@@ -48,10 +48,10 @@
# Uncomment one of these lines as appropriate to override the default:
#
# untagged-source source
-#untagged-source precious
+untagged-source precious
# untagged-source backup
# untagged-source junk
-untagged-source unrecognized
+# untagged-source unrecognized
#
# naming convention regexps
signature.asc
(application/pgp-signature, 189 B)
-----BEGIN PGP SIGNATURE----- Version: GnuPG v1.2.4 (GNU/Linux) iD8DBQBAsqJSI5+kQ8LJcoIRAoa2AJ9s8Qca7rAdjCTUY+HbtGoH7yJ5VQCdHRmO LoSCla7DxdBAJ2zkkzrG3FU= =lGOr -----END PGP SIGNATURE-----