Show More
@@ -298,14 +298,12 NoConfigDefault = NoConfigDefault() | |||
|
298 | 298 | |
|
299 | 299 | class ArgParseConfigLoader(CommandLineConfigLoader): |
|
300 | 300 | |
|
301 | # arguments = [(('-f','--file'),dict(type=str,dest='file'))] | |
|
302 | arguments = () | |
|
303 | ||
|
304 | def __init__(self, argv=None, *args, **kw): | |
|
301 | def __init__(self, argv=None, arguments=(), *args, **kw): | |
|
305 | 302 | """Create a config loader for use with argparse. |
|
306 | 303 | |
|
307 |
With the exception of argv, other args and kwargs |
|
|
308 | passed onto the constructor of :class:`argparse.ArgumentParser`. | |
|
304 | With the exception of ``argv`` and ``arguments``, other args and kwargs | |
|
305 | arguments here are passed onto the constructor of | |
|
306 | :class:`argparse.ArgumentParser`. | |
|
309 | 307 | |
|
310 | 308 | Parameters |
|
311 | 309 | ---------- |
@@ -313,11 +311,16 class ArgParseConfigLoader(CommandLineConfigLoader): | |||
|
313 | 311 | argv : optional, list |
|
314 | 312 | If given, used to read command-line arguments from, otherwise |
|
315 | 313 | sys.argv[1:] is used. |
|
314 | ||
|
315 | arguments : optional, tuple | |
|
316 | Description of valid command-line arguments, to be called in sequence | |
|
317 | with parser.add_argument() to configure the parser. | |
|
316 | 318 | """ |
|
317 | 319 | super(CommandLineConfigLoader, self).__init__() |
|
318 | 320 | if argv == None: |
|
319 | 321 | argv = sys.argv[1:] |
|
320 | 322 | self.argv = argv |
|
323 | self.arguments = arguments | |
|
321 | 324 | self.args = args |
|
322 | 325 | self.kw = kw |
|
323 | 326 |
@@ -66,15 +66,13 class TestArgParseCL(TestCase): | |||
|
66 | 66 | |
|
67 | 67 | def test_basic(self): |
|
68 | 68 | |
|
69 | class MyLoader(ArgParseConfigLoader): | |
|
70 | arguments = ( | |
|
69 | arguments = ( | |
|
71 | 70 | (('-f','--foo'), dict(dest='Global.foo', type=str)), |
|
72 | 71 | (('-b',), dict(dest='MyClass.bar', type=int)), |
|
73 | 72 | (('-n',), dict(dest='n', action='store_true')), |
|
74 | 73 | (('Global.bam',), dict(type=str)) |
|
75 | 74 | ) |
|
76 | ||
|
77 | cl = MyLoader() | |
|
75 | cl = ArgParseConfigLoader(arguments=arguments) | |
|
78 | 76 | config = cl.load_config('-f hi -b 10 -n wow'.split()) |
|
79 | 77 | self.assertEquals(config.Global.foo, 'hi') |
|
80 | 78 | self.assertEquals(config.MyClass.bar, 10) |
@@ -33,7 +33,7 import logging | |||
|
33 | 33 | import os |
|
34 | 34 | import sys |
|
35 | 35 | |
|
36 | from IPython.core import release | |
|
36 | from IPython.core import release, crashhandler | |
|
37 | 37 | from IPython.utils.genutils import get_ipython_dir, get_ipython_package_dir |
|
38 | 38 | from IPython.config.loader import ( |
|
39 | 39 | PyFileConfigLoader, |
@@ -77,6 +77,29 class ApplicationError(Exception): | |||
|
77 | 77 | pass |
|
78 | 78 | |
|
79 | 79 | |
|
80 | app_cl_args = ( | |
|
81 | (('--ipython-dir', ), dict( | |
|
82 | dest='Global.ipython_dir',type=unicode, | |
|
83 | help='Set to override default location of Global.ipython_dir.', | |
|
84 | default=NoConfigDefault, | |
|
85 | metavar='Global.ipython_dir') ), | |
|
86 | (('-p', '--profile',), dict( | |
|
87 | dest='Global.profile',type=unicode, | |
|
88 | help='The string name of the ipython profile to be used.', | |
|
89 | default=NoConfigDefault, | |
|
90 | metavar='Global.profile') ), | |
|
91 | (('--log-level',), dict( | |
|
92 | dest="Global.log_level",type=int, | |
|
93 | help='Set the log level (0,10,20,30,40,50). Default is 30.', | |
|
94 | default=NoConfigDefault, | |
|
95 | metavar='Global.log_level')), | |
|
96 | (('--config-file',), dict( | |
|
97 | dest='Global.config_file',type=unicode, | |
|
98 | help='Set the config file name to override default.', | |
|
99 | default=NoConfigDefault, | |
|
100 | metavar='Global.config_file')), | |
|
101 | ) | |
|
102 | ||
|
80 | 103 | class Application(object): |
|
81 | 104 | """Load a config, construct components and set them running.""" |
|
82 | 105 | |
@@ -94,11 +117,17 class Application(object): | |||
|
94 | 117 | ipython_dir = None |
|
95 | 118 | #: A reference to the argv to be used (typically ends up being sys.argv[1:]) |
|
96 | 119 | argv = None |
|
120 | #: Default command line arguments. Subclasses should create a new tuple | |
|
121 | #: that *includes* these. | |
|
122 | cl_arguments = app_cl_args | |
|
97 | 123 | |
|
98 | 124 | # Private attributes |
|
99 | 125 | _exiting = False |
|
100 | 126 | _initialized = False |
|
101 | 127 | |
|
128 | # Class choices for things that will be instantiated at runtime. | |
|
129 | _CrashHandler = crashhandler.CrashHandler | |
|
130 | ||
|
102 | 131 | def __init__(self, argv=None): |
|
103 | 132 | self.argv = sys.argv[1:] if argv is None else argv |
|
104 | 133 | self.init_logger() |
@@ -125,39 +154,49 class Application(object): | |||
|
125 | 154 | |
|
126 | 155 | if self._initialized: |
|
127 | 156 | return |
|
128 | ||
|
129 | self.attempt(self.create_default_config) | |
|
157 | ||
|
158 | # The first part is protected with an 'attempt' wrapper, that will log | |
|
159 | # failures with the basic system traceback machinery. Once our crash | |
|
160 | # handler is in place, we can let any subsequent exception propagate, | |
|
161 | # as our handler will log it with much better detail than the default. | |
|
162 | self.attempt(self.create_crash_handler) | |
|
163 | self.create_default_config() | |
|
130 | 164 | self.log_default_config() |
|
131 | 165 | self.set_default_config_log_level() |
|
132 |
self. |
|
|
133 |
self. |
|
|
166 | self.pre_load_command_line_config() | |
|
167 | self.load_command_line_config() | |
|
134 | 168 | self.set_command_line_config_log_level() |
|
135 |
self. |
|
|
169 | self.post_load_command_line_config() | |
|
136 | 170 | self.log_command_line_config() |
|
137 |
self. |
|
|
138 |
self. |
|
|
139 |
self. |
|
|
140 |
self. |
|
|
141 |
self. |
|
|
142 |
self. |
|
|
171 | self.find_ipython_dir() | |
|
172 | self.find_resources() | |
|
173 | self.find_config_file_name() | |
|
174 | self.find_config_file_paths() | |
|
175 | self.pre_load_file_config() | |
|
176 | self.load_file_config() | |
|
143 | 177 | self.set_file_config_log_level() |
|
144 |
self. |
|
|
178 | self.post_load_file_config() | |
|
145 | 179 | self.log_file_config() |
|
146 |
self. |
|
|
180 | self.merge_configs() | |
|
147 | 181 | self.log_master_config() |
|
148 |
self. |
|
|
149 |
self. |
|
|
150 |
self. |
|
|
182 | self.pre_construct() | |
|
183 | self.construct() | |
|
184 | self.post_construct() | |
|
151 | 185 | self._initialized = True |
|
152 | 186 | |
|
153 | 187 | def start(self): |
|
154 | 188 | self.initialize() |
|
155 |
self. |
|
|
189 | self.start_app() | |
|
156 | 190 | |
|
157 | 191 | #------------------------------------------------------------------------- |
|
158 | 192 | # Various stages of Application creation |
|
159 | 193 | #------------------------------------------------------------------------- |
|
160 | 194 | |
|
195 | def create_crash_handler(self): | |
|
196 | """Create a crash handler, typically setting sys.excepthook to it.""" | |
|
197 | self.crash_handler = self._CrashHandler(self, self.name) | |
|
198 | sys.excepthook = self.crash_handler | |
|
199 | ||
|
161 | 200 | def create_default_config(self): |
|
162 | 201 | """Create defaults that can't be set elsewhere. |
|
163 | 202 | |
@@ -185,10 +224,9 class Application(object): | |||
|
185 | 224 | |
|
186 | 225 | def create_command_line_config(self): |
|
187 | 226 | """Create and return a command line config loader.""" |
|
188 |
return |
|
|
189 | description=self.description, | |
|
190 | version=release.version | |
|
191 | ) | |
|
227 | return ArgParseConfigLoader(self.argv, self.cl_arguments, | |
|
228 | description=self.description, | |
|
229 | version=release.version) | |
|
192 | 230 | |
|
193 | 231 | def pre_load_command_line_config(self): |
|
194 | 232 | """Do actions just before loading the command line config.""" |
@@ -384,7 +422,10 class Application(object): | |||
|
384 | 422 | raise |
|
385 | 423 | except: |
|
386 | 424 | if action == 'abort': |
|
425 | self.log.critical("Aborting application: %s" % self.name, | |
|
426 | exc_info=True) | |
|
387 | 427 | self.abort() |
|
428 | raise | |
|
388 | 429 | elif action == 'exit': |
|
389 | 430 | self.exit(0) |
|
390 | 431 |
@@ -28,10 +28,8 from IPython.core import release | |||
|
28 | 28 | from IPython.core import ultratb |
|
29 | 29 | from IPython.external.Itpl import itpl |
|
30 | 30 | |
|
31 | from IPython.utils.genutils import * | |
|
32 | ||
|
33 | 31 | #**************************************************************************** |
|
34 | class CrashHandler: | |
|
32 | class CrashHandler(object): | |
|
35 | 33 | """Customizable crash handlers for IPython-based systems. |
|
36 | 34 | |
|
37 | 35 | Instances of this class provide a __call__ method which can be used as a |
@@ -41,15 +39,15 class CrashHandler: | |||
|
41 | 39 | |
|
42 | 40 | """ |
|
43 | 41 | |
|
44 |
def __init__(self, |
|
|
45 | bug_tracker,crash_report_fname, | |
|
42 | def __init__(self,app, app_name, contact_name=None, contact_email=None, | |
|
43 | bug_tracker=None, crash_report_fname='CrashReport.txt', | |
|
46 | 44 | show_crash_traceback=True): |
|
47 | 45 | """New crash handler. |
|
48 | 46 | |
|
49 | 47 | Inputs: |
|
50 | 48 | |
|
51 |
- |
|
|
52 | for internal information. | |
|
49 | - app: a running application instance, which will be queried at crash | |
|
50 | time for internal information. | |
|
53 | 51 | |
|
54 | 52 | - app_name: a string containing the name of your application. |
|
55 | 53 | |
@@ -77,13 +75,14 class CrashHandler: | |||
|
77 | 75 | """ |
|
78 | 76 | |
|
79 | 77 | # apply args into instance |
|
80 | self.IP = IP # IPython instance | |
|
78 | self.app = app | |
|
81 | 79 | self.app_name = app_name |
|
82 | 80 | self.contact_name = contact_name |
|
83 | 81 | self.contact_email = contact_email |
|
84 | 82 | self.bug_tracker = bug_tracker |
|
85 | 83 | self.crash_report_fname = crash_report_fname |
|
86 | 84 | self.show_crash_traceback = show_crash_traceback |
|
85 | self.section_sep = '\n\n'+'*'*75+'\n\n' | |
|
87 | 86 | |
|
88 | 87 | # Hardcoded defaults, which can be overridden either by subclasses or |
|
89 | 88 | # at runtime for the instance. |
@@ -124,7 +123,7 $self.bug_tracker | |||
|
124 | 123 | #color_scheme = 'Linux' # dbg |
|
125 | 124 | |
|
126 | 125 | try: |
|
127 |
rptdir = self. |
|
|
126 | rptdir = self.app.ipython_dir | |
|
128 | 127 | except: |
|
129 | 128 | rptdir = os.getcwd() |
|
130 | 129 | if not os.path.isdir(rptdir): |
@@ -134,7 +133,7 $self.bug_tracker | |||
|
134 | 133 | # properly expanded out in the user message template |
|
135 | 134 | self.crash_report_fname = report_name |
|
136 | 135 | TBhandler = ultratb.VerboseTB(color_scheme=color_scheme, |
|
137 |
|
|
|
136 | long_header=1) | |
|
138 | 137 | traceback = TBhandler.text(etype,evalue,etb,context=31) |
|
139 | 138 | |
|
140 | 139 | # print traceback to screen |
@@ -159,70 +158,62 $self.bug_tracker | |||
|
159 | 158 | |
|
160 | 159 | def make_report(self,traceback): |
|
161 | 160 | """Return a string containing a crash report.""" |
|
162 | ||
|
163 | sec_sep = '\n\n'+'*'*75+'\n\n' | |
|
164 | ||
|
161 | import platform | |
|
162 | ||
|
163 | sec_sep = self.section_sep | |
|
164 | ||
|
165 | 165 | report = [] |
|
166 | 166 | rpt_add = report.append |
|
167 | 167 | |
|
168 | 168 | rpt_add('*'*75+'\n\n'+'IPython post-mortem report\n\n') |
|
169 |
rpt_add('IPython version: %s |
|
|
170 |
rpt_add('BZR revision : %s |
|
|
171 | rpt_add('Platform info : os.name -> %s, sys.platform -> %s' % | |
|
169 | rpt_add('IPython version: %s \n' % release.version) | |
|
170 | rpt_add('BZR revision : %s \n' % release.revision) | |
|
171 | rpt_add('Platform info : os.name -> %s, sys.platform -> %s\n' % | |
|
172 | 172 | (os.name,sys.platform) ) |
|
173 | rpt_add(sec_sep+'Current user configuration structure:\n\n') | |
|
174 | rpt_add(pformat(self.IP.dict())) | |
|
175 | rpt_add(sec_sep+'Crash traceback:\n\n' + traceback) | |
|
173 | rpt_add(' : %s\n' % platform.platform()) | |
|
174 | rpt_add('Python info : %s\n' % sys.version) | |
|
175 | ||
|
176 | 176 | try: |
|
177 | rpt_add(sec_sep+"History of session input:") | |
|
178 | for line in self.IP.user_ns['_ih']: | |
|
179 |
|
|
|
180 | rpt_add('\n*** Last line of input (may not be in above history):\n') | |
|
181 | rpt_add(self.IP._last_input_line+'\n') | |
|
177 | config = pformat(self.app.config) | |
|
178 | rpt_add(sec_sep+'Current user configuration structure:\n\n') | |
|
179 | rpt_add(config) | |
|
182 | 180 | except: |
|
183 | 181 | pass |
|
182 | rpt_add(sec_sep+'Crash traceback:\n\n' + traceback) | |
|
184 | 183 | |
|
185 | 184 | return ''.join(report) |
|
186 | 185 | |
|
186 | ||
|
187 | 187 | class IPythonCrashHandler(CrashHandler): |
|
188 | 188 | """sys.excepthook for IPython itself, leaves a detailed report on disk.""" |
|
189 | 189 | |
|
190 | def __init__(self,IP): | |
|
190 | def __init__(self, app, app_name='IPython'): | |
|
191 | 191 | |
|
192 | 192 | # Set here which of the IPython authors should be listed as contact |
|
193 | 193 | AUTHOR_CONTACT = 'Fernando' |
|
194 | 194 | |
|
195 | 195 | # Set argument defaults |
|
196 | app_name = 'IPython' | |
|
197 | 196 | bug_tracker = 'https://bugs.launchpad.net/ipython/+filebug' |
|
198 | 197 | contact_name,contact_email = release.authors[AUTHOR_CONTACT][:2] |
|
199 | 198 | crash_report_fname = 'IPython_crash_report.txt' |
|
200 | 199 | # Call parent constructor |
|
201 |
CrashHandler.__init__(self, |
|
|
200 | CrashHandler.__init__(self,app,app_name,contact_name,contact_email, | |
|
202 | 201 | bug_tracker,crash_report_fname) |
|
203 | 202 | |
|
204 | 203 | def make_report(self,traceback): |
|
205 | 204 | """Return a string containing a crash report.""" |
|
206 | 205 | |
|
207 | sec_sep = '\n\n'+'*'*75+'\n\n' | |
|
208 | ||
|
209 | report = [] | |
|
206 | sec_sep = self.section_sep | |
|
207 | # Start with parent report | |
|
208 | report = [super(IPythonCrashHandler, self).make_report(traceback)] | |
|
209 | # Add interactive-specific info we may have | |
|
210 | 210 | rpt_add = report.append |
|
211 | ||
|
212 | rpt_add('*'*75+'\n\n'+'IPython post-mortem report\n\n') | |
|
213 | rpt_add('IPython version: %s \n\n' % release.version) | |
|
214 | rpt_add('BZR revision : %s \n\n' % release.revision) | |
|
215 | rpt_add('Platform info : os.name -> %s, sys.platform -> %s' % | |
|
216 | (os.name,sys.platform) ) | |
|
217 | rpt_add(sec_sep+'Current user configuration structure:\n\n') | |
|
218 | # rpt_add(pformat(self.IP.dict())) | |
|
219 | rpt_add(sec_sep+'Crash traceback:\n\n' + traceback) | |
|
220 | 211 | try: |
|
221 | 212 | rpt_add(sec_sep+"History of session input:") |
|
222 |
for line in self. |
|
|
213 | for line in self.app.shell.user_ns['_ih']: | |
|
223 | 214 | rpt_add(line) |
|
224 | 215 | rpt_add('\n*** Last line of input (may not be in above history):\n') |
|
225 |
rpt_add(self. |
|
|
216 | rpt_add(self.app.shell._last_input_line+'\n') | |
|
226 | 217 | except: |
|
227 | 218 | pass |
|
228 | 219 |
@@ -28,6 +28,7 import logging | |||
|
28 | 28 | import os |
|
29 | 29 | import sys |
|
30 | 30 | |
|
31 | from IPython.core import crashhandler | |
|
31 | 32 | from IPython.core import release |
|
32 | 33 | from IPython.core.application import Application, BaseAppArgParseConfigLoader |
|
33 | 34 | from IPython.core.error import UsageError |
@@ -280,19 +281,18 cl_args = ( | |||
|
280 | 281 | ) |
|
281 | 282 | |
|
282 | 283 | |
|
283 | class IPythonAppCLConfigLoader(BaseAppArgParseConfigLoader): | |
|
284 | ||
|
285 | arguments = cl_args | |
|
286 | ||
|
287 | ||
|
288 | 284 | default_config_file_name = u'ipython_config.py' |
|
289 | 285 | |
|
290 | ||
|
291 | 286 | class IPythonApp(Application): |
|
292 | 287 | name = u'ipython' |
|
293 | 288 | description = 'IPython: an enhanced interactive Python shell.' |
|
294 | 289 | config_file_name = default_config_file_name |
|
295 | 290 | |
|
291 | cl_arguments = Application.cl_arguments + cl_args | |
|
292 | ||
|
293 | # Private and configuration attributes | |
|
294 | _CrashHandler = crashhandler.IPythonCrashHandler | |
|
295 | ||
|
296 | 296 | def __init__(self, argv=None, **shell_params): |
|
297 | 297 | """Create a new IPythonApp. |
|
298 | 298 | |
@@ -309,6 +309,7 class IPythonApp(Application): | |||
|
309 | 309 | super(IPythonApp, self).__init__(argv) |
|
310 | 310 | self.shell_params = shell_params |
|
311 | 311 | |
|
312 | ||
|
312 | 313 | def create_default_config(self): |
|
313 | 314 | super(IPythonApp, self).create_default_config() |
|
314 | 315 | # Eliminate multiple lookups |
@@ -340,13 +341,6 class IPythonApp(Application): | |||
|
340 | 341 | Global.wthread = False |
|
341 | 342 | Global.gthread = False |
|
342 | 343 | |
|
343 | def create_command_line_config(self): | |
|
344 | """Create and return a command line config loader.""" | |
|
345 | return IPythonAppCLConfigLoader(self.argv, | |
|
346 | description=self.description, | |
|
347 | version=release.version | |
|
348 | ) | |
|
349 | ||
|
350 | 344 | def load_file_config(self): |
|
351 | 345 | if hasattr(self.command_line_config.Global, 'quick'): |
|
352 | 346 | if self.command_line_config.Global.quick: |
@@ -1167,37 +1167,14 class InteractiveShell(Component, Magic): | |||
|
1167 | 1167 | color_scheme='NoColor', |
|
1168 | 1168 | tb_offset = 1) |
|
1169 | 1169 | |
|
1170 | # IPython itself shouldn't crash. This will produce a detailed | |
|
1171 | # post-mortem if it does. But we only install the crash handler for | |
|
1172 | # non-threaded shells, the threaded ones use a normal verbose reporter | |
|
1173 | # and lose the crash handler. This is because exceptions in the main | |
|
1174 | # thread (such as in GUI code) propagate directly to sys.excepthook, | |
|
1175 | # and there's no point in printing crash dumps for every user exception. | |
|
1176 | if self.isthreaded: | |
|
1177 | ipCrashHandler = ultratb.FormattedTB() | |
|
1178 | else: | |
|
1179 | from IPython.core import crashhandler | |
|
1180 | ipCrashHandler = crashhandler.IPythonCrashHandler(self) | |
|
1181 | self.set_crash_handler(ipCrashHandler) | |
|
1170 | # The instance will store a pointer to the system-wide exception hook, | |
|
1171 | # so that runtime code (such as magics) can access it. This is because | |
|
1172 | # during the read-eval loop, it may get temporarily overwritten. | |
|
1173 | self.sys_excepthook = sys.excepthook | |
|
1182 | 1174 | |
|
1183 | 1175 | # and add any custom exception handlers the user may have specified |
|
1184 | 1176 | self.set_custom_exc(*custom_exceptions) |
|
1185 | 1177 | |
|
1186 | def set_crash_handler(self, crashHandler): | |
|
1187 | """Set the IPython crash handler. | |
|
1188 | ||
|
1189 | This must be a callable with a signature suitable for use as | |
|
1190 | sys.excepthook.""" | |
|
1191 | ||
|
1192 | # Install the given crash handler as the Python exception hook | |
|
1193 | sys.excepthook = crashHandler | |
|
1194 | ||
|
1195 | # The instance will store a pointer to this, so that runtime code | |
|
1196 | # (such as magics) can access it. This is because during the | |
|
1197 | # read-eval loop, it gets temporarily overwritten (to deal with GUI | |
|
1198 | # frameworks). | |
|
1199 | self.sys_excepthook = sys.excepthook | |
|
1200 | ||
|
1201 | 1178 | def set_custom_exc(self,exc_tuple,handler): |
|
1202 | 1179 | """set_custom_exc(exc_tuple,handler) |
|
1203 | 1180 | |
@@ -1880,7 +1857,7 class InteractiveShell(Component, Magic): | |||
|
1880 | 1857 | if (self.SyntaxTB.last_syntax_error and |
|
1881 | 1858 | self.autoedit_syntax): |
|
1882 | 1859 | self.edit_syntax_error() |
|
1883 | ||
|
1860 | ||
|
1884 | 1861 | # We are off again... |
|
1885 | 1862 | __builtin__.__dict__['__IPYTHON__active'] -= 1 |
|
1886 | 1863 |
@@ -77,12 +77,6 class PrefilterFrontEnd(LineFrontEndBase): | |||
|
77 | 77 | """ |
|
78 | 78 | if argv is None: |
|
79 | 79 | argv = ['--no-banner'] |
|
80 | # This is a hack to avoid the IPython exception hook to trigger | |
|
81 | # on exceptions (https://bugs.launchpad.net/bugs/337105) | |
|
82 | # XXX: This is horrible: module-leve monkey patching -> side | |
|
83 | # effects. | |
|
84 | from IPython.core import iplib | |
|
85 | iplib.InteractiveShell.isthreaded = True | |
|
86 | 80 | |
|
87 | 81 | LineFrontEndBase.__init__(self, *args, **kwargs) |
|
88 | 82 | self.shell.output_trap = RedirectorOutputTrap( |
@@ -88,9 +88,6 def isolate_ipython0(func): | |||
|
88 | 88 | del user_ns[k] |
|
89 | 89 | for k in new_globals: |
|
90 | 90 | del user_global_ns[k] |
|
91 | # Undo the hack at creation of PrefilterFrontEnd | |
|
92 | from IPython.core import iplib | |
|
93 | iplib.InteractiveShell.isthreaded = False | |
|
94 | 91 | return out |
|
95 | 92 | |
|
96 | 93 | my_func.__name__ = func.__name__ |
General Comments 0
You need to be logged in to leave comments.
Login now