Method Proxy (obj._)
User data often contains keys that collide with library method names —
items, keys, values, update, to_dict, copy, and
so on. Letting such data silently shadow the method would break the
API. RecursiveNamespaceV2 solves this with a single reserved attribute:
obj._, which exposes every public method through a proxy.
Why
Without the proxy:
rn = RNS({"items": [1, 2, 3]}) # would shadow rn.items()
rn.items() # TypeError: 'list' object is not callable
The library solves this by giving every method a second, collision-free
home: obj._. Data with method-name keys is accepted (you’ll see a
FutureWarning reminding you that the matching method must be
called as obj._.<name>()); only the _ proxy itself is reserved
and raises KeyError if you try to use "_" as a data key.
Three equivalent call shapes
All three converge on the same implementation in _StaticImpl:
rn = RNS({"name": "John", "address": {"city": "Anytown"}})
# 1. Bound proxy — preferred. Same ergonomics as a regular method.
rn._.to_dict()
rn._.val_set("address.zip", "12345")
# 2. Class-level static container — useful in tests and tooling.
RNS._.to_dict(rn)
RNS._.val_get(rn, "address.city")
# 3. Legacy direct call — works, but emits DeprecationWarning.
rn.to_dict()
Migration plan
The migration runs over three releases:
Phase 1 (this release).
obj._ships alongside the legacy direct-call API. Direct method calls emit aDeprecationWarningpointing to the proxy form. Data keys that collide with public method names are accepted with aFutureWarning(a louder category, visible under Python’s default filter); only_itself raises.Phase 2. The warning is tightened (raised in stricter test configurations).
Phase 3 (next major release). Direct method access is removed. The shadow warning goes away because there is no method left to shadow. Only
_stays reserved.
What lives where
Instance methods (
to_dict,items,val_set, …) live in_StaticImpland are reached viaobj._orRNS._.Classmethod factories (
from_json,from_toml,load_json,load_toml) stay on the class. There is noobj._.from_json(...); useRNS.from_json(...).Dunders (
__setitem__,__len__,__copy__, …) and private helpers (_re,_chain_set_array, …) stay on the class. They are not part of the public API.
Warning visibility (read this if you’re integrating RNS)
RNS emits two warning categories with intentionally different visibility:
Shadow events — your data key collides with a method name (e.g.
RNS({"items": "..."})). Emitted asFutureWarning.FutureWarningis shown under Python’s default filter, so the collision surfaces in production logs the first time it happens — exactly the case where the in-memory object is now subtly broken (obj.itemsis a string,obj.items()raisesTypeError) while the JSON output still looks correct.Direct-call deprecation — you called
obj.to_dict()instead ofobj._.to_dict(). Emitted asDeprecationWarning. Python’s default filter suppressesDeprecationWarningraised outside__main__, so these will be silent in normal application runs. To see them during migration, opt in:# one-off PYTHONWARNINGS=default python app.py # in CI / tests pytest -W default
Or, in a logging-based app, route
warningsthrough your logger at startup so the filter no longer applies:import logging logging.captureWarnings(True)
The rationale for the split: a shadow event is caused by end-user data flowing into RNS and silently breaks the in-memory object, so it must be unmissable. A direct-call deprecation is a developer choosing the legacy API, and migration is something the developer opts into when they’re ready — the standard Python convention.
Suppressing the warning during migration
While you migrate, you can silence the warning per-callsite:
import warnings
with warnings.catch_warnings():
warnings.simplefilter(
"ignore",
DeprecationWarning,
)
rn.to_dict() # silent
Or globally in your test config (pyproject.toml):
[tool.pytest.ini_options]
filterwarnings = [
"ignore:Calling .* directly on an RNS instance is deprecated:DeprecationWarning",
]
Caveats
Subclass overrides of public methods are honoured by the legacy direct call but not by
_StaticImpl.method(obj)— the static dispatcher always runs the base implementation. Prefer overriding the shim and calling through the proxy in Phase 1; full subclass support will arrive when direct methods are removed.Type checkers see
obj._asAnybecause_BoundProxyuses__getattr__. IDE autocomplete still works viadir(obj._); full static typing arrives in a follow-up.obj._ is RNS._is false: class access returns the static container, instance access returns a fresh bound proxy.