Skip to content

Commit

Permalink
requires/types: introduce expression type (expr.py)
Browse files Browse the repository at this point in the history
`expression` is a new requirement type that allows user to express a requirement
in human-readable, SQL-like syntax. It has a proper grammar to allow user to write
checks as easily digestable manner, which will improve the QOL for scenario writers
and code reviewers. The grammar is constructed using `pyparsing` library.

The current grammar supports the following constructs:

- Keywords (True, False, None)
- Constants (Integer, Float, String literal)
- Runtime variables (Python properties)
- Functions(len, not, file, systemd, read_ini, read_cert)
- Arithmetic operators(sign, mul, div, add, sub, exp)
- Comparison operators(<, <=, >, >=, ==, !=, in)
- Logical operators(and, or, not)
- Comments('#', '//', '/*...*/')

Also, the following changes have been made:

- Updated requirement_types.rst to document the new Expression requirement type.
- Moved property resolver logic from YPropertyBase to PythonEntityResolver class.
- Added `pyparsing` as a dependency.
- Added unit tests for the new code, the coverage rate is 100 pct for the expr.py

This patch rewrites some of the scenario checks in the new expression syntax to
demonstrate the difference and provide examples.

Signed-off-by: Mustafa Kemal Gilor <[email protected]>
  • Loading branch information
xmkg committed Aug 16, 2024
1 parent e0cef93 commit c5efc64
Show file tree
Hide file tree
Showing 29 changed files with 3,039 additions and 344 deletions.
620 changes: 620 additions & 0 deletions doc/source/contrib/language_ref/property_ref/requirement_types.rst

Large diffs are not rendered by default.

105 changes: 105 additions & 0 deletions hotsos/core/alias.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
"""Aliasing utilities."""

from hotsos.core.log import log


class AliasAlreadyInUseError(Exception):
"""Raised when an alias is already in use."""

def __init__(self, name):
self.message = f"Alias '{name}` already in use!"

def __str__(self):
return self.message


class AliasForbiddenError(Exception):
"""Raised when an alias is forbidden to use."""

def __init__(self, name):
self.message = f"Alias '{name}` is forbidden!"

def __str__(self):
return self.message


class AliasRegistry:
"""
A class that provides a registry for aliasing Python things.
"""

# A class-level dictionary to store registered aliases.
registry = {}

@staticmethod
def register(name, decoratee):
"""
Register a function, method, or property under an alias.
This method handles different types of Python objects and creates
appropriate wrappers or registrations based on the object type.
Args:
name (str): The alias under which to register the decoratee.
decoratee (callable or property): The Python object to be
registered.
Raises:
AliasAlreadyInUseError: If the alias name is already registered.
AliasForbiddenError: If the alias name starts with "hotsos."
"""
isprop = isinstance(decoratee, property)
target = decoratee.fget if isprop else decoratee

if name.startswith("hotsos."):
raise AliasForbiddenError(name)

if name in AliasRegistry.registry:
log.debug("alias registration failed -- already in use(`%s`)",
name)
raise AliasAlreadyInUseError(name)

import_path = f"{target.__module__}.{target.__qualname__}"
log.debug("registering alias `%s` --> {%s}", name, import_path)
# Register full import path.
AliasRegistry.registry[name] = import_path

@staticmethod
def resolve(the_alias, default=None):
"""
Retrieve a registered alias.
Args:
the_alias (str): The alias to retrieve.
Returns:
callable: The function or wrapper associated with the alias.
Raises:
NoSuchAliasError: No such alias in the registry.
"""

if the_alias not in AliasRegistry.registry:
log.debug(
"alias `%s` not found in the registry, "
"returning the default value",
the_alias,
)
return default

value = AliasRegistry.registry[the_alias]
log.debug("alias %s resolved to %s", the_alias, value)
return value


def alias(argument):
"""Create an alias for a property, function or a thing."""

def real_decorator(func):
"""We're not wrapping the func as we don't want
to do anything at runtime. We just want to alias
`func` to some user-defined name and call it on-demand."""
AliasRegistry.register(argument, func)
return func

return real_decorator
8 changes: 8 additions & 0 deletions hotsos/core/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ class NotEnoughParametersError(Exception):
"""Raised when an operation did not get enough parameters to operate."""


class TooManyParametersError(Exception):
"""Raised when an operation did get more parameters than expected."""


class MissingRequiredParameterError(Exception):
"""Raised when an operation did not get a parameter required
for the operation."""
Expand All @@ -59,3 +63,7 @@ class PreconditionError(Exception):

class ExpectationNotMetError(Exception):
"""Raised when an operation's expectation is not met."""


class NoSuchPropertyError(Exception):
"""Raised when an object does not have a property where it should."""
6 changes: 4 additions & 2 deletions hotsos/core/plugins/kernel/memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,11 +106,13 @@ def huge_pages_enabled(self):

@property
def hugetlb_to_mem_total_percentage(self):
return round((self.Hugetlb * 100) / self.MemTotal)
return (self.MemTotal and round(
(self.Hugetlb * 100) / self.MemTotal))

@property
def mem_avail_to_mem_total_percentage(self):
return round((self.MemAvailable * 100) / self.MemTotal)
return (self.MemTotal and round(
(self.MemAvailable * 100) / self.MemTotal))

