text: # Copyright 2019 Red Hat
  #
  #    Licensed under the Apache License, Version 2.0 (the "License"); you may
  #    not use this file except in compliance with the License. You may obtain
  #    a copy of the License at
  #
  #         http://www.apache.org/licenses/LICENSE-2.0
  #
  #    Unless required by applicable law or agreed to in writing, software
  #    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  #    License for the specific language governing permissions and limitations
  #    under the License.
  from __future__ import absolute_import
  
  import inspect
  
  from oslo_log import log
  import testtools
  
  from tobiko.common import _exception
  from tobiko.common import _fixture
  from tobiko.common import _skip
  
  
  LOG = log.getLogger(__name__)
  
  
  def runs_operation(operation_object):
      return RunsOperationProperty(operation_object)
  
  
  class RunsOperations(object):
  
      def setUp(self):
          super(RunsOperations, self).setUp()  # pylint: disable=no-member
          for operation_name in get_runs_operation_names(self):
              get_operation(operation_name)
  
  
  def add_runs_operation_name(obj, name):
      required_names = getattr(obj, '__tobiko_runs_operations__', None)
      if not isinstance(required_names, set):
          if required_names:
              required_names = set(required_names)
          else:
              required_names = set()
          # cache list for later use
          obj.__tobiko_runs_operations__ = required_names
      required_names.add(name)
  
  
  def get_runs_operation_names(obj):
      '''Get operation names required by given :param obj:'''
  
      name, obj = _fixture.get_name_and_object(obj)
  
      operations = getattr(obj, '__tobiko_runs_operations__', set())
      if isinstance(operations, tuple):
          return operations
  
      try:
          # froze required names to avoid re-discovering them again
          obj.__tobiko_runs_operations__ = tuple()
      except AttributeError:
          # ignore this object because not defined by Python code
          return tuple()
  
      LOG.debug('Discover operations for object %r', name)
      if operations:
          operations = set(operations)
  
      if inspect.ismethod(obj) or inspect.isfunction(obj):
          pass  # decorated functions have __tobiko_runs_operations__ defined
  
      else:
          if not inspect.isclass(obj):
              obj = type(obj)
          operations.update(get_runs_operation_names_from_class(obj))
  
      # Return every operation in alphabetical order and
      # froze required names to avoid re-discovering them again
      operations = tuple(sorted(operations))
      obj.__tobiko_runs_operations__ = operations
      return operations
  
  
  def get_runs_operation_names_from_class(cls):
      """Get list of members of type RequiredFixtureProperty of given class"""
  
      # inspect.getmembers() would iterate over such many testtools.TestCase
      # members too, so let exclude members from those very common base classes
      # that we know doesn't have members of type RunsOperationProperty
      base_classes = cls.__mro__
      for base_class in [testtools.TestCase, _fixture.SharedFixture]:
          if issubclass(cls, base_class):
              base_classes = base_classes[:base_classes.index(base_class)]
              break
  
      # Get all members for selected class without calling properties or methods
      members = {}
      for base_class in reversed(base_classes):
          members.update(base_class.__dict__)
  
      # Return all member operation names
      operations = set()
      for name, member in sorted(members.items()):
          if not name.startswith('__'):
              operations.update(get_runs_operation_names(obj=member))
      return operations
  
  
  class RunsOperationProperty(object):
  
      def __init__(self, obj):
          self.name, self.obj = _fixture.get_name_and_object(obj)
  
      @property
      def __tobiko_runs_operations__(self):
          return (self.name,)
  
      def before_operation(self, obj):
  
          def is_before():
              return self.get_operation().is_before
  
          decorator = _skip.skip_unless(f"Before operation {self.name}",
                                        is_before)
  
          return self.with_operation(decorator(obj))
  
      def after_operation(self, obj):
  
          def is_after():
              return self.get_operation().is_after
  
          decorator = _skip.skip_unless(f"After operation {self.name}",
                                        is_after)
  
          return self.with_operation(decorator(obj))
  
      def with_operation(self, obj):
          add_runs_operation_name(obj, self.name)
          return obj
  
      def __get__(self, instance, _):
          if instance is None:
              return self
          else:
              return self.get_operation()
  
      def get_operation(self):
          return get_operation(self.obj)
  
  
  def before_operation(obj):
      return RunsOperationProperty(obj).before_operation
  
  
  def after_operation(obj):
      return RunsOperationProperty(obj).after_operation
  
  
  def with_operation(obj):
      return RunsOperationProperty(obj).with_operation
  
  
  def get_operation(obj):
      operation = _fixture.get_fixture(obj)
      _exception.check_valid_type(operation, Operation)
  
      if operation.is_before and operation_config().run_operations:
          operation = _fixture.setup_fixture(obj)
  
      return operation
  
  
  def get_operation_name(obj):
      return _fixture.get_object_name(obj)
  
  
  def operation_config():
      return _fixture.setup_fixture(OperationsConfigFixture)
  
  
  class OperationsConfigFixture(_fixture.SharedFixture):
  
      after_operations = None
      run_operations = None
  
      def setup_fixture(self):
          from tobiko import config
          self.after_operations = set(
              config.get_list_env('TOBIKO_AFTER_OPERATIONS'))
          self.run_operations = config.get_bool_env(
              'TOBIKO_RUN_OPERATIONS') or False
  
  
  class Operation(_fixture.SharedFixture):
  
      @property
      def operation_name(self):
          return _fixture.get_fixture_name(self)
  
      @property
      def is_before(self):
          return not self.is_after
  
      _is_after = None
  
      @property
      def is_after(self):
          is_after = self._is_after
          if is_after is None:
              config = operation_config()
              is_after = self.operation_name in config.after_operations
              self._is_after = is_after
          return is_after
  
      def setup_fixture(self):
          if not self.is_after:
              LOG.debug('Executing operation: %r', self.operation_name)
              try:
                  self.run_operation()
              except Exception as ex:
                  LOG.debug('Operation %r failed: %r', self.operation_name, ex)
                  raise
              else:
                  LOG.debug('Operation executed: %r', self.operation_name)
              finally:
                  self._is_after = True
  
      def run_operation(self):
          raise NotImplementedError
  