Skip to content

Pylint Fix for expression branch #1083

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
May 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions flow360/component/simulation/blueprint/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
"""Blueprint: Safe function serialization and visual programming integration."""

from .codegen.generator import model_to_function
from .codegen.parser import expr_to_model, function_to_model
from flow360.component.simulation.blueprint.core.generator import model_to_function
from flow360.component.simulation.blueprint.core.parser import (
expr_to_model,
function_to_model,
)

from .core.function import Function
from .core.types import Evaluable

__all__ = ["Function", "function_to_model", "model_to_function", "expr_to_model"]
__all__ = ["Function", "Evaluable", "function_to_model", "model_to_function", "expr_to_model"]
4 changes: 0 additions & 4 deletions flow360/component/simulation/blueprint/codegen/__init__.py

This file was deleted.

9 changes: 8 additions & 1 deletion flow360/component/simulation/blueprint/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
Tuple,
)
from .function import Function
from .generator import expr_to_code, model_to_function, stmt_to_code
from .parser import function_to_model
from .statements import (
Assign,
AugAssign,
Expand All @@ -25,7 +27,7 @@
StatementType,
TupleUnpack,
)
from .types import Evaluable
from .types import Evaluable, TargetSyntax


def _model_rebuild() -> None:
Expand Down Expand Up @@ -101,4 +103,9 @@ def _model_rebuild() -> None:
"Function",
"EvaluationContext",
"ReturnValue",
"Evaluable",
"expr_to_code",
"stmt_to_code",
"model_to_function",
"function_to_model",
]
69 changes: 67 additions & 2 deletions flow360/component/simulation/blueprint/core/context.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""Evaluation context that contains references to known symbols"""

from typing import Any, Optional

from .resolver import CallableResolver
from flow360.component.simulation.blueprint.core.resolver import CallableResolver


class ReturnValue(Exception):
Expand All @@ -17,15 +19,44 @@ def __init__(self, value: Any):
class EvaluationContext:
"""
Manages variable scope and access during function evaluation.

This class stores named values and optionally resolves names through a
`CallableResolver` when not already defined in the context.
"""

def __init__(
self, resolver: CallableResolver, initial_values: Optional[dict[str, Any]] = None
) -> None:
"""
Initialize the evaluation context.

Args:
resolver (CallableResolver): A resolver used to look up callable names
and constants if not explicitly defined.
initial_values (Optional[dict[str, Any]]): Initial variable values to populate
the context with.
"""
self._values = initial_values or {}
self._resolver = resolver

def get(self, name: str, resolve: bool = True) -> Any:
"""
Retrieve a value by name from the context.

If the name is not explicitly defined and `resolve` is True,
attempt to resolve it using the resolver.

Args:
name (str): The variable or callable name to retrieve.
resolve (bool): Whether to attempt to resolve the name if it's undefined.

Returns:
Any: The corresponding value.

Raises:
NameError: If the name is not found and cannot be resolved.
ValueError: If resolution is disabled and the name is undefined.
"""
if name not in self._values:
# Try loading from builtin callables/constants if possible
try:
Expand All @@ -39,14 +70,48 @@ def get(self, name: str, resolve: bool = True) -> Any:
return self._values[name]

def set(self, name: str, value: Any) -> None:
"""
Assign a value to a name in the context.

Args:
name (str): The variable name to set.
value (Any): The value to assign.
"""
self._values[name] = value

def resolve(self, name):
"""
Resolve a name using the provided resolver.

Args:
name (str): The name to resolve.

Returns:
Any: The resolved callable or constant.

Raises:
ValueError: If the name cannot be resolved by the resolver.
"""
return self._resolver.get_allowed_callable(name)

def can_evaluate(self, name) -> bool:
"""
Check if the name can be evaluated via the resolver.

Args:
name (str): The name to check.

Returns:
bool: True if the name is allowed and resolvable, False otherwise.
"""
return self._resolver.can_evaluate(name)

def copy(self) -> "EvaluationContext":
"""Create a copy of the current context."""
"""
Create a copy of the current context.

Returns:
EvaluationContext: A new context instance with the same resolver and a copy
of the current variable values.
"""
return EvaluationContext(self._resolver, dict(self._values))
66 changes: 37 additions & 29 deletions flow360/component/simulation/blueprint/core/expressions.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
"""Data models and evaluator functions for rvalue expression elements"""

import abc
from typing import Annotated, Any, Literal, Union

import pydantic as pd

from ..utils.operators import BINARY_OPERATORS, UNARY_OPERATORS
from .context import EvaluationContext
from .types import Evaluable

ExpressionType = Annotated[
# pylint: disable=duplicate-code
Union[
"Name",
"Constant",
Expand All @@ -21,19 +26,29 @@
]


class Expression(pd.BaseModel):
"""
Base class for expressions (like x > 3, range(n), etc.).
class Expression(pd.BaseModel, Evaluable, metaclass=abc.ABCMeta):
"""
Base class for expressions (like `x > 3`, `range(n)`, etc.).

def evaluate(self, context: EvaluationContext, strict: bool) -> Any:
raise NotImplementedError
Subclasses must implement the `evaluate` and `used_names` methods
to support context-based evaluation and variable usage introspection.
"""

