PageRenderTime 50ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 0ms

/betse_test/fixture/simconf/simconfclser.py

https://gitlab.com/betse/betse
Python | 259 lines | 46 code | 30 blank | 183 comment | 0 complexity | 19c651cae6cb3b54fd6d8f024c89b505 MD5 | raw file
  1. #!/usr/bin/env python3
  2. # --------------------( LICENSE )--------------------
  3. # Copyright 2014-2022 by Alexis Pietak & Cecil Curry.
  4. # See "LICENSE" for further details.
  5. '''
  6. Fixture classes encapsulating test-related simulation configurations.
  7. '''
  8. #FIXME: Most use of the increasingly obsolete "SimConfTestInternal.config"
  9. #wrapper attribute (both here and everywhere else) should be replaced by use of
  10. #the new "SimConfTestInternal.p" property, which increasingly provides all test
  11. #functionality.
  12. # ....................{ IMPORTS }....................
  13. from abc import ABCMeta
  14. from betse.util.type.decorator.deccls import abstractproperty
  15. from betse.util.type.types import type_check
  16. from py._path.local import LocalPath
  17. # ....................{ SUPERCLASSES }....................
  18. class SimConfTestABC(object, metaclass=ABCMeta):
  19. '''
  20. Abstract base class of all simulation configuration context subclasses,
  21. each encapsulating state and metadata for a test-related simulation
  22. configuration.
  23. Simulation configuration fixtures typically return instances of this class
  24. as a means of communicating this context to other fixtures and tests.
  25. Attributes
  26. ----------
  27. conf_dirname : str
  28. Absolute dirname of the directory of the simulation configuration file
  29. wrapped by this wrapper (i.e., of :attr:`conf_filename`).
  30. conf_filename : str
  31. Absolute filename of the simulation configuration file wrapped by this
  32. wrapper.
  33. See Also
  34. ----------
  35. https://py.readthedocs.org/en/latest/path.html
  36. Official :mod:`py.path` submodule documentation.
  37. '''
  38. # ..................{ INITIALIZERS }..................
  39. @type_check
  40. def __init__(self, conf_filename: str) -> None:
  41. '''
  42. Initialize this simulation configuration context.
  43. Parameters
  44. ----------
  45. conf_filename : str
  46. Absolute filename of the simulation configuration file encapsulated
  47. by this context. Although this file need *not* physically exist
  48. before this object is instantiated (i.e., before this method is
  49. called), the subclass should ensure this file exists once this
  50. object is instantiated (i.e., immediately before the subclass
  51. implementation of this method returns).
  52. '''
  53. # Defer heavyweight imports.
  54. from betse.util.path import pathnames
  55. # Classify all passed parameters publicly, permitting access by tests.
  56. self.conf_filename = conf_filename
  57. self.conf_dirname = pathnames.get_dirname(self.conf_filename)
  58. # ..................{ CONTEXTS }..................
  59. def context(self) -> 'contextlib.contextmanager':
  60. '''
  61. Context manager changing the current working directory (CWD) of the
  62. current test to the directory containing this configuration file for
  63. the duration of this context.
  64. Default simulation configuration paths are relative to the directory
  65. containing the simulation configuration file: namely, this temporary
  66. directory. Changing directories resolves these paths to this directory.
  67. (Failing to do so would incorrectly resolve these paths to the current
  68. directory with predictably disastrous outcomes.) While this class could
  69. instead globally search-and-replace all relative simulation
  70. configuration paths with absolute paths, doing so would be considerably
  71. more complex, fragile, and error-prone than changing directories.
  72. '''
  73. # Defer heavyweight imports.
  74. from betse.util.os.shell import shelldir
  75. # Defer to the generator returned by the following utility function.
  76. return shelldir.setting_cwd(self.conf_dirname)
  77. # ..................{ SUBCLASS }..................
  78. # Subclasses are required to define the following read-only properties.
  79. @abstractproperty
  80. def p(self) -> 'betse.science.parameters.Parameters':
  81. '''
  82. High-level simulation configuration encapsulated by this test wrapper.
  83. '''
  84. pass
  85. # ....................{ SUBCLASSES }....................
  86. class SimConfTestInternal(SimConfTestABC):
  87. '''
  88. Simulation configuration context subclass encapsulating a temporary
  89. simulation configuration file internally isolated to the current test.
  90. Since this configuration is guaranteed to be test-specific and hence safely
  91. modifiable, caller fixtures and tests are advised to modify the contents of
  92. this configuration (e.g., to exercise specific feature sets and edge
  93. cases).
  94. Attributes
  95. ----------
  96. config : SimConfigWrapper
  97. Simulation configuration wrapper wrapping the low-level dictionary
  98. deserialized from the YAML-formatted simulation configuration file with
  99. path :attr:`conf_filepath`. Note the contents of this dictionary may be
  100. desynchronized from those of this file. For efficiency, callers may
  101. modify this dictionary to suite test requirements *before*
  102. reserializing this dictionary back to this file.
  103. conf_filepath : LocalPath
  104. Absolute filename of a temporary simulation configuration file specific
  105. to the parent fixture as a :class:`py.path.local` instance, defining an
  106. object-oriented superset of the non-object-oriented :mod:`os.path`
  107. module.
  108. See Also
  109. ----------
  110. https://py.readthedocs.org/en/latest/path.html
  111. Official :mod:`py.path` submodule documentation.
  112. '''
  113. # ..................{ INITIALIZERS }..................
  114. @type_check
  115. def __init__(
  116. self,
  117. src_conf_filename: str,
  118. trg_conf_filepath: LocalPath,
  119. ) -> None:
  120. '''
  121. Initialize this context by copying the passed source to target
  122. simulation configuration file.
  123. Specifically, this method (in order):
  124. #. Copies the passed source to target simulation configuration file.
  125. #. Copies *all* external resources (e.g., image files) referenced and
  126. hence required by the passed source simulation configuration file to
  127. the directory of the passed target simulation configuration file.
  128. #. Sanitizes the copied simulation configuration file for all child
  129. fixtures and tests by unconditionally disabling options either
  130. requiring interactive input *or* displaying interactive output.
  131. This method does *not* minimize the space and time costs of running
  132. simulations configured by this configuration. Doing so is typically
  133. desirable but can obscure edge-case issues, including computational
  134. instability produced by the default non-minified time steps.
  135. Caveats
  136. ----------
  137. For efficiency, note that the configuration changes applied by this
  138. method (listed above) reside *only* in-memory; they have yet to be
  139. written back to disk. Callers are required to do so manually (e.g., via
  140. the optional ``is_overwrite_conf`` parameter passed to the
  141. :meth:`CLISimTester.run_subcommands` method).
  142. Parameters
  143. ----------
  144. src_conf_filename : str
  145. Absolute filename of the source simulation configuration file from
  146. which this method safely copies the passed target simulation
  147. configuration file to.
  148. trg_conf_filepath : LocalPath
  149. Absolute filename of the target simulation configuration file to
  150. which this method safely copies both the passed source simulation
  151. configuration file *and* all external resources (e.g., image files)
  152. referenced and hence required by that file. For compliance with
  153. :mod:`pytest` internals, this filename is a high-level
  154. :class:`py.path.local` instance rather than a low-level string.
  155. Raises
  156. ----------
  157. BetseDirException
  158. If any external file required by this target file already exists.
  159. BetseFileException
  160. If this target file already exists.
  161. '''
  162. # Defer heavyweight imports. Notably, the "simconfwrapper" submodule
  163. # inherits from the main codebase and is thus *NOT* safely importable
  164. # at module scope.
  165. from betse.science.parameters import Parameters
  166. from betse_test.fixture.simconf.simconfwrapper import (
  167. SimConfigTestWrapper)
  168. # Absolute filename of this file.
  169. trg_conf_filename = str(trg_conf_filepath)
  170. # Initialize our superclass with this filename.
  171. super().__init__(conf_filename=trg_conf_filename)
  172. # Classify the passed parameters. While the "self.config" object
  173. # classified below provides this filename as a low-level string, this
  174. # high-level "py.path.local" instance is useful in fixtures and tests.
  175. self.conf_filepath = trg_conf_filepath
  176. # Copy this source to target file.
  177. p = Parameters()
  178. p.copy(
  179. src_conf_filename=src_conf_filename,
  180. trg_conf_filename=trg_conf_filename)
  181. # Test-specific wrapper encapsulating this file.
  182. self.config = SimConfigTestWrapper(p)
  183. # Sanitize this configuration for all child fixtures and tests.
  184. self.config.disable_interaction()
  185. # ..................{ SUPERCLASS }..................
  186. @property
  187. def p(self) -> 'betse.science.parameters.Parameters':
  188. return self.config.p
  189. class SimConfTestExternal(SimConfTestABC):
  190. '''
  191. Simulation configuration context subclass encapsulating an external
  192. simulation configuration file residing at any directory and hence *not*
  193. necessarily isolated to the current test.
  194. Attributes
  195. ----------
  196. _p : Parameters
  197. High-level simulation configuration encapsulating this external
  198. simulation configuration file.
  199. '''
  200. # ..................{ INITIALIZERS }..................
  201. def __init__(self, *args, **kwargs) -> None:
  202. '''
  203. Initialize this simulation configuration context.
  204. '''
  205. # Defer heavyweight imports.
  206. from betse.science.parameters import Parameters
  207. # Initialize our superclass with all passed parameters.
  208. super().__init__(*args, **kwargs)
  209. # In-memory simulation configuration deserialized from this file.
  210. self._p = Parameters.make(conf_filename=self.conf_filename)
  211. # ..................{ SUPERCLASS }..................
  212. @property
  213. def p(self) -> 'betse.science.parameters.Parameters':
  214. return self._p