test_configargparse.py 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905
  1. import argparse
  2. import configargparse
  3. import functools
  4. import inspect
  5. import logging
  6. import sys
  7. import tempfile
  8. import types
  9. if sys.version_info < (2, 7):
  10. import unittest2 as unittest
  11. else:
  12. import unittest
  13. if sys.version_info >= (3, 0):
  14. from io import StringIO
  15. else:
  16. from StringIO import StringIO
  17. # enable logging to simplify debugging
  18. logger = logging.getLogger()
  19. logger.level = logging.DEBUG
  20. stream_handler = logging.StreamHandler(sys.stdout)
  21. logger.addHandler(stream_handler)
  22. def replace_error_method(arg_parser):
  23. """Swap out arg_parser's error(..) method so that instead of calling
  24. sys.exit(..) it just raises an error.
  25. """
  26. def error_method(self, message):
  27. raise argparse.ArgumentError(None, message)
  28. def exit_method(self, status, message):
  29. self._exit_method_called = True
  30. arg_parser._exit_method_called = False
  31. arg_parser.error = types.MethodType(error_method, arg_parser)
  32. arg_parser.exit = types.MethodType(exit_method, arg_parser)
  33. return arg_parser
  34. class TestCase(unittest.TestCase):
  35. def initParser(self, *args, **kwargs):
  36. p = configargparse.ArgParser(*args, **kwargs)
  37. self.parser = replace_error_method(p)
  38. self.add_arg = self.parser.add_argument
  39. self.parse = self.parser.parse_args
  40. self.parse_known = self.parser.parse_known_args
  41. self.format_values = self.parser.format_values
  42. self.format_help = self.parser.format_help
  43. if not hasattr(self, "assertRegex"):
  44. self.assertRegex = self.assertRegexpMatches
  45. if not hasattr(self, "assertRaisesRegex"):
  46. self.assertRaisesRegex = self.assertRaisesRegexp
  47. self.assertParseArgsRaises = functools.partial(self.assertRaisesRegex,
  48. argparse.ArgumentError, callable_obj = self.parse)
  49. return self.parser
  50. class TestBasicUseCases(TestCase):
  51. def setUp(self):
  52. self.initParser(args_for_setting_config_path=[])
  53. def testBasicCase1(self):
  54. ## Test command line and config file values
  55. self.add_arg("filenames", nargs="+", help="positional arg")
  56. self.add_arg("-x", "--arg-x", action="store_true")
  57. self.add_arg("-y", "--arg-y", dest="y1", type=int, required=True)
  58. self.add_arg("--arg-z", action="append", type=float, required=True)
  59. # make sure required args are enforced
  60. self.assertParseArgsRaises("too few arg"
  61. if sys.version_info < (3,3) else
  62. "the following arguments are required", args="")
  63. self.assertParseArgsRaises("argument -y/--arg-y is required"
  64. if sys.version_info < (3,3) else
  65. "the following arguments are required: -y/--arg-y",
  66. args="-x --arg-z 11 file1.txt")
  67. self.assertParseArgsRaises("argument --arg-z is required"
  68. if sys.version_info < (3,3) else
  69. "the following arguments are required: --arg-z",
  70. args="file1.txt file2.txt file3.txt -x -y 1")
  71. # check values after setting args on command line
  72. ns = self.parse(args="file1.txt --arg-x -y 3 --arg-z 10",
  73. config_file_contents="")
  74. self.assertListEqual(ns.filenames, ["file1.txt"])
  75. self.assertEqual(ns.arg_x, True)
  76. self.assertEqual(ns.y1, 3)
  77. self.assertEqual(ns.arg_z, [10])
  78. self.assertRegex(self.format_values(),
  79. 'Command Line Args: file1.txt --arg-x -y 3 --arg-z 10')
  80. # check values after setting args in config file
  81. ns = self.parse(args="file1.txt file2.txt", config_file_contents="""
  82. # set all required args in config file
  83. arg-x = True
  84. arg-y = 10
  85. arg-z = 30
  86. arg-z = 40
  87. """)
  88. self.assertListEqual(ns.filenames, ["file1.txt", "file2.txt"])
  89. self.assertEqual(ns.arg_x, True)
  90. self.assertEqual(ns.y1, 10)
  91. self.assertEqual(ns.arg_z, [40])
  92. self.assertRegex(self.format_values(),
  93. 'Command Line Args: \s+ file1.txt file2.txt\n'
  94. 'Config File \(method arg\):\n'
  95. ' arg-x: \s+ True\n'
  96. ' arg-y: \s+ 10\n'
  97. ' arg-z: \s+ 40\n')
  98. # check values after setting args in both command line and config file
  99. ns = self.parse(args="file1.txt file2.txt --arg-x -y 3 --arg-z 100 ",
  100. config_file_contents="""arg-y = 31.5
  101. arg-z = 30
  102. """)
  103. self.format_help()
  104. self.format_values()
  105. self.assertListEqual(ns.filenames, ["file1.txt", "file2.txt"])
  106. self.assertEqual(ns.arg_x, True)
  107. self.assertEqual(ns.y1, 3)
  108. self.assertEqual(ns.arg_z, [100])
  109. self.assertRegex(self.format_values(),
  110. "Command Line Args: file1.txt file2.txt --arg-x -y 3 --arg-z 100")
  111. def testBasicCase2(self, use_groups=False):
  112. ## Test command line, config file and env var values
  113. default_config_file = tempfile.NamedTemporaryFile(mode="w", delete=True)
  114. default_config_file.flush()
  115. p = self.initParser(default_config_files=['/etc/settings.ini',
  116. '/home/jeff/.user_settings', default_config_file.name])
  117. p.add_arg('vcf', nargs='+', help='Variant file(s)')
  118. if not use_groups:
  119. self.add_arg('--genome', help='Path to genome file', required=True)
  120. self.add_arg('-v', dest='verbose', action='store_true')
  121. self.add_arg('-g', '--my-cfg-file', required=True,
  122. is_config_file=True)
  123. self.add_arg('-d', '--dbsnp', env_var='DBSNP_PATH')
  124. self.add_arg('-f', '--format',
  125. choices=["BED", "MAF", "VCF", "WIG", "R"],
  126. dest="fmt", metavar="FRMT", env_var="OUTPUT_FORMAT",
  127. default="BED")
  128. else:
  129. g = p.add_argument_group(title="g1")
  130. g.add_arg('--genome', help='Path to genome file', required=True)
  131. g.add_arg('-v', dest='verbose', action='store_true')
  132. g.add_arg('-g', '--my-cfg-file', required=True,
  133. is_config_file=True)
  134. g = p.add_argument_group(title="g2")
  135. g.add_arg('-d', '--dbsnp', env_var='DBSNP_PATH')
  136. g.add_arg('-f', '--format',
  137. choices=["BED", "MAF", "VCF", "WIG", "R"],
  138. dest="fmt", metavar="FRMT", env_var="OUTPUT_FORMAT",
  139. default="BED")
  140. # make sure required args are enforced
  141. self.assertParseArgsRaises("too few arg"
  142. if sys.version_info < (3,3) else
  143. "the following arguments are required: vcf, -g/--my-cfg-file",
  144. args="--genome hg19")
  145. self.assertParseArgsRaises("not found: file.txt", args="-g file.txt")
  146. # check values after setting args on command line
  147. config_file2 = tempfile.NamedTemporaryFile(mode="w", delete=True)
  148. config_file2.flush()
  149. ns = self.parse(args="--genome hg19 -g %s bla.vcf " % config_file2.name)
  150. self.assertEqual(ns.genome, "hg19")
  151. self.assertEqual(ns.verbose, False)
  152. self.assertEqual(ns.dbsnp, None)
  153. self.assertEqual(ns.fmt, "BED")
  154. self.assertListEqual(ns.vcf, ["bla.vcf"])
  155. self.assertRegex(self.format_values(),
  156. 'Command Line Args: --genome hg19 -g [^\s]+ bla.vcf\n'
  157. 'Defaults:\n'
  158. ' --format: \s+ BED\n')
  159. # check precedence: args > env > config > default using the --format arg
  160. default_config_file.write("--format MAF")
  161. default_config_file.flush()
  162. ns = self.parse(args="--genome hg19 -g %s f.vcf " % config_file2.name)
  163. self.assertEqual(ns.fmt, "MAF")
  164. self.assertRegex(self.format_values(),
  165. 'Command Line Args: --genome hg19 -g [^\s]+ f.vcf\n'
  166. 'Config File \([^\s]+\):\n'
  167. ' --format: \s+ MAF\n')
  168. config_file2.write("--format VCF")
  169. config_file2.flush()
  170. ns = self.parse(args="--genome hg19 -g %s f.vcf " % config_file2.name)
  171. self.assertEqual(ns.fmt, "VCF")
  172. self.assertRegex(self.format_values(),
  173. 'Command Line Args: --genome hg19 -g [^\s]+ f.vcf\n'
  174. 'Config File \([^\s]+\):\n'
  175. ' --format: \s+ VCF\n')
  176. ns = self.parse(env_vars={"OUTPUT_FORMAT":"R", "DBSNP_PATH":"/a/b.vcf"},
  177. args="--genome hg19 -g %s f.vcf " % config_file2.name)
  178. self.assertEqual(ns.fmt, "R")
  179. self.assertEqual(ns.dbsnp, "/a/b.vcf")
  180. self.assertRegex(self.format_values(),
  181. 'Command Line Args: --genome hg19 -g [^\s]+ f.vcf\n'
  182. 'Environment Variables:\n'
  183. ' DBSNP_PATH: \s+ /a/b.vcf\n'
  184. ' OUTPUT_FORMAT: \s+ R\n')
  185. ns = self.parse(env_vars={"OUTPUT_FORMAT":"R", "DBSNP_PATH":"/a/b.vcf",
  186. "ANOTHER_VAR":"something"},
  187. args="--genome hg19 -g %s --format WIG f.vcf" % config_file2.name)
  188. self.assertEqual(ns.fmt, "WIG")
  189. self.assertEqual(ns.dbsnp, "/a/b.vcf")
  190. self.assertRegex(self.format_values(),
  191. 'Command Line Args: --genome hg19 -g [^\s]+ --format WIG f.vcf\n'
  192. 'Environment Variables:\n'
  193. ' DBSNP_PATH: \s+ /a/b.vcf\n')
  194. if not use_groups:
  195. self.assertRegex(self.format_help(),
  196. 'usage: .* \[-h\] --genome GENOME \[-v\] -g MY_CFG_FILE'
  197. ' \[-d DBSNP\]\s+\[-f FRMT\]\s+vcf \[vcf ...\]\n\n' +
  198. 9*'(.+\s+)'+ # repeated 8 times because .+ matches atmost 1 line
  199. 'positional arguments:\n'
  200. ' vcf \s+ Variant file\(s\)\n\n'
  201. 'optional arguments:\n'
  202. ' -h, --help \s+ show this help message and exit\n'
  203. ' --genome GENOME \s+ Path to genome file\n'
  204. ' -v\n'
  205. ' -g MY_CFG_FILE, --my-cfg-file MY_CFG_FILE\n'
  206. ' -d DBSNP, --dbsnp DBSNP\s+\[env var: DBSNP_PATH\]\n'
  207. ' -f FRMT, --format FRMT\s+\[env var: OUTPUT_FORMAT\]\n')
  208. else:
  209. self.assertRegex(self.format_help(),
  210. 'usage: .* \[-h\] --genome GENOME \[-v\] -g MY_CFG_FILE'
  211. ' \[-d DBSNP\]\s+\[-f FRMT\]\s+vcf \[vcf ...\]\n\n'+
  212. 9*'.+\s+'+ # repeated 8 times because .+ matches atmost 1 line
  213. 'positional arguments:\n'
  214. ' vcf \s+ Variant file\(s\)\n\n'
  215. 'optional arguments:\n'
  216. ' -h, --help \s+ show this help message and exit\n\n'
  217. 'g1:\n'
  218. ' --genome GENOME \s+ Path to genome file\n'
  219. ' -v\n'
  220. ' -g MY_CFG_FILE, --my-cfg-file MY_CFG_FILE\n\n'
  221. 'g2:\n'
  222. ' -d DBSNP, --dbsnp DBSNP\s+\[env var: DBSNP_PATH\]\n'
  223. ' -f FRMT, --format FRMT\s+\[env var: OUTPUT_FORMAT\]\n')
  224. self.assertParseArgsRaises("invalid choice: 'ZZZ'",
  225. args="--genome hg19 -g %s --format ZZZ f.vcf" % config_file2.name)
  226. self.assertParseArgsRaises("unrecognized arguments: --bla",
  227. args="--bla --genome hg19 -g %s f.vcf" % config_file2.name)
  228. default_config_file.close()
  229. config_file2.close()
  230. def testBasicCase2_WithGroups(self):
  231. self.testBasicCase2(use_groups=True)
  232. def testMutuallyExclusiveArgs(self):
  233. config_file = tempfile.NamedTemporaryFile(mode="w", delete=True)
  234. p = self.parser
  235. g = p.add_argument_group(title="group1")
  236. g.add_arg('--genome', help='Path to genome file', required=True)
  237. g.add_arg('-v', dest='verbose', action='store_true')
  238. g = p.add_mutually_exclusive_group(required=True)
  239. g.add_arg('-f1', '--type1-cfg-file', is_config_file=True)
  240. g.add_arg('-f2', '--type2-cfg-file', is_config_file=True)
  241. g = p.add_mutually_exclusive_group(required=True)
  242. g.add_arg('-f', '--format', choices=["BED", "MAF", "VCF", "WIG", "R"],
  243. dest="fmt", metavar="FRMT", env_var="OUTPUT_FORMAT",
  244. default="BED")
  245. g.add_arg('-b', '--bam', dest='fmt', action="store_const", const="BAM",
  246. env_var='BAM_FORMAT')
  247. ns = self.parse(args="--genome hg19 -f1 %s --bam" % config_file.name)
  248. self.assertEqual(ns.genome, "hg19")
  249. self.assertEqual(ns.verbose, False)
  250. self.assertEqual(ns.fmt, "BAM")
  251. ns = self.parse(env_vars={"BAM_FORMAT" : "true"},
  252. args="--genome hg19 -f1 %s" % config_file.name)
  253. self.assertEqual(ns.genome, "hg19")
  254. self.assertEqual(ns.verbose, False)
  255. self.assertEqual(ns.fmt, "BAM")
  256. self.assertRegex(self.format_values(),
  257. 'Command Line Args: --genome hg19 -f1 [^\s]+\n'
  258. 'Environment Variables:\n'
  259. ' BAM_FORMAT: \s+ true\n'
  260. 'Defaults:\n'
  261. ' --format: \s+ BED\n')
  262. self.assertRegex(self.format_help(),
  263. 'usage: .* \[-h\] --genome GENOME \[-v\]\s+ \(-f1 TYPE1_CFG_FILE \|'
  264. ' \s*-f2 TYPE2_CFG_FILE\)\s+\(-f FRMT \| -b\)\n\n' +
  265. 7*'.+\s+'+ # repeated 7 times because .+ matches atmost 1 line
  266. 'optional arguments:\n'
  267. ' -h, --help show this help message and exit\n'
  268. ' -f1 TYPE1_CFG_FILE, --type1-cfg-file TYPE1_CFG_FILE\n'
  269. ' -f2 TYPE2_CFG_FILE, --type2-cfg-file TYPE2_CFG_FILE\n'
  270. ' -f FRMT, --format FRMT\s+\[env var: OUTPUT_FORMAT\]\n'
  271. ' -b, --bam\s+\[env var: BAM_FORMAT\]\n\n'
  272. 'group1:\n'
  273. ' --genome GENOME Path to genome file\n'
  274. ' -v\n')
  275. config_file.close()
  276. def testSubParsers(self):
  277. config_file1 = tempfile.NamedTemporaryFile(mode="w", delete=True)
  278. config_file1.write("--i = B")
  279. config_file1.flush()
  280. config_file2 = tempfile.NamedTemporaryFile(mode="w", delete=True)
  281. config_file2.write("p = 10")
  282. config_file2.flush()
  283. parser = configargparse.ArgumentParser(prog="myProg")
  284. subparsers = parser.add_subparsers(title="actions")
  285. parent_parser = configargparse.ArgumentParser(add_help=False)
  286. parent_parser.add_argument("-p", "--p", type=int, required=True,
  287. help="set db parameter")
  288. create_p = subparsers.add_parser("create", parents=[parent_parser],
  289. help="create the orbix environment")
  290. create_p.add_argument("--i", env_var="INIT", choices=["A","B"],
  291. default="A")
  292. create_p.add_argument("-config", is_config_file=True)
  293. update_p = subparsers.add_parser("update", parents=[parent_parser],
  294. help="update the orbix environment")
  295. update_p.add_argument("-config2", is_config_file=True, required=True)
  296. ns = parser.parse_args(args = "create -p 2 -config "+config_file1.name)
  297. self.assertEqual(ns.p, 2)
  298. self.assertEqual(ns.i, "B")
  299. ns = parser.parse_args(args = "update -config2 " + config_file2.name)
  300. self.assertEqual(ns.p, 10)
  301. config_file1.close()
  302. config_file2.close()
  303. def testAddArgsErrors(self):
  304. self.assertRaisesRegex(ValueError, "arg with "
  305. "is_write_out_config_file_arg=True can't also have "
  306. "is_config_file_arg=True", self.add_arg, "-x", "--X",
  307. is_config_file=True, is_write_out_config_file_arg=True)
  308. self.assertRaisesRegex(ValueError, "arg with "
  309. "is_write_out_config_file_arg=True must have action='store'",
  310. self.add_arg, "-y", "--Y", action="append",
  311. is_write_out_config_file_arg=True)
  312. def testConfigFileSyntax(self):
  313. self.add_arg('-x', required=True, type=int)
  314. self.add_arg('--y', required=True, type=float)
  315. self.add_arg('--z')
  316. self.add_arg('--b', action="store_true")
  317. self.add_arg('--a', action="append", type=int)
  318. ns = self.parse(args="-x 1", env_vars={}, config_file_contents="""
  319. #inline comment 1
  320. # inline comment 2
  321. # inline comment 3
  322. ;inline comment 4
  323. ; inline comment 5
  324. ;inline comment 6
  325. --- # separator 1
  326. ------------- # separator 2
  327. y=1.1
  328. y = 2.1
  329. y= 3.1 # with comment
  330. y= 4.1 ; with comment
  331. ---
  332. y:5.1
  333. y : 6.1
  334. y: 7.1 # with comment
  335. y: 8.1 ; with comment
  336. ---
  337. y \t 9.1
  338. y 10.1
  339. y 11.1 # with comment
  340. y 12.1 ; with comment
  341. ---
  342. b
  343. b = True
  344. b: True
  345. ----
  346. a = 33
  347. """)
  348. self.assertEqual(ns.x, 1)
  349. self.assertEqual(ns.y, 12.1)
  350. self.assertEqual(ns.z, None)
  351. self.assertEqual(ns.b, True)
  352. self.assertEqual(ns.a, [33])
  353. self.assertRegex(self.format_values(),
  354. 'Command Line Args: \s+ -x 1\n'
  355. 'Config File \(method arg\):\n'
  356. ' y: \s+ 12.1\n'
  357. ' b: \s+ True\n'
  358. ' a: \s+ 33\n')
  359. # -x is not a long arg so can't be set via config file
  360. self.assertParseArgsRaises("argument -x is required"
  361. if sys.version_info < (3,3) else
  362. "the following arguments are required: -x, --y",
  363. config_file_contents="-x 3")
  364. self.assertParseArgsRaises("invalid float value: 'abc'",
  365. args="-x 5",
  366. config_file_contents="y: abc")
  367. self.assertParseArgsRaises("argument --y is required"
  368. if sys.version_info < (3,3) else
  369. "the following arguments are required: --y",
  370. args="-x 5",
  371. config_file_contents="z: 1")
  372. self.assertParseArgsRaises("Unexpected line 0",
  373. config_file_contents="z z 1")
  374. # test unknown config file args
  375. self.assertParseArgsRaises("bla",
  376. args="-x 1 --y 2.3",
  377. config_file_contents="bla=3")
  378. ns, args = self.parse_known("-x 10 --y 3.8",
  379. config_file_contents="bla=3",
  380. env_vars={"bla": "2"})
  381. self.assertListEqual(args, ["--bla", "3"])
  382. self.initParser(ignore_unknown_config_file_keys=False)
  383. ns, args = self.parse_known(args="-x 1", config_file_contents="bla=3",
  384. env_vars={"bla": "2"})
  385. self.assertListEqual(args, ["--bla", "3", "-x", "1"])
  386. def testConfigOrEnvValueErrors(self):
  387. # error should occur when a flag arg is set to something other than "true" or "false"
  388. self.initParser()
  389. self.add_arg("--height", env_var = "HEIGHT", required=True)
  390. self.add_arg("--do-it", dest="x", env_var = "FLAG1", action="store_true")
  391. self.add_arg("--dont-do-it", dest="x", env_var = "FLAG2", action="store_false")
  392. ns = self.parse("", env_vars = {"HEIGHT": "tall", "FLAG1": "yes"})
  393. self.assertEqual(ns.height, "tall")
  394. self.assertEqual(ns.x, True)
  395. ns = self.parse("", env_vars = {"HEIGHT": "tall", "FLAG2": "no"})
  396. self.assertEqual(ns.x, False)
  397. # error should occur when flag arg is given a value
  398. self.initParser()
  399. self.add_arg("-v", "--verbose", env_var="VERBOSE", action="store_true")
  400. self.assertParseArgsRaises("Unexpected value for VERBOSE: 'bla'. "
  401. "Expecting 'true', 'false', 'yes', or 'no'",
  402. env_vars={"VERBOSE" : "bla"})
  403. ns = self.parse("",
  404. config_file_contents="verbose=true",
  405. env_vars={"HEIGHT": "true"})
  406. self.assertEqual(ns.verbose, True)
  407. ns = self.parse("",
  408. config_file_contents="verbose",
  409. env_vars={"HEIGHT": "true"})
  410. self.assertEqual(ns.verbose, True)
  411. ns = self.parse("", env_vars = {"HEIGHT": "true", "VERBOSE": "true"})
  412. self.assertEqual(ns.verbose, True)
  413. ns = self.parse("", config_file_contents="--verbose",
  414. env_vars = {"HEIGHT": "true"})
  415. self.assertEqual(ns.verbose, True)
  416. # error should occur is non-append arg is given a list value
  417. self.initParser()
  418. self.add_arg("-f", "--file", env_var="FILES", action="append", type=int)
  419. ns = self.parse("", env_vars = {"file": "[1,2,3]", "VERBOSE": "true"})
  420. self.assertEqual(ns.file, None)
  421. def testAutoEnvVarPrefix(self):
  422. self.initParser(auto_env_var_prefix="TEST_")
  423. self.add_arg("-a", "--arg0", is_config_file_arg=True)
  424. self.add_arg("-b", "--arg1", is_write_out_config_file_arg=True)
  425. self.add_arg("-x", "--arg2", env_var="TEST2", type=int)
  426. self.add_arg("-y", "--arg3", action="append", type=int)
  427. self.add_arg("-z", "--arg4", required=True)
  428. self.add_arg("-w", "--arg4-more", required=True)
  429. ns = self.parse("", env_vars = {
  430. "TEST_ARG0": "0",
  431. "TEST_ARG1": "1",
  432. "TEST_ARG2": "2",
  433. "TEST2": "22",
  434. "TEST_ARG4": "arg4_value",
  435. "TEST_ARG4_MORE": "magic"})
  436. self.assertEqual(ns.arg0, None)
  437. self.assertEqual(ns.arg1, None)
  438. self.assertEqual(ns.arg2, 22)
  439. self.assertEqual(ns.arg4, "arg4_value")
  440. self.assertEqual(ns.arg4_more, "magic")
  441. class TestMisc(TestCase):
  442. # TODO test different action types with config file, env var
  443. """Test edge cases"""
  444. def setUp(self):
  445. self.initParser(args_for_setting_config_path=[])
  446. def testGlobalInstances(self, name=None):
  447. p = configargparse.getArgumentParser(name, prog="prog", usage="test")
  448. self.assertEqual(p.usage, "test")
  449. self.assertEqual(p.prog, "prog")
  450. self.assertRaisesRegex(ValueError, "kwargs besides 'name' can only be "
  451. "passed in the first time", configargparse.getArgumentParser, name,
  452. prog="prog")
  453. p2 = configargparse.getArgumentParser(name)
  454. self.assertEqual(p, p2)
  455. def testGlobalInstances_WithName(self):
  456. self.testGlobalInstances("name1")
  457. self.testGlobalInstances("name2")
  458. def testAddArguments_ArgValidation(self):
  459. self.assertRaises(ValueError, self.add_arg, 'positional', env_var="bla")
  460. action = self.add_arg('positional')
  461. self.assertIsNotNone(action)
  462. self.assertEqual(action.dest, "positional")
  463. def testAddArguments_IsConfigFilePathArg(self):
  464. self.assertRaises(ValueError, self.add_arg, 'c', action="store_false",
  465. is_config_file=True)
  466. self.add_arg("-c", "--config", is_config_file=True)
  467. self.add_arg("--x", required=True)
  468. # verify parsing from config file
  469. config_file = tempfile.NamedTemporaryFile(mode="w", delete=True)
  470. config_file.write("x=bla")
  471. config_file.flush()
  472. ns = self.parse(args="-c %s" % config_file.name)
  473. self.assertEqual(ns.x, "bla")
  474. def testConstructor_ConfigFileArgs(self):
  475. # Test constructor args:
  476. # args_for_setting_config_path
  477. # config_arg_is_required
  478. # config_arg_help_message
  479. temp_cfg = tempfile.NamedTemporaryFile(mode="w", delete=True)
  480. temp_cfg.write("genome=hg19")
  481. temp_cfg.flush()
  482. self.initParser(args_for_setting_config_path=["-c", "--config"],
  483. config_arg_is_required = True,
  484. config_arg_help_message = "my config file",
  485. default_config_files=[temp_cfg.name])
  486. self.add_arg('--genome', help='Path to genome file', required=True)
  487. self.assertParseArgsRaises("argument -c/--config is required"
  488. if sys.version_info < (3,3) else
  489. "arguments are required: -c/--config",)
  490. temp_cfg2 = tempfile.NamedTemporaryFile(mode="w", delete=True)
  491. ns = self.parse("-c " + temp_cfg2.name)
  492. self.assertEqual(ns.genome, "hg19")
  493. # temp_cfg2 config file should override default config file values
  494. temp_cfg2.write("genome=hg20")
  495. temp_cfg2.flush()
  496. ns = self.parse("-c " + temp_cfg2.name)
  497. self.assertEqual(ns.genome, "hg20")
  498. self.assertRegex(self.format_help(),
  499. 'usage: .* \[-h\] -c CONFIG_FILE --genome GENOME\n\n'+
  500. 7*'.+\s+'+ # repeated 7 times because .+ matches atmost 1 line
  501. 'optional arguments:\n'
  502. ' -h, --help\s+ show this help message and exit\n'
  503. ' -c CONFIG_FILE, --config CONFIG_FILE\s+ my config file\n'
  504. ' --genome GENOME\s+ Path to genome file\n')
  505. # just run print_values() to make sure it completes and returns None
  506. self.assertEqual(self.parser.print_values(file=sys.stderr), None)
  507. # test ignore_unknown_config_file_keys=False
  508. self.initParser(ignore_unknown_config_file_keys=False)
  509. self.assertRaisesRegex(argparse.ArgumentError, "unrecognized arguments",
  510. self.parse, config_file_contents="arg1 = 3")
  511. ns, args = self.parse_known(config_file_contents="arg1 = 3")
  512. self.assertEqual(getattr(ns, "arg1", ""), "")
  513. # test ignore_unknown_config_file_keys=True
  514. self.initParser(ignore_unknown_config_file_keys=True)
  515. ns = self.parse(args="", config_file_contents="arg1 = 3")
  516. self.assertEqual(getattr(ns, "arg1", ""), "")
  517. ns, args = self.parse_known(config_file_contents="arg1 = 3")
  518. self.assertEqual(getattr(ns, "arg1", ""), "")
  519. def test_FormatHelp(self):
  520. self.initParser(args_for_setting_config_path=["-c", "--config"],
  521. config_arg_is_required = True,
  522. config_arg_help_message = "my config file",
  523. default_config_files=["~/.myconfig"],
  524. args_for_writing_out_config_file=["-w", "--write-config"],
  525. )
  526. self.add_arg('--arg1', help='Arg1 help text', required=True)
  527. self.add_arg('--flag', help='Flag help text', action="store_true")
  528. self.assertRegex(self.format_help(),
  529. 'usage: .* \[-h\] -c CONFIG_FILE\s+'
  530. '\[-w CONFIG_OUTPUT_PATH\]\s* --arg1 ARG1\s*\[--flag\]\s*'
  531. 'Args that start with \'--\' \(eg. --arg1\) can also be set in a '
  532. 'config file\s*\(~/.myconfig or specified via -c\).\s*'
  533. 'Config file syntax allows: key=value,\s*flag=true, stuff=\[a,b,c\] '
  534. '\(for details, see syntax at https://goo.gl/R74nmi\).\s*'
  535. 'If an arg is specified in more than\s*one place, then '
  536. 'commandline values\s*override config file values which override\s*'
  537. 'defaults.\s*'
  538. 'optional arguments:\s*'
  539. '-h, --help \s* show this help message and exit\n\s*'
  540. '-c CONFIG_FILE, --config CONFIG_FILE\s+my config file\s*'
  541. '-w CONFIG_OUTPUT_PATH, --write-config CONFIG_OUTPUT_PATH\s*takes\s*'
  542. 'the current command line args and writes them\s*'
  543. 'out to a config file at the given path, then exits\s*'
  544. '--arg1 ARG1\s*Arg1 help text\s*'
  545. '--flag \s*Flag help text'
  546. )
  547. def testConstructor_WriteOutConfigFileArgs(self):
  548. # Test constructor args:
  549. # args_for_writing_out_config_file
  550. # write_out_config_file_arg_help_message
  551. cfg_f = tempfile.NamedTemporaryFile(mode="w+", delete=True)
  552. self.initParser(args_for_writing_out_config_file=["-w"],
  553. write_out_config_file_arg_help_message="write config")
  554. self.add_arg("-not-config-file-settable")
  555. self.add_arg("--config-file-settable-arg", type=int)
  556. self.add_arg("--config-file-settable-arg2", type=int, default=3)
  557. self.add_arg("--config-file-settable-flag", action="store_true")
  558. self.add_arg("-l", "--config-file-settable-list", action="append")
  559. # write out a config file
  560. command_line_args = "-w %s " % cfg_f.name
  561. command_line_args += "--config-file-settable-arg 1 "
  562. command_line_args += "--config-file-settable-flag "
  563. command_line_args += "-l a -l b -l c -l d "
  564. self.assertFalse(self.parser._exit_method_called)
  565. ns = self.parse(command_line_args)
  566. self.assertTrue(self.parser._exit_method_called)
  567. cfg_f.seek(0)
  568. expected_config_file_contents = "config-file-settable-arg = 1\n"
  569. expected_config_file_contents += "config-file-settable-flag = true\n"
  570. expected_config_file_contents += "config-file-settable-list = [a, b, c, d]\n"
  571. expected_config_file_contents += "config-file-settable-arg2 = 3\n"
  572. self.assertEqual(cfg_f.read().strip(),
  573. expected_config_file_contents.strip())
  574. self.assertRaisesRegex(ValueError, "Couldn't open / for writing:",
  575. self.parse, args = command_line_args + " -w /")
  576. def testConstructor_WriteOutConfigFileArgs2(self):
  577. # Test constructor args:
  578. # args_for_writing_out_config_file
  579. # write_out_config_file_arg_help_message
  580. cfg_f = tempfile.NamedTemporaryFile(mode="w+", delete=True)
  581. self.initParser(args_for_writing_out_config_file=["-w"],
  582. write_out_config_file_arg_help_message="write config")
  583. self.add_arg("-not-config-file-settable")
  584. self.add_arg("-a", "--arg1", type=int, env_var="ARG1")
  585. self.add_arg("-b", "--arg2", type=int, default=3)
  586. self.add_arg("-c", "--arg3")
  587. self.add_arg("-d", "--arg4")
  588. self.add_arg("-e", "--arg5")
  589. self.add_arg("--config-file-settable-flag", action="store_true",
  590. env_var="FLAG_ARG")
  591. self.add_arg("-l", "--config-file-settable-list", action="append")
  592. # write out a config file
  593. command_line_args = "-w %s " % cfg_f.name
  594. command_line_args += "-l a -l b -l c -l d "
  595. self.assertFalse(self.parser._exit_method_called)
  596. ns = self.parse(command_line_args,
  597. env_vars={"ARG1": "10", "FLAG_ARG": "true",
  598. "SOME_OTHER_ENV_VAR": "2"},
  599. config_file_contents="arg3 = bla3\narg4 = bla4")
  600. self.assertTrue(self.parser._exit_method_called)
  601. cfg_f.seek(0)
  602. expected_config_file_contents = "config-file-settable-list = [a, b, c, d]\n"
  603. expected_config_file_contents += "arg1 = 10\n"
  604. expected_config_file_contents += "config-file-settable-flag = True\n"
  605. expected_config_file_contents += "arg3 = bla3\n"
  606. expected_config_file_contents += "arg4 = bla4\n"
  607. expected_config_file_contents += "arg2 = 3\n"
  608. self.assertEqual(cfg_f.read().strip(),
  609. expected_config_file_contents.strip())
  610. self.assertRaisesRegex(ValueError, "Couldn't open / for writing:",
  611. self.parse, args = command_line_args + " -w /")
  612. def testMethodAliases(self):
  613. p = self.parser
  614. p.add("-a", "--arg-a", default=3)
  615. p.add_arg("-b", "--arg-b", required=True)
  616. p.add_argument("-c")
  617. g1 = p.add_argument_group(title="group1", description="descr")
  618. g1.add("-d", "--arg-d", required=True)
  619. g1.add_arg("-e", "--arg-e", required=True)
  620. g1.add_argument("-f", "--arg-f", default=5)
  621. g2 = p.add_mutually_exclusive_group(required=True)
  622. g2.add("-x", "--arg-x")
  623. g2.add_arg("-y", "--arg-y")
  624. g2.add_argument("-z", "--arg-z", default=5)
  625. # verify that flags must be globally unique
  626. g2 = p.add_argument_group(title="group2", description="descr")
  627. self.assertRaises(argparse.ArgumentError, g1.add, "-c")
  628. self.assertRaises(argparse.ArgumentError, g2.add, "-f")
  629. self.initParser()
  630. p = self.parser
  631. options = p.parse(args=[])
  632. self.assertDictEqual(vars(options), {})
  633. class TestConfigFileParsers(TestCase):
  634. """Test ConfigFileParser subclasses in isolation"""
  635. def testDefaultConfigFileParser_Basic(self):
  636. p = configargparse.DefaultConfigFileParser()
  637. self.assertTrue(len(p.get_syntax_description()) > 0)
  638. # test the simplest case
  639. input_config_str = StringIO("""a: 3\n""")
  640. parsed_obj = p.parse(input_config_str)
  641. output_config_str = p.serialize(parsed_obj)
  642. self.assertEqual(input_config_str.getvalue().replace(": ", " = "),
  643. output_config_str)
  644. self.assertDictEqual(parsed_obj, dict([('a', '3')]))
  645. def testDefaultConfigFileParser_All(self):
  646. p = configargparse.DefaultConfigFileParser()
  647. # test the all syntax case
  648. config_lines = [
  649. "# comment1 ",
  650. "[ some section ]",
  651. "----",
  652. "---------",
  653. "_a: 3",
  654. "; comment2 ",
  655. "_b = c",
  656. "_list_arg1 = [a, b, c]",
  657. "_str_arg = true",
  658. "_list_arg2 = [1, 2, 3]",
  659. ]
  660. # test parse
  661. input_config_str = StringIO("\n".join(config_lines)+"\n")
  662. parsed_obj = p.parse(input_config_str)
  663. # test serialize
  664. output_config_str = p.serialize(parsed_obj)
  665. self.assertEqual("\n".join(
  666. l.replace(': ', ' = ') for l in config_lines if l.startswith('_'))+"\n",
  667. output_config_str)
  668. self.assertDictEqual(parsed_obj, dict([
  669. ('_a', '3'),
  670. ('_b', 'c'),
  671. ('_list_arg1', ['a', 'b', 'c']),
  672. ('_str_arg', 'true'),
  673. ('_list_arg2', ['1', '2', '3']),
  674. ]))
  675. self.assertListEqual(parsed_obj['_list_arg1'], ['a', 'b', 'c'])
  676. self.assertListEqual(parsed_obj['_list_arg2'], ['1', '2', '3'])
  677. def testYAMLConfigFileParser_Basic(self):
  678. try:
  679. import yaml
  680. except:
  681. logging.warning("WARNING: PyYAML not installed. "
  682. "Couldn't test YAMLConfigFileParser")
  683. return
  684. p = configargparse.YAMLConfigFileParser()
  685. self.assertTrue(len(p.get_syntax_description()) > 0)
  686. input_config_str = StringIO("""a: '3'\n""")
  687. parsed_obj = p.parse(input_config_str)
  688. output_config_str = p.serialize(dict(parsed_obj))
  689. self.assertEqual(input_config_str.getvalue(), output_config_str)
  690. self.assertDictEqual(parsed_obj, dict([('a', '3')]))
  691. def testYAMLConfigFileParser_All(self):
  692. try:
  693. import yaml
  694. except:
  695. logging.warning("WARNING: PyYAML not installed. "
  696. "Couldn't test YAMLConfigFileParser")
  697. return
  698. p = configargparse.YAMLConfigFileParser()
  699. # test the all syntax case
  700. config_lines = [
  701. "a: '3'",
  702. "list_arg:",
  703. "- 1",
  704. "- 2",
  705. "- 3",
  706. ]
  707. # test parse
  708. input_config_str = StringIO("\n".join(config_lines)+"\n")
  709. parsed_obj = p.parse(input_config_str)
  710. # test serialize
  711. output_config_str = p.serialize(parsed_obj)
  712. self.assertEqual(input_config_str.getvalue(), output_config_str)
  713. self.assertDictEqual(parsed_obj, dict([
  714. ('a', '3'),
  715. ('list_arg', [1,2,3]),
  716. ]))
  717. ################################################################################
  718. # since configargparse should work as a drop-in replacement for argparse
  719. # in all situations, run argparse unittests on configargparse by modifying
  720. # their source code to use configargparse.ArgumentParser
  721. try:
  722. import test.test_argparse
  723. #Sig = test.test_argparse.Sig
  724. #NS = test.test_argparse.NS
  725. except ImportError:
  726. if sys.version_info < (2, 7):
  727. logging.info("\n\n" + ("=" * 30) +
  728. "\nINFO: Skipping tests for argparse (Python < 2.7)\n"
  729. + ("=" * 30) + "\n")
  730. else:
  731. logging.error("\n\n"
  732. "============================\n"
  733. "ERROR: Many tests couldn't be run because 'import test.test_argparse' "
  734. "failed. Try building/installing python from source rather than through"
  735. " a package manager.\n"
  736. "============================\n")
  737. else:
  738. test_argparse_source_code = inspect.getsource(test.test_argparse)
  739. test_argparse_source_code = test_argparse_source_code.replace(
  740. 'argparse.ArgumentParser', 'configargparse.ArgumentParser')
  741. # run or debug a subset of the argparse tests
  742. #test_argparse_source_code = test_argparse_source_code.replace(
  743. # "(TestCase)", "").replace(
  744. # "(ParserTestCase)", "").replace(
  745. # "(HelpTestCase)", "").replace(
  746. # ", TestCase", "").replace(
  747. # ", ParserTestCase", "")
  748. #test_argparse_source_code = test_argparse_source_code.replace(
  749. # "class TestMessageContentError", "class TestMessageContentError(TestCase)")
  750. exec(test_argparse_source_code)
  751. # print argparse unittest source code
  752. def print_source_code(source_code, line_numbers, context_lines=10):
  753. for n in line_numbers:
  754. logging.debug("##### Code around line %s #####" % n)
  755. lines_to_print = set(range(n - context_lines, n + context_lines))
  756. for n2, line in enumerate(source_code.split("\n"), 1):
  757. if n2 in lines_to_print:
  758. logging.debug("%s %5d: %s" % (
  759. "**" if n2 == n else " ", n2, line))
  760. #print_source_code(test_argparse_source_code, [4540, 4565])