Skip to content

Dict subclasses not properly handled in CEL evaluation #22

@hardbyte

Description

@hardbyte

Problem

The Rust CEL implementation doesn't properly handle Python dict subclasses when used as variables in CEL Context. When evaluating expressions that access attributes on dict subclass instances, the evaluation returns None instead of the expected values.

Reproduction

from cel import cel

class CustomDict(dict):
    """A dict subclass that loads values on access"""
    def __init__(self):
        super().__init__()
        self['key'] = 'value'

# Test with regular dict - works
regular_context = {'data': {'key': 'value'}}
env1 = cel.Context(variables=regular_context)
result1 = cel.evaluate('data.key', env1)
print(f"Regular dict: {result1}")  # Output: "value"

# Test with dict subclass - doesn't work
custom_dict = CustomDict()
custom_context = {'data': custom_dict}
env2 = cel.Context(variables=custom_context)
result2 = cel.evaluate('data.key', env2)
print(f"Dict subclass: {result2}")  # Output: None (expected: "value")

Impact

This issue affects code that uses dict subclasses for:

  • Lazy loading values (like LazyFileLoadingDict that loads file contents on first access)
  • Custom dict behaviors (caching, validation, etc.)
  • Any dict wrapper or proxy pattern

Use Case

In netchecks, we use a LazyFileLoadingDict to load configuration files from a directory only when accessed:

class LazyFileLoadingDict(dict):
    def __init__(self, directory):
        super().__init__()
        for filename in os.listdir(directory):
            self[filename] = None
    
    def __getitem__(self, key):
        if super().__getitem__(key) is None:
            # Load file contents on first access
            with open(os.path.join(self.directory, key)) as f:
                self[key] = f.read()
        return super().__getitem__(key)

This worked with the Python celpy library but breaks with the Rust implementation.

Current Workaround

We're currently working around this by materializing the dict subclass to a regular dict before passing to CEL:

lazy_dict = LazyFileLoadingDict(directory)
context = {'data': dict(lazy_dict)}  # Convert to regular dict

However, this defeats the purpose of lazy loading and requires changes in calling code.

Expected Behavior

Dict subclasses should be treated the same as regular dicts when accessing their contents via CEL expressions. The Rust CEL implementation should use dict protocol (__getitem__) rather than attribute access when evaluating member access on dict-like objects.

Environment

  • python-common-expression-language: 0.5.3
  • Python: 3.12
  • OS: Linux

Related

This regression was introduced when switching from the Python celpy library to the Rust-based implementation. The Python implementation properly handled dict subclasses.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions