Skip to content

Commit

Permalink
Merge branch 'main' into filter-doc
Browse files Browse the repository at this point in the history
  • Loading branch information
The-Mule authored Jun 5, 2024
2 parents 665f56d + fe38c42 commit 8f2125d
Show file tree
Hide file tree
Showing 15 changed files with 345 additions and 127 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/pre-commit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: pre-commit
on:
pull_request:
push:
branches: [master]
branches: [main]

jobs:
pre-commit:
Expand Down
18 changes: 0 additions & 18 deletions .travis.yml

This file was deleted.

14 changes: 14 additions & 0 deletions docs/features.rst
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,20 @@ The key content attributes are not supposed to be hard-coded in
the Flexible Metadata Format but freely configurable. Multiple key
content attributes (e.g. script & backend) could be used as well.

Select
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Sometimes it is necessary to select node from the metadata tree
even though it is not a leaf. For example, when virtual tests are
created from a parent test but one wants to keep the parent available
as a test as well. On the other hand, one might want to hide leaf node,
instead of deleting it completely. To do so, one can set the directive::

/:
select: boolean

By default all leaves have it set to ``true`` (such node is selected)
and branches have set it to ``false`` (such node is not selected).

.. _virtual:

Expand Down
174 changes: 87 additions & 87 deletions fmf/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from pprint import pformat as pretty
from typing import Any, Dict, Optional, Protocol

import jsonschema
from ruamel.yaml import YAML
from ruamel.yaml.constructor import DuplicateKeyError
from ruamel.yaml.error import YAMLError
Expand All @@ -24,6 +23,7 @@
SUFFIX = ".fmf"
MAIN = "main" + SUFFIX
IGNORED_DIRECTORIES = ['/dev', '/proc', '/sys']
ADJUST_CONTROL_KEYS = ['because', 'continue', 'when']


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand Down Expand Up @@ -250,10 +250,12 @@ def check(value, type_, name=None):
for key, value in directives.items():
if key == "inherit":
check(value, bool, name="inherit")
continue
# No other directive supported
raise fmf.utils.FormatError(
f"Unknown fmf directive '{key}' in '{self.name}'.")
elif key == "select":
check(value, bool, name="select")
else:
# No other directive supported
raise fmf.utils.FormatError(
f"Unknown fmf directive '{key}' in '{self.name}'.")

# Everything ok, store the directives
self._directives.update(directives)
Expand Down Expand Up @@ -341,7 +343,8 @@ def update(self, data):
log.data(pretty(self.data))

def adjust(self, context, key='adjust', undecided='skip',
case_sensitive=True, decision_callback=None):
case_sensitive=True, decision_callback=None,
additional_rules=None):
"""
Adjust tree data based on provided context and rules
Expand All @@ -363,6 +366,11 @@ class describing the environment context. By default, the key
Optional 'decision_callback' callback would be called for every adjust
rule inspected, with three arguments: current fmf node, current
adjust rule, and whether it was applied or not.
Optional 'additional_rules' parameter can be used to specify rules
that should be applied after those from the node itself.
These additional rules are processed even when an applied
rule defined in the node has ``continue: false`` set.
"""

# Check context sanity
Expand All @@ -372,7 +380,7 @@ class describing the environment context. By default, the key

# Adjust rules should be a dictionary or a list of dictionaries
try:
rules = copy.deepcopy(self.data[key])
rules = self.data[key]
log.debug("Applying adjust rules for '{}'.".format(self))
log.data(rules)
if isinstance(rules, dict):
Expand All @@ -385,65 +393,75 @@ class describing the environment context. By default, the key
except KeyError:
rules = []

context.case_sensitive = case_sensitive

# Check and apply each rule
for rule in rules:

# Rule must be a dictionary
if not isinstance(rule, dict):
raise utils.FormatError("Adjust rule should be a dictionary.")

original_rule = rule.copy()

# Missing 'when' means always enabled rule
try:
condition = rule.pop('when')
except KeyError:
condition = True

# The optional 'continue' key should be a bool
continue_ = rule.pop('continue', True)
if not isinstance(continue_, bool):
raise utils.FormatError(
"The 'continue' value should be bool, "
"got '{}'.".format(continue_))

# The 'because' key is reserved for optional comments (ignored)
rule.pop('because', None)

# Apply remaining rule attributes if context matches
try:
if context.matches(condition):
if decision_callback:
decision_callback(self, original_rule, True)
# Accept same type as rules from data
if additional_rules is None:
additional_rules = []
elif isinstance(additional_rules, dict):
additional_rules = [additional_rules]

self._merge_special(self.data, rule)
context.case_sensitive = case_sensitive

# First matching rule wins, skip the rest unless continue
if not continue_:
break
else:
# 'continue' has to affect only its rule_set
for rule_set in rules, additional_rules:
# Check and apply each rule
for rule in rule_set:
# Rule must be a dictionary
if not isinstance(rule, dict):
raise utils.FormatError("Adjust rule should be a dictionary.")

# Missing 'when' means always enabled rule
try:
condition = rule['when']
except KeyError:
condition = True

# The optional 'continue' key should be a bool
continue_ = rule.get('continue', True)
if not isinstance(continue_, bool):
raise utils.FormatError(
"The 'continue' value should be bool, "
"got '{}'.".format(continue_))

# Apply remaining rule attributes if context matches
try:
if context.matches(condition):
if decision_callback:
decision_callback(self, rule, True)

# Remove special keys (when, because...) from the rule
apply_rule = {
key: value
for key, value in rule.items()
if key not in ADJUST_CONTROL_KEYS
}
self._merge_special(self.data, apply_rule)

# First matching rule wins, skip the rest of this set unless continue
if not continue_:
break
else:
if decision_callback:
decision_callback(self, rule, False)
# Handle undecided rules as requested
except fmf.context.CannotDecide:
if decision_callback:
decision_callback(self, original_rule, False)
# Handle undecided rules as requested
except fmf.context.CannotDecide:
if decision_callback:
decision_callback(self, original_rule, None)
decision_callback(self, rule, None)

if undecided == 'skip':
continue
elif undecided == 'raise':
raise
else:
raise utils.GeneralError(
"Invalid value for the 'undecided' parameter. Should "
"be 'skip' or 'raise', got '{}'.".format(undecided))
if undecided == 'skip':
continue
elif undecided == 'raise':
raise
else:
raise utils.GeneralError(
"Invalid value for the 'undecided' parameter. Should "
"be 'skip' or 'raise', got '{}'.".format(undecided))

# Adjust all child nodes as well
for child in self.children.values():
child.adjust(context, key, undecided,
case_sensitive=case_sensitive, decision_callback=decision_callback)
case_sensitive=case_sensitive,
decision_callback=decision_callback,
additional_rules=additional_rules)

def get(self, name=None, default=None):
"""
Expand Down Expand Up @@ -572,12 +590,20 @@ def grow(self, path):

def climb(self, whole=False):
""" Climb through the tree (iterate leaf/all nodes) """
if whole or not self.children:
if whole or self.select:
yield self
for name, child in self.children.items():
for node in child.climb(whole):
yield node

@property
def select(self):
""" Respect directive, otherwise by being leaf/branch node"""
try:
return self._directives["select"]
except KeyError:
return not self.children

def find(self, name):
""" Find node with given name """
for node in self.climb(whole=True):
Expand Down Expand Up @@ -610,7 +636,7 @@ def prune(self, whole=False, keys=None, names=None, filters=None,
continue
# Apply filters and conditions if given
try:
if not all([utils.filter(filter, node.data, regexp=True)
if not all([utils.filter(filter, node.data, regexp=True, name=node.name)
for filter in filters]):
continue
if not all([utils.evaluate(condition, node.data, node)
Expand Down Expand Up @@ -720,33 +746,7 @@ def validate(self, schema, schema_store=None):
Raises utils.JsonSchemaError if the supplied schema was invalid.
"""
schema_store = schema_store or {}
try:
resolver = jsonschema.RefResolver.from_schema(
schema, store=schema_store)
except AttributeError as error:
raise utils.JsonSchemaError(
f'Provided schema cannot be loaded: {error}')

validator = jsonschema.Draft4Validator(schema, resolver=resolver)

try:
validator.validate(self.data)
return utils.JsonSchemaValidationResult(True, [])

# Data file validated by schema contains errors
except jsonschema.exceptions.ValidationError:
return utils.JsonSchemaValidationResult(
False, list(validator.iter_errors(self.data)))

# Schema file is invalid
except (
jsonschema.exceptions.SchemaError,
jsonschema.exceptions.RefResolutionError,
jsonschema.exceptions.UnknownType
) as error:
raise utils.JsonSchemaError(
f'Errors found in provided schema: {error}')
return utils.validate_data(self.data, schema, schema_store=schema_store)

def _locate_raw_data(self):
"""
Expand Down
Loading

0 comments on commit 8f2125d

Please sign in to comment.