cyclopts icon indicating copy to clipboard operation
cyclopts copied to clipboard

`NameError: name 'Annotated' is not defined`

Open domWalters opened this issue 11 months ago • 3 comments

Note: This is quite a weird error, it took a while for me to be able to cut down my application to a minimal version that kept the error.

Attempted Explanation

If from __future__ import annotations is used in a file where a class decorated with @Parameter @dataclass is defined (class used to define common parameters) and that class is used as a base in another file, a runtime error will occur when any parameter from the inherited class is used.

Replication

  • Make a file a.py:
from __future__ import annotations

from dataclasses import dataclass
from typing import Annotated

from cyclopts import Parameter


@Parameter(name="*")
@dataclass
class A:

    verbose: Annotated[bool, Parameter(negative="")] = False
  • Make a file main.py (this has to be a separate file, merging these two files avoids the issue):
#!/usr/bin/env python3

from dataclasses import dataclass

from cyclopts import App

from a import A

app = App()


@dataclass
class B(A):
    ...


@app.command()
def bad(
    *,
    common: B | None = None,
) -> None:
    ...


if __name__ == "__main__":
    app()
  • Run:
chmod +x main.py`
uv venv --python 3.10
source .venv/bin/activate
uv pip install cyclopts==3.16.1
./main.py bad --verbose
  • Output:
Traceback (most recent call last):
  File "/tmp/cyclopts/minimal/./main.py", line 26, in <module>
    app()
  File "/tmp/cyclopts/minimal/.venv/lib/python3.10/site-packages/cyclopts/core.py", line 1213, in __call__
    command, bound, _ = self.parse_args(
  File "/tmp/cyclopts/minimal/.venv/lib/python3.10/site-packages/cyclopts/core.py", line 1128, in parse_args
    command, bound, unused_tokens, ignored, argument_collection = self._parse_known_args(
  File "/tmp/cyclopts/minimal/.venv/lib/python3.10/site-packages/cyclopts/core.py", line 1018, in _parse_known_args
    bound, unused_tokens = create_bound_arguments(
  File "/tmp/cyclopts/minimal/.venv/lib/python3.10/site-packages/cyclopts/bind.py", line 351, in create_bound_arguments
    argument_collection._convert()
  File "/tmp/cyclopts/minimal/.venv/lib/python3.10/site-packages/cyclopts/argument.py", line 190, in _convert
    argument.convert_and_validate()
  File "/tmp/cyclopts/minimal/.venv/lib/python3.10/site-packages/cyclopts/argument.py", line 1181, in convert_and_validate
    val = self.convert(converter=converter)
  File "/tmp/cyclopts/minimal/.venv/lib/python3.10/site-packages/cyclopts/argument.py", line 1097, in convert
    self.value = self._convert(converter=converter)
  File "/tmp/cyclopts/minimal/.venv/lib/python3.10/site-packages/cyclopts/argument.py", line 1070, in _convert
    self._run_missing_keys_checker(data)
  File "/tmp/cyclopts/minimal/.venv/lib/python3.10/site-packages/cyclopts/argument.py", line 1281, in _run_missing_keys_checker
    if not (missing_keys := self._missing_keys_checker(self, data)):
  File "/tmp/cyclopts/minimal/.venv/lib/python3.10/site-packages/cyclopts/argument.py", line 101, in inner
    field_info = get_field_info(argument.hint)
  File "/tmp/cyclopts/minimal/.venv/lib/python3.10/site-packages/cyclopts/field_info.py", line 149, in _generic_class_field_infos
    for name, field_info in signature_parameters(f.__init__).items():
  File "/tmp/cyclopts/minimal/.venv/lib/python3.10/site-packages/cyclopts/field_info.py", line 295, in signature_parameters
    type_hints = get_type_hints(func, include_extras=True)
  File "/home/dwalters/.local/share/uv/python/cpython-3.10.16-linux-x86_64-gnu/lib/python3.10/typing.py", line 1871, in get_type_hints
    value = _eval_type(value, globalns, localns)
  File "/home/dwalters/.local/share/uv/python/cpython-3.10.16-linux-x86_64-gnu/lib/python3.10/typing.py", line 327, in _eval_type
    return t._evaluate(globalns, localns, recursive_guard)
  File "/home/dwalters/.local/share/uv/python/cpython-3.10.16-linux-x86_64-gnu/lib/python3.10/typing.py", line 694, in _evaluate
    eval(self.__forward_code__, globalns, localns),
  File "<string>", line 1, in <module>
NameError: name 'Annotated' is not defined

If you remove the from __future__ line, this example will work fine.

domWalters avatar May 13 '25 18:05 domWalters

This might be related to #352

domWalters avatar May 13 '25 18:05 domWalters

For reference as to what I did that caused this to happen:

  • I changed my ruff config to add the following:
[tool.ruff.lint.isort]
required-imports = ["from __future__ import annotations"]
  • This caused issues with cyclopts as this combined with some flake8 rules caused imports that cyclopts uses at runtime to be moved to TYPE_CHECKING blocks.
    • I fixed this with:
[tool.ruff.lint.flake8-type-checking]
exempt-modules = ["cyclopts", "typing"]

This error started occurring as a result, although I didn't realised until sometime later after making a load of other changes.

The following prevented the I002 rule from attempting to add the from __future__ ... line to the file with the issue:

[tool.ruff.lint.per-file-ignores]
"/path/to/my/version/of/a.py" = ["I002"]

This allows me to avoid the issue, whilst still mandating that from __future__ ... be used in every file except the one that causes this issue.

domWalters avatar May 13 '25 18:05 domWalters

hmmm, this is definitely weird (and maybe actually a bug in python's typing? Definitely want to investigate further before making any claims).

If you add the following unused imports to your main.py, it all works:

from typing import Annotated
from cyclopts import Parameter

BrianPugh avatar May 14 '25 00:05 BrianPugh

related: I think pydantic had to solve a very similar problem, and we can re-use their solution. Related links:

  • https://github.com/pydantic/pydantic/blob/103f64da67ad23dfdc7406187db32019a0204d70/pydantic/_internal/_typing_extra.py#L210
  • https://github.com/pydantic/pydantic/blob/103f64da67ad23dfdc7406187db32019a0204d70/docs/internals/resolving_annotations.md?plain=1#L43

BrianPugh avatar May 16 '25 13:05 BrianPugh

More related:

  • https://github.com/python/cpython/issues/89687

BrianPugh avatar May 21 '25 01:05 BrianPugh

@domWalters So I'm not 100% sure how to proceed with this:

  1. I made this branch that I think fixes it, but the code is kind of jank. Further still, docstrings from class A are not being resolved properly. I could probably pile on more jank to fix this, but I'm not sure if it's the way to go.
  2. It seems like CPython has some bugs related to this.
  3. It looks like they might be fixing those dataclass bugs, but it would only be backported to 3.13 and 3.14.
  4. PEP-649 and PEP-749 deprecate the usage of from __future__ import annotations

BrianPugh avatar May 21 '25 02:05 BrianPugh

@domWalters any additional input? Otherwise I'll close this issue as "not planned."

BrianPugh avatar Jun 06 '25 01:06 BrianPugh

Apologies for not replying sooner.

I think not planned is fair as this does seem like more of an issue with python itself.

Might make sense to mention it in some FAQ on the docs?

domWalters avatar Jun 08 '25 09:06 domWalters

Added a "Known Issues" page to the documentation. Let me know if you have any further issues!

BrianPugh avatar Jun 09 '25 13:06 BrianPugh