From 41a575cf1a571b01aa4ed6e860f27c7003355501 Mon Sep 17 00:00:00 2001 From: Thor Whalen <1906276+thorwhalen@users.noreply.github.com> Date: Tue, 20 Oct 2026 18:28:22 +0000 Subject: [PATCH] param_default: treat any i2 placeholder default as no default Use i2.is_placeholder_default (NotSet or any i2.DefaultPlaceholder, e.g. the marker methodize(..., default_from=...) puts in a signature), falling back to is_not_set / NotSet for older i2. Closes #49. Co-Authored-By: Claude Opus 5.5 --- py2http/decorators.py | 2 +- py2http/schema_tools.py | 27 ++++++++++-------- py2http/tests/test_not_set_defaults.py | 39 ++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 12 deletions(-) diff --git a/py2http/decorators.py b/py2http/decorators.py index 1f6106c..dbfad1f 100644 --- a/py2http/decorators.py +++ b/py2http/decorators.py @@ -395,7 +395,7 @@ def from_func(cls, func, _dflt_default=None): } def dflt_of(param): - dflt = param_default(param) # i2's NotSet sentinel counts as "no default" + dflt = param_default(param) # i2 placeholder defaults (e.g. NotSet) count as "no default" return _dflt_default if dflt is Parameter.empty else dflt _name_and_dflts = {x.name: dflt_of(x) for x in params} diff --git a/py2http/schema_tools.py b/py2http/schema_tools.py index b47be4f..e9925ff 100644 --- a/py2http/schema_tools.py +++ b/py2http/schema_tools.py @@ -11,20 +11,25 @@ from i2.errors import InputError try: - from i2 import is_not_set -except ImportError: # older i2: same sentinel, not exported from the root yet - from i2.deco import NotSet as _NotSet + from i2 import is_placeholder_default +except ImportError: # older i2: no DefaultPlaceholder yet, so only NotSet is a placeholder + try: + from i2 import is_not_set as is_placeholder_default + except ImportError: # older still: same sentinel, not exported from the root yet + from i2.deco import NotSet as _NotSet - def is_not_set(x) -> bool: - """Return True iff ``x`` is ``i2``'s ``NotSet`` sentinel.""" - return x is _NotSet + def is_placeholder_default(x) -> bool: + """Return True iff ``x`` is ``i2``'s ``NotSet`` sentinel.""" + return x is _NotSet def param_default(param: Parameter): - """Return ``param.default``, or ``Parameter.empty`` if it is ``i2``'s ``NotSet``. + """Return ``param.default``, or ``Parameter.empty`` if it is a placeholder default. - ``NotSet`` in a signature means "no value given", not a real default, so schema - builders treat that param as required, with no default (it is not JSON + A placeholder default is ``i2``'s ``NotSet`` sentinel or any + ``i2.DefaultPlaceholder`` (such as the marker ``methodize(..., default_from=...)`` + puts in a signature). It means "no concrete value here", not a real default, so + schema builders treat that param as required, with no default (it is not JSON serializable either). >>> from i2.deco import NotSet @@ -34,7 +39,7 @@ def param_default(param: Parameter): """ default = param.default - return Parameter.empty if is_not_set(default) else default + return Parameter.empty if is_placeholder_default(default) else default COMPLEX_TYPE_MAPPING = {} @@ -164,7 +169,7 @@ def mk_input_schema_from_func(func, exclude_keys=None, include_func_params=False default_type = Any p = {} - default = param_default(param) # i2's NotSet sentinel counts as "no default" + default = param_default(param) # i2 placeholder defaults (e.g. NotSet) count as "no default" if default is not Parameter.empty: if type(default) in JSON_TYPES: default_type = type(default) diff --git a/py2http/tests/test_not_set_defaults.py b/py2http/tests/test_not_set_defaults.py index d6b5d3d..5faab49 100644 --- a/py2http/tests/test_not_set_defaults.py +++ b/py2http/tests/test_not_set_defaults.py @@ -6,7 +6,9 @@ """ import json +from inspect import Parameter +import pytest from i2 import Sig from i2.deco import NotSet @@ -71,3 +73,40 @@ def mk_factory(): _, spec = mk_routes_and_openapi_specs([with_not_set]) assert "NotSet" not in json.dumps(spec) assert spec == mk_routes_and_openapi_specs([plain])[1] + + +try: + from i2 import is_placeholder_default as _ # noqa: F401 + + _HAS_PLACEHOLDERS = True +except ImportError: + _HAS_PLACEHOLDERS = False + + +@pytest.mark.skipif(not _HAS_PLACEHOLDERS, reason="i2 without DefaultPlaceholder") +def test_methodize_default_from_placeholder_is_no_default(): + """i2mint/py2http#49: a ``methodize(..., default_from=...)`` default is a marker, + not a value: the schema keeps that param required, with no default. + """ + from dataclasses import dataclass + + from i2.wrapper import methodize + + def baz(x, y: int, z: int, *, d: float = 1.5): + return x + y * z + d + + @dataclass + class K: + x: int + zoo: int = 2 + bar = methodize(baz, bind="x", default_from=[("z", "zoo")]) + + method = K(1).bar + assert Sig(method).parameters["z"].default is not Parameter.empty # marker is there + + schema = mk_input_schema_from_func(method) + assert schema["required"] == ["y", "z"] + assert "default" not in schema["properties"]["z"] + + specifier = ParamsSpecifier.from_func(method, _dflt_default="dflt") + assert specifier._name_and_dflts == {"y": "dflt", "z": "dflt", "d": 1.5}