def used_names(self) -> set[str]:
"""
Return a set of variable names used by the expression.

Returns:
set[str]: A set of strings representing variable names used in the expression.
"""
raise NotImplementedError


class Name(Expression, Evaluable):
class Name(Expression):
"""
Expression representing a name qualifier
"""

type: Literal["Name"] = "Name"
id: str

Expand All @@ -53,6 +68,10 @@ def used_names(self) -> set[str]:


class Constant(Expression):
"""
Expression representing a constant numeric value
"""

type: Literal["Constant"] = "Constant"
value: Any

Expand All @@ -64,13 +83,15 @@ def used_names(self) -> set[str]:


class UnaryOp(Expression):
"""
Expression representing a unary operation
"""

type: Literal["UnaryOp"] = "UnaryOp"
op: str
operand: "ExpressionType"

def evaluate(self, context: EvaluationContext, strict: bool) -> Any:
from ..utils.operators import UNARY_OPERATORS

operand_val = self.operand.evaluate(context, strict)

if self.op not in UNARY_OPERATORS:
Expand All @@ -84,8 +105,7 @@ def used_names(self) -> set[str]:

class BinOp(Expression):
"""
For simplicity, we use the operator's class name as a string
(e.g. 'Add', 'Sub', 'Gt', etc.).
Expression representing a binary operation
"""

type: Literal["BinOp"] = "BinOp"
Expand All @@ -94,8 +114,6 @@ class BinOp(Expression):
right: "ExpressionType"

def evaluate(self, context: EvaluationContext, strict: bool) -> Any:
from ..utils.operators import BINARY_OPERATORS

left_val = self.left.evaluate(context, strict)
right_val = self.right.evaluate(context, strict)

Expand All @@ -111,6 +129,9 @@ def used_names(self) -> set[str]:


class Subscript(Expression):
"""
Expression representing an iterable object subscript
"""

type: Literal["Subscript"] = "Subscript"
value: "ExpressionType"
Expand All @@ -123,9 +144,11 @@ def evaluate(self, context: EvaluationContext, strict: bool) -> Any:

if self.ctx == "Load":
return value[item]
elif self.ctx == "Store":
if self.ctx == "Store":
raise NotImplementedError("Subscripted writes are not supported yet")

raise ValueError(f"Invalid subscript context {self.ctx}")

def used_names(self) -> set[str]:
value = self.value.used_names()
item = self.slice.used_names()
Expand Down Expand Up @@ -163,21 +186,6 @@ class CallModel(Expression):
kwargs: dict[str, "ExpressionType"] = {}

def evaluate(self, context: EvaluationContext, strict: bool) -> Any:
"""Evaluate the function call in the given context.

Handles both direct function calls and method calls by properly resolving
the function qualname through the context and whitelisting system.

Args:
context: The execution context containing variable bindings

Returns:
The result of the function call

Raises:
ValueError: If the function is not allowed or evaluation fails
AttributeError: If an intermediate attribute access fails
"""
try:
# Split into parts for attribute traversal
parts = self.func_qualname.split(".")
Expand Down Expand Up @@ -215,7 +223,7 @@ def used_names(self) -> set[str]:
for arg in self.args:
names = names.union(arg.used_names())

for keyword, arg in self.kwargs.items():
for _, arg in self.kwargs.items():
names = names.union(arg.used_names())

return names
Expand Down
13 changes: 6 additions & 7 deletions flow360/component/simulation/blueprint/core/function.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Data models and evaluator functions for full Python function definitions"""

from typing import Any

import pydantic as pd
Expand All @@ -18,21 +20,18 @@ def name(arg1, arg2, ...):
defaults: dict[str, Any]
body: list[StatementType]

def __call__(self, *call_args: Any) -> Any:
# Create empty context first
context = EvaluationContext()

def __call__(self, context: EvaluationContext, *call_args: Any) -> Any:
# Add default values
for arg_name, default_val in self.defaults.items():
context.set(arg_name, default_val)
self.context.set(arg_name, default_val)

# Add call arguments
for arg_name, arg_val in zip(self.args, call_args, strict=False):
context.set(arg_name, arg_val)
self.context.set(arg_name, arg_val)

try:
for stmt in self.body:
stmt.evaluate(context)
stmt.evaluate(self.context)
except ReturnValue as rv:
return rv.value

Expand Down
Loading