Coverage for tests/test_argumentParser.py: 18%
Shortcuts on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#
2# LSST Data Management System
3# Copyright 2008-2015 AURA/LSST.
4#
5# This product includes software developed by the
6# LSST Project (http://www.lsst.org/).
7#
8# This program is free software: you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published by
10# the Free Software Foundation, either version 3 of the License, or
11# (at your option) any later version.
12#
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the LSST License Statement and
19# the GNU General Public License along with this program. If not,
20# see <https://www.lsstcorp.org/LegalNotices/>.
21#
22import itertools
23import os
24import shutil
25import tempfile
26import unittest
27import contextlib
29import lsst.utils
30import lsst.utils.tests
31import lsst.pex.config as pexConfig
32import lsst.pipe.base as pipeBase
33from lsst.pipe.base.task_logging import getTaskLogger
35ObsTestDir = lsst.utils.getPackageDir("obs_test")
36DataPath = os.path.realpath(os.path.join(ObsTestDir, "data", "input"))
37LocalDataPath = os.path.join(os.path.dirname(__file__), "data")
40@contextlib.contextmanager
41def capture():
42 """Context manager to intercept stdout/err
44 Use as:
45 with capture() as out:
46 print 'hi'
48 http://stackoverflow.com/questions/5136611/capture-stdout-from-a-script-in-python
49 """
50 import sys
51 from io import StringIO
52 oldout, olderr = sys.stdout, sys.stderr
53 try:
54 out = [StringIO(), StringIO()]
55 sys.stdout, sys.stderr = out
56 yield out
57 finally:
58 sys.stdout, sys.stderr = oldout, olderr
59 out[0] = out[0].getvalue()
60 out[1] = out[1].getvalue()
63class SubConfig(pexConfig.Config):
64 intItem = pexConfig.Field(doc="sample int field", dtype=int, default=8)
67class SampleConfig(pexConfig.Config):
68 boolItem = pexConfig.Field(doc="sample bool field", dtype=bool, default=True)
69 floatItem = pexConfig.Field(doc="sample float field", dtype=float, default=3.1)
70 strItem = pexConfig.Field(doc="sample str field", dtype=str, default="strDefault")
71 subItem = pexConfig.ConfigField(doc="sample subfield", dtype=SubConfig)
72 multiDocItem = pexConfig.Field(doc="1. sample... \n#2...multiline \n##3...#\n###4...docstring",
73 dtype=str, default="multiLineDoc")
74 dsType = pexConfig.Field(doc="dataset type for --id argument", dtype=str, default="calexp")
75 dsTypeNoDefault = pexConfig.Field(doc="dataset type for --id argument; no default", dtype=str,
76 optional=True)
79class ArgumentParserTestCase(unittest.TestCase):
80 """A test case for ArgumentParser."""
82 def setUp(self):
83 self.ap = pipeBase.InputOnlyArgumentParser(name="argumentParser")
84 self.ap.add_id_argument("--id", "raw", "help text")
85 self.ap.add_id_argument("--otherId", "raw", "more help")
86 self.config = SampleConfig()
87 os.environ.pop("PIPE_INPUT_ROOT", None)
88 os.environ.pop("PIPE_CALIB_ROOT", None)
89 os.environ.pop("PIPE_OUTPUT_ROOT", None)
91 def tearDown(self):
92 del self.ap
93 del self.config
95 def testBasicId(self):
96 """Test --id basics"""
97 namespace = self.ap.parse_args(
98 config=self.config,
99 args=[DataPath, "--id", "visit=1", "filter=g"],
100 )
101 self.assertEqual(len(namespace.id.idList), 1)
102 self.assertEqual(len(namespace.id.refList), 1)
104 namespace = self.ap.parse_args(
105 config=self.config,
106 args=[DataPath, "--id", "visit=22", "filter=g"],
107 )
108 self.assertEqual(len(namespace.id.idList), 1)
109 self.assertEqual(len(namespace.id.refList), 0) # no data for this ID
111 def testOtherId(self):
112 """Test --other"""
113 # By itself
114 namespace = self.ap.parse_args(
115 config=self.config,
116 args=[DataPath, "--other", "visit=99"],
117 )
118 self.assertEqual(len(namespace.otherId.idList), 1)
119 self.assertEqual(len(namespace.otherId.refList), 0)
121 # And together
122 namespace = self.ap.parse_args(
123 config=self.config,
124 args=[DataPath, "--id", "visit=1",
125 "--other", "visit=99"],
126 )
127 self.assertEqual(len(namespace.id.idList), 1)
128 self.assertEqual(len(namespace.id.refList), 1)
129 self.assertEqual(len(namespace.otherId.idList), 1)
130 self.assertEqual(len(namespace.otherId.refList), 0) # no data for this ID
132 def testIdCross(self):
133 """Test --id cross product, including order"""
134 visitList = (1, 2, 3)
135 filterList = ("g", "r")
136 namespace = self.ap.parse_args(
137 config=self.config,
138 args=[
139 DataPath,
140 "--id",
141 "filter=%s" % ("^".join(filterList),),
142 "visit=%s" % ("^".join(str(visit) for visit in visitList),),
143 ]
144 )
145 self.assertEqual(len(namespace.id.idList), 6)
146 predValList = itertools.product(filterList, visitList)
147 for id, predVal in zip(namespace.id.idList, predValList):
148 idVal = tuple(id[key] for key in ("filter", "visit"))
149 self.assertEqual(idVal, predVal)
150 self.assertEqual(len(namespace.id.refList), 3) # only have data for three of these
152 def testIdDuplicate(self):
153 """Verify that each ID name can only appear once in a given ID argument"""
154 with self.assertRaises(SystemExit):
155 self.ap.parse_args(config=self.config,
156 args=[DataPath, "--id", "visit=1", "visit=2"],
157 )
159 def testConfigBasics(self):
160 """Test --config"""
161 namespace = self.ap.parse_args(
162 config=self.config,
163 args=[DataPath, "--config", "boolItem=False", "floatItem=-67.1",
164 "strItem=overridden value", "subItem.intItem=5", "multiDocItem=edited value"],
165 )
166 self.assertEqual(namespace.config.boolItem, False)
167 self.assertEqual(namespace.config.floatItem, -67.1)
168 self.assertEqual(namespace.config.strItem, "overridden value")
169 self.assertEqual(namespace.config.subItem.intItem, 5)
170 self.assertEqual(namespace.config.multiDocItem, "edited value")
172 def testConfigLeftToRight(self):
173 """Verify that order of overriding config values is left to right"""
174 namespace = self.ap.parse_args(
175 config=self.config,
176 args=[DataPath,
177 "--config", "floatItem=-67.1", "strItem=overridden value",
178 "--config", "strItem=final value"],
179 )
180 self.assertEqual(namespace.config.floatItem, -67.1)
181 self.assertEqual(namespace.config.strItem, "final value")
183 def testConfigWrongNames(self):
184 """Verify that incorrect names for config fields are caught"""
185 with self.assertRaises(SystemExit):
186 self.ap.parse_args(config=self.config,
187 args=[DataPath, "--config", "missingItem=-67.1"],
188 )
190 def testShow(self):
191 """Test --show"""
192 with capture() as out:
193 self.ap.parse_args(
194 config=self.config,
195 args=[DataPath, "--show", "config", "data", "tasks", "run"],
196 )
197 res = out[0]
198 self.assertIn("config.floatItem", res)
199 self.assertIn("config.subItem", res)
200 self.assertIn("config.boolItem", res)
201 self.assertIn("config.strItem", res)
202 self.assertIn("config.multiDocItem", res)
203 # Test show with exact config name and with one sided, embedded, and two sided globs
204 for configStr in ("config.multiDocItem", "*ultiDocItem", "*ulti*Item", "*ultiDocI*"):
205 with capture() as out:
206 self.ap.parse_args(self.config, [DataPath, "--show", "config=" + configStr, "run"])
207 res = out[0]
208 self.assertIn("config.multiDocItem", res)
210 with self.assertRaises(SystemExit):
211 self.ap.parse_args(config=self.config,
212 args=[DataPath, "--show", "config"],
213 )
214 with self.assertRaises(SystemExit):
215 self.ap.parse_args(config=self.config,
216 args=[DataPath, "--show", "config=X"],
217 )
219 # Test show with glob for single and multi-line doc strings
220 for configStr, assertStr in ("*strItem*", "strDefault"), ("*multiDocItem", "multiLineDoc"):
221 with capture() as out:
222 self.ap.parse_args(self.config, [DataPath, "--show", "config=" + configStr, "run"])
223 stdout = out[0]
224 stdoutList = stdout.rstrip().split("\n")
225 self.assertGreater(len(stdoutList), 2) # at least 2 docstring lines (1st line is always a \n)
226 # and 1 config parameter
227 answer = [ans for ans in stdoutList if not ans.startswith("#")] # get rid of comment lines
228 answer = [ans for ans in answer if ":NOIGNORECASE to prevent this" not in ans]
229 self.assertEqual(len(answer), 2) # only 1 config item matches (+ 1 \n entry per doc string)
230 self.assertIn(assertStr, answer[1])
232 with self.assertRaises(SystemExit):
233 self.ap.parse_args(config=self.config,
234 args=[DataPath, "--show", "badname", "run"],
235 )
236 with self.assertRaises(SystemExit):
237 self.ap.parse_args(config=self.config,
238 args=[DataPath, "--show"], # no --show arguments
239 )
241 # Test show with and without case sensitivity
242 for configStr, shouldMatch in [("*multiDocItem*", True),
243 ("*multidocitem*", True),
244 ("*multidocitem*:NOIGNORECASE", False)]:
245 with capture() as out:
246 self.ap.parse_args(self.config, [DataPath, "--show", "config=" + configStr, "run"])
247 res = out[0]
249 if shouldMatch:
250 self.assertIn("config.multiDocItem", res)
251 else:
252 self.assertNotIn("config.multiDocItem", res)
254 def testConfigFileBasics(self):
255 """Test --configfile"""
256 configFilePath = os.path.join(LocalDataPath, "argumentParserConfig.py")
257 namespace = self.ap.parse_args(
258 config=self.config,
259 args=[DataPath, "--configfile", configFilePath],
260 )
261 self.assertEqual(namespace.config.floatItem, -9e9)
262 self.assertEqual(namespace.config.strItem, "set in override file")
264 def testConfigFileLeftRight(self):
265 """Verify that order of setting values is with a mix of config file and config is left to right"""
266 configFilePath = os.path.join(LocalDataPath, "argumentParserConfig.py")
267 namespace = self.ap.parse_args(
268 config=self.config,
269 args=[DataPath,
270 "--config", "floatItem=5.5",
271 "--configfile", configFilePath,
272 "--config", "strItem=value from cmd line"],
273 )
274 self.assertEqual(namespace.config.floatItem, -9e9)
275 self.assertEqual(namespace.config.strItem, "value from cmd line")
277 def testConfigFileMissingFiles(self):
278 """Verify that missing config override files are caught"""
279 with self.assertRaises(SystemExit):
280 self.ap.parse_args(config=self.config,
281 args=[DataPath, "--configfile", "missingFile"],
282 )
284 def testAtFile(self):
285 """Test @file"""
286 argPath = os.path.join(LocalDataPath, "args.txt")
287 namespace = self.ap.parse_args(
288 config=self.config,
289 args=[DataPath, "@%s" % (argPath,)],
290 )
291 self.assertEqual(len(namespace.id.idList), 1)
292 self.assertEqual(namespace.config.floatItem, 4.7)
293 self.assertEqual(namespace.config.strItem, "new value")
295 def testLogLevel(self):
296 """Test --loglevel"""
297 for logLevel in ("trace", "debug", "Info", "WARN", "eRRoR", "fatal"):
298 namespace = self.ap.parse_args(
299 config=self.config,
300 args=[DataPath, "--loglevel", logLevel],
301 )
302 intLevel = getattr(namespace.log, logLevel.upper())
303 print("testing logLevel=%r" % (logLevel,))
304 self.assertEqual(namespace.log.getEffectiveLevel(), intLevel)
305 self.assertFalse(hasattr(namespace, "loglevel"))
307 bazLevel = "TRACE"
308 namespace = self.ap.parse_args(
309 config=self.config,
310 args=[DataPath, "--loglevel", logLevel,
311 "foo.bar=%s" % (logLevel,),
312 "baz=INFO",
313 "baz=%s" % bazLevel, # test that later values override earlier values
314 ],
315 )
316 self.assertEqual(namespace.log.getEffectiveLevel(), intLevel)
317 self.assertEqual(getTaskLogger("foo.bar").getEffectiveLevel(), intLevel)
318 self.assertEqual(getTaskLogger("baz").getEffectiveLevel(),
319 getattr(getTaskLogger(), bazLevel))
321 with self.assertRaises(SystemExit):
322 self.ap.parse_args(config=self.config,
323 args=[DataPath, "--loglevel", "1234"],
324 )
326 with self.assertRaises(SystemExit):
327 self.ap.parse_args(config=self.config,
328 args=[DataPath, "--loglevel", "INVALID_LEVEL"],
329 )
331 def testPipeVars(self):
332 """Test handling of $PIPE_x_ROOT environment variables, where x is INPUT, CALIB or OUTPUT
333 """
334 os.environ["PIPE_INPUT_ROOT"] = DataPath
335 namespace = self.ap.parse_args(
336 config=self.config,
337 args=["."],
338 )
339 self.assertEqual(namespace.input, os.path.abspath(DataPath))
340 self.assertEqual(namespace.calib, None)
342 os.environ["PIPE_CALIB_ROOT"] = DataPath
343 namespace = self.ap.parse_args(
344 config=self.config,
345 args=["."],
346 )
347 self.assertEqual(namespace.input, os.path.abspath(DataPath))
348 self.assertEqual(namespace.calib, os.path.abspath(DataPath))
350 os.environ.pop("PIPE_CALIB_ROOT", None)
351 os.environ["PIPE_OUTPUT_ROOT"] = DataPath
352 namespace = self.ap.parse_args(
353 config=self.config,
354 args=["."],
355 )
356 self.assertEqual(namespace.input, os.path.abspath(DataPath))
357 self.assertEqual(namespace.calib, None)
358 self.assertEqual(namespace.output, None)
360 def testBareHelp(self):
361 """Make sure bare help does not print an error message (ticket #3090)
362 """
363 for helpArg in ("-h", "--help"):
364 try:
365 self.ap.parse_args(
366 config=self.config,
367 args=[helpArg],
368 )
369 self.fail("should have raised SystemExit")
370 except SystemExit as e:
371 self.assertEqual(e.code, 0)
373 def testDatasetArgumentBasics(self):
374 """Test DatasetArgument basics"""
375 dsTypeHelp = "help text for dataset argument"
376 for name in (None, "--foo"):
377 for default in (None, "raw"):
378 argName = name if name is not None else "--id_dstype"
379 ap = pipeBase.InputOnlyArgumentParser(name="argumentParser")
380 dsType = pipeBase.DatasetArgument(name=name, help=dsTypeHelp, default=default)
381 self.assertEqual(dsType.help, dsTypeHelp)
383 ap.add_id_argument("--id", dsType, "help text")
384 namespace = ap.parse_args(
385 config=self.config,
386 args=[DataPath,
387 argName, "calexp",
388 "--id", "visit=2",
389 ],
390 )
391 self.assertEqual(namespace.id.datasetType, "calexp")
392 self.assertEqual(len(namespace.id.idList), 1)
394 del namespace
396 if default is None:
397 # make sure dataset type argument is required
398 with self.assertRaises(SystemExit):
399 ap.parse_args(
400 config=self.config,
401 args=[DataPath,
402 "--id", "visit=2",
403 ],
404 )
405 else:
406 namespace = ap.parse_args(
407 config=self.config,
408 args=[DataPath,
409 "--id", "visit=2",
410 ],
411 )
412 self.assertEqual(namespace.id.datasetType, default)
413 self.assertEqual(len(namespace.id.idList), 1)
415 def testDatasetArgumentPositional(self):
416 """Test DatasetArgument with a positional argument"""
417 name = "foo"
418 defaultDsTypeHelp = "dataset type to process from input data repository"
419 ap = pipeBase.InputOnlyArgumentParser(name="argumentParser")
420 dsType = pipeBase.DatasetArgument(name=name)
421 self.assertEqual(dsType.help, defaultDsTypeHelp)
423 ap.add_id_argument("--id", dsType, "help text")
424 namespace = ap.parse_args(
425 config=self.config,
426 args=[DataPath,
427 "calexp",
428 "--id", "visit=2",
429 ],
430 )
431 self.assertEqual(namespace.id.datasetType, "calexp")
432 self.assertEqual(len(namespace.id.idList), 1)
434 # make sure dataset type argument is required
435 with self.assertRaises(SystemExit):
436 ap.parse_args(
437 config=self.config,
438 args=[DataPath,
439 "--id", "visit=2",
440 ],
441 )
443 def testConfigDatasetTypeFieldDefault(self):
444 """Test ConfigDatasetType with a config field that has a default value"""
445 # default value for config field "dsType" is "calexp";
446 # use a different value as the default for the ConfigDatasetType
447 # so the test can tell the difference
448 name = "dsType"
449 ap = pipeBase.InputOnlyArgumentParser(name="argumentParser")
450 dsType = pipeBase.ConfigDatasetType(name=name)
452 ap.add_id_argument("--id", dsType, "help text")
453 namespace = ap.parse_args(
454 config=self.config,
455 args=[DataPath,
456 "--id", "visit=2",
457 ],
458 )
459 self.assertEqual(namespace.id.datasetType, "calexp") # default of config field dsType
460 self.assertEqual(len(namespace.id.idList), 1)
462 def testConfigDatasetTypeNoFieldDefault(self):
463 """Test ConfigDatasetType with a config field that has no default value"""
464 name = "dsTypeNoDefault"
465 ap = pipeBase.InputOnlyArgumentParser(name="argumentParser")
466 dsType = pipeBase.ConfigDatasetType(name=name)
468 ap.add_id_argument("--id", dsType, "help text")
469 # neither the argument nor the config field has a default,
470 # so the user must specify the argument (or specify doMakeDataRefList=False
471 # and post-process the ID list)
472 with self.assertRaises(RuntimeError):
473 ap.parse_args(
474 config=self.config,
475 args=[DataPath,
476 "--id", "visit=2",
477 ],
478 )
480 def testOutputs(self):
481 """Test output directories, specified in different ways"""
482 parser = pipeBase.ArgumentParser(name="argumentParser")
483 self.assertTrue(parser.requireOutput)
485 # Location of our working repository
486 # We'll start by creating this, then use it as the basis for further tests
487 # It's removed at the end of the try/finally block below
488 repositoryPath = tempfile.mkdtemp()
489 try:
490 # Given input at DataPath, demonstrate that we can create a new
491 # output repository at repositoryPath
492 args = parser.parse_args(config=self.config, args=[DataPath, "--output", repositoryPath])
493 self.assertEqual(args.input, DataPath)
494 self.assertEqual(args.output, repositoryPath)
495 self.assertIsNone(args.rerun)
497 # Now based on our new output repository, demonstrate that we can create a rerun at rerun/foo
498 args = parser.parse_args(config=self.config, args=[repositoryPath, "--rerun", "foo"])
499 self.assertEqual(args.input, repositoryPath)
500 self.assertEqual(args.output, os.path.join(repositoryPath, "rerun", "foo"))
501 self.assertEqual(args.rerun, ["foo"])
503 # Now check that that we can chain the above rerun into another
504 args = parser.parse_args(config=self.config, args=[repositoryPath, "--rerun", "foo:bar"])
505 self.assertEqual(args.input, os.path.join(repositoryPath, "rerun", "foo"))
506 self.assertEqual(args.output, os.path.join(repositoryPath, "rerun", "bar"))
507 self.assertEqual(args.rerun, ["foo", "bar"])
509 # Finally, check that the above also works if the rerun directory already exists
510 rerunPath = tempfile.mkdtemp(dir=os.path.join(repositoryPath, "rerun"))
511 rerun = os.path.basename(rerunPath)
512 try:
513 args = parser.parse_args(config=self.config, args=[repositoryPath, "--rerun", rerun])
514 self.assertEqual(args.input, repositoryPath)
515 self.assertEqual(args.output, os.path.join(repositoryPath, "rerun", rerun))
516 self.assertEqual(args.rerun, [rerun])
517 finally:
518 shutil.rmtree(rerunPath)
520 finally:
521 shutil.rmtree(repositoryPath)
523 # Finally, check that we raise an appropriate error if we don't specify an output location at all
524 with self.assertRaises(SystemExit):
525 parser.parse_args(config=self.config, args=[DataPath, ])
527 def testReuseOption(self):
528 self.ap.addReuseOption(["a", "b", "c"])
529 namespace = self.ap.parse_args(
530 config=self.config,
531 args=[DataPath, "--reuse-outputs-from", "b"],
532 )
533 self.assertEqual(namespace.reuse, ["a", "b"])
534 namespace = self.ap.parse_args(
535 config=self.config,
536 args=[DataPath, "--reuse-outputs-from", "all"],
537 )
538 self.assertEqual(namespace.reuse, ["a", "b", "c"])
539 namespace = self.ap.parse_args(
540 config=self.config,
541 args=[DataPath],
542 )
543 self.assertEqual(namespace.reuse, [])
546class MyMemoryTestCase(lsst.utils.tests.MemoryTestCase):
547 pass
550def setup_module(module):
551 lsst.utils.tests.init()
554if __name__ == "__main__": 554 ↛ 555line 554 didn't jump to line 555, because the condition on line 554 was never true
555 lsst.utils.tests.init()
556 unittest.main()