Skip to content

lookup_default returns Sentinel.UNSET instead of None #3145

@peterlynch

Description

@peterlynch

Upgrading click to 8.3.0 or 8.3.1 causes a regression in lookup_default

Reproduce

Change the click dependency in the below reproduce case to 8.2.1 and the assertion does not fail.

Install uv. Run with uv run repro_click_unset_regression.py or simply `repro_click_unset_regression.py'.

#!/usr/bin/env -S uv run
# /// script
# dependencies = ["click==8.3.1"]
# ///
"""
Run with uv installed:
  repro_click_unset_regression.py
Expected (pre 8.3.0): lookup_default returns the prefix-level default string or None.
Actual (8.3.0+): returns Sentinel.UNSET instead of None.
"""
import importlib.metadata
import sys
import click

class CustomClickContext(click.Context):
    def lookup_default(self, name: str, call: bool = False):
        if call:
            default = super().lookup_default(name, call=True)  # call: Literal[True]
        else:
            default = super().lookup_default(name, call=False)  # call: Literal[False]

        # Original logic (kept to reproduce): treats any non-None (including sentinel) as a real default.
        if default is not None:
            return default
        prefix = name.split("_", 1)[0]
        group = getattr(self, "default_map", {}).get(prefix)
        if group:
            return group.get(name)
        return default

def describe(value):
    cls = value.__class__
    return {
        "repr": repr(value),
        "type": f"{cls.__module__}.{cls.__qualname__}",
        "has_name_attr": hasattr(value, "name"),
        "name_attr": getattr(value, "name", None),
    }

def main():
    default_map = {"app": {"email": "[email protected]"}}
    cmd = click.Command("get-views")
    ctx = CustomClickContext(cmd, info_name=None)
    ctx.default_map = default_map

    val = ctx.lookup_default("email", False)
    info = describe(val)
    print(f"click version (importlib.metadata): {importlib.metadata.version('click')}")
    print("lookup_default raw:", info)
    # This assert fails under ≥ 8.3.0 because val is the internal Sentinel.UNSET object.
    assert val is None, (
        f"Regression: got {info['repr']} ({info['type']}) instead of expected builtins.NoneType"
    )

if __name__ == "__main__":
    try:
        main()
    except AssertionError as e:
        print("AssertionError:", e)
        sys.exit(1)

Expected

Return None instead of internal Sentinel.UNSET detail.

Environment:

  • Python version: 3.14
  • Click version: 8.30, 8.3.1

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugf:contextFeature for context object

    Type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions