##// END OF EJS Templates
system commands are properly interpolated in sh profile again
vivainio -
Show More
@@ -1,434 +1,434 b''
1 ''' IPython customization API
1 ''' IPython customization API
2
2
3 Your one-stop module for configuring & extending ipython
3 Your one-stop module for configuring & extending ipython
4
4
5 The API will probably break when ipython 1.0 is released, but so
5 The API will probably break when ipython 1.0 is released, but so
6 will the other configuration method (rc files).
6 will the other configuration method (rc files).
7
7
8 All names prefixed by underscores are for internal use, not part
8 All names prefixed by underscores are for internal use, not part
9 of the public api.
9 of the public api.
10
10
11 Below is an example that you can just put to a module and import from ipython.
11 Below is an example that you can just put to a module and import from ipython.
12
12
13 A good practice is to install the config script below as e.g.
13 A good practice is to install the config script below as e.g.
14
14
15 ~/.ipython/my_private_conf.py
15 ~/.ipython/my_private_conf.py
16
16
17 And do
17 And do
18
18
19 import_mod my_private_conf
19 import_mod my_private_conf
20
20
21 in ~/.ipython/ipythonrc
21 in ~/.ipython/ipythonrc
22
22
23 That way the module is imported at startup and you can have all your
23 That way the module is imported at startup and you can have all your
24 personal configuration (as opposed to boilerplate ipythonrc-PROFILENAME
24 personal configuration (as opposed to boilerplate ipythonrc-PROFILENAME
25 stuff) in there.
25 stuff) in there.
26
26
27 -----------------------------------------------
27 -----------------------------------------------
28 import IPython.ipapi
28 import IPython.ipapi
29 ip = IPython.ipapi.get()
29 ip = IPython.ipapi.get()
30
30
31 def ankka_f(self, arg):
31 def ankka_f(self, arg):
32 print "Ankka",self,"says uppercase:",arg.upper()
32 print "Ankka",self,"says uppercase:",arg.upper()
33
33
34 ip.expose_magic("ankka",ankka_f)
34 ip.expose_magic("ankka",ankka_f)
35
35
36 ip.magic('alias sayhi echo "Testing, hi ok"')
36 ip.magic('alias sayhi echo "Testing, hi ok"')
37 ip.magic('alias helloworld echo "Hello world"')
37 ip.magic('alias helloworld echo "Hello world"')
38 ip.system('pwd')
38 ip.system('pwd')
39
39
40 ip.ex('import re')
40 ip.ex('import re')
41 ip.ex("""
41 ip.ex("""
42 def funcci(a,b):
42 def funcci(a,b):
43 print a+b
43 print a+b
44 print funcci(3,4)
44 print funcci(3,4)
45 """)
45 """)
46 ip.ex("funcci(348,9)")
46 ip.ex("funcci(348,9)")
47
47
48 def jed_editor(self,filename, linenum=None):
48 def jed_editor(self,filename, linenum=None):
49 print "Calling my own editor, jed ... via hook!"
49 print "Calling my own editor, jed ... via hook!"
50 import os
50 import os
51 if linenum is None: linenum = 0
51 if linenum is None: linenum = 0
52 os.system('jed +%d %s' % (linenum, filename))
52 os.system('jed +%d %s' % (linenum, filename))
53 print "exiting jed"
53 print "exiting jed"
54
54
55 ip.set_hook('editor',jed_editor)
55 ip.set_hook('editor',jed_editor)
56
56
57 o = ip.options
57 o = ip.options
58 o.autocall = 2 # FULL autocall mode
58 o.autocall = 2 # FULL autocall mode
59
59
60 print "done!"
60 print "done!"
61 '''
61 '''
62
62
63 # stdlib imports
63 # stdlib imports
64 import __builtin__
64 import __builtin__
65 import sys
65 import sys
66
66
67 # our own
67 # our own
68 from IPython.genutils import warn,error
68 from IPython.genutils import warn,error
69
69
70 class TryNext(Exception):
70 class TryNext(Exception):
71 """Try next hook exception.
71 """Try next hook exception.
72
72
73 Raise this in your hook function to indicate that the next hook handler
73 Raise this in your hook function to indicate that the next hook handler
74 should be used to handle the operation. If you pass arguments to the
74 should be used to handle the operation. If you pass arguments to the
75 constructor those arguments will be used by the next hook instead of the
75 constructor those arguments will be used by the next hook instead of the
76 original ones.
76 original ones.
77 """
77 """
78
78
79 def __init__(self, *args, **kwargs):
79 def __init__(self, *args, **kwargs):
80 self.args = args
80 self.args = args
81 self.kwargs = kwargs
81 self.kwargs = kwargs
82
82
83 class IPyAutocall:
83 class IPyAutocall:
84 """ Instances of this class are always autocalled
84 """ Instances of this class are always autocalled
85
85
86 This happens regardless of 'autocall' variable state. Use this to
86 This happens regardless of 'autocall' variable state. Use this to
87 develop macro-like mechanisms.
87 develop macro-like mechanisms.
88 """
88 """
89
89
90 def set_ip(self,ip):
90 def set_ip(self,ip):
91 """ Will be used to set _ip point to current ipython instance b/f call
91 """ Will be used to set _ip point to current ipython instance b/f call
92
92
93 Override this method if you don't want this to happen.
93 Override this method if you don't want this to happen.
94
94
95 """
95 """
96 self._ip = ip
96 self._ip = ip
97
97
98
98
99 # contains the most recently instantiated IPApi
99 # contains the most recently instantiated IPApi
100
100
101 class IPythonNotRunning:
101 class IPythonNotRunning:
102 """Dummy do-nothing class.
102 """Dummy do-nothing class.
103
103
104 Instances of this class return a dummy attribute on all accesses, which
104 Instances of this class return a dummy attribute on all accesses, which
105 can be called and warns. This makes it easier to write scripts which use
105 can be called and warns. This makes it easier to write scripts which use
106 the ipapi.get() object for informational purposes to operate both with and
106 the ipapi.get() object for informational purposes to operate both with and
107 without ipython. Obviously code which uses the ipython object for
107 without ipython. Obviously code which uses the ipython object for
108 computations will not work, but this allows a wider range of code to
108 computations will not work, but this allows a wider range of code to
109 transparently work whether ipython is being used or not."""
109 transparently work whether ipython is being used or not."""
110
110
111 def __init__(self,warn=True):
111 def __init__(self,warn=True):
112 if warn:
112 if warn:
113 self.dummy = self._dummy_warn
113 self.dummy = self._dummy_warn
114 else:
114 else:
115 self.dummy = self._dummy_silent
115 self.dummy = self._dummy_silent
116
116
117 def __str__(self):
117 def __str__(self):
118 return "<IPythonNotRunning>"
118 return "<IPythonNotRunning>"
119
119
120 __repr__ = __str__
120 __repr__ = __str__
121
121
122 def __getattr__(self,name):
122 def __getattr__(self,name):
123 return self.dummy
123 return self.dummy
124
124
125 def _dummy_warn(self,*args,**kw):
125 def _dummy_warn(self,*args,**kw):
126 """Dummy function, which doesn't do anything but warn."""
126 """Dummy function, which doesn't do anything but warn."""
127
127
128 warn("IPython is not running, this is a dummy no-op function")
128 warn("IPython is not running, this is a dummy no-op function")
129
129
130 def _dummy_silent(self,*args,**kw):
130 def _dummy_silent(self,*args,**kw):
131 """Dummy function, which doesn't do anything and emits no warnings."""
131 """Dummy function, which doesn't do anything and emits no warnings."""
132 pass
132 pass
133
133
134 _recent = None
134 _recent = None
135
135
136
136
137 def get(allow_dummy=False,dummy_warn=True):
137 def get(allow_dummy=False,dummy_warn=True):
138 """Get an IPApi object.
138 """Get an IPApi object.
139
139
140 If allow_dummy is true, returns an instance of IPythonNotRunning
140 If allow_dummy is true, returns an instance of IPythonNotRunning
141 instead of None if not running under IPython.
141 instead of None if not running under IPython.
142
142
143 If dummy_warn is false, the dummy instance will be completely silent.
143 If dummy_warn is false, the dummy instance will be completely silent.
144
144
145 Running this should be the first thing you do when writing extensions that
145 Running this should be the first thing you do when writing extensions that
146 can be imported as normal modules. You can then direct all the
146 can be imported as normal modules. You can then direct all the
147 configuration operations against the returned object.
147 configuration operations against the returned object.
148 """
148 """
149 global _recent
149 global _recent
150 if allow_dummy and not _recent:
150 if allow_dummy and not _recent:
151 _recent = IPythonNotRunning(dummy_warn)
151 _recent = IPythonNotRunning(dummy_warn)
152 return _recent
152 return _recent
153
153
154 class IPApi:
154 class IPApi:
155 """ The actual API class for configuring IPython
155 """ The actual API class for configuring IPython
156
156
157 You should do all of the IPython configuration by getting an IPApi object
157 You should do all of the IPython configuration by getting an IPApi object
158 with IPython.ipapi.get() and using the attributes and methods of the
158 with IPython.ipapi.get() and using the attributes and methods of the
159 returned object."""
159 returned object."""
160
160
161 def __init__(self,ip):
161 def __init__(self,ip):
162
162
163 # All attributes exposed here are considered to be the public API of
163 # All attributes exposed here are considered to be the public API of
164 # IPython. As needs dictate, some of these may be wrapped as
164 # IPython. As needs dictate, some of these may be wrapped as
165 # properties.
165 # properties.
166
166
167 self.magic = ip.ipmagic
167 self.magic = ip.ipmagic
168
168
169 self.system = ip.ipsystem
169 self.system = ip.system
170
170
171 self.set_hook = ip.set_hook
171 self.set_hook = ip.set_hook
172
172
173 self.set_custom_exc = ip.set_custom_exc
173 self.set_custom_exc = ip.set_custom_exc
174
174
175 self.user_ns = ip.user_ns
175 self.user_ns = ip.user_ns
176
176
177 self.set_crash_handler = ip.set_crash_handler
177 self.set_crash_handler = ip.set_crash_handler
178
178
179 # Session-specific data store, which can be used to store
179 # Session-specific data store, which can be used to store
180 # data that should persist through the ipython session.
180 # data that should persist through the ipython session.
181 self.meta = ip.meta
181 self.meta = ip.meta
182
182
183 # The ipython instance provided
183 # The ipython instance provided
184 self.IP = ip
184 self.IP = ip
185
185
186 global _recent
186 global _recent
187 _recent = self
187 _recent = self
188
188
189 # Use a property for some things which are added to the instance very
189 # Use a property for some things which are added to the instance very
190 # late. I don't have time right now to disentangle the initialization
190 # late. I don't have time right now to disentangle the initialization
191 # order issues, so a property lets us delay item extraction while
191 # order issues, so a property lets us delay item extraction while
192 # providing a normal attribute API.
192 # providing a normal attribute API.
193 def get_db(self):
193 def get_db(self):
194 """A handle to persistent dict-like database (a PickleShareDB object)"""
194 """A handle to persistent dict-like database (a PickleShareDB object)"""
195 return self.IP.db
195 return self.IP.db
196
196
197 db = property(get_db,None,None,get_db.__doc__)
197 db = property(get_db,None,None,get_db.__doc__)
198
198
199 def get_options(self):
199 def get_options(self):
200 """All configurable variables."""
200 """All configurable variables."""
201
201
202 # catch typos by disabling new attribute creation. If new attr creation
202 # catch typos by disabling new attribute creation. If new attr creation
203 # is in fact wanted (e.g. when exposing new options), do allow_new_attr(True)
203 # is in fact wanted (e.g. when exposing new options), do allow_new_attr(True)
204 # for the received rc struct.
204 # for the received rc struct.
205
205
206 self.IP.rc.allow_new_attr(False)
206 self.IP.rc.allow_new_attr(False)
207 return self.IP.rc
207 return self.IP.rc
208
208
209 options = property(get_options,None,None,get_options.__doc__)
209 options = property(get_options,None,None,get_options.__doc__)
210
210
211 def expose_magic(self,magicname, func):
211 def expose_magic(self,magicname, func):
212 ''' Expose own function as magic function for ipython
212 ''' Expose own function as magic function for ipython
213
213
214 def foo_impl(self,parameter_s=''):
214 def foo_impl(self,parameter_s=''):
215 """My very own magic!. (Use docstrings, IPython reads them)."""
215 """My very own magic!. (Use docstrings, IPython reads them)."""
216 print 'Magic function. Passed parameter is between < >: <'+parameter_s+'>'
216 print 'Magic function. Passed parameter is between < >: <'+parameter_s+'>'
217 print 'The self object is:',self
217 print 'The self object is:',self
218
218
219 ipapi.expose_magic("foo",foo_impl)
219 ipapi.expose_magic("foo",foo_impl)
220 '''
220 '''
221
221
222 import new
222 import new
223 im = new.instancemethod(func,self.IP, self.IP.__class__)
223 im = new.instancemethod(func,self.IP, self.IP.__class__)
224 setattr(self.IP, "magic_" + magicname, im)
224 setattr(self.IP, "magic_" + magicname, im)
225
225
226 def ex(self,cmd):
226 def ex(self,cmd):
227 """ Execute a normal python statement in user namespace """
227 """ Execute a normal python statement in user namespace """
228 exec cmd in self.user_ns
228 exec cmd in self.user_ns
229
229
230 def ev(self,expr):
230 def ev(self,expr):
231 """ Evaluate python expression expr in user namespace
231 """ Evaluate python expression expr in user namespace
232
232
233 Returns the result of evaluation"""
233 Returns the result of evaluation"""
234 return eval(expr,self.user_ns)
234 return eval(expr,self.user_ns)
235
235
236 def runlines(self,lines):
236 def runlines(self,lines):
237 """ Run the specified lines in interpreter, honoring ipython directives.
237 """ Run the specified lines in interpreter, honoring ipython directives.
238
238
239 This allows %magic and !shell escape notations.
239 This allows %magic and !shell escape notations.
240
240
241 Takes either all lines in one string or list of lines.
241 Takes either all lines in one string or list of lines.
242 """
242 """
243 if isinstance(lines,basestring):
243 if isinstance(lines,basestring):
244 self.IP.runlines(lines)
244 self.IP.runlines(lines)
245 else:
245 else:
246 self.IP.runlines('\n'.join(lines))
246 self.IP.runlines('\n'.join(lines))
247
247
248 def to_user_ns(self,vars):
248 def to_user_ns(self,vars):
249 """Inject a group of variables into the IPython user namespace.
249 """Inject a group of variables into the IPython user namespace.
250
250
251 Inputs:
251 Inputs:
252
252
253 - vars: string with variable names separated by whitespace
253 - vars: string with variable names separated by whitespace
254
254
255 This utility routine is meant to ease interactive debugging work,
255 This utility routine is meant to ease interactive debugging work,
256 where you want to easily propagate some internal variable in your code
256 where you want to easily propagate some internal variable in your code
257 up to the interactive namespace for further exploration.
257 up to the interactive namespace for further exploration.
258
258
259 When you run code via %run, globals in your script become visible at
259 When you run code via %run, globals in your script become visible at
260 the interactive prompt, but this doesn't happen for locals inside your
260 the interactive prompt, but this doesn't happen for locals inside your
261 own functions and methods. Yet when debugging, it is common to want
261 own functions and methods. Yet when debugging, it is common to want
262 to explore some internal variables further at the interactive propmt.
262 to explore some internal variables further at the interactive propmt.
263
263
264 Examples:
264 Examples:
265
265
266 To use this, you first must obtain a handle on the ipython object as
266 To use this, you first must obtain a handle on the ipython object as
267 indicated above, via:
267 indicated above, via:
268
268
269 import IPython.ipapi
269 import IPython.ipapi
270 ip = IPython.ipapi.get()
270 ip = IPython.ipapi.get()
271
271
272 Once this is done, inside a routine foo() where you want to expose
272 Once this is done, inside a routine foo() where you want to expose
273 variables x and y, you do the following:
273 variables x and y, you do the following:
274
274
275 def foo():
275 def foo():
276 ...
276 ...
277 x = your_computation()
277 x = your_computation()
278 y = something_else()
278 y = something_else()
279
279
280 # This pushes x and y to the interactive prompt immediately, even
280 # This pushes x and y to the interactive prompt immediately, even
281 # if this routine crashes on the next line after:
281 # if this routine crashes on the next line after:
282 ip.to_user_ns('x y')
282 ip.to_user_ns('x y')
283 ...
283 ...
284 # return
284 # return
285
285
286 If you need to rename variables, just use ip.user_ns with dict
286 If you need to rename variables, just use ip.user_ns with dict
287 and update:
287 and update:
288
288
289 # exposes variables 'foo' as 'x' and 'bar' as 'y' in IPython
289 # exposes variables 'foo' as 'x' and 'bar' as 'y' in IPython
290 # user namespace
290 # user namespace
291 ip.user_ns.update(dict(x=foo,y=bar))
291 ip.user_ns.update(dict(x=foo,y=bar))
292 """
292 """
293
293
294 # print 'vars given:',vars # dbg
294 # print 'vars given:',vars # dbg
295 # Get the caller's frame to evaluate the given names in
295 # Get the caller's frame to evaluate the given names in
296 cf = sys._getframe(1)
296 cf = sys._getframe(1)
297
297
298 user_ns = self.user_ns
298 user_ns = self.user_ns
299
299
300 for name in vars.split():
300 for name in vars.split():
301 try:
301 try:
302 user_ns[name] = eval(name,cf.f_globals,cf.f_locals)
302 user_ns[name] = eval(name,cf.f_globals,cf.f_locals)
303 except:
303 except:
304 error('could not get var. %s from %s' %
304 error('could not get var. %s from %s' %
305 (name,cf.f_code.co_name))
305 (name,cf.f_code.co_name))
306
306
307 def expand_alias(self,line):
307 def expand_alias(self,line):
308 """ Expand an alias in the command line
308 """ Expand an alias in the command line
309
309
310 Returns the provided command line, possibly with the first word
310 Returns the provided command line, possibly with the first word
311 (command) translated according to alias expansion rules.
311 (command) translated according to alias expansion rules.
312
312
313 [ipython]|16> _ip.expand_aliases("np myfile.txt")
313 [ipython]|16> _ip.expand_aliases("np myfile.txt")
314 <16> 'q:/opt/np/notepad++.exe myfile.txt'
314 <16> 'q:/opt/np/notepad++.exe myfile.txt'
315 """
315 """
316
316
317 pre,fn,rest = self.IP.split_user_input(line)
317 pre,fn,rest = self.IP.split_user_input(line)
318 res = pre + self.IP.expand_aliases(fn,rest)
318 res = pre + self.IP.expand_aliases(fn,rest)
319 return res
319 return res
320
320
321 def defalias(self, name, cmd):
321 def defalias(self, name, cmd):
322 """ Define a new alias
322 """ Define a new alias
323
323
324 _ip.defalias('bb','bldmake bldfiles')
324 _ip.defalias('bb','bldmake bldfiles')
325
325
326 Creates a new alias named 'bb' in ipython user namespace
326 Creates a new alias named 'bb' in ipython user namespace
327 """
327 """
328
328
329
329
330 nargs = cmd.count('%s')
330 nargs = cmd.count('%s')
331 if nargs>0 and cmd.find('%l')>=0:
331 if nargs>0 and cmd.find('%l')>=0:
332 raise Exception('The %s and %l specifiers are mutually exclusive '
332 raise Exception('The %s and %l specifiers are mutually exclusive '
333 'in alias definitions.')
333 'in alias definitions.')
334
334
335 else: # all looks OK
335 else: # all looks OK
336 self.IP.alias_table[name] = (nargs,cmd)
336 self.IP.alias_table[name] = (nargs,cmd)
337
337
338 def defmacro(self, *args):
338 def defmacro(self, *args):
339 """ Define a new macro
339 """ Define a new macro
340
340
341 2 forms of calling:
341 2 forms of calling:
342
342
343 mac = _ip.defmacro('print "hello"\nprint "world"')
343 mac = _ip.defmacro('print "hello"\nprint "world"')
344
344
345 (doesn't put the created macro on user namespace)
345 (doesn't put the created macro on user namespace)
346
346
347 _ip.defmacro('build', 'bldmake bldfiles\nabld build winscw udeb')
347 _ip.defmacro('build', 'bldmake bldfiles\nabld build winscw udeb')
348
348
349 (creates a macro named 'build' in user namespace)
349 (creates a macro named 'build' in user namespace)
350 """
350 """
351
351
352 import IPython.macro
352 import IPython.macro
353
353
354 if len(args) == 1:
354 if len(args) == 1:
355 return IPython.macro.Macro(args[0])
355 return IPython.macro.Macro(args[0])
356 elif len(args) == 2:
356 elif len(args) == 2:
357 self.user_ns[args[0]] = IPython.macro.Macro(args[1])
357 self.user_ns[args[0]] = IPython.macro.Macro(args[1])
358 else:
358 else:
359 return Exception("_ip.defmacro must be called with 1 or 2 arguments")
359 return Exception("_ip.defmacro must be called with 1 or 2 arguments")
360
360
361 def set_next_input(self, s):
361 def set_next_input(self, s):
362 """ Sets the 'default' input string for the next command line.
362 """ Sets the 'default' input string for the next command line.
363
363
364 Requires readline.
364 Requires readline.
365
365
366 Example:
366 Example:
367
367
368 [D:\ipython]|1> _ip.set_next_input("Hello Word")
368 [D:\ipython]|1> _ip.set_next_input("Hello Word")
369 [D:\ipython]|2> Hello Word_ # cursor is here
369 [D:\ipython]|2> Hello Word_ # cursor is here
370 """
370 """
371
371
372 self.IP.rl_next_input = s
372 self.IP.rl_next_input = s
373
373
374
374
375 def launch_new_instance(user_ns = None):
375 def launch_new_instance(user_ns = None):
376 """ Make and start a new ipython instance.
376 """ Make and start a new ipython instance.
377
377
378 This can be called even without having an already initialized
378 This can be called even without having an already initialized
379 ipython session running.
379 ipython session running.
380
380
381 This is also used as the egg entry point for the 'ipython' script.
381 This is also used as the egg entry point for the 'ipython' script.
382
382
383 """
383 """
384 ses = make_session(user_ns)
384 ses = make_session(user_ns)
385 ses.mainloop()
385 ses.mainloop()
386
386
387
387
388 def make_user_ns(user_ns = None):
388 def make_user_ns(user_ns = None):
389 """Return a valid user interactive namespace.
389 """Return a valid user interactive namespace.
390
390
391 This builds a dict with the minimal information needed to operate as a
391 This builds a dict with the minimal information needed to operate as a
392 valid IPython user namespace, which you can pass to the various embedding
392 valid IPython user namespace, which you can pass to the various embedding
393 classes in ipython.
393 classes in ipython.
394 """
394 """
395
395
396 if user_ns is None:
396 if user_ns is None:
397 # Set __name__ to __main__ to better match the behavior of the
397 # Set __name__ to __main__ to better match the behavior of the
398 # normal interpreter.
398 # normal interpreter.
399 user_ns = {'__name__' :'__main__',
399 user_ns = {'__name__' :'__main__',
400 '__builtins__' : __builtin__,
400 '__builtins__' : __builtin__,
401 }
401 }
402 else:
402 else:
403 user_ns.setdefault('__name__','__main__')
403 user_ns.setdefault('__name__','__main__')
404 user_ns.setdefault('__builtins__',__builtin__)
404 user_ns.setdefault('__builtins__',__builtin__)
405
405
406 return user_ns
406 return user_ns
407
407
408
408
409 def make_user_global_ns(ns = None):
409 def make_user_global_ns(ns = None):
410 """Return a valid user global namespace.
410 """Return a valid user global namespace.
411
411
412 Similar to make_user_ns(), but global namespaces are really only needed in
412 Similar to make_user_ns(), but global namespaces are really only needed in
413 embedded applications, where there is a distinction between the user's
413 embedded applications, where there is a distinction between the user's
414 interactive namespace and the global one where ipython is running."""
414 interactive namespace and the global one where ipython is running."""
415
415
416 if ns is None: ns = {}
416 if ns is None: ns = {}
417 return ns
417 return ns
418
418
419
419
420 def make_session(user_ns = None):
420 def make_session(user_ns = None):
421 """Makes, but does not launch an IPython session.
421 """Makes, but does not launch an IPython session.
422
422
423 Later on you can call obj.mainloop() on the returned object.
423 Later on you can call obj.mainloop() on the returned object.
424
424
425 Inputs:
425 Inputs:
426
426
427 - user_ns(None): a dict to be used as the user's namespace with initial
427 - user_ns(None): a dict to be used as the user's namespace with initial
428 data.
428 data.
429
429
430 WARNING: This should *not* be run when a session exists already."""
430 WARNING: This should *not* be run when a session exists already."""
431
431
432 import IPython
432 import IPython
433 return IPython.Shell.start(user_ns)
433 return IPython.Shell.start(user_ns)
434
434
General Comments 0
You need to be logged in to leave comments. Login now