@property
def hugep_used_to_hugep_total_percentage(self):
Expand Down
14 changes: 13 additions & 1 deletion hotsos/core/ycheck/engine/properties/checks.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from functools import cached_property

from propertree.propertree2 import PTreeLogicalGrouping
from propertree.propertree2 import (
PTreeLogicalGrouping,
PTreeOverrideLiteralType
)
from hotsos.core.log import log
from hotsos.core.ycheck.engine.properties.common import (
YPropertyOverrideBase,
Expand All @@ -11,6 +14,9 @@
from hotsos.core.ycheck.engine.properties.requires.requires import (
YPropertyRequires
)
from hotsos.core.ycheck.engine.properties.requires.types.expr import (
YPropertyExpr
)
from hotsos.core.ycheck.engine.properties.search import (
YPropertySearch,
)
Expand Down Expand Up @@ -162,6 +168,12 @@ def result(self):
stop_executon = False
for member in self.members:
for item in member:
# Allow implicit declaration of expression.
if isinstance(item, PTreeOverrideLiteralType):
item = YPropertyExpr(item.root, "expression",
item.content, item.override_path,
context=self.context)

# Ignore these here as they are used by search properties.
if isinstance(item, YPropertyInput):
continue
Expand Down
93 changes: 52 additions & 41 deletions hotsos/core/ycheck/engine/properties/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,44 +284,19 @@ def __getattr__(self, key):
return self.data.get(key)


class YPropertyBase(PTreeOverrideBase):
"""
Base class used for all YAML property objects. Implements and extends
PTreeOverrideBase to provide frequently used methods and helpers to
property implementations.
"""
def __init__(self, *args, **kwargs):
self._cache = PropertyCache()
super().__init__(*args, **kwargs)

def resolve_var(self, name):
"""
Resolve variable with name to value. This can be used speculatively and
will return the name as value if it can't be resolved.
"""
if not name.startswith('$'):
return name

if hasattr(self, 'context'):
if self.context.vars:
_name = name.partition('$')[2]
return self.context.vars.resolve(_name)
class PythonEntityResolver:
"""A class to resolve Python entities (e.g. variable, property)
by their import path."""

log.warning("could not resolve var '%s' - vars not found in "
"context", name)
# Class-level cache for Python module imports for future use

return name
import_cache = None

@property
def cache(self):
"""
All properties get their own cache object that they can use as they
wish.
"""
return self._cache
def __init__(self, context):
self.context = context

def _load_from_import_cache(self, key):
""" Retrieve from global context if one exists.
""" Retrieve from global cache if one exists.
@param key: key to retrieve
"""
Expand Down Expand Up @@ -374,16 +349,14 @@ def _add_to_import_cache(self, key, value):
@param key: key to save
@param value: value to save
"""
if self.context is None:
log.info("context not available - cannot save '%s'", key)

if not self.import_cache:
self.import_cache = {key: value}
log.debug("import cache initialized with initial key `%s`", key)
return

c = getattr(self.context, 'import_cache')
if c:
c[key] = value
else:
c = {key: value}
setattr(self.context, 'import_cache', c)
self.import_cache[key] = value
log.debug("import cache updated for key %s", key)

def get_cls(self, import_str):
""" Import and instantiate Python class.
Expand Down Expand Up @@ -535,6 +508,44 @@ def get_import(self, import_str):
return self.get_attribute(import_str)


class YPropertyBase(PTreeOverrideBase, PythonEntityResolver):
"""
Base class used for all YAML property objects. Implements and extends
PTreeOverrideBase to provide frequently used methods and helpers to
property implementations.
"""

def __init__(self, *args, **kwargs):
self._cache = PropertyCache()
super().__init__(*args, **kwargs)

def resolve_var(self, name):
"""
Resolve variable with name to value. This can be used speculatively and
will return the name as value if it can't be resolved.
"""
if not name.startswith('$'):
return name

if hasattr(self, 'context'):
if self.context.vars:
_name = name.partition('$')[2]
return self.context.vars.resolve(_name)

log.warning("could not resolve var '%s' - vars not found in "
"context", name)

return name

@property
def cache(self):
"""
All properties get their own cache object that they can use as they
wish.
"""
return self._cache


class YPropertyOverrideBase(YPropertyBase, PTreeOverrideBase):
""" Base class for all simple/flat property objects. """

Expand Down
4 changes: 3 additions & 1 deletion hotsos/core/ycheck/engine/properties/requires/requires.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
property as rproperty,
path,
varops,
expr,
)

CACHE_CHECK_KEY = '__PREVIOUSLY_CACHED_PROPERTY_TYPE'
Expand Down Expand Up @@ -94,7 +95,8 @@ class YPropertyRequires(YPropertyMappedOverrideBase):
systemd.YRequirementTypeSystemd,
rproperty.YRequirementTypeProperty,
path.YRequirementTypePath,
varops.YPropertyVarOps]
varops.YPropertyVarOps,
expr.YPropertyExpr,]
# We want to be able to use this property both on its own and as a member
# of other mapping properties e.g. Checks. The following setting enables
# this.
Expand Down
Loading

0 comments on commit c5efc64

Please sign in to comment.