def _log_metadata(self) -> None: ''' Log low-level metadata pertaining to the current application. Specifically, this method logs (in no particular order): * The subclass of the currently registered application singleton. * The enabling of the default segmentation fault handler. * Whether a testing environment is detected. * Whether a headless environment is detected. ''' # Avoid circular import dependencies. from betse.util.app.meta import appmetaone from betse.util.os import displays from betse.util.test import tsttest from betse.util.type.obj import objects # Log this metadata. logs.log_debug( 'Application singleton "%s" established.', objects.get_class_name_unqualified(appmetaone.get_app_meta())) logs.log_debug( 'Default segmentation fault handler enabled.') logs.log_debug( 'Testing environment detected: %r', tsttest.is_testing()) logs.log_debug( 'Headless environment detected: %r', displays.is_headless())
def die_if_empty( sequence: SequenceTypes, exception_message: StrOrNoneTypes = None) -> None: ''' Raise an exception with the passed message (defaulting to a human-readable message) if the passed sequence is **empty** (i.e., contains *no* items). Parameters ---------- sequence: SequenceTypes Sequence to be validated. exception_message : StrOrNoneTypes Exception message to be raised. Defaults to ``None``, in which case an exception message synthesized from the passed arguments is raised. Raises ---------- BetseSequenceException If the sequence is empty. ''' # Avoid circular import dependencies. from betse.util.type.obj import objects # If this sequence is empty, raise an exception. if is_empty(sequence): # If no exception message was passed, synthesize one. if not exception_message: exception_message = 'Sequence "{}" empty.'.format( objects.get_class_name_unqualified(sequence)) # Raise this exception. raise BetseSequenceException(exception_message)
def die_unless_has_method(obj: object, *method_names: str) -> None: ''' Raise an exception unless the passed object defines all methods with the passed names. Parameters ---------- obj : object Object to be validated. method_names : tuple[str] Tuple of the names of all methods to validate this object to define. Raises ---------- BetseMethodException If one or more such methods are *not* bound to this object. ''' # Avoid circular import dependencies. from betse.util.type.obj import objects # If this object fails to define one or more of these methods... if not has_method(obj, *method_names): # For the name of each such method... for method_name in method_names: # If this object fails to define a method with this name, raise an # exception describing this failure. if not has_method(obj, method_name): raise BetseMethodException('Method "{}.{}" undefined.'.format( objects.get_class_name_unqualified(obj), method_name))
def get_enum_name_from_member(enum_member: EnumMemberType) -> str: ''' Unqualified name of the enumeration class to which the passed enumeration member belongs (e.g., ``SolverType`` given a member ``SolverType.FULL``). Parameters ---------- enum_member : EnumMemberType Enumeration member to be inspected. Returns ---------- str Unqualified name of this member's enumeration class. ''' # Avoid circular import dependencies. from betse.util.type.obj import objects # Strange, but true. We don't ask questions. Neither should you. return objects.get_class_name_unqualified(enum_member)
def iter_runners_enabled(self, phase: SimPhase) -> GeneratorType: ''' Generator yielding the method and simulation configuration of each **enabled simulation pipeline runner** (i.e., method bound to this pipeline decorated by the :func:`piperunner` decorator *and* enabled by this simulation configuration) for the passed simulation phase. If this simulation phase disables this pipeline, this generator reduces to the empty generator (i.e., yields no values); else, this generator excludes: * All methods defined by this pipeline subclass *not* decorated by the :func:`piperunner` decorator. * All methods defined by this pipeline subclass decorated by that decorator currently disabled by their simulation configuration (e.g., such that the YAML-backed value of the ``enabled`` key in this configuration evaluates to ``False``). Parameters ---------- phase : SimPhase Current simulation phase. Yields ---------- (runner_method : MethodType, runner_conf : SimConfExportABC) 2-tuple where: * ``runner_method`` is the method implementing this runner, whose method name is guaranteed to be prefixed by the substring :attr:`_RUNNER_METHOD_NAME_PREFIX`. Note that this method object defines the following custom instance variables: * ``metadata``, whose value is an instance of the :class:`SimPipeRunnerMetadata` class. * ``runner_conf`` is the YAML-backed list item configuring this runner, derived from the YAML-formatted simulation configuration file associated with the passed simulation phase. Raises ---------- BetseSimPipeException If any simulation runner configured for this pipeline by the passed simulation phase is **unrecognized** (i.e., if this pipeline defines no corresponding method). ''' # If this pipeline is disabled, log this fact and return immediately. if not self._is_enabled(phase): logs.log_debug('Ignoring disabled %s...', self._noun_plural_lowercase) return # Else, this pipeline is enabled. # For each runner configuration specified for this pipeline... for runner_conf in self.iter_runners_conf(phase): # If this configuration is *NOT* YAML-backed, raise an exception. objtest.die_unless_instance(obj=runner_conf, cls=SimConfExportABC) # If this runner is disabled, log this fact and ignore this runner. if not runner_conf.is_enabled: logs.log_debug('Ignoring disabled %s "%s"...', self._noun_singular_lowercase, runner_conf.kind) continue # Else, this runner is enabled. # Name of the pipeline method implementing this runner. runner_method_name = (self._RUNNER_METHOD_NAME_PREFIX + runner_conf.kind) # Method running this runner if recognized *OR* "None" otherwise. runner_method = objects.get_callable_or_none( obj=self, callable_name=runner_method_name) # If this runner is unrecognized, raise an exception. if runner_method is None: raise BetseSimPipeException( '{} "{}" unrecognized ' '(i.e., method {}.{}() not found).'.format( self._noun_singular_uppercase, runner_conf.kind, objects.get_class_name_unqualified(self), runner_method_name)) # Else, this runner is recognized. # Yield a 2-tuple of this runner's method and configuration. yield runner_method, runner_conf
def init_sans_libs(self) -> None: ''' Initialize this application *except* mandatory third-party dependencies of this application, which requires external resources (e.g., command-line options, configuration files) to have been parsed. Specifically, the default implementation of this method (in order): #. Enables Python's standard handler for segmentation faults. #. Globalizes the passed application metadata singleton. #. Enables this application's default logging configuration. #. Validates but does *not* initialize all mandatory third-party dependencies of this application, which the :meth:`init_libs` method does so subsequently. #. Validates core directories and files required at program startup, creating all such directories and files that do *not* already exist and are reasonably creatable. #. Validates the active Python interpreter (e.g., to support multithreading). #. Validates the underlying operating system (e.g., to *not* be a vanilla Windows shell environment ala either CMD.exe or PowerShell). To support caller-specific error handling, this function is intended to be called immediately *after* this application begins catching otherwise uncaught exceptions. Caveats ---------- **This method is idempotent.** This method has been explicitly designed to be safely recallable. Each subsequent invocation of this method following the first simply reinitializes this application. While typically useless, idempotency is required by low-level automation to guarantee consistency across repeated runs (e.g., tests, scripts). Design ---------- This method intentionally avoids initializing mandatory dependencies, as doing so would require late-time startup logic assumed by such initiazilation to have already been performed -- namely, finalization of the logging configuration and hence command-line argument parsing. By compare, this method is internally called by the :meth:`__init__` method called as the first statement of this application. Since no startup logic has been performed yet, initialization of dependencies is deferred until significantly later in the startup process. ''' # Avoid circular import dependencies. from betse.lib import libs from betse.util.io.error import errfault from betse.util.io.log import logconfig, logs from betse.util.os import oses from betse.util.py import pys from betse.util.test import tests from betse.util.type.obj import objects # Enable Python's standard handler for segmentation faults *BEFORE* # performing any further logic, any of which could conceivably trigger # a segmentation fault and hence process termination. errfault.handle_faults() # Enable our default logging configuration for the current Python # process *BEFORE* performing any validation, thus logging any # exceptions raised by this validation. logconfig.init() # Log all prior behaviour. Attempting to do so *BEFORE* enabling our # default logging configuration above would silently fail, since the # standard "logging" API silently squelches debug messages by default. logs.log_debug('Application singleton "%s" established.', objects.get_class_name_unqualified((self))) logs.log_debug('Default segementation fault handler enabled.') logs.log_debug('Testing environment detected: %r', tests.is_testing()) # Validate mandatory dependencies. Avoid initializing these # dependencies now (e.g., by calling init_libs()). Doing so requires # finalization of the logging configuration (e.g., by parsing CLI # options), which has yet to happen this early in the lifecycle. libs.die_unless_runtime_mandatory_all() # Validate the active Python interpreter and operating system *AFTER* # mandatory dependencies. While the former (mostly) comprises # unenforced recommendations, the latter comprises enforced # requirements and should thus be validated first. oses.init() pys.init()