Uh oh!
There was an error while loading. Please reload this page.
fix metadata arglists for python defaults - #285
Conversation
jjtolton
commented
Jun 17, 2026
Initial impression, big improvement in dev experience. Doing a little adversarial testing to make sure nothing breaks with weird or opaque types. |
jjtolton
commented
Jun 17, 2026
@jsavyasachi Overall I think this is a great direction. However, there are some edge cases to consider.
![]()
![]()
Technically #4 is actually a bug in the way libpython-clj handles RecursionErrors when stringifying, the recursive repr should result in a stackoverflow and be a special case of (#3) but it's a landmine currently. In general, I think this is a good direction -- the only thing missing is some graceful degradation for the above cases that allows a developer to diagnose and debug without the repl experience being unceremoniously degraded by surprisingy metadata handling. Python adversarial testing fileimportfunctoolsclassBadStr:
"""repr/str both raise -- triggers the throw path in py-default->jvm."""def__repr__(self):
raiseValueError('boom repr')
def__str__(self):
raiseValueError('boom str')
classWeirdStr:
def__repr__(self):
return'x'*40classHugeReprModel:
"""Simulates a PyTorch-style model whose repr is enormous (many nested layers)."""def__init__(self, n_layers=300):
self.n_layers=n_layersdef__repr__(self):
lines= ["GnarlyNet("]
foriinrange(self.n_layers):
lines.append(
f" (layer{i}): Linear(in_features=4096, out_features=4096, bias=True)")
lines.append(f" (act{i}): GELU(approximate='none')")
lines.append(f" (drop{i}): Dropout(p=0.1, inplace=False)")
lines.append(")")
return"\n".join(lines)
__str__=__repr__classRecursiveRepr:
"""repr recurses without bound -> Python raises RecursionError."""def__repr__(self):
returnrepr(self)
__str__=__repr__classHangRepr:
"""repr never returns -> str(x) blocks forever while holding the GIL."""def__repr__(self):
whileTrue:
pass__str__=__repr___bad=BadStr()
_weird=WeirdStr()
_sentinel=object()
_partial=functools.partial(int, 0)
_recursive=RecursiveRepr()
_hang=HangRepr()
_huge=HugeReprModel(300)
deff_class(x=int):
"""Default is a Python class object."""returnxdeff_lambda(x=lambdaa: a):
"""Default is a lambda."""returnxdeff_badstr(x=_bad):
"""Default's str()/repr() raise."""returnxdeff_weird(x=_weird):
"""Default has a custom repr."""returnxdeff_sentinel(x=_sentinel):
"""Default is a bare sentinel object()."""returnxdeff_partial(x=_partial):
"""Default is a functools.partial."""returnxdeff_nested_opaque(x=(int, str)):
"""Default is a tuple of opaque class objects (nested case)."""returnxdeff_huge(model=_huge):
"""Default is an object with a massive repr -- the 'detonate the arglists' case."""returnmodeldeff_kw_huge(*, model=_huge):
"""Keyword-only variant of the massive-repr default."""returnmodeldeff_recursive(x=_recursive):
"""Default's repr recurses infinitely -> RecursionError."""returnxdeff_hang(x=_hang):
"""Default's repr never returns -> str() hangs forever."""returnxdeff_mixed(a, b=1, c=int, *, d=_sentinel, e=2):
"""Mixed positional/keyword-only with a couple of opaque defaults."""return (a, b, c, d, e) |
Thanks for the adversarial pass. I ported your file into
Two I left out of this PR:
Still learning, so happy to hear your thoughts and make further changes if necessary! |
py-default->jvm stringified opaque Python defaults with str(py-str x), which (1) was unbounded for huge reprs, (2) only handled a top-level opaque so a collection default (e.g. a tuple of classes) leaked raw pointers, and (3) threw when a default's repr raised, dropping the var from the namespace. safe-py-str truncates long reprs and falls back to "<unprintable>" when repr/str raises; py-default->jvm now falls back to a str() of the whole default whenever any nested value is opaque. Recursive- and hanging-repr cases are left out: the former is a native crash in libpython-clj's stringify path, the latter needs a timeout.
3127adf to
0952848Comparejjtolton
commented
Jun 18, 2026
Great work and much appreciated! I will take a look today. I agree the last two are out of scope (unless you wanted an extra challenge) as but that's defense in depth against someone hanging a repr, so I would not call it high priority. the goal of course is to make sure no one blames libpython-clj for poorly written python code 😇 |
Uh oh!
There was an error while loading. Please reload this page.
cnuernber
commented
Jun 22, 2026
Thanks @jsavyasachi and @jjtolton for this work - it looks sufficient to me to move forward. |
Agreed. Looks good. There's one extreme pathological edge case if someone put in a massive repr, it gets fully materialized before the truncation occurs, but that can be deferred. |


Fixes#284.
Fixes#283.
Changes
<class 'int'>, instead of exposing pointer metadata.:arglistsmetadata for vars created bypy/from-import, so Clojure doc output can render the signature before the docstring.clojure.repl/docoutput.Verification
Note: the PATH shim was needed locally because Java is x86_64 while the default Homebrew Python is arm64.