PageRenderTime 37ms CodeModel.GetById 20ms RepoModel.GetById 1ms app.codeStats 0ms

/betse/cli/climain.py

https://gitlab.com/dglmoore/betse
Python | 368 lines | 230 code | 29 blank | 109 comment | 7 complexity | f2fd78a4a571024eb561aca0570487e8 MD5 | raw file
  1. #!/usr/bin/env python3
  2. # --------------------( LICENSE )--------------------
  3. # Copyright 2014-2017 by Alexis Pietak & Cecil Curry
  4. # See "LICENSE" for further details.
  5. '''
  6. Concrete subclasses defining this application's command line interface (CLI).
  7. '''
  8. # ....................{ IMPORTS }....................
  9. #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
  10. # WARNING: To raise human-readable exceptions on application startup, the
  11. # top-level of this module may import *ONLY* from submodules guaranteed to:
  12. # * Exist, including standard Python and application modules.
  13. # * Never raise exceptions on importation (e.g., due to module-level logic).
  14. #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
  15. from betse import metadata
  16. from betse.cli import clicmd, cliinfo, cliutil
  17. from betse.cli.cliabc import CLIABC
  18. from betse.cli.clicmd import SUBCOMMANDS_PREFIX, SUBCOMMANDS_SUFFIX
  19. from betse.util.io.log import logs
  20. from betse.util.path import files, pathnames
  21. from betse.util.py import pyident, pys
  22. from betse.util.type.call.memoizers import property_cached
  23. from betse.util.type.obj import objects
  24. from betse.util.type.types import MappingType
  25. # ....................{ SUBCLASS }....................
  26. class BetseCLI(CLIABC):
  27. '''
  28. Command line interface (CLI) for this application.
  29. Attributes
  30. ----------
  31. _arg_parser_plot : ArgParserType
  32. Subparser parsing arguments passed to the ``plot`` subcommand.
  33. _arg_subparsers_top : ArgParserType
  34. Subparsers parsing top-level subcommands (e.g., ``plot``).
  35. _arg_subparsers_plot : ArgParserType
  36. Subparsers parsing ``plot`` subcommands (e.g., ``plot seed``).
  37. '''
  38. # ..................{ INITIALIZERS }..................
  39. def __init__(self) -> None:
  40. # Initialize our superclass.
  41. super().__init__()
  42. # Nullify attributes for safety.
  43. self._arg_parser_plot = None
  44. self._arg_subparsers_top = None
  45. self._arg_subparsers_plot = None
  46. # ..................{ SUPERCLASS ~ args }..................
  47. @property
  48. def _arg_parser_top_kwargs(self) -> MappingType:
  49. return {
  50. # Human-readable multi-sentence application description.
  51. 'description': metadata.DESCRIPTION,
  52. # Human-readable multi-sentence application help suffix.
  53. 'epilog': SUBCOMMANDS_SUFFIX,
  54. }
  55. def _config_arg_parsing(self) -> None:
  56. # Collection of top-level argument subparsers.
  57. self._arg_subparsers_top = self._arg_parser.add_subparsers(
  58. # Name of the attribute storing the passed subcommand name.
  59. dest='subcommand_name_top',
  60. # Title of the subcommand section in help output.
  61. title='subcommands',
  62. # Description to be printed *BEFORE* subcommand help.
  63. description=cliutil.expand_help(SUBCOMMANDS_PREFIX),
  64. )
  65. #FIXME: Refactor this logic as follows:
  66. #
  67. #* The local "subcommand_name_to_subparser" dicitonary should be
  68. # replaced by performing the following in the body of this loop:
  69. # * If a global "help.SUBCOMMANDS_{subcommand.name}" dictionary
  70. # exists, non-recursively loop over that dictionary in the same
  71. # manner as well. Hence, this generalizes support of subcommand
  72. # subcommands. Nice!
  73. #* Likewise, if a "self._arg_parser_{subcommand.name}" instance
  74. # variable exists, set that variable to this parser. Such logic
  75. # should, arguably, be performed by _add_subcommand().
  76. # Dictionary mapping from the name of each top-level subcommand to the
  77. # argument subparser parsing that subcommand.
  78. subcommand_name_to_subparser = clicmd.add_top(
  79. arg_subparsers=self._arg_subparsers_top,
  80. arg_subparser_kwargs=self._arg_parser_kwargs)
  81. # Configure arg parsing for subcommands of the "plot" subcommand.
  82. self._arg_parser_plot = subcommand_name_to_subparser['plot']
  83. self._config_arg_parsing_plot()
  84. # ..................{ SUBCOMMAND ~ plot }..................
  85. def _config_arg_parsing_plot(self) -> None:
  86. '''
  87. Configure argument parsing for subcommands of the ``plot`` subcommand.
  88. '''
  89. # Collection of all subcommands of the "plot" subcommand.
  90. self._arg_subparsers_plot = self._arg_parser_plot.add_subparsers(
  91. # Name of the attribute storing the passed subcommand name.
  92. dest='subcommand_name_plot',
  93. # Title of the subcommand section in help output.
  94. title='plot subcommands',
  95. # Description to be printed *BEFORE* subcommand help.
  96. description=cliutil.expand_help(SUBCOMMANDS_PREFIX),
  97. )
  98. # Dictionary mapping from the name of each "plot" subcommand to the
  99. # argument subparser parsing that subcommand.
  100. clicmd.add_plot(
  101. arg_subparsers=self._arg_subparsers_plot,
  102. arg_subparser_kwargs=self._arg_parser_kwargs)
  103. # ..................{ SUPERCLASS ~ cli }..................
  104. def _do(self) -> object:
  105. '''
  106. Implement this command-line interface (CLI).
  107. If a subcommand was passed, this method runs this subcommand and returns
  108. the result of doing so; else, this method prints help output and returns
  109. the current instance of this object.
  110. '''
  111. # If no subcommand was passed...
  112. if not self._args.subcommand_name_top:
  113. #,Print help output. Note that this common case constitutes neither
  114. # a fatal error nor a non-fatal warning.
  115. print()
  116. self._arg_parser.print_help()
  117. # Return the current instance of this object. While trivial, this
  118. # behaviour simplifies memory profiling of this object.
  119. return self
  120. # Else, a subcommand was passed.
  121. #
  122. # Sanitized name of this subcommand.
  123. subcommand_name_top = pyident.sanitize_snakecase(
  124. self._args.subcommand_name_top)
  125. # Name of the method running this subcommand.
  126. subcommand_method_name = '_do_' + subcommand_name_top
  127. # Method running this subcommand. If this method does *NOT* exist,
  128. # get_method() will raise a non-human-readable exception. Usually, that
  129. # would be bad. In this case, however, argument parsing coupled with a
  130. # reliable class implementation guarantees this method to exist.
  131. subcommand_method = objects.get_method(
  132. obj=self, method_name=subcommand_method_name)
  133. # Run this subcommand and return the result of doing so (if any).
  134. return subcommand_method()
  135. # ..................{ SUBCOMMANDS_TOP ~ info }..................
  136. def _show_header(self) -> None:
  137. logs.log_info(cliinfo.get_header())
  138. def _do_info(self) -> None:
  139. '''
  140. Run the ``info`` subcommand.
  141. '''
  142. cliinfo.log_info()
  143. # ..................{ SUBCOMMANDS_TOP ~ sim }..................
  144. def _do_try(self) -> object:
  145. '''
  146. Run the ``try`` subcommand and return the result of doing so.
  147. '''
  148. # Basename of the sample configuration file to be created.
  149. config_basename = 'sample_sim.yaml'
  150. # Relative path of this file, relative to the current directory.
  151. self._args.conf_filename = pathnames.join(
  152. 'sample_sim', config_basename)
  153. #FIXME: Insufficient. We only want to reuse this file if this file's
  154. #version is identical to that of the default YAML configuration file's
  155. #version. Hence, this logic should (arguably) be shifted elsewhere --
  156. #probably into "betse.science.sim_config".
  157. # If this file already exists, reuse this file.
  158. if files.is_file(self._args.conf_filename):
  159. logs.log_info(
  160. 'Reusing simulation configuration "%s".', config_basename)
  161. # Else, create this file.
  162. else:
  163. self._do_config()
  164. # Run all general-purposes phases, thus excluding network-specific
  165. # phases (e.g., "_do_sim_brn", "_do_sim_grn"), in the expected order.
  166. self._do_seed()
  167. self._do_init()
  168. self._do_sim()
  169. self._do_plot_seed()
  170. self._do_plot_init()
  171. # Return the value returned by the last such phase, permitting this
  172. # subcommand to be memory profiled. While any value would technically
  173. # suffice, the value returned by the last such phase corresponds to a
  174. # complete simulation run and hence is likely to consume maximal memory.
  175. return self._do_plot_sim()
  176. def _do_config(self) -> None:
  177. '''
  178. Run the ``config`` subcommand.
  179. '''
  180. # Avoid importing modules importing dependencies at the top level.
  181. from betse.science.config import confio
  182. confio.write_default(self._args.conf_filename)
  183. def _do_seed(self) -> object:
  184. '''
  185. Run the ``seed`` subcommand and return the result of doing so.
  186. '''
  187. return self._sim_runner.seed()
  188. def _do_init(self) -> object:
  189. '''
  190. Run the ``init`` subcommand and return the result of doing so.
  191. '''
  192. return self._sim_runner.init()
  193. def _do_sim(self) -> object:
  194. '''
  195. Run the ``sim`` subcommand and return the result of doing so.
  196. '''
  197. return self._sim_runner.sim()
  198. def _do_sim_brn(self) -> object:
  199. '''
  200. Run the ``sim-brn`` subcommand and return the result of doing so.
  201. '''
  202. return self._sim_runner.sim_brn()
  203. def _do_sim_grn(self) -> object:
  204. '''
  205. Run the ``sim-grn`` subcommand and return the result of doing so.
  206. '''
  207. return self._sim_runner.sim_grn()
  208. def _do_plot(self) -> object:
  209. '''
  210. Run the ``plot`` subcommand and return the result of doing so.
  211. '''
  212. # If no subcommand was passed, print help output and return. See the
  213. # _do() method for similar logic and commentary.
  214. if not self._args.subcommand_name_plot:
  215. print()
  216. self._arg_parser_plot.print_help()
  217. return
  218. # Run this subcommand's subcommand and return the result of doing so.
  219. # See the _run() method for similar logic and commentary.
  220. subcommand_name_plot = pyident.sanitize_snakecase(
  221. self._args.subcommand_name_plot)
  222. subcommand_method_name = '_do_plot_' + subcommand_name_plot
  223. subcommand_method = getattr(self, subcommand_method_name)
  224. return subcommand_method()
  225. def _do_plot_seed(self) -> object:
  226. '''
  227. Run the ``plot`` subcommand's ``seed`` subcommand and return the result
  228. of doing so.
  229. '''
  230. return self._sim_runner.plot_seed()
  231. def _do_plot_init(self) -> object:
  232. '''
  233. Run the ``plot`` subcommand's ``init`` subcommand and return the result
  234. of doing so.
  235. '''
  236. return self._sim_runner.plot_init()
  237. def _do_plot_sim(self) -> object:
  238. '''
  239. Run the ``plot`` subcommand's ``sim`` subcommand and return the result
  240. of doing so.
  241. '''
  242. return self._sim_runner.plot_sim()
  243. def _do_plot_sim_brn(self) -> object:
  244. '''
  245. Run the ``plot`` subcommand's ``sim-brn`` subcommand and return the
  246. result of doing so.
  247. '''
  248. return self._sim_runner.plot_brn()
  249. def _do_plot_sim_grn(self) -> object:
  250. '''
  251. Run the ``plot`` subcommand's ``sim-grn`` subcommand and return the
  252. result of doing so.
  253. '''
  254. return self._sim_runner.plot_grn()
  255. def _do_repl(self) -> None:
  256. '''
  257. Run the ``repl`` subcommand.
  258. '''
  259. # In the unlikely edge-case of the "repl" subcommand being erroneously
  260. # run by a functional test, prohibit this by raising an exception.
  261. # Permitting this would probably cause tests to indefinitely hang.
  262. if pys.is_testing():
  263. from betse.exceptions import BetseTestException
  264. raise BetseTestException(
  265. 'REPL unavailable during testing for safety.')
  266. # Defer heavyweight imports until *AFTER* possibly failing above.
  267. from betse.cli.repl import repls
  268. # Start the desired REPL.
  269. repls.start_repl()
  270. # ..................{ GETTERS }..................
  271. @property_cached
  272. def _sim_runner(self):
  273. '''
  274. Simulation runner preconfigured with sane defaults.
  275. '''
  276. # Defer heavyweight imports.
  277. from betse.science.simrunner import SimRunner
  278. # Return this runner.
  279. return SimRunner(conf_filename=self._args.conf_filename)