Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions src/click/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -702,9 +702,29 @@ def lookup_default(self, name: str, call: bool = True) -> t.Any | None:
:param call: If the default is a callable, call it. Disable to
return the callable instead.

.. versionchanged:: 8.3.2
Returns ``None`` instead of internal sentinel when no default
is found in the default map.

.. versionchanged:: 8.0
Added the ``call`` parameter.
"""
value = self._lookup_default(name, call)
# Normalize UNSET to None for public API compatibility.
# Internal code uses _lookup_default directly to distinguish
# between "no value" (UNSET) and "explicitly set to None".
if value is UNSET:
return None
return value

def _lookup_default(self, name: str, call: bool = True) -> t.Any:
"""Internal method to get default from default_map.

Returns UNSET sentinel if no default is found. Use lookup_default()
for public API which normalizes UNSET to None.

:meta private:
"""
if self.default_map is not None:
value = self.default_map.get(name, UNSET)

Expand Down Expand Up @@ -2278,7 +2298,7 @@ def get_default(
.. versionchanged:: 8.0
Added the ``call`` parameter.
"""
value = ctx.lookup_default(self.name, call=False) # type: ignore
value = ctx._lookup_default(self.name, call=False) # type: ignore

if value is UNSET:
value = self.default
Expand Down Expand Up @@ -2321,7 +2341,7 @@ def consume_value(
source = ParameterSource.ENVIRONMENT

if value is UNSET:
default_map_value = ctx.lookup_default(self.name) # type: ignore
default_map_value = ctx._lookup_default(self.name) # type: ignore
if default_map_value is not UNSET:
value = default_map_value
source = ParameterSource.DEFAULT_MAP
Expand Down
56 changes: 56 additions & 0 deletions tests/test_defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,59 @@ def prefers_red(color):
assert "red" in result.output
result = runner.invoke(prefers_red, ["--green"])
assert "green" in result.output


def test_lookup_default_returns_none_not_sentinel(runner):
"""Test that lookup_default returns None when parameter not in default_map.

Regression test for issue #3145.
"""

@click.command()
@click.option("--email")
@click.pass_context
def cmd(ctx, email):
# When key not in default_map, lookup_default should return None
result = ctx.lookup_default("nonexistent")
assert result is None, f"Expected None, got {result!r}"
click.echo("OK")

result = runner.invoke(cmd)
assert result.exit_code == 0
assert "OK" in result.output


def test_lookup_default_returns_none_with_empty_default_map(runner):
"""Test that lookup_default returns None even when default_map exists but key missing."""

@click.command()
@click.option("--name", default="test")
@click.pass_context
def cmd(ctx, name):
# Set default_map but query for nonexistent key
ctx.default_map = {"other_param": "value"}
result = ctx.lookup_default("missing_key")
assert result is None, f"Expected None, got {result!r}"
click.echo("OK")

result = runner.invoke(cmd)
assert result.exit_code == 0
assert "OK" in result.output


def test_lookup_default_still_returns_actual_defaults(runner):
"""Test that lookup_default still returns actual values from default_map."""

@click.command()
@click.option("--name")
@click.pass_context
def cmd(ctx, name):
ctx.default_map = {"email": "test@example.com"}
# Should return the actual default when present
result = ctx.lookup_default("email")
assert result == "test@example.com"
click.echo("OK")

result = runner.invoke(cmd)
assert result.exit_code == 0
assert "OK" in result.output
Loading