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
7 changes: 7 additions & 0 deletions src/outlines/types/dsl.py
Original file line number Diff line number Diff line change
Expand Up @@ -766,6 +766,13 @@ def python_types_to_terms(ptype: Any, recursion_depth: int = 0) -> Term:
return types.datetime

# Basic type instances
if ptype is None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_handle_union peels off both type(None) and None because typing normalizes the two, but this only catches the bare value: get_args(List[None]) is (NoneType,), so List[None] / Dict[str, None] / Tuple[None, ...] still recurse into NoneType and hit the "Type NoneType is currently not supported" raise. Worth widening to if ptype is None or ptype is type(None) and adding a container case to the test?

# ``None`` used as a literal value (e.g. ``Literal[None]`` or a bare
# ``None`` member) maps to the bare ``None`` keyword, mirroring how
# ``Optional``/``Union`` render their None member in ``_handle_union``.
# Kept as a ``Regex`` (like ``True``/``False``) so it stays unquoted
# when the value ends up nested inside a container type.
return Regex("None")
if isinstance(ptype, bool):
return Regex(str(ptype))
elif is_str_instance(ptype):
Expand Down
31 changes: 31 additions & 0 deletions tests/types/test_dsl.py
Original file line number Diff line number Diff line change
Expand Up @@ -704,6 +704,23 @@ def test_dsl_literal_bool():
assert result_both == Alternatives([Regex("True"), Regex("False")])


def test_dsl_literal_none():
# Literal[None] previously raised "Type None is currently not supported"
# because the None value was recursed into as if it were a type. It should
# resolve to the bare ``None`` keyword, the same way Optional/Union render
# their None member.
result_none = python_types_to_terms(Literal[None])
assert isinstance(result_none, Alternatives)
assert result_none.terms == [Regex("None")]

# None mixed with other literal values must resolve too.
result_mixed = python_types_to_terms(Literal["active", None])
assert result_mixed == Alternatives([String("active"), Regex("None")])

# A bare None value maps to the same bare keyword as it does in a union.
assert python_types_to_terms(None) == Regex("None")


def test_dsl_numeric_literal_escapes_regex_metacharacters():
# A float's ``.`` must match literally, not act as a regex wildcard.
term = python_types_to_terms(1.5)
Expand Down Expand Up @@ -1186,6 +1203,20 @@ def test_e2e_optional_none_not_quoted_in_containers():
assert not _re.fullmatch(literal_pattern, "[None]")


def test_e2e_list_literal_none():
"""``Literal[None]`` renders the bare ``None`` keyword, both standalone and
nested in a container, consistently with ``Optional``/``Union``."""
standalone = to_regex(python_types_to_terms(Literal[None]))
assert _re.fullmatch(standalone, "None")
assert not _re.fullmatch(standalone, '"None"')

list_pattern = to_regex(python_types_to_terms(list[Literal["yes", None]]))
assert _re.fullmatch(list_pattern, "[None]")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

list[Literal[None]] is still generating [None], which json.loads rejects; the JSON spelling here is null. Should _ensure_json_quoted translate this branch in container contexts, the same way #1947 handles True/False, while keeping standalone Literal[None] as None?

assert _re.fullmatch(list_pattern, '["yes", None]')
# The None branch stays a bare keyword and is not JSON-quoted like a string.
assert not _re.fullmatch(list_pattern, '["None"]')


def test_to_regex():
string_term = String("hello")
assert to_regex(string_term) == r"hello"
Expand Down
Loading