I have about a dozen functions (that are run as scripts) that have very similar sections interleaved with specialized code. I copied two of the scripts below. Here’s the first:
def pitz_estimate_task():
p = optparse.OptionParser()
p.add_option('--version', action='store_true',
help='Print the version and exit')
# This script requires these arguments.
p.set_usage("%prog task [estimate]")
options, args = p.parse_args()
if options.version:
print_version()
return
# This is unique to this script.
if not args:
p.print_usage()
return
# And now we're back to boring generic stuff.
pitzdir = Project.find_pitzdir(options.pitzdir)
proj = Project.from_pitzdir(pitzdir)
proj.find_me()
# This section is specific to this script.
t = proj[args[0]]
if len(args) == 2:
est = proj[args[1]]
else:
est = Estimate.choose_from_already_instantiated()
t['estimate'] = est
# That was the last thing that was specific to just this script.
# Save the project (generic).
proj.save_entities_to_yaml_files()
That script does some “generic” stuff to build an object p, then adds on some extra tweaks to p, and uses p to build an options object and an args object.
Then the script does some generic stuff to build a proj object based on the data in the options.pitzdir object, and does some various method calls on the proj object.
And here’s another script:
def pitz_attach_file():
p = optparse.OptionParser()
p.add_option('--version', action='store_true',
help='Print the version and exit')
# Notice this line is different than the one in pitz_estimate_task.
p.set_usage("%prog entity file-to-attach")
options, args = p.parse_args()
if options.version:
print_version()
return
# This section is different too.
if len(args) != 2:
p.print_usage()
return
# Back to the generic code to build the project.
pitzdir = Project.find_pitzdir(options.pitzdir)
proj = Project.from_pitzdir(pitzdir)
proj.find_me()
# Some interesting stuff that is specific just for this script.
e, filepath = proj[args[0]], args[1]
e.save_attachment(filepath)
# Save the project. (Generic).
proj.save_entities_to_yaml_files()
So, the pattern in every script is: generic code, specific code, generic code, specific code, generic code. And each step depends on the previous step.
I know I could do stuff like wrap all the generic stuff into functions, but I’m not really a fan of that approach. I’m looking for an interesting way to reduce all repetition, but keep the legibility. I’m thinking some nested context managers or decorators might be the way to go. I like to hear ideas from other people, so, please, let me hear them.
By the way, all this code is from the command-line module of pitz, available here. That’s where you can see all the different variations on the same theme.
One approach could be the “Template”-pattern (or “Self-Delegation”, as it is sometimes called). Do a Base class “ScriptBase” like that:
———-
class ScriptBase(object):
def __init__(self):
self._p = optparse.OptionParser()
self._p.add_option('–version', action='store_true',
help='Print the version and exit')
def set_parser_usage(self, usage):
self._p.set_usage(usage)
def pre_load_project_hook(self):
pass
def post_load_project_hook(self):
pass
def run():
self._options, self._args = self._p.parse_args()
if self.options.version:
print_version()
return
self.pre_load_project_hook()
pitzdir = Project.find_pitzdir(options.pitzdir)
self._proj = Project.from_pitzdir(pitzdir)
self._proj.find_me()
self.post_load_project_hook()
proj.save_entities_to_yaml_files()
# and a script inherits from it:
class PitzAttachFile(ScriptBase):
def __init__(self):
self.set_parser_usage(“%prog entity file-to-attach”)
def pre_load_project_hook(self):
if len(self._args) != 2:
self._p.print_usage()
return
def post_load_project_hook(self):
e, filepath = self._proj[args[0]], args[1]
e.save_attachment(filepath)
# and here we run the script:
pitz_attach_file = PitzAttachFile()
pitz_attach_file.run()
——-
Note that this is totally untested and written in the comment field, so indentation might be wrong etc., but I hope to get across what this is all about – it's a “script framework” (Hollywood-principle: Don't call us, we call you).
You can make a hook mandatory by raising a NotImplementedError in the base class.
Linus, thanks for the idea! I like the idea of using hooks that get specified for each case.
Know of any good sources for code samples to automate 3270 scripts through Access VBA?
That Sounds interesting, I agree with you.Please keep at your good work, I would come back often.*
That Sounds interesting, I agree with you.Please keep at your good work, I would come back often.*