Skip to content
Open
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
45 changes: 45 additions & 0 deletions MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ git_override(

# https://registry.bazel.build/modules/cel-conformance
bazel_dep(name = "cel-conformance", version = "0.1.0", repo_name = "cel_conformance")

# TODO(b/582952496): Remove git_override once cel-conformance is published on BCR.
git_override(
module_name = "cel-conformance",
Expand Down Expand Up @@ -62,12 +63,33 @@ single_version_override(
version = "0.2.18",
)

# https://registry.bazel.build/modules/rules_license
bazel_dep(name = "rules_license", version = "1.0.0")

# https://registry.bazel.build/modules/rules_proto
bazel_dep(name = "rules_proto", version = "7.1.0")

# https://registry.bazel.build/modules/rules_python
bazel_dep(name = "rules_python", version = "1.9.0")

# https://registry.bazel.build/modules/yaml-cpp
bazel_dep(name = "yaml-cpp", version = "0.9.0")

# To update: copy _CEL_POLICY_TAG and _CEL_POLICY_SHA pick a commit from
# https://github.com/cel-expr/cel-policy
_CEL_POLICY_TAG = "8e4cf0378e3f0d3406b537afbcc6f59562652ad6"

_CEL_POLICY_SHA = "c3aaed647f04efb7f77d67de21f21720d298938a066cec7f94dd9e24f9a8d88f"

http_archive = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")

http_archive(
name = "cel_policy",
sha256 = _CEL_POLICY_SHA,
strip_prefix = "cel-policy-%s" % _CEL_POLICY_TAG,
url = "https://github.com/cel-expr/cel-policy/archive/%s.tar.gz" % _CEL_POLICY_TAG,
)

# Configure rules_python's hermetic toolchains to resolve external
# dependencies natively. Under Windows, compiling pybind C++ extensions
# against Python 3.11 headers requires that the execution environment
Expand All @@ -84,6 +106,29 @@ use_repo(python, "pythons_hub")

register_toolchains("@pythons_hub//:all")

# PyYAML (pure-Python sources) for the policy conformance test. PyYAML is not
# on BCR; to update, pick a tag from https://github.com/yaml/pyyaml/tags
_PYYAML_VERSION = "6.0.2"

_PYYAML_SHA = "9377c381ac3fccad8df73d96b5139ef8b1a2c57a0d913e95ab0a2275d66b5caa"

http_archive(
name = "pyyaml",
build_file_content = """
load("@rules_python//python:py_library.bzl", "py_library")

py_library(
name = "yaml",
srcs = glob(["lib/yaml/*.py"]),
imports = ["lib"],
visibility = ["//visibility:public"],
)
""",
sha256 = _PYYAML_SHA,
strip_prefix = "pyyaml-%s" % _PYYAML_VERSION,
url = "https://github.com/yaml/pyyaml/archive/refs/tags/%s.tar.gz" % _PYYAML_VERSION,
)

pybind11_internal_configure = use_extension(
"@pybind11_bazel//:internal_configure.bzl",
"internal_configure_extension",
Expand Down
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,25 @@ env = cel.NewEnv(pool, extensions=[ext_math.ExtMath()])
expr = env.compile("math.sqrt(4)")
```

#### Working with optional values

When `ext_optional.ExtOptional()` is enabled, CEL `optional_type` values map to
`cel.Optional` (`cel.Optional.of(value)` and `cel.Optional.none()`), and
optional variable/function types can be declared with `cel.Type.Optional(...)`
(or `cel.Type.OPTIONAL` for `optional_type<dyn>`):

```python
env = cel.NewEnv(
variables={"x": cel.Type.Optional(cel.Type.INT)},
extensions=[ext_optional.ExtOptional()],
)
res = env.compile("x").eval(data={"x": cel.Optional.of(42)})
opt = res.plain_value() # cel.Optional.of(42)
print(opt.has_value()) # True
print(opt.value()) # 42
print(opt.or_value(0)) # 42
```

#### Defining a custom extension in Python

You can define custom functions and pass them as an extension.
Expand Down
34 changes: 33 additions & 1 deletion cel_expr_python/BUILD
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
load("@pybind11_bazel//:build_defs.bzl", "pybind_extension", "pybind_library")
load("@rules_cc//cc:cc_library.bzl", "cc_library")
load("@rules_python//python:py_library.bzl", "py_library")
load("@rules_python//python:py_test.bzl", "py_test")

package(default_visibility = ["//visibility:public"])
Expand All @@ -20,6 +21,7 @@ pybind_library(
"py_cel_expression.cc",
"py_cel_function.cc",
"py_cel_function_decl.cc",
"py_cel_optional.cc",
"py_cel_options.cc",
"py_cel_overload.cc",
"py_cel_python_extension.cc",
Expand All @@ -38,6 +40,7 @@ pybind_library(
"py_cel_expression.h",
"py_cel_function.h",
"py_cel_function_decl.h",
"py_cel_optional.h",
"py_cel_options.h",
"py_cel_overload.h",
"py_cel_python_extension.h",
Expand All @@ -50,6 +53,7 @@ pybind_library(
tags = ["freethreading_compatible"],
visibility = [":__subpackages__"],
deps = [
":cel_compiler_capsule",
":cel_extension",
":status_macros",
"@com_google_absl//absl/base",
Expand Down Expand Up @@ -142,6 +146,18 @@ pybind_library(
],
)

# Shares the `cel::Compiler` owned by a `cel.Env` with separately loaded
# pybind11 modules (e.g. `cel_policy`).
pybind_library(
name = "cel_compiler_capsule",
hdrs = ["cel_compiler_capsule.h"],
tags = ["freethreading_compatible"],
deps = [
":cel_extension",
"@com_google_cel_cpp//compiler",
],
)

cc_library(
name = "status_macros",
hdrs = ["status_macros.h"],
Expand All @@ -150,20 +166,34 @@ cc_library(
],
)

py_library(
name = "leak_check",
testonly = True,
srcs = ["leak_check.py"],
visibility = [":__subpackages__"],
)

py_test(
name = "cel_test",
srcs = ["cel_test.py"],
data = [
":cel",
"//cel_expr_python/ext:ext_optional",
],
deps = [
":leak_check",
"//testing:proto2_test_all_types_py_pb2",
"@com_google_absl_py//absl/testing:absltest",
"@com_google_absl_py//absl/testing:parameterized",
"@com_google_cel_spec//proto/cel/expr:checked_py_pb2",
"@com_google_protobuf//:protobuf",
"@com_google_protobuf//:protobuf_python",
] + select({
"@platforms//os:windows": [],
"//conditions:default": [":cel"],
"//conditions:default": [
":cel",
"//cel_expr_python/ext:ext_optional",
],
}),
)

Expand All @@ -174,6 +204,7 @@ py_test(
":cel",
],
deps = [
":leak_check",
"//testing:proto2_test_all_types_py_pb2",
"@com_google_absl_py//absl/testing:absltest",
] + select({
Expand All @@ -196,6 +227,7 @@ py_test(
deps = [
"//testing:proto2_test_all_types_py_pb2",
"@com_google_absl_py//absl/testing:absltest",
"@com_google_absl_py//absl/testing:parameterized",
] + select({
"@platforms//os:windows": [],
"//conditions:default": [
Expand Down
24 changes: 21 additions & 3 deletions cel_expr_python/cel.pyi
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
from collections.abc import Callable, Mapping, Sequence
import datetime
from typing import Any, ClassVar, TypeAlias, Union
from typing import Any, ClassVar, Generic, TypeAlias, TypeVar, Union
from google.protobuf import descriptor_pool as proto_descriptor_pool
from google.protobuf import message as proto_message

_T = TypeVar('_T')
_D = TypeVar('_D')

class Activation: pass

class CelExtension(CelExtensionBase):
Expand Down Expand Up @@ -46,6 +49,18 @@ class FunctionDecl:
class Overload:
def __init__(self, id: str | None = ..., return_type: Type = ..., parameters: Sequence[Type] = ..., is_member: bool = ..., impl: Callable[..., Any] = ..., signature: str | None = ...) -> None: ...

class Optional(Generic[_T]):
def __init__(self) -> None: ...
@staticmethod
def of(value: _T) -> Optional[_T]: ...
@staticmethod
def none() -> Optional[Any]: ...
def has_value(self) -> bool: ...
def value(self) -> _T: ...
def or_value(self, default: _D) -> _T | _D: ...
def __bool__(self) -> bool: ...
def __eq__(self, other: object) -> bool: ...

class Type:
BOOL: ClassVar[Type] = ...
BYTES: ClassVar[Type] = ...
Expand All @@ -57,6 +72,7 @@ class Type:
LIST: ClassVar[Type] = ...
MAP: ClassVar[Type] = ...
NULL: ClassVar[Type] = ...
OPTIONAL: ClassVar[Type] = ...
STRING: ClassVar[Type] = ...
TIMESTAMP: ClassVar[Type] = ...
TYPE: ClassVar[Type] = ...
Expand All @@ -70,15 +86,17 @@ class Type:
@staticmethod
def Map(key_type: Type, element_type: Type) -> Type: ...
@staticmethod
def Optional(element_type: Type = ...) -> Type: ...
@staticmethod
def Type(element_type: Type) -> Type: ...
def is_assignable_from(self, arg0: Type) -> bool: ...
def is_message(self) -> bool: ...
def name(self) -> str: ...
def __eq__(self, arg0: object) -> bool: ...

BaseValue: TypeAlias = Union[None, bool, int, float, str, bytes, proto_message.Message, datetime.datetime, datetime.timedelta, Type]
PlainValue: TypeAlias = Union[BaseValue, list[PlainValue], dict[PlainValue, PlainValue]]
CelValue: TypeAlias = Union[BaseValue, list[Value], dict[PlainValue, Value]]
PlainValue: TypeAlias = Union[BaseValue, Optional[PlainValue], list[PlainValue], dict[PlainValue, PlainValue]]
CelValue: TypeAlias = Union[BaseValue, Optional[Value], list[Value], dict[PlainValue, Value]]

class Value:
def equals(self, other: Value) -> bool: ...
Expand Down
86 changes: 86 additions & 0 deletions cel_expr_python/cel_compiler_capsule.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* Copyright 2026 Google LLC
*
* 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.
*/

// Shares the `cel::Compiler` owned by a `cel.Env` with pybind11 modules that
// are built and loaded separately from the `cel` module (e.g. `cel_policy`)
// without linking against the `cel` module's internals.
//
// The `cel` module exposes the compiler via the private
// `cel.Env._compiler_capsule()` method, which returns a PyCapsule created by
// `MakeCelCompilerCapsule()`. Other modules read it with `GetCelCompiler()`.

#ifndef THIRD_PARTY_CEL_PYTHON_CEL_COMPILER_CAPSULE_H_
#define THIRD_PARTY_CEL_PYTHON_CEL_COMPILER_CAPSULE_H_

#include <Python.h> // IWYU pragma: keep - Needed for PyCapsule_GetPointer

#include <memory>
#include <utility>

#include "compiler/compiler.h"
#include "cel_expr_python/cel_extension.h"
#include <pybind11/pybind11.h>

namespace cel_python {

// Name of the PyCapsule returned by `cel.Env._compiler_capsule()`. The capsule
// holds a heap-allocated `std::shared_ptr<const cel::Compiler>`, which keeps
// the environment that owns the compiler alive for the capsule's lifetime.
inline constexpr char kCelCompilerCapsuleName[] =
"cel_python.CompilerSharedPtr";

// Wraps `compiler` in a PyCapsule named `kCelCompilerCapsuleName`. The capsule
// shares ownership of `compiler` and releases it when garbage collected.
inline pybind11::capsule MakeCelCompilerCapsule(
std::shared_ptr<const cel::Compiler> compiler) {
return pybind11::capsule(
new std::shared_ptr<const cel::Compiler>(std::move(compiler)),
kCelCompilerCapsuleName, [](void* ptr) {
delete static_cast<std::shared_ptr<const cel::Compiler>*>(ptr);
});
}

// Returns the compiler owned by `env`, which must be a `cel.Env` instance.
// The returned pointer shares ownership of the compiler, so it remains valid
// even if `env` is garbage collected. Requires the GIL.
//
// Throws `TypeError` if `env` is not a `cel.Env`.
inline std::shared_ptr<const cel::Compiler> GetCelCompiler(
pybind11::handle env) {
PYBIND11_CONSTINIT static pybind11::gil_safe_call_once_and_store<
pybind11::object>
env_type_storage;
const pybind11::object& env_type =
env_type_storage
.call_once_and_store_result([] {
return pybind11::module_::import(CEL_MODULE_NAME).attr("Env");
})
.get_stored();
if (!pybind11::isinstance(env, env_type)) {
throw pybind11::type_error("env must be an instance of cel.Env");
}
pybind11::capsule capsule =
env.attr("_compiler_capsule")().cast<pybind11::capsule>();
void* ptr = PyCapsule_GetPointer(capsule.ptr(), kCelCompilerCapsuleName);
if (ptr == nullptr) {
throw pybind11::error_already_set();
}
return *static_cast<std::shared_ptr<const cel::Compiler>*>(ptr);
}

} // namespace cel_python

#endif // THIRD_PARTY_CEL_PYTHON_CEL_COMPILER_CAPSULE_H_
Loading
Loading