Repository: PrefectHQ/prefect

Issue:
Making a `Flow` with parameter type hints guarded by `if TYPE_CHECKING:` fails
### First check

- [X] I added a descriptive title to this issue.
- [X] I used the GitHub search to find a similar issue and didn't find it.
- [X] I searched the Prefect documentation for this issue.
- [X] I checked that this issue is related to Prefect and not one of its dependencies.

### Bug summary

As a follow-up to #11578, when a type-hint is guarded behind an `if TYPE_CHECKING:` block, Prefect can't construct the `Flow`. It's obvious _why_ this occurs, but maybe there's something we should do here. Honestly, I can't think of any practical solution though.

### Reproduction

```python
from __future__ import annotations
from prefect import flow
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    class Test:
        pass

@flow
def foo(x: Test):
    return

assert foo
```

The above fails with a `NameError: name 'Test' is not defined`. However, do the following and it works:

```python
from __future__ import annotations
from prefect import flow

class Test:
    pass

@flow
def foo(x: Test):
    return

assert foo
```

### Error

```python
File /mnt/c/Users/asros/github/prefect/src/prefect/flows.py:1380, in flow(__fn, name, version, flow_run_name, retries, retry_delay_seconds, task_runner, description, timeout_seconds, validate_parameters, persist_result, result_storage, result_serializer, cache_result_in_memory, log_prints, on_completion, on_failure, on_cancellation, on_crashed)
   1278 """
   1279 Decorator to designate a function as a Prefect workflow.
   1280
   (...)
   1375     >>>     pass
   1376 """
   1377 if __fn:
   1378     return cast(
   1379         Flow[P, R],
-> 1380         Flow(
   1381             fn=__fn,
   1382             name=name,
   1383             version=version,
   1384             flow_run_name=flow_run_name,
   1385             task_runner=task_runner,
   1386             description=description,
   1387             timeout_seconds=timeout_seconds,
   1388             validate_parameters=validate_parameters,
   1389             retries=retries,
   1390             retry_delay_seconds=retry_delay_seconds,
   1391             persist_result=persist_result,
   1392             result_storage=result_storage,
   1393             result_serializer=result_serializer,
   1394             cache_result_in_memory=cache_result_in_memory,
   1395             log_prints=log_prints,
   1396             on_completion=on_completion,
   1397             on_failure=on_failure,
   1398             on_cancellation=on_cancellation,
   1399             on_crashed=on_crashed,
   1400         ),
   1401     )
   1402 else:
   1403     return cast(
   1404         Callable[[Callable[P, R]], Flow[P, R]],
   1405         partial(
   (...)
   1425         ),
   1426     )

File /mnt/c/Users/asros/github/prefect/src/prefect/context.py:185, in PrefectObjectRegistry.register_instances.<locals>.__register_init__(__self__, *args, **kwargs)
    183 registry = cls.get()
    184 try:
--> 185     __init__(__self__, *args, **kwargs)
    186 except Exception as exc:
    187     if not registry or not registry.capture_failures:

File /mnt/c/Users/asros/github/prefect/src/prefect/flows.py:307, in Flow.__init__(self, fn, name, version, flow_run_name, retries, retry_delay_seconds, task_runner, description, timeout_seconds, validate_parameters, persist_result, result_storage, result_serializer, cache_result_in_memory, log_prints, on_completion, on_failure, on_cancellation, on_crashed)
    297 self.retries = (
    298     retries if retries is not None else PREFECT_FLOW_DEFAULT_RETRIES.value()
    299 )
    301 self.retry_delay_seconds = (
    302     retry_delay_seconds
    303     if retry_delay_seconds is not None
    304     else PREFECT_FLOW_DEFAULT_RETRY_DELAY_SECONDS.value()
    305 )
--> 307 self.parameters = parameter_schema(self.fn)
    308 self.should_validate_parameters = validate_parameters
    310 if self.should_validate_parameters:
    311     # Try to create the validated function now so that incompatibility can be
    312     # raised at declaration time rather than at runtime
    313     # We cannot, however, store the validated function on the flow because it
    314     # is not picklable in some environments

File /mnt/c/Users/asros/github/prefect/src/prefect/utilities/callables.py:316, in parameter_schema(fn)
    301 """Given a function, generates an OpenAPI-compatible description
    302 of the function's arguments, including:
    303     - name
   (...)
    313     ParameterSchema: the argument schema
    314 """
    315 if (3, 10) <= sys.version_info < (3, 13):
--> 316     signature = inspect.signature(fn, eval_str=True)
    317 else:
    318     signature = inspect.signature(fn)

File ~/software/miniconda/envs/prefect/lib/python3.10/inspect.py:3254, in signature(obj, follow_wrapped, globals, locals, eval_str)
   3252 def signature(obj, *, follow_wrapped=True, globals=None, locals=None, eval_str=False):
   3253     """Get a signature object for the passed callable."""
-> 3254     return Signature.from_callable(obj, follow_wrapped=follow_wrapped,
   3255                                    globals=globals, locals=locals, eval_str=eval_str)

File ~/software/miniconda/envs/prefect/lib/python3.10/inspect.py:3002, in Signature.from_callable(cls, obj, follow_wrapped, globals, locals, eval_str)
   2998 @classmethod
   2999 def from_callable(cls, obj, *,
   3000                   follow_wrapped=True, globals=None, locals=None, eval_str=False):
   3001     """Constructs Signature for the given callable object."""
-> 3002     return _signature_from_callable(obj, sigcls=cls,
   3003                                     follow_wrapper_chains=follow_wrapped,
   3004                                     globals=globals, locals=locals, eval_str=eval_str)

File ~/software/miniconda/envs/prefect/lib/python3.10/inspect.py:2463, in _signature_from_callable(obj, follow_wrapper_chains, skip_bound_arg, globals, locals, eval_str, sigcls)
   2458             return sig.replace(parameters=new_params)
   2460 if isfunction(obj) or _signature_is_functionlike(obj):
   2461     # If it's a pure Python function, or an object that is duck type
   2462     # of a Python function (Cython functions, for instance), then:
-> 2463     return _signature_from_function(sigcls, obj,
   2464                                     skip_bound_arg=skip_bound_arg,
   2465                                     globals=globals, locals=locals, eval_str=eval_str)
   2467 if _signature_is_builtin(obj):
   2468     return _signature_from_builtin(sigcls, obj,
   2469                                    skip_bound_arg=skip_bound_arg)

File ~/software/miniconda/envs/prefect/lib/python3.10/inspect.py:2307, in _signature_from_function(cls, func, skip_bound_arg, globals, locals, eval_str)
   2305 keyword_only_count = func_code.co_kwonlyargcount
   2306 keyword_only = arg_names[pos_count:pos_count + keyword_only_count]
-> 2307 annotations = get_annotations(func, globals=globals, locals=locals, eval_str=eval_str)
   2308 defaults = func.__defaults__
   2309 kwdefaults = func.__kwdefaults__

File ~/software/miniconda/envs/prefect/lib/python3.10/inspect.py:175, in get_annotations(obj, globals, locals, eval_str)
    172 if locals is None:
    173     locals = obj_locals
--> 175 return_value = {key:
    176     value if not isinstance(value, str) else eval(value, globals, locals)
    177     for key, value in ann.items() }
    178 return return_value

File ~/software/miniconda/envs/prefect/lib/python3.10/inspect.py:176, in <dictcomp>(.0)
    172 if locals is None:
    173     locals = obj_locals
    175 return_value = {key:
--> 176     value if not isinstance(value, str) else eval(value, globals, locals)
    177     for key, value in ann.items() }
    178 return return_value

File <string>:1

NameError: name 'Test' is not defined
```


### Versions

Version:             2.14.13+22.g8c71e5653b.dirty
API version:         0.8.4
Python version:      3.10.13
Git commit:          8c71e565
Built:               Wed, Jan 10, 2024 3:36 PM
OS/Arch:             linux/x86_64
Profile:             default
Server type:         cloud

### Additional context




Discussion:
Closing this because that was a mistake on my part. Should not have been opened until #11578 is merged.

Resolving patch:
diff --git a/src/prefect/utilities/callables.py b/src/prefect/utilities/callables.py
--- a/src/prefect/utilities/callables.py
+++ b/src/prefect/utilities/callables.py
@@ -2,7 +2,6 @@
 Utilities for working with Python callables.
 """
 import inspect
-import sys
 from functools import partial
 from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple
 
@@ -312,9 +311,9 @@ def parameter_schema(fn: Callable) -> ParameterSchema:
     Returns:
         ParameterSchema: the argument schema
     """
-    if (3, 10) <= sys.version_info < (3, 13):
+    try:
         signature = inspect.signature(fn, eval_str=True)
-    else:
+    except (NameError, TypeError):
         signature = inspect.signature(fn)
 
     model_fields = {}
@@ -341,7 +340,7 @@ class ModelConfig:
             create_schema(
                 "CheckParameter", model_cfg=ModelConfig, **{name: (type_, field)}
             )
-        except ValueError:
+        except (ValueError, TypeError):
             # This field's type is not valid for schema creation, update it to `Any`
             type_ = Any
         model_fields[name] = (type_, field)


Test patch:
diff --git a/tests/test_flows_compat.py b/tests/test_flows_compat.py
--- a/tests/test_flows_compat.py
+++ b/tests/test_flows_compat.py
@@ -1,20 +1,30 @@
 from __future__ import annotations
 
-import sys
-
-import pytest
+from typing import TYPE_CHECKING
 
 from prefect import flow
 
+if TYPE_CHECKING:
+
+    class Test2:
+        pass
+
 
 class Test:
     pass
 
 
-@pytest.mark.skipif(sys.version_info < (3, 10), reason="requires python 3.10 or higher")
-def test_class_destringify():
+def test_class_arg():
     @flow
-    def foo(x: Test):
-        return
+    def foo(x: Test) -> Test:
+        return x
+
+    assert foo
+
+
+def test_class_arg2():
+    @flow(validate_parameters=False)
+    def foo(x: Test2) -> Test2:
+        return x
 
     assert foo