Skip to content

Commit 9cf4f00

Browse files
committed
fix: enormously obscure bug in the typing module
1 parent df64a7e commit 9cf4f00

File tree

4 files changed

+41
-7
lines changed

4 files changed

+41
-7
lines changed

docs/Changelog.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Changelog
22

3-
## 1.1.0 (2025-09-11)
3+
## 1.1.0 (2025-09-12)
44
* CHANGED – some [`run`][mininterface.run] arguments are no longer positional and can only be passed as keyword arguments
55
* feat: interactive CLI (nested configuration missing fields dialogs)
66
* feat: [`run`][mininterface.run] add_version, add_version_package, add_quiet flags

mininterface/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
from ._lib.run import run
32
from ._mininterface import Mininterface
43
from .exceptions import Cancelled

mininterface/tag/flag.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -204,8 +204,6 @@ class Env:
204204
# for `Annotated[Blank4[int], Literal[fn()]]` for static literal values
205205

206206

207-
208-
209207
if not TYPE_CHECKING:
210208
# In runtime, flag must not be an Annotated but a full class.
211209
# When type checking, flag must not be a full class otherwise
@@ -283,4 +281,4 @@ def instance_from_str(args):
283281
instance_from_str=lambda args: instance_from_str(args),
284282
is_instance=lambda ins: ins is None or isinstance(ins, types),
285283
str_from_instance=lambda ins: [str(ins) + f" / or if left blank: {default_val}"],
286-
)
284+
)

mininterface/tag/tag_factory.py

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from pathlib import Path
55
from typing import Any, Iterable, Literal, Type, get_origin, get_type_hints
66

7-
from annotated_types import BaseMetadata, GroupedMetadata
7+
from annotated_types import BaseMetadata, GroupedMetadata, Len
88

99
from . import DatetimeTag, SelectTag, Tag
1010
from .callback_tag import CallbackTag
@@ -105,11 +105,48 @@ def tag_factory(
105105
new.annotation = annotation or field_type.__origin__
106106
# Annotated[date, Tag(name="hello")] = datetime.fromisoformat(...) -> DatetimeTag(date=True)
107107
tag = tag_assure_type(new._fetch_from(Tag(*args, **kwargs), include_ref=True))
108-
elif isinstance(metadata, (BaseMetadata, GroupedMetadata)):
108+
elif isinstance(metadata, (BaseMetadata, Len)):
109+
# Why not checking `GroupedMetadata` instead of `Len`? See below. You won't believe.
109110
validators.append(metadata)
110111
if not tag:
111112
tag = tag_assure_type(Tag(val, description, annotation, *args, **kwargs))
112113

113114
if validators: # we prepend annotated_types validators to the current validator
114115
tag._add_validation(validators)
115116
return tag
117+
118+
# NOTE I'd like to check `GroupedMetadata` instead of the `Len` (the only currently supported GroupedMetadata).
119+
# However, that's not possible because a mere checking of some things from the typing module,
120+
# like `isinstance(Literal[2], GroupedMetadata)`, will add a trailing __annotations__ to some objects in the typing module.
121+
#
122+
# Once upon a day, I thought I debug a pretty obscure case from tyro, concerning global imports of a primitive specs registry
123+
# when doing tests in paralel. The bug appeared reliably but only on the server, never on the localhost, and for Python3.12+
124+
# only (which I am running to on localhost). I find out a test fails when another specific test is there. But it was not the end,
125+
# the problem lied not in tyro but deeper, some chances are there is a bug in the Python itself. It turned out
126+
# a mere presence of a Literal in an Annotated statement cause a bug in an independent test.
127+
#
128+
# The dark magic happens in typing._proto_hook at line
129+
# `getattr(base, '__annotations__', {})`
130+
# while `base = _LiteralSpecialForm)`
131+
#
132+
# This utterly obscure case will make tyro cycle when handling unions,
133+
# Union must not have trailing __annotations__
134+
# `runm([Subc1, Subc2])` # will cycle
135+
# So the code breaks up a different time on another place. Tremendous.
136+
#
137+
# https://github.com/annotated-types/annotated-types/issues/94
138+
#
139+
# ```python
140+
# from typing import Literal, Optional, Union
141+
# from annotated_types import GroupedMetadata, Len
142+
# print(hasattr(Union[1, 2],"__annotations__")) # False
143+
# print(hasattr(Optional,"__annotations__")) # False
144+
# print(hasattr(Literal,"__annotations__")) # False
145+
#
146+
# isinstance(Literal, GroupedMetadata)
147+
#
148+
# print(Union[1, 2].__annotations__) # {}
149+
# print(Optional.__annotations__) # {}
150+
# print(Literal.__annotations__) # {}
151+
# ````
152+
#

0 commit comments

Comments
 (0)