1+ import sys , inspect
2+
3+ def classAsFunction (cls ):
4+ if not isinstance (cls , type ):
5+ return cls
6+
7+ class Class (cls ):
8+ def __new__ (cls , args ):
9+ instance = super (Callable , cls ).__new__ (cls )
10+ return instance .__init__ (args )
11+
12+ return Class
13+
14+ @classAsFunction
15+ class Callable (object ):
16+ class SubCommand (Exception ):
17+ pass
18+ class Redirect (Exception ):
19+ pass
20+ class Error (Exception ):
21+ pass
22+
23+ def __init__ (self , args ):
24+ return self .call ("" , args )
25+
26+ def call (self , cmd , args ):
27+ cmd = cmd .strip ()
28+
29+ try :
30+ handler = getattr (self , "action" + "" .join (map (lambda part : part [0 ].upper () + part [1 :] if part != "" else "" , cmd .split (" " ))))
31+ except AttributeError :
32+ all_commands = [name [6 ].lower () + name [7 :] for name in dir (self ) if name .startswith ("action" ) and len (name ) > 6 ]
33+ raise Callable .Error ("Unknown command '%s'. Allowed commands are: %s" % (cmd , ", " .join (all_commands )))
34+
35+ self .checkCall (cmd , handler , args )
36+
37+ try :
38+ return self .callArgs (handler , args )
39+ except Callable .SubCommand as e :
40+ if len (args ) == 0 :
41+ raise Callable .Error ("'%s' command is not a command but has subcommands." % cmd )
42+
43+ if len (tuple (e )) == 0 :
44+ # Remove first argument and call it
45+ return self .call ("%s %s" % (cmd , args [0 ]), args [1 :])
46+ else :
47+ # Remove first argument and call given command
48+ return self .call (tuple (e )[0 ], args [1 :])
49+ except Callable .Redirect as e :
50+ if len (tuple (e )) == 0 :
51+ # Remove first argument and call it (as SubCommand)
52+ if len (args ) == 0 :
53+ raise Callable .Error ("'%s' command is not a command but has subcommands." % cmd )
54+ return self .call ("%s %s" % (cmd , args [0 ]), args [1 :])
55+ elif len (tuple (e )) == 1 :
56+ # Call given value
57+ return self .call (tuple (e )[0 ], args )
58+ else :
59+ # Call given value and arguments
60+ return self .call (tuple (e )[0 ], tuple (e )[1 ])
61+
62+ def checkCall (self , cmd , func , argv ):
63+ import inspect
64+
65+ expected_args = inspect .getargspec (func ).args [1 :] # Split "self"
66+ defaults = inspect .getargspec (func ).defaults or tuple ()
67+
68+ if self .checkArgs (cmd , func , argv ):
69+ return True
70+
71+ if len (defaults ) > 0 :
72+ default_args = reversed (zip (reversed (expected_args ), reversed (defaults )))
73+ default_args = map (lambda arg : "%s=%s" % arg , default_args )
74+ expected_args = expected_args [:- len (default_args )] + default_args
75+
76+ raise Callable .Error ("Allowed arguments: %s" % ", " .join (expected_args ))
77+
78+ def checkArgs (self , cmd , func , argv ):
79+ args , kwargs = self .parseArgs (argv )
80+
81+ import inspect
82+
83+ expected_args = inspect .getargspec (func ).args [1 :] # Split "self"
84+ varargs = inspect .getargspec (func ).varargs
85+ keywords = inspect .getargspec (func ).keywords
86+ defaults = inspect .getargspec (func ).defaults or tuple ()
87+
88+ resulting_args = dict ()
89+ if varargs is not None :
90+ resulting_args [varargs ] = []
91+ if keywords is not None :
92+ resulting_args [keywords ] = {}
93+
94+ # Positional arguments
95+ for cnt , value in enumerate (args ):
96+ if cnt < len (expected_args ):
97+ # Passed just as argument
98+ resulting_args [expected_args [cnt ]] = value
99+ else :
100+ # Passed to *args
101+ if varargs is None :
102+ raise Callable .Error ("Too many positional arguments passed to '%s': expected at most %s, got %s." % (cmd , len (expected_args ), len (args )))
103+ else :
104+ resulting_args [varargs ].append (value )
105+
106+ # Named arguments
107+ handled_kwargs = []
108+ for name , value in kwargs .iteritems ():
109+ if name in handled_kwargs :
110+ raise Callable .Error ("'%s' was passed to '%s' as named argument several times." % (name , cmd ))
111+
112+ handled_kwargs .append (name )
113+
114+ if name in expected_args :
115+ # Passed just as argument
116+ if name in resulting_args :
117+ raise Callable .Error ("'%s' was passed to '%s' as both positional argument and named." % (name , cmd ))
118+
119+ resulting_args [name ] = value
120+ else :
121+ # Passed to **kwargs
122+ if keywords is None :
123+ raise Callable .Error ("Unknown named argument '%s' passed to '%s'." % (name , cmd ))
124+ else :
125+ resulting_args [keywords ][name ] = value
126+
127+ # Defaults
128+ if len (defaults ) > 0 :
129+ for cnt , name in enumerate (expected_args [- len (defaults ):]):
130+ if name not in resulting_args :
131+ resulting_args [name ] = defaults [cnt ]
132+
133+ # Check that all the arguments were passed
134+ for name in expected_args :
135+ if name not in resulting_args :
136+ raise Callable .Error ("Argument '%s' was not passed to '%s'." % (name , cmd ))
137+
138+ return True
139+
140+ def parseArgs (self , argv ):
141+ args = []
142+ kwargs = {}
143+
144+ kwname = None
145+
146+ for arg in argv :
147+ if arg .startswith ("--" ):
148+ if kwname is not None :
149+ kwargs [kwname ] = True
150+
151+ kwname = arg [2 :]
152+ else :
153+ if kwname is None :
154+ args .append (arg )
155+ else :
156+ kwargs [kwname ] = arg
157+ kwname = None
158+
159+ if kwname is not None :
160+ kwargs [kwname ] = True
161+
162+ return args , kwargs
163+
164+ def callArgs (self , handler , argv ):
165+ args , kwargs = self .parseArgs (argv )
166+ return handler (* args , ** kwargs )
167+
168+ def action (self , * args , ** kwargs ):
169+ raise Callable .SubCommand
170+
171+ class WithHelp (Callable ):
172+ def actionHelp (self , * cmd ):
173+ if cmd in [[], ["" ], tuple (), ("" ,)]:
174+ # Print info about the class
175+ print inspect .cleandoc (self .__doc__ )
176+ return
177+
178+ try :
179+ handler = getattr (self , "action" + "" .join (map (lambda part : part [0 ].upper () + part [1 :], cmd )))
180+ if handler .__doc__ is not None :
181+ print inspect .cleandoc (handler .__doc__ )
182+ return
183+ except AttributeError :
184+ pass
185+
186+ if cmd == ["help" ] or cmd == ("help" ,):
187+ # Unable to find info on topic 'help' - no __doc__ in 'help' method or no 'help' method, use default help
188+ print inspect .cleandoc (self .__doc__ )
189+ return
190+
191+ print "Unknown topic '%s'" % " " .join (cmd )
192+
193+ Callable .WithHelp = WithHelp
0 commit comments