def extend(self, cls): '''Extend the parameter space with the local parameter space.''' local_param_space = getattr(cls, self.local_namespace_name, dict()) for name, p in local_param_space.items(): try: filt_vals = p.filter_params(self.params.get(name, ())) except Exception: raise else: try: self.params[name] = (tuple(filt_vals) + p.values) except TypeError: raise ReframeSyntaxError( f"'filter_param' must return an iterable " f"(parameter {name!r})") from None # Clear the local param space local_param_space.clear() # If any previously declared parameter was defined in the class body # by directly assigning it a value, raise an error. Parameters must be # changed using the `x = parameter([...])` syntax. for key, values in cls.__dict__.items(): if key in self.params: raise ReframeSyntaxError( f'parameter {key!r} must be modified through the built-in ' f'parameter type')
def _expand_partitions_envs(self, obj): '''Process the partitions and programming environs of the parent.''' try: part = tuple(obj.valid_systems) except AttributeError: raise ReframeSyntaxError( f"'valid_systems' is undefined in test {obj.name}") else: rt = runtime.runtime() if '*' in part or rt.system.name in part: part = tuple(p.fullname for p in rt.system.partitions) try: prog_envs = tuple(obj.valid_prog_environs) except AttributeError: raise ReframeSyntaxError( f"'valid_prog_environs' is undefined in test {obj.name}") else: if '*' in prog_envs: all_pes = set() for p in runtime.runtime().system.partitions: for e in p.environs: all_pes.add(e.name) prog_envs = tuple(all_pes) return part, prog_envs
def __setitem__(self, key, value): if isinstance(value, variables.TestVar): # Insert the attribute in the variable namespace try: self['_rfm_local_var_space'][key] = value value.__set_name__(self, key) except KeyError: raise ReframeSyntaxError( f'variable {key!r} is already declared') from None # Override the regular class attribute (if present) and return self._namespace.pop(key, None) return elif isinstance(value, parameters.TestParam): # Insert the attribute in the parameter namespace try: self['_rfm_local_param_space'][key] = value except KeyError: raise ReframeSyntaxError( f'parameter {key!r} is already declared in this class' ) from None # Override the regular class attribute (if present) and return self._namespace.pop(key, None) return elif key in self['_rfm_local_param_space']: raise ReframeSyntaxError(f'cannot override parameter {key!r}') else: # Insert the items manually to overide the namespace clash # check from the base namespace. self._namespace[key] = value # Register functions decorated with either @sanity_function or # @performance_variables or @performance_function decorators. if hasattr(value, '_rfm_sanity_fn'): try: super().__setitem__('_rfm_sanity', value) except KeyError: raise ReframeSyntaxError( 'the @sanity_function decorator can only be used ' 'once in the class body') from None elif hasattr(value, '_rfm_perf_key'): try: self['_rfm_perf_fns'][key] = value except KeyError: raise ReframeSyntaxError( f'the performance function {key!r} has already been ' f'defined in this class') from None # Register the final methods if hasattr(value, '_rfm_final'): self['_rfm_final_methods'].add(key) # Register the hooks - if a value does not meet the conditions # it will be simply ignored self['_rfm_hook_registry'].add(value)
def join(self, other, cls): '''Join other parameter space into the current one. Join two different parameter spaces into a single one. Both parameter spaces must be an instance ot the ParamSpace class. This method will raise an error if a parameter is defined in the two parameter spaces to be merged. :param other: instance of the ParamSpace class. :param cls: the target class. ''' for name in other.params: # With multiple inheritance, a single parameter # could be doubly defined and lead to repeated # values if self.defines(name) and other.defines(name): raise ReframeSyntaxError( f'parameter space conflict: ' f'parameter {name!r} is defined in more than ' f'one base class of class {cls.__qualname__!r}' ) if not self.defines(name): # If we do not define the parameter, take it from other self.params[name] = other.params[name]
def __setattr__(cls, name, value): '''Handle the special treatment required for variables and parameters. A variable's default value can be updated when accessed as a regular class attribute. This behavior does not apply when the assigned value is a descriptor object. In that case, the task of setting the value is delegated to the base :func:`__setattr__` (this is to comply with standard Python behavior). However, since the variables are already descriptors which are injected during class instantiation, we disallow any attempt to override this descriptor (since it would be silently re-overridden in any case). Altering the value of a parameter when accessed as a class attribute is not allowed. This would break the parameter space internals. ''' # Try to treat `name` as variable if cls.setvar(name, value): return # Try to treat `name` as a parameter try: # Catch attempts to override a test parameter param_space = super().__getattribute__('_rfm_param_space') if name in param_space.params: raise ReframeSyntaxError(f'cannot override parameter {name!r}') except AttributeError: '''Catch early access attempt to the parameter space.''' # Treat `name` as normal class attribute super().__setattr__(name, value)
def run_after(stage): '''Decorator for attaching a test method to a pipeline stage. .. deprecated:: 3.7.0 Please use the :func:`~reframe.core.pipeline.RegressionMixin.run_after` built-in function. ''' warn.user_deprecation_warning( 'using the @rfm.run_after decorator from the rfm module is ' 'deprecated; please use the built-in decorator @run_after instead.', from_version='3.7.0' ) if stage not in _USER_PIPELINE_STAGES: raise ReframeSyntaxError( f'invalid pipeline stage specified: {stage!r}') # Map user stage names to the actual pipeline functions if needed if stage == 'init': stage = '__init__' elif stage == 'compile': stage = 'compile_wait' elif stage == 'run': stage = 'run_wait' return hooks.attach_to('post_' + stage)
def setvar(cls, name, value): '''Set the value of a variable. :param name: The name of the variable. :param value: The value of the variable. :returns: :class:`True` if the variable was set. A variable will *not* be set, if it does not exist or when an attempt is made to set it with its underlying descriptor. This happens during the variable injection time and it should be delegated to the class' :func:`__setattr__` method. :raises ReframeSyntaxError: If an attempt is made to override a variable with a descriptor other than its underlying one. ''' try: var_space = super().__getattribute__('_rfm_var_space') if name in var_space: if not hasattr(value, '__get__'): var_space[name].define(value) return True elif var_space[name].field is not value: desc = '.'.join([cls.__qualname__, name]) raise ReframeSyntaxError( f'cannot override variable descriptor {desc!r}') else: # Variable is being injected return False except AttributeError: '''Catch early access attempt to the variable space.''' return False
def join(self, other, cls): '''Join other parameter space into the current one. Join two different parameter spaces into a single one. Both parameter spaces must be an instance ot the ParamSpace class. This method will raise an error if a parameter is defined in the two parameter spaces to be merged. :param other: instance of the ParamSpace class. :param cls: the target class. ''' for key in other.params: # With multiple inheritance, a single parameter # could be doubly defined and lead to repeated # values if (key in self.params and self.params[key] != () and other.params[key] != ()): raise ReframeSyntaxError( f'parameter space conflict: ' f'parameter {key!r} is defined in more than ' f'one base class of class {cls.__qualname__!r}') self.params[key] = (other.params.get(key, ()) + self.params.get(key, ()))
def _validate_test(cls): if not issubclass(cls, RegressionTest): raise ReframeSyntaxError('the decorated class must be a ' 'subclass of RegressionTest') if (cls.is_abstract()): raise ValueError(f'decorated test ({cls.__qualname__!r}) has one or ' f'more undefined parameters')
def _validate_test(cls): if not issubclass(cls, RegressionTest): raise ReframeSyntaxError('the decorated class must be a ' 'subclass of RegressionTest') if (cls.is_abstract()): raise ValueError(f'decorated test ({cls.__qualname__!r}) is an' f' abstract test')
def extend(self, cls): '''Extend the VarSpace with the content in the LocalVarSpace. Merge the VarSpace inherited from the base classes with the LocalVarSpace. Note that the LocalVarSpace can also contain define and undefine actions on existing vars. Thus, since it does not make sense to define and undefine a var in the same class, the order on which the define and undefine functions are called is not preserved. In fact, applying more than one of these actions on the same var for the same local var space is disallowed. ''' local_varspace = getattr(cls, self.local_namespace_name) for key, var in local_varspace.items(): if isinstance(var, TestVar): # Disable redeclaring a variable if key in self.vars: raise ReframeSyntaxError( f'cannot redeclare the variable {key!r}' ) # Add a new var self.vars[key] = var # If any previously declared variable was defined in the class body # by directly assigning it a value, retrieve this value from the class # namespace and update it into the variable space. _assigned_vars = set() for key, value in cls.__dict__.items(): if key in self.vars: self.vars[key].define(value) _assigned_vars.add(key) elif value is Undefined: # Cannot be set as Undefined if not a variable raise ReframeSyntaxError( f'{key!r} has not been declared as a variable' ) # Delete the vars from the class __dict__. for key in _assigned_vars: delattr(cls, key) # Clear the local var space local_varspace.clear()
def _do_register(cls): if _validate_test(cls): if not cls.param_space.is_empty(): raise ReframeSyntaxError( f'{cls.__qualname__!r} is already a parameterized test') for args in inst: _register_parameterized_test(cls, args) return cls
def run_before(stage): '''Decorator for attaching a test method to a pipeline stage. .. deprecated:: 3.7.0 Please use the :func:`~reframe.core.pipeline.RegressionMixin.run_before` built-in function. ''' warn.user_deprecation_warning( 'using the @rfm.run_before decorator from the rfm module is ' 'deprecated; please use the built-in decorator @run_before instead.', from_version='3.7.0') if stage not in _USER_PIPELINE_STAGES: raise ReframeSyntaxError( f'invalid pipeline stage specified: {stage!r}') if stage == 'init': raise ReframeSyntaxError('pre-init hooks are not allowed') return hooks.attach_to('pre_' + stage)
def __getitem__(self, key): '''Expose and control access to the local namespaces. Variables may only be retrieved if their value has been previously set. Accessing a parameter in the class body is disallowed (the actual test parameter is set during the class instantiation). ''' try: return super().__getitem__(key) except KeyError as err: try: # Handle variable access return self['_rfm_local_var_space'][key] except KeyError: # Handle parameter access if key in self['_rfm_local_param_space']: raise ReframeSyntaxError( 'accessing a test parameter from the class ' 'body is disallowed' ) from None elif key in self['_rfm_local_fixture_space']: raise ReframeSyntaxError( 'accessing a fixture from the class body is ' 'disallowed' ) from None else: # As the last resource, look if key is a variable in # any of the base classes. If so, make its value # available in the current class' namespace. for b in self['_rfm_bases']: if key in b._rfm_var_space: # Store a deep-copy of the variable's # value and return. v = b._rfm_var_space[key].default_value self._namespace[key] = v return self._namespace[key] # If 'key' is neither a variable nor a parameter, # raise the exception from the base __getitem__. raise err from None
def __init__(cls, name, bases, namespace, **kwargs): super().__init__(name, bases, namespace, **kwargs) # Create a set with the attribute names already in use. cls._rfm_dir = set() for base in bases: if hasattr(base, '_rfm_dir'): cls._rfm_dir.update(base._rfm_dir) used_attribute_names = set(cls._rfm_dir) # Build the var space and extend the target namespace variables.VarSpace(cls, used_attribute_names) used_attribute_names.update(cls._rfm_var_space.vars) # Build the parameter space parameters.ParamSpace(cls, used_attribute_names) # Update used names set with the local __dict__ cls._rfm_dir.update(cls.__dict__) # Set up the hooks for the pipeline stages based on the _rfm_attach # attribute; all dependencies will be resolved first in the post-setup # phase if not assigned elsewhere hooks = HookRegistry.create(namespace) for b in bases: if hasattr(b, '_rfm_pipeline_hooks'): hooks.update(getattr(b, '_rfm_pipeline_hooks')) cls._rfm_pipeline_hooks = hooks # HookRegistry(local_hooks) cls._final_methods = { v.__name__ for v in namespace.values() if hasattr(v, '_rfm_final') } # Add the final functions from its parents cls._final_methods.update(*(b._final_methods for b in bases if hasattr(b, '_final_methods'))) if hasattr(cls, '_rfm_special_test') and cls._rfm_special_test: return for v in namespace.values(): for b in bases: if not hasattr(b, '_final_methods'): continue if callable(v) and v.__name__ in b._final_methods: msg = (f"'{cls.__qualname__}.{v.__name__}' attempts to " f"override final method " f"'{b.__qualname__}.{v.__name__}'; " f"you should use the pipeline hooks instead") raise ReframeSyntaxError(msg)
def sanity(self, cls, illegal_names=None): '''Sanity checks post-creation of the namespace. By default, we make illegal to have any item in the namespace that clashes with a member of the target class. ''' if illegal_names is None: illegal_names = set(dir(cls)) for key in self._namespace: if key in illegal_names: raise ReframeSyntaxError(f'{key!r} already defined in class ' f'{cls.__qualname__!r}')
def join(self, other, cls): '''Join other fixture spaces into the current one. :param other: instance of the FixtureSpace class. :param cls: the target class. ''' for key, value in other.fixtures.items(): if key in self.fixtures: raise ReframeSyntaxError( f'fixture space conflict: ' f'fixture {key!r} is defined in more than ' f'one base class of {cls.__qualname__!r}') self.fixtures[key] = value
def extend(self, cls): '''Extend the inherited fixture space with the local fixture space.''' local_fixture_space = getattr(cls, self.local_namespace_name, False) while local_fixture_space: name, fixture = local_fixture_space.popitem() self.fixtures[name] = fixture # If any previously declared fixture was defined in the class body # by directly assigning it a value, raise an error. Fixtures must be # changed using the `x = fixture(...)` syntax. for key in self.fixtures: if key in cls.__dict__: raise ReframeSyntaxError( f'fixture {key!r} can only be redefined through the ' f'fixture built-in')
def join(self, other, cls): '''Join an existing VarSpace into the current one. :param other: instance of the VarSpace class. :param cls: the target class. ''' for key, var in other.items(): # Make doubly declared vars illegal. Note that this will be # triggered when inheriting from multiple RegressionTest classes. if key in self.vars: raise ReframeSyntaxError( f'variable {key!r} is declared in more than one of the ' f'parent classes of class {cls.__qualname__!r}') self.vars[key] = copy.deepcopy(var) # Carry over the set of injected variables self._injected_vars.update(other._injected_vars)
def update(self, other): '''Update this parameter from another one. The values from the other parameter will be filtered according to the filter function of this one and prepended to this parameter's values. ''' try: filt_vals = self.filter_params(other.values) except Exception: raise else: try: self.values = tuple(filt_vals) + self.values except TypeError: raise ReframeSyntaxError( f"'filter_param' must return an iterable" ) from None
def _validate_test(cls): if not issubclass(cls, RegressionTest): raise ReframeSyntaxError('the decorated class must be a ' 'subclass of RegressionTest') if (cls.is_abstract()): getlogger().warning(f'skipping test {cls.__qualname__!r}: ' f'test has one or more undefined parameters') return False conditions = [VersionValidator(v) for v in cls._rfm_required_version] if (cls._rfm_required_version and not any(c.validate(osext.reframe_version()) for c in conditions)): getlogger().warning(f"skipping incompatible test " f"'{cls.__qualname__}': not valid for ReFrame " f"version {osext.reframe_version().split('-')[0]}") return False return True
def extend(self, cls): '''Extend the parameter space with the local parameter space.''' local_param_space = getattr(cls, self.local_namespace_name, dict()) for name, p in local_param_space.items(): if name in self.params: p.update(self.params[name]) self.params[name] = p # Clear the local param space local_param_space.clear() # If any previously declared parameter was defined in the class body # by directly assigning it a value, raise an error. Parameters must be # changed using the `x = parameter([...])` syntax. for key, values in cls.__dict__.items(): if key in self.params: raise ReframeSyntaxError( f'parameter {key!r} must be modified through the built-in ' f'parameter type')
def _validate_test(cls): if not issubclass(cls, RegressionTest): raise ReframeSyntaxError('the decorated class must be a ' 'subclass of RegressionTest')
def __init__(cls, name, bases, namespace, **kwargs): super().__init__(name, bases, namespace, **kwargs) # Create a set with the attribute names already in use. cls._rfm_dir = set() for base in (b for b in bases if hasattr(b, '_rfm_dir')): cls._rfm_dir.update(base._rfm_dir) used_attribute_names = set(cls._rfm_dir) # Build the var space and extend the target namespace variables.VarSpace(cls, used_attribute_names) used_attribute_names.update(cls._rfm_var_space.vars) # Build the parameter space parameters.ParamSpace(cls, used_attribute_names) # Update used names set with the local __dict__ cls._rfm_dir.update(cls.__dict__) # Update the hook registry with the bases for base in cls._rfm_bases: cls._rfm_hook_registry.update(base._rfm_hook_registry, denied_hooks=namespace) # Search the bases if no local sanity functions exist. if '_rfm_sanity' not in namespace: for base in cls._rfm_bases: if hasattr(base, '_rfm_sanity'): cls._rfm_sanity = getattr(base, '_rfm_sanity') if cls._rfm_sanity.__name__ in namespace: raise ReframeSyntaxError( f'{cls.__qualname__!r} overrides the candidate ' f'sanity function ' f'{cls._rfm_sanity.__qualname__!r} without ' f'defining an alternative') break # Update the performance function dict with the bases. for base in cls._rfm_bases: for k, v in base._rfm_perf_fns.items(): if k not in namespace: try: cls._rfm_perf_fns[k] = v except KeyError: '''Performance function overridden by other class''' # Add the final functions from its parents cls._rfm_final_methods.update(*(b._rfm_final_methods for b in cls._rfm_bases)) if getattr(cls, '_rfm_override_final', None): return for b in cls._rfm_bases: for key in b._rfm_final_methods: if key in namespace and callable(namespace[key]): msg = (f"'{cls.__qualname__}.{key}' attempts to " f"override final method " f"'{b.__qualname__}.{key}'; " f"you should use the pipeline hooks instead") raise ReframeSyntaxError(msg)
def required_version(*versions): '''Class decorator for specifying the required ReFrame versions for the following test. If the test is not compatible with the current ReFrame version it will be skipped. :arg versions: A list of ReFrame version specifications that this test is allowed to run. A version specification string can have one of the following formats: 1. ``VERSION``: Specifies a single version. 2. ``{OP}VERSION``, where ``{OP}`` can be any of ``>``, ``>=``, ``<``, ``<=``, ``==`` and ``!=``. For example, the version specification string ``'>=3.5.0'`` will allow the following test to be loaded only by ReFrame 3.5.0 and higher. The ``==VERSION`` specification is the equivalent of ``VERSION``. 3. ``V1..V2``: Specifies a range of versions. You can specify multiple versions with this decorator, such as ``@required_version('3.5.1', '>=3.5.6')``, in which case the test will be selected if *any* of the versions is satisfied, even if the versions specifications are conflicting. .. versionadded:: 2.13 .. versionchanged:: 3.5.0 Passing ReFrame version numbers that do not comply with the `semantic versioning <https://semver.org/>`__ specification is deprecated. Examples of non-compliant version numbers are ``3.5`` and ``3.5-dev0``. These should be written as ``3.5.0`` and ``3.5.0-dev.0``. .. deprecated:: 3.5.0 Please set the ``require_version`` parameter in the class definition instead. ''' warn.user_deprecation_warning( "the '@required_version' decorator is deprecated; please set " "the 'require_version' parameter in the class definition instead", from_version='3.7.0' ) if not versions: raise ReframeSyntaxError('no versions specified') conditions = [VersionValidator(v) for v in versions] def _skip_tests(cls): mod = inspect.getmodule(cls) if not hasattr(mod, '__rfm_skip_tests'): mod.__rfm_skip_tests = set() if not hasattr(mod, '_rfm_test_registry'): mod._rfm_test_registry = TestRegistry() if not any(c.validate(osext.reframe_version()) for c in conditions): getlogger().warning( f"skipping incompatible test '{cls.__qualname__}': not valid " f"for ReFrame version {osext.reframe_version().split('-')[0]}" ) if cls in mod._rfm_test_registry: mod._rfm_test_registry.skip(cls) else: mod.__rfm_skip_tests.add(cls) return cls return _skip_tests
def __setitem__(self, key, value): raise ReframeSyntaxError( f'cannot set item {key!r} into a {type(self).__qualname__} object')
def add(self, fixture, variant_num, parent_test): '''Register a fixture. This method mangles the fixture name, ensuring that different fixture variants (see above for the definition of a fixture variant in the context of a fixture registry) have a different name. This method is aware of the public members from the :class:`TestFixture`. Fixtures `steal` the valid environments and valid partitions from their parent test, where the number of combinations that trickle down into the fixture varies depending on the fixture's scope. The rationale is as follows for the different scopes: - session: Only one environment+partition combination per fixture. This kind of fixture may be shared across all tests. The name for fixtures with this scope is mangled with the system name. This is necesssary to avoid name conflicts with classes that are used both as regular tests and fixtures with session scope. - partition: Only one environment per partition. This kind of fixture may be shared amongst all the tests running on the same partition. The name is mangled to include the partition where fixture will run. - environment: One fixture per available environment+partition combination. This kind of fixture may be shared only with other tests that execute on the same environment+partition combination. The name is mangled to contain the partition and the environment where the fixture will run. - test: Use the environments and partitions from the parent test without any modifications. Fixtures using this scope are private to the parent test and they will not be shared with any other test in the session. The fixture name is mangled to contain the name of the parent test the fixture belongs to. If a fixture modifies the default value of any of its attributes, these modified variables are always mangled into the fixture name regardless of the scope used. These variables are sorted by their name before they're accounted for into the name mangling, so the order on which they're specified is irrelevant. This method returns a list with the mangled names of the newly registered fixtures. :param fixture: An instance of :class:`TestFixture`. :param variant_num: The variant index for the given ``fixture``. :param parent_test: The parent test. ''' cls = fixture.cls scope = fixture.scope fname = fixture.cls.variant_name(variant_num) variables = fixture.variables reg_names = [] self._registry.setdefault(cls, dict()) # Mangle the fixture name with the modified variables (sorted by keys) if variables: vname = ''.join((f'%{k}={utils.toalphanum(str(v))}' for k, v in sorted(variables.items()))) if self._hash: vname = '_' + sha256(vname.encode('utf-8')).hexdigest()[:8] fname += vname # Select only the valid partitions try: valid_sysenv = runtime.valid_sysenv_comb( parent_test.valid_systems, parent_test.valid_prog_environs) except AttributeError as e: msg = e.args[0] + f' in test {parent_test.display_name!r}' raise ReframeSyntaxError(msg) from None # Return if there are no valid system/environment combinations if not valid_sysenv: return [] # Register the fixture if scope == 'session': # The name is mangled with the system name # Pick the first valid system/environment combination pname, ename = None, None for part, environs in valid_sysenv.items(): pname = part.fullname for env in environs: ename = env.name break if ename is None: # No valid environments found return [] # Register the fixture fixt_data = FixtureData(variant_num, [ename], [pname], variables, scope, self._sys_name) name = f'{cls.__name__}_{fixt_data.mashup()}' self._registry[cls][name] = fixt_data reg_names.append(name) elif scope == 'partition': for part, environs in valid_sysenv.items(): # The mangled name contains the full partition name # Select an environment supported by the partition pname = part.fullname try: ename = environs[0].name except IndexError: continue # Register the fixture fixt_data = FixtureData(variant_num, [ename], [pname], variables, scope, pname) name = f'{cls.__name__}_{fixt_data.mashup()}' self._registry[cls][name] = fixt_data reg_names.append(name) elif scope == 'environment': for part, environs in valid_sysenv.items(): for env in environs: # The mangled name contains the full part and env names # Register the fixture pname, ename = part.fullname, env.name fixt_data = FixtureData(variant_num, [ename], [pname], variables, scope, f'{pname}+{ename}') name = f'{cls.__name__}_{fixt_data.mashup()}' self._registry[cls][name] = fixt_data reg_names.append(name) elif scope == 'test': # The mangled name contains the parent test name. # Register the fixture fixt_data = FixtureData(variant_num, list(parent_test.valid_prog_environs), list(parent_test.valid_systems), variables, scope, parent_test.unique_name) name = f'{cls.__name__}_{fixt_data.mashup()}' self._registry[cls][name] = fixt_data reg_names.append(name) return reg_names
def _check_is_defined(self): if not self.is_defined(): raise ReframeSyntaxError( f'variable {self._name} is not assigned a value')
def __init__(cls, name, bases, namespace, **kwargs): super().__init__(name, bases, namespace, **kwargs) # Create a set with the attribute names already in use. cls._rfm_dir = set() for base in (b for b in bases if hasattr(b, '_rfm_dir')): cls._rfm_dir.update(base._rfm_dir) used_attribute_names = set(cls._rfm_dir) # Build the var space and extend the target namespace variables.VarSpace(cls, used_attribute_names) used_attribute_names.update(cls._rfm_var_space.vars) # Build the parameter space parameters.ParamSpace(cls, used_attribute_names) # Update used names set with the local __dict__ cls._rfm_dir.update(cls.__dict__) # Set up the hooks for the pipeline stages based on the _rfm_attach # attribute; all dependencies will be resolved first in the post-setup # phase if not assigned elsewhere hook_reg = hooks.HookRegistry.create(namespace) for base in (b for b in bases if hasattr(b, '_rfm_pipeline_hooks')): hook_reg.update(getattr(base, '_rfm_pipeline_hooks'), denied_hooks=namespace) cls._rfm_pipeline_hooks = hook_reg # Gather all the locally defined sanity functions based on the # _rfm_sanity_fn attribute. sn_fn = [v for v in namespace.values() if hasattr(v, '_rfm_sanity_fn')] if sn_fn: cls._rfm_sanity = sn_fn[0] if len(sn_fn) > 1: raise ReframeSyntaxError( f'{cls.__qualname__!r} defines more than one sanity ' 'function in the class body.') else: # Search the bases if no local sanity functions exist. for base in (b for b in bases if hasattr(b, '_rfm_sanity')): cls._rfm_sanity = getattr(base, '_rfm_sanity') if cls._rfm_sanity.__name__ in namespace: raise ReframeSyntaxError( f'{cls.__qualname__!r} overrides the candidate ' f'sanity function ' f'{cls._rfm_sanity.__qualname__!r} without ' f'defining an alternative') break cls._final_methods = { v.__name__ for v in namespace.values() if hasattr(v, '_rfm_final') } # Add the final functions from its parents cls._final_methods.update(*(b._final_methods for b in bases if hasattr(b, '_final_methods'))) if getattr(cls, '_rfm_special_test', None): return bases_w_final = [b for b in bases if hasattr(b, '_final_methods')] for v in namespace.values(): for b in bases_w_final: if callable(v) and v.__name__ in b._final_methods: msg = (f"'{cls.__qualname__}.{v.__name__}' attempts to " f"override final method " f"'{b.__qualname__}.{v.__name__}'; " f"you should use the pipeline hooks instead") raise ReframeSyntaxError(msg)
def __init__(cls, name, bases, namespace, **kwargs): super().__init__(name, bases, namespace, **kwargs) # Create a set with the attribute names already in use. cls._rfm_dir = set() for base in (b for b in bases if hasattr(b, '_rfm_dir')): cls._rfm_dir.update(base._rfm_dir) used_attribute_names = set(cls._rfm_dir).union( {h.__name__ for h in cls._rfm_local_hook_registry}) # Build the different global class namespaces namespace_types = (variables.VarSpace, parameters.ParamSpace, fixtures.FixtureSpace) for ns_type in namespace_types: ns = ns_type(cls, used_attribute_names) setattr(cls, ns.namespace_name, ns) used_attribute_names.update(ns.data()) # Update used names set with the local __dict__ cls._rfm_dir.update(cls.__dict__) # Populate the global hook registry with the hook registries of the # parent classes in reverse MRO order for c in list(reversed(cls.mro()))[:-1]: if hasattr(c, '_rfm_local_hook_registry'): cls._rfm_hook_registry.update(c._rfm_local_hook_registry, denied_hooks=namespace) cls._rfm_hook_registry.update(cls._rfm_local_hook_registry) # Search the bases if no local sanity functions exist. if '_rfm_sanity' not in namespace: for base in cls._rfm_bases: if hasattr(base, '_rfm_sanity'): cls._rfm_sanity = getattr(base, '_rfm_sanity') if cls._rfm_sanity.__name__ in namespace: raise ReframeSyntaxError( f'{cls.__qualname__!r} overrides the candidate ' f'sanity function ' f'{cls._rfm_sanity.__qualname__!r} without ' f'defining an alternative') break # Update the performance function dict with the bases. for base in cls._rfm_bases: for k, v in base._rfm_perf_fns.items(): if k not in namespace: try: cls._rfm_perf_fns[k] = v except KeyError: '''Performance function overridden by other class''' # Add the final functions from its parents cls._rfm_final_methods.update(*(b._rfm_final_methods for b in cls._rfm_bases)) if getattr(cls, '_rfm_override_final', None): return for b in cls._rfm_bases: for key in b._rfm_final_methods: if key in namespace and callable(namespace[key]): msg = (f"'{cls.__qualname__}.{key}' attempts to " f"override final method " f"'{b.__qualname__}.{key}'; " f"you should use the pipeline hooks instead") raise ReframeSyntaxError(msg)