0

I have two classes:

OptionParser
Application

stored in separate files (option_parser.py and application.py). The former defines how command line input should be handled. The latter imports the former, reads the user input, and carries on with the request.

How do I call a method defined in application.py's Application without importing it inside option_parser.py (as that would lead to an infinite loop, since application.py imports OptionParser from option_parser.py). If I use set_defaults(func=method_name), then I am told that global function method_name is not defined in that scope.

I already have a workaround for this, but it just doesn't seem right:

if self.options.action == 'tags':
    self.list_tags()
elif self.options.action == 'branches':
    self.list_branches()

I've tried to find the answer in the official documentation, Google, and on SO - to no avail.

user1766396
  • 33
  • 1
  • 5
  • What seems wrong about that? – jonrsharpe Mar 20 '17 at 07:14
  • It doesn't seem like correct solution. I'd like to be able to use: `self.options.func()` – user1766396 Mar 20 '17 at 07:18
  • Could you give some more context - that seems like a very close coupling between the argument parsing and your class, which will make both harder to test. – jonrsharpe Mar 20 '17 at 07:23
  • One other workaround I've been thinking about was to split the code into three files/classes: OptionParser, Application and Wrapper. Wrapper would import OptionParser and Application, OptionParser would import Application (so that Application's method can be supplied to `set_defaults(func=)`, and Application would not import OptionParser (instead, the instance of the class would be passed as an argument). Does that make sense? – user1766396 Mar 20 '17 at 07:25
  • Not really. Again, give more context; [edit] the question to give a less abstract demonstration of the functionality you're working on. – jonrsharpe Mar 20 '17 at 07:26
  • Perhaps the coupling is too close. The problem is that I am refactoring a toolset, and I am currently translating tools that have been written in Bash (developer A) to Python so that all tools are within the same framework (developer B). Refactoring the framework itself is probably not an option (at least for the time being). But it is still much better than the tools written in Bash. – user1766396 Mar 20 '17 at 07:28
  • Added less abstract context. – user1766396 Mar 20 '17 at 07:45
  • That's made things, if anything, less clear. You appear to have an instance method pretending to be a class method, for one. The code won't run, it's not a [mcve]. Perhaps, if you have working code that you think could be generally improved, it would be better suited to [codereview.se]? Note that they will require full code in context and will review with a broader remit than the single block you've highlighted. – jonrsharpe Mar 20 '17 at 07:48

1 Answers1

0

I assume you are using set_defaults(func=method_name) with a subparser as illustrated in the argparse docs? That's an easy way of invoking a function. It takes advantage of flexibility built into defining defaults and setting attributes in the args namespace. But there's no code devoted to this feature.

Associating functions with actions like this is typically the easiest way to handle the different actions for each of your subparsers. However, if it is necessary to check the name of the subparser that was invoked, the dest keyword argument to the add_subparsers() call will work:

That 2nd sentence is giving you permission to use

if self.options.action == 'tags'

In your case set_defaults is not the easiest way.

argparse is primarily a means of finding out what your user wants. It isn't meant to be a dispatcher. Usually that's the responsibility of your code.

My impression from other argparse questions is that most of the time programmers don't use this set_defaults option. A search of SO for [argparse] set_default only turns up 37 posts (answers or questions) (compared to 1.5K questions for just the argparse tag).


Here's a working script that implements my idea of passing set_defaults values to an parser setup function. It's one file, but the parser part could just as well be imported.

import argparse

def make_parser(defaults):
    parser = argparse.ArgumentParser()
    sp = parser.add_subparsers(dest='cmdstr')
    sp.required = True
    for key in defaults:
        spp = sp.add_parser(key)
        spp.set_defaults(cmd=defaults[key])
    return parser

def foo(args):
    print('foo')

def bar(args):
    print('bar')

if __name__=='__main__':
    dd = {'cmd1': foo, 'cmd2': bar}
    parser = make_parser(dd)
    args = parser.parse_args()
    print(args)
    args.cmd(args)

Sample calls:

0930:~/mypy$ python3 stack42897689.py cmd1
Namespace(cmd=<function foo at 0xb70c2dac>, cmdstr='cmd1')
foo
0931:~/mypy$ python3 stack42897689.py cmd2
Namespace(cmd=<function bar at 0xb7145614>, cmdstr='cmd2')
bar

With this dd dictionary, I could just as easily have done the dispatching with:

dd[args.cmdstr](None)

The basic goal is to pair up a commandline string with a function call. set_defaults is one way to do within argparse. But dictionary like dd is also widely used in Python code. And so are if/else statements.

hpaulj
  • 221,503
  • 14
  • 230
  • 353
  • OK, thanks. I guess I will have to be content with the "workaround" solution, then. – user1766396 Mar 20 '17 at 07:47
  • BTW, the reason is that I wanted to avoid using conditional statement blocks, and instead resort to calling correct function right away. – user1766396 Mar 20 '17 at 08:14
  • You could pass functions from the main to the imported module via function call parameters. I added a crude sketch to my answer. Details will have to wait. – hpaulj Mar 20 '17 at 09:33
  • Hm, I will consider that. Thank you. Not sure code reviewers will not frown upon this, but this is certainly something worth inspecting. – user1766396 Mar 20 '17 at 09:50