Summary
This is an issue with pydantic, but affects ccflow.
Local Pydantic model classes capture their defining function's local variables in
__pydantic_parent_namespace__. When cloudpickle serializes an instance of
that local model class, it can follow that namespace and fail on unrelated,
unpickleable locals.
This affects ccflow because ccflow supports local-scope BaseModel /
ContextBase classes via local persistence.
Minimal Pydantic Repro
import threading
import cloudpickle
import pydantic
from pydantic import BaseModel
print(f"pydantic={pydantic.__version__}")
class Poison:
def __init__(self):
self.lock = threading.RLock()
def repro():
poison = Poison()
class LocalModel(BaseModel):
x: int
print(sorted(LocalModel.__pydantic_parent_namespace__))
print("captures_poison=", "poison" in LocalModel.__pydantic_parent_namespace__)
cloudpickle.dumps(LocalModel(x=1))
repro()
Typical failure:
pydantic=2.13.4
['poison']
captures_poison=True
TypeError: cannot pickle '_thread.RLock' object
Minimal ccflow Repro
import threading
from importlib.metadata import version
import cloudpickle
import pydantic
from ccflow import ContextBase
print(f"ccflow={version('ccflow')}")
print(f"pydantic={pydantic.__version__}")
class Poison:
def __init__(self):
self.lock = threading.RLock()
def repro():
poison = Poison()
class LocalContext(ContextBase):
x: int
print(sorted(LocalContext.__pydantic_parent_namespace__))
print("captures_poison=", "poison" in LocalContext.__pydantic_parent_namespace__)
cloudpickle.dumps(LocalContext(x=1))
repro()
Typical failure:
ccflow=0.8.3
pydantic=2.13.4
['poison']
captures_poison=True
TypeError: cannot pickle '_thread.RLock' object
Why It Appeared Now
PR #215 changed ccflow BaseModel on Pydantic >= 2.13 to use raw Pydantic
ModelMetaclass:
_BASE_MODEL_METACLASS = ModelMetaclass if _USE_RUNTIME_POLYMORPHIC_SERIALIZATION else _SerializeAsAnyMeta
Before that, ccflow always routed class construction through
_SerializeAsAnyMeta. Pydantic still created __pydantic_parent_namespace__,
but it captured ccflow metaclass locals rather than user function locals.
After the change, local ccflow models use raw ModelMetaclass, so Pydantic
captures the user's defining function frame. Any unrelated local in that frame
can become part of the pickle graph.
What It Pertains To
This is about local-scope model classes:
def f():
class LocalContext(ContextBase):
x: int
Module-scope model classes are not affected in normal use because they do not
get this function parent namespace and are importable by module path.
Summary
This is an issue with pydantic, but affects ccflow.
Local Pydantic model classes capture their defining function's local variables in
__pydantic_parent_namespace__. Whencloudpickleserializes an instance ofthat local model class, it can follow that namespace and fail on unrelated,
unpickleable locals.
This affects ccflow because ccflow supports local-scope
BaseModel/ContextBaseclasses via local persistence.Minimal Pydantic Repro
Typical failure:
Minimal ccflow Repro
Typical failure:
Why It Appeared Now
PR #215 changed ccflow
BaseModelon Pydantic >= 2.13 to use raw PydanticModelMetaclass:Before that, ccflow always routed class construction through
_SerializeAsAnyMeta. Pydantic still created__pydantic_parent_namespace__,but it captured ccflow metaclass locals rather than user function locals.
After the change, local ccflow models use raw
ModelMetaclass, so Pydanticcaptures the user's defining function frame. Any unrelated local in that frame
can become part of the pickle graph.
What It Pertains To
This is about local-scope model classes:
Module-scope model classes are not affected in normal use because they do not
get this function parent namespace and are importable by module path.