##// END OF EJS Templates
Fixing tests in IPython.testing.
Brian Granger -
Show More
@@ -1,282 +1,283 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """IPython Test Suite Runner.
2 """IPython Test Suite Runner.
3
3
4 This module provides a main entry point to a user script to test IPython
4 This module provides a main entry point to a user script to test IPython
5 itself from the command line. There are two ways of running this script:
5 itself from the command line. There are two ways of running this script:
6
6
7 1. With the syntax `iptest all`. This runs our entire test suite by
7 1. With the syntax `iptest all`. This runs our entire test suite by
8 calling this script (with different arguments) or trial recursively. This
8 calling this script (with different arguments) or trial recursively. This
9 causes modules and package to be tested in different processes, using nose
9 causes modules and package to be tested in different processes, using nose
10 or trial where appropriate.
10 or trial where appropriate.
11 2. With the regular nose syntax, like `iptest -vvs IPython`. In this form
11 2. With the regular nose syntax, like `iptest -vvs IPython`. In this form
12 the script simply calls nose, but with special command line flags and
12 the script simply calls nose, but with special command line flags and
13 plugins loaded.
13 plugins loaded.
14
14
15 For now, this script requires that both nose and twisted are installed. This
15 For now, this script requires that both nose and twisted are installed. This
16 will change in the future.
16 will change in the future.
17 """
17 """
18
18
19 #-----------------------------------------------------------------------------
19 #-----------------------------------------------------------------------------
20 # Module imports
20 # Module imports
21 #-----------------------------------------------------------------------------
21 #-----------------------------------------------------------------------------
22
22
23 import os
23 import os
24 import os.path as path
24 import os.path as path
25 import sys
25 import sys
26 import subprocess
26 import subprocess
27 import time
27 import time
28 import warnings
28 import warnings
29
29
30 import nose.plugins.builtin
30 import nose.plugins.builtin
31 from nose.core import TestProgram
31 from nose.core import TestProgram
32
32
33 from IPython.platutils import find_cmd
33 from IPython.platutils import find_cmd
34 from IPython.testing.plugin.ipdoctest import IPythonDoctest
34 from IPython.testing.plugin.ipdoctest import IPythonDoctest
35
35
36 pjoin = path.join
36 pjoin = path.join
37
37
38 #-----------------------------------------------------------------------------
38 #-----------------------------------------------------------------------------
39 # Logic for skipping doctests
39 # Logic for skipping doctests
40 #-----------------------------------------------------------------------------
40 #-----------------------------------------------------------------------------
41
41
42 def test_for(mod):
42 def test_for(mod):
43 """Test to see if mod is importable."""
43 """Test to see if mod is importable."""
44 try:
44 try:
45 __import__(mod)
45 __import__(mod)
46 except ImportError:
46 except ImportError:
47 return False
47 return False
48 else:
48 else:
49 return True
49 return True
50
50
51 have_curses = test_for('_curses')
51 have_curses = test_for('_curses')
52 have_wx = test_for('wx')
52 have_wx = test_for('wx')
53 have_zi = test_for('zope.interface')
53 have_zi = test_for('zope.interface')
54 have_twisted = test_for('twisted')
54 have_twisted = test_for('twisted')
55 have_foolscap = test_for('foolscap')
55 have_foolscap = test_for('foolscap')
56 have_objc = test_for('objc')
56 have_objc = test_for('objc')
57 have_pexpect = test_for('pexpect')
57 have_pexpect = test_for('pexpect')
58
58
59 # For the IPythonDoctest plugin, we need to exclude certain patterns that cause
59 # For the IPythonDoctest plugin, we need to exclude certain patterns that cause
60 # testing problems. We should strive to minimize the number of skipped
60 # testing problems. We should strive to minimize the number of skipped
61 # modules, since this means untested code. As the testing machinery
61 # modules, since this means untested code. As the testing machinery
62 # solidifies, this list should eventually become empty.
62 # solidifies, this list should eventually become empty.
63 EXCLUDE = [pjoin('IPython', 'external'),
63 EXCLUDE = [pjoin('IPython', 'external'),
64 pjoin('IPython', 'frontend', 'process', 'winprocess.py'),
64 pjoin('IPython', 'frontend', 'process', 'winprocess.py'),
65 pjoin('IPython_doctest_plugin'),
65 pjoin('IPython_doctest_plugin'),
66 pjoin('IPython', 'Gnuplot'),
66 pjoin('IPython', 'Gnuplot'),
67 pjoin('IPython', 'Extensions', 'ipy_'),
67 pjoin('IPython', 'Extensions', 'ipy_'),
68 pjoin('IPython', 'Extensions', 'clearcmd'),
68 pjoin('IPython', 'Extensions', 'clearcmd'),
69 pjoin('IPython', 'Extensions', 'PhysicalQInteractive'),
69 pjoin('IPython', 'Extensions', 'PhysicalQInteractive'),
70 pjoin('IPython', 'Extensions', 'scitedirector'),
70 pjoin('IPython', 'Extensions', 'scitedirector'),
71 pjoin('IPython', 'Extensions', 'numeric_formats'),
71 pjoin('IPython', 'Extensions', 'numeric_formats'),
72 pjoin('IPython', 'testing', 'attic'),
72 pjoin('IPython', 'testing', 'attic'),
73 pjoin('IPython', 'testing', 'tutils')
73 pjoin('IPython', 'testing', 'tutils'),
74 pjoin('IPython', 'testing', 'tools')
74 ]
75 ]
75
76
76 if not have_wx:
77 if not have_wx:
77 EXCLUDE.append(pjoin('IPython', 'Extensions', 'igrid'))
78 EXCLUDE.append(pjoin('IPython', 'Extensions', 'igrid'))
78 EXCLUDE.append(pjoin('IPython', 'gui'))
79 EXCLUDE.append(pjoin('IPython', 'gui'))
79 EXCLUDE.append(pjoin('IPython', 'frontend', 'wx'))
80 EXCLUDE.append(pjoin('IPython', 'frontend', 'wx'))
80
81
81 if not have_objc:
82 if not have_objc:
82 EXCLUDE.append(pjoin('IPython', 'frontend', 'cocoa'))
83 EXCLUDE.append(pjoin('IPython', 'frontend', 'cocoa'))
83
84
84 if not have_curses:
85 if not have_curses:
85 EXCLUDE.append(pjoin('IPython', 'Extensions', 'ibrowse'))
86 EXCLUDE.append(pjoin('IPython', 'Extensions', 'ibrowse'))
86
87
87 if not sys.platform == 'win32':
88 if not sys.platform == 'win32':
88 EXCLUDE.append(pjoin('IPython', 'platutils_win32'))
89 EXCLUDE.append(pjoin('IPython', 'platutils_win32'))
89
90
90 if not os.name == 'posix':
91 if not os.name == 'posix':
91 EXCLUDE.append(pjoin('IPython', 'platutils_posix'))
92 EXCLUDE.append(pjoin('IPython', 'platutils_posix'))
92
93
93 if not have_pexpect:
94 if not have_pexpect:
94 EXCLUDE.append(pjoin('IPython', 'irunner'))
95 EXCLUDE.append(pjoin('IPython', 'irunner'))
95
96
96 # This is needed for the reg-exp to match on win32 in the ipdoctest plugin.
97 # This is needed for the reg-exp to match on win32 in the ipdoctest plugin.
97 if sys.platform == 'win32':
98 if sys.platform == 'win32':
98 EXCLUDE = [s.replace('\\','\\\\') for s in EXCLUDE]
99 EXCLUDE = [s.replace('\\','\\\\') for s in EXCLUDE]
99
100
100
101
101 #-----------------------------------------------------------------------------
102 #-----------------------------------------------------------------------------
102 # Functions and classes
103 # Functions and classes
103 #-----------------------------------------------------------------------------
104 #-----------------------------------------------------------------------------
104
105
105 def run_iptest():
106 def run_iptest():
106 """Run the IPython test suite using nose.
107 """Run the IPython test suite using nose.
107
108
108 This function is called when this script is **not** called with the form
109 This function is called when this script is **not** called with the form
109 `iptest all`. It simply calls nose with appropriate command line flags
110 `iptest all`. It simply calls nose with appropriate command line flags
110 and accepts all of the standard nose arguments.
111 and accepts all of the standard nose arguments.
111 """
112 """
112
113
113 warnings.filterwarnings('ignore',
114 warnings.filterwarnings('ignore',
114 'This will be removed soon. Use IPython.testing.util instead')
115 'This will be removed soon. Use IPython.testing.util instead')
115
116
116 argv = sys.argv + [
117 argv = sys.argv + [
117 # Loading ipdoctest causes problems with Twisted.
118 # Loading ipdoctest causes problems with Twisted.
118 # I am removing this as a temporary fix to get the
119 # I am removing this as a temporary fix to get the
119 # test suite back into working shape. Our nose
120 # test suite back into working shape. Our nose
120 # plugin needs to be gone through with a fine
121 # plugin needs to be gone through with a fine
121 # toothed comb to find what is causing the problem.
122 # toothed comb to find what is causing the problem.
122 '--with-ipdoctest',
123 '--with-ipdoctest',
123 '--ipdoctest-tests','--ipdoctest-extension=txt',
124 '--ipdoctest-tests','--ipdoctest-extension=txt',
124 '--detailed-errors',
125 '--detailed-errors',
125
126
126 # We add --exe because of setuptools' imbecility (it
127 # We add --exe because of setuptools' imbecility (it
127 # blindly does chmod +x on ALL files). Nose does the
128 # blindly does chmod +x on ALL files). Nose does the
128 # right thing and it tries to avoid executables,
129 # right thing and it tries to avoid executables,
129 # setuptools unfortunately forces our hand here. This
130 # setuptools unfortunately forces our hand here. This
130 # has been discussed on the distutils list and the
131 # has been discussed on the distutils list and the
131 # setuptools devs refuse to fix this problem!
132 # setuptools devs refuse to fix this problem!
132 '--exe',
133 '--exe',
133 ]
134 ]
134
135
135 # Detect if any tests were required by explicitly calling an IPython
136 # Detect if any tests were required by explicitly calling an IPython
136 # submodule or giving a specific path
137 # submodule or giving a specific path
137 has_tests = False
138 has_tests = False
138 for arg in sys.argv:
139 for arg in sys.argv:
139 if 'IPython' in arg or arg.endswith('.py') or \
140 if 'IPython' in arg or arg.endswith('.py') or \
140 (':' in arg and '.py' in arg):
141 (':' in arg and '.py' in arg):
141 has_tests = True
142 has_tests = True
142 break
143 break
143
144
144 # If nothing was specifically requested, test full IPython
145 # If nothing was specifically requested, test full IPython
145 if not has_tests:
146 if not has_tests:
146 argv.append('IPython')
147 argv.append('IPython')
147
148
148 # Construct list of plugins, omitting the existing doctest plugin, which
149 # Construct list of plugins, omitting the existing doctest plugin, which
149 # ours replaces (and extends).
150 # ours replaces (and extends).
150 plugins = [IPythonDoctest(EXCLUDE)]
151 plugins = [IPythonDoctest(EXCLUDE)]
151 for p in nose.plugins.builtin.plugins:
152 for p in nose.plugins.builtin.plugins:
152 plug = p()
153 plug = p()
153 if plug.name == 'doctest':
154 if plug.name == 'doctest':
154 continue
155 continue
155
156
156 #print '*** adding plugin:',plug.name # dbg
157 #print '*** adding plugin:',plug.name # dbg
157 plugins.append(plug)
158 plugins.append(plug)
158
159
159 TestProgram(argv=argv,plugins=plugins)
160 TestProgram(argv=argv,plugins=plugins)
160
161
161
162
162 class IPTester(object):
163 class IPTester(object):
163 """Call that calls iptest or trial in a subprocess.
164 """Call that calls iptest or trial in a subprocess.
164 """
165 """
165 def __init__(self,runner='iptest',params=None):
166 def __init__(self,runner='iptest',params=None):
166 """ """
167 """ """
167 if runner == 'iptest':
168 if runner == 'iptest':
168 self.runner = ['iptest','-v']
169 self.runner = ['iptest','-v']
169 else:
170 else:
170 self.runner = [find_cmd('trial')]
171 self.runner = [find_cmd('trial')]
171 if params is None:
172 if params is None:
172 params = []
173 params = []
173 if isinstance(params,str):
174 if isinstance(params,str):
174 params = [params]
175 params = [params]
175 self.params = params
176 self.params = params
176
177
177 # Assemble call
178 # Assemble call
178 self.call_args = self.runner+self.params
179 self.call_args = self.runner+self.params
179
180
180 def run(self):
181 def run(self):
181 """Run the stored commands"""
182 """Run the stored commands"""
182 return subprocess.call(self.call_args)
183 return subprocess.call(self.call_args)
183
184
184
185
185 def make_runners():
186 def make_runners():
186 """Define the modules and packages that need to be tested.
187 """Define the modules and packages that need to be tested.
187 """
188 """
188
189
189 # This omits additional top-level modules that should not be doctested.
190 # This omits additional top-level modules that should not be doctested.
190 # XXX: Shell.py is also ommited because of a bug in the skip_doctest
191 # XXX: Shell.py is also ommited because of a bug in the skip_doctest
191 # decorator. See ticket https://bugs.launchpad.net/bugs/366209
192 # decorator. See ticket https://bugs.launchpad.net/bugs/366209
192 top_mod = \
193 top_mod = \
193 ['background_jobs.py', 'ColorANSI.py', 'completer.py', 'ConfigLoader.py',
194 ['background_jobs.py', 'ColorANSI.py', 'completer.py', 'ConfigLoader.py',
194 'CrashHandler.py', 'Debugger.py', 'deep_reload.py', 'demo.py',
195 'CrashHandler.py', 'Debugger.py', 'deep_reload.py', 'demo.py',
195 'DPyGetOpt.py', 'dtutils.py', 'excolors.py', 'FakeModule.py',
196 'DPyGetOpt.py', 'dtutils.py', 'excolors.py', 'FakeModule.py',
196 'generics.py', 'genutils.py', 'history.py', 'hooks.py', 'ipapi.py',
197 'generics.py', 'genutils.py', 'history.py', 'hooks.py', 'ipapi.py',
197 'iplib.py', 'ipmaker.py', 'ipstruct.py', 'Itpl.py',
198 'iplib.py', 'ipmaker.py', 'ipstruct.py', 'Itpl.py',
198 'Logger.py', 'macro.py', 'Magic.py', 'OInspect.py',
199 'Logger.py', 'macro.py', 'Magic.py', 'OInspect.py',
199 'OutputTrap.py', 'platutils.py', 'prefilter.py', 'Prompts.py',
200 'OutputTrap.py', 'platutils.py', 'prefilter.py', 'Prompts.py',
200 'PyColorize.py', 'Release.py', 'rlineimpl.py', 'shadowns.py',
201 'PyColorize.py', 'Release.py', 'rlineimpl.py', 'shadowns.py',
201 'shellglobals.py', 'strdispatch.py', 'twshell.py',
202 'shellglobals.py', 'strdispatch.py', 'twshell.py',
202 'ultraTB.py', 'upgrade_dir.py', 'usage.py', 'wildcard.py',
203 'ultraTB.py', 'upgrade_dir.py', 'usage.py', 'wildcard.py',
203 # See note above for why this is skipped
204 # See note above for why this is skipped
204 # 'Shell.py',
205 # 'Shell.py',
205 'winconsole.py']
206 'winconsole.py']
206
207
207 if have_pexpect:
208 if have_pexpect:
208 top_mod.append('irunner.py')
209 top_mod.append('irunner.py')
209
210
210 # These are tested by nose, so skip IPython.kernel
211 # These are tested by nose, so skip IPython.kernel
211 top_pack = ['config','Extensions','frontend',
212 top_pack = ['config','Extensions','frontend',
212 'testing','tests','tools','UserConfig']
213 'testing','tests','tools','UserConfig']
213
214
214 if have_wx:
215 if have_wx:
215 top_pack.append('gui')
216 top_pack.append('gui')
216
217
217 modules = ['IPython.%s' % m[:-3] for m in top_mod ]
218 modules = ['IPython.%s' % m[:-3] for m in top_mod ]
218 packages = ['IPython.%s' % m for m in top_pack ]
219 packages = ['IPython.%s' % m for m in top_pack ]
219
220
220 # Make runners
221 # Make runners
221 runners = dict(zip(top_pack, [IPTester(params=v) for v in packages]))
222 runners = dict(zip(top_pack, [IPTester(params=v) for v in packages]))
222
223
223 # Test IPython.kernel using trial if twisted is installed
224 # Test IPython.kernel using trial if twisted is installed
224 if have_zi and have_twisted and have_foolscap:
225 if have_zi and have_twisted and have_foolscap:
225 runners['trial'] = IPTester('trial',['IPython'])
226 runners['trial'] = IPTester('trial',['IPython'])
226
227
227 runners['modules'] = IPTester(params=modules)
228 runners['modules'] = IPTester(params=modules)
228
229
229 return runners
230 return runners
230
231
231
232
232 def run_iptestall():
233 def run_iptestall():
233 """Run the entire IPython test suite by calling nose and trial.
234 """Run the entire IPython test suite by calling nose and trial.
234
235
235 This function constructs :class:`IPTester` instances for all IPython
236 This function constructs :class:`IPTester` instances for all IPython
236 modules and package and then runs each of them. This causes the modules
237 modules and package and then runs each of them. This causes the modules
237 and packages of IPython to be tested each in their own subprocess using
238 and packages of IPython to be tested each in their own subprocess using
238 nose or twisted.trial appropriately.
239 nose or twisted.trial appropriately.
239 """
240 """
240 runners = make_runners()
241 runners = make_runners()
241 # Run all test runners, tracking execution time
242 # Run all test runners, tracking execution time
242 failed = {}
243 failed = {}
243 t_start = time.time()
244 t_start = time.time()
244 for name,runner in runners.iteritems():
245 for name,runner in runners.iteritems():
245 print '*'*77
246 print '*'*77
246 print 'IPython test set:',name
247 print 'IPython test set:',name
247 res = runner.run()
248 res = runner.run()
248 if res:
249 if res:
249 failed[name] = res
250 failed[name] = res
250 t_end = time.time()
251 t_end = time.time()
251 t_tests = t_end - t_start
252 t_tests = t_end - t_start
252 nrunners = len(runners)
253 nrunners = len(runners)
253 nfail = len(failed)
254 nfail = len(failed)
254 # summarize results
255 # summarize results
255 print
256 print
256 print '*'*77
257 print '*'*77
257 print 'Ran %s test sets in %.3fs' % (nrunners, t_tests)
258 print 'Ran %s test sets in %.3fs' % (nrunners, t_tests)
258 print
259 print
259 if not failed:
260 if not failed:
260 print 'OK'
261 print 'OK'
261 else:
262 else:
262 # If anything went wrong, point out what command to rerun manually to
263 # If anything went wrong, point out what command to rerun manually to
263 # see the actual errors and individual summary
264 # see the actual errors and individual summary
264 print 'ERROR - %s out of %s test sets failed.' % (nfail, nrunners)
265 print 'ERROR - %s out of %s test sets failed.' % (nfail, nrunners)
265 for name in failed:
266 for name in failed:
266 failed_runner = runners[name]
267 failed_runner = runners[name]
267 print '-'*40
268 print '-'*40
268 print 'Runner failed:',name
269 print 'Runner failed:',name
269 print 'You may wish to rerun this one individually, with:'
270 print 'You may wish to rerun this one individually, with:'
270 print ' '.join(failed_runner.call_args)
271 print ' '.join(failed_runner.call_args)
271 print
272 print
272
273
273
274
274 def main():
275 def main():
275 if sys.argv[1] == 'all':
276 if sys.argv[1] == 'all':
276 run_iptestall()
277 run_iptestall()
277 else:
278 else:
278 run_iptest()
279 run_iptest()
279
280
280
281
281 if __name__ == '__main__':
282 if __name__ == '__main__':
282 main() No newline at end of file
283 main()
@@ -1,90 +1,89 b''
1 """Generic testing tools that do NOT depend on Twisted.
1 """Generic testing tools that do NOT depend on Twisted.
2
2
3 In particular, this module exposes a set of top-level assert* functions that
3 In particular, this module exposes a set of top-level assert* functions that
4 can be used in place of nose.tools.assert* in method generators (the ones in
4 can be used in place of nose.tools.assert* in method generators (the ones in
5 nose can not, at least as of nose 0.10.4).
5 nose can not, at least as of nose 0.10.4).
6
6
7 Note: our testing package contains testing.util, which does depend on Twisted
7 Note: our testing package contains testing.util, which does depend on Twisted
8 and provides utilities for tests that manage Deferreds. All testing support
8 and provides utilities for tests that manage Deferreds. All testing support
9 tools that only depend on nose, IPython or the standard library should go here
9 tools that only depend on nose, IPython or the standard library should go here
10 instead.
10 instead.
11
11
12
12
13 Authors
13 Authors
14 -------
14 -------
15 - Fernando Perez <Fernando.Perez@berkeley.edu>
15 - Fernando Perez <Fernando.Perez@berkeley.edu>
16 """
16 """
17
17
18 #*****************************************************************************
18 #*****************************************************************************
19 # Copyright (C) 2009 The IPython Development Team
19 # Copyright (C) 2009 The IPython Development Team
20 #
20 #
21 # Distributed under the terms of the BSD License. The full license is in
21 # Distributed under the terms of the BSD License. The full license is in
22 # the file COPYING, distributed as part of this software.
22 # the file COPYING, distributed as part of this software.
23 #*****************************************************************************
23 #*****************************************************************************
24
24
25 #-----------------------------------------------------------------------------
25 #-----------------------------------------------------------------------------
26 # Required modules and packages
26 # Required modules and packages
27 #-----------------------------------------------------------------------------
27 #-----------------------------------------------------------------------------
28
28
29 # Standard Python lib
30 import os
29 import os
31 import sys
30 import sys
32
31
33 # Third-party
34 import nose.tools as nt
32 import nose.tools as nt
35
33
36 # From this project
37 from IPython.tools import utils
34 from IPython.tools import utils
35 from IPython.testing import decorators as dec
38
36
39 #-----------------------------------------------------------------------------
37 #-----------------------------------------------------------------------------
40 # Globals
38 # Globals
41 #-----------------------------------------------------------------------------
39 #-----------------------------------------------------------------------------
42
40
43 # Make a bunch of nose.tools assert wrappers that can be used in test
41 # Make a bunch of nose.tools assert wrappers that can be used in test
44 # generators. This will expose an assert* function for each one in nose.tools.
42 # generators. This will expose an assert* function for each one in nose.tools.
45
43
46 _tpl = """
44 _tpl = """
47 def %(name)s(*a,**kw):
45 def %(name)s(*a,**kw):
48 return nt.%(name)s(*a,**kw)
46 return nt.%(name)s(*a,**kw)
49 """
47 """
50
48
51 for _x in [a for a in dir(nt) if a.startswith('assert')]:
49 for _x in [a for a in dir(nt) if a.startswith('assert')]:
52 exec _tpl % dict(name=_x)
50 exec _tpl % dict(name=_x)
53
51
54 #-----------------------------------------------------------------------------
52 #-----------------------------------------------------------------------------
55 # Functions and classes
53 # Functions and classes
56 #-----------------------------------------------------------------------------
54 #-----------------------------------------------------------------------------
57
55
56
58 def full_path(startPath,files):
57 def full_path(startPath,files):
59 """Make full paths for all the listed files, based on startPath.
58 """Make full paths for all the listed files, based on startPath.
60
59
61 Only the base part of startPath is kept, since this routine is typically
60 Only the base part of startPath is kept, since this routine is typically
62 used with a script's __file__ variable as startPath. The base of startPath
61 used with a script's __file__ variable as startPath. The base of startPath
63 is then prepended to all the listed files, forming the output list.
62 is then prepended to all the listed files, forming the output list.
64
63
65 Parameters
64 Parameters
66 ----------
65 ----------
67 startPath : string
66 startPath : string
68 Initial path to use as the base for the results. This path is split
67 Initial path to use as the base for the results. This path is split
69 using os.path.split() and only its first component is kept.
68 using os.path.split() and only its first component is kept.
70
69
71 files : string or list
70 files : string or list
72 One or more files.
71 One or more files.
73
72
74 Examples
73 Examples
75 --------
74 --------
76
75
77 >>> full_path('/foo/bar.py',['a.txt','b.txt'])
76 >>> full_path('/foo/bar.py',['a.txt','b.txt'])
78 ['/foo/a.txt', '/foo/b.txt']
77 ['/foo/a.txt', '/foo/b.txt']
79
78
80 >>> full_path('/foo',['a.txt','b.txt'])
79 >>> full_path('/foo',['a.txt','b.txt'])
81 ['/a.txt', '/b.txt']
80 ['/a.txt', '/b.txt']
82
81
83 If a single file is given, the output is still a list:
82 If a single file is given, the output is still a list:
84 >>> full_path('/foo','a.txt')
83 >>> full_path('/foo','a.txt')
85 ['/a.txt']
84 ['/a.txt']
86 """
85 """
87
86
88 files = utils.list_strings(files)
87 files = utils.list_strings(files)
89 base = os.path.split(startPath)[0]
88 base = os.path.split(startPath)[0]
90 return [ os.path.join(base,f) for f in files ]
89 return [ os.path.join(base,f) for f in files ]
General Comments 0
You need to be logged in to leave comments. Login now