Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

from __future__ import annotations causes flow initialization to fail with issubclass arg must be a class #7502

Closed
4 tasks done
NodeJSmith opened this issue Nov 10, 2022 · 11 comments · Fixed by #11578
Closed
4 tasks done
Labels
bug Something isn't working great writeup This is a wonderful example of our standards needs:research Blocked by investigation into feasibility and cause orchestration Related to Orchestration features status:accepted We may work on this; we will accept work from external contributors

Comments

@NodeJSmith
Copy link
Contributor

NodeJSmith commented Nov 10, 2022

First check

  • I added a descriptive title to this issue.
  • I used the GitHub search to find a similar issue and didn't find it.
  • I searched the Prefect documentation for this issue.
  • I checked that this issue is related to Prefect and not one of its dependencies.

Bug summary

When using from __future__ import annotations flow initialization fails on pydantic.create_model with message issubclass() arg must be a class.

Reproduction

from __future__ import annotations

from datetime import date

from prefect import flow


@flow
def main(dt: date):
    print("test")


if __name__ == "__main__":
    main(date(2021, 6, 30))

Error

Traceback (most recent call last):
  File "/usr/lib/python3.8/runpy.py", line 194, in _run_module_as_main
    return _run_code(code, main_globals, None,
  File "/usr/lib/python3.8/runpy.py", line 87, in _run_code
    exec(code, run_globals)
  File "/home/my_name/.vscode-server/extensions/ms-python.python-2022.18.2/pythonFiles/lib/python/debugpy/adapter/../../debugpy/launcher/../../debugpy/__main__.py", line 39, in <module>
    cli.main()
  File "/home/my_name/.vscode-server/extensions/ms-python.python-2022.18.2/pythonFiles/lib/python/debugpy/adapter/../../debugpy/launcher/../../debugpy/../debugpy/server/cli.py", line 430, in main
    run()
  File "/home/my_name/.vscode-server/extensions/ms-python.python-2022.18.2/pythonFiles/lib/python/debugpy/adapter/../../debugpy/launcher/../../debugpy/../debugpy/server/cli.py", line 284, in run_file
    runpy.run_path(target, run_name="__main__")
  File "/home/my_name/.vscode-server/extensions/ms-python.python-2022.18.2/pythonFiles/lib/python/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_runpy.py", line 321, in run_path
    return _run_module_code(code, init_globals, run_name,
  File "/home/my_name/.vscode-server/extensions/ms-python.python-2022.18.2/pythonFiles/lib/python/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_runpy.py", line 135, in _run_module_code
    _run_code(code, mod_globals, init_globals,
  File "/home/my_name/.vscode-server/extensions/ms-python.python-2022.18.2/pythonFiles/lib/python/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_runpy.py", line 124, in _run_code
    exec(code, run_globals)
  File "/home/my_name/source/my_project/src/generate_report.py", line 11, in <module>
    def main(dt: date):
  File "/home/my_name/source/my_project/venv/lib/python3.8/site-packages/prefect/flows.py", line 619, in flow
    Flow(
  File "/home/my_name/source/my_project/venv/lib/python3.8/site-packages/prefect/context.py", line 163, in __register_init__
    __init__(__self__, *args, **kwargs)
  File "/home/my_name/source/my_project/venv/lib/python3.8/site-packages/prefect/flows.py", line 171, in __init__
    self.parameters = parameter_schema(self.fn)
  File "/home/my_name/source/my_project/venv/lib/python3.8/site-packages/prefect/utilities/callables.py", line 159, in parameter_schema
    pydantic.create_model(
  File "pydantic/main.py", line 664, in pydantic.main.BaseModel.schema
  File "pydantic/schema.py", line 186, in pydantic.schema.model_schema
  File "pydantic/schema.py", line 580, in pydantic.schema.model_process_schema
  File "pydantic/schema.py", line 621, in pydantic.schema.model_type_schema
  File "pydantic/schema.py", line 254, in pydantic.schema.field_schema
  File "pydantic/schema.py", line 526, in pydantic.schema.field_type_schema
  File "pydantic/schema.py", line 921, in pydantic.schema.field_singleton_schema
  File "/usr/lib/python3.8/abc.py", line 102, in __subclasscheck__
    return _abc_subclasscheck(cls, subclass)

Versions

Version:             2.6.6
API version:         0.8.3
Python version:      3.8.10
Git commit:          87767cda
Built:               Thu, Nov 3, 2022 1:15 PM
OS/Arch:             linux/x86_64
Profile:             data
Server type:         cloud

Additional context

This is caused by PEP 563, specifically:

This PEP proposes changing function annotations and variable annotations so that they are no longer evaluated at function definition time. Instead, they are preserved in __annotations__ in string form.

When using this import statement the value of type_ in the below statement in parameter_schema.py is a string with the value 'date'.

pydantic.create_model("CheckParameter", __config__=ModelConfig, **{name:(type_, field)}).schema(by_alias=True)

If I manually change the value of type_ to date (the class), then no error is raised. I don't have enough familiarity with typing.get_type_hints to provide suggestions, but I did confirm that if I use typing.get_type_hints(fn) I do get the date class, which we could then pass to pydantic.create_model.

@NodeJSmith NodeJSmith added bug Something isn't working status:triage labels Nov 10, 2022
@zanieb zanieb added status:upstream An upstream issue caused by a bug in one of our dependencies status:accepted We may work on this; we will accept work from external contributors priority:medium great writeup This is a wonderful example of our standards and removed status:triage status:upstream An upstream issue caused by a bug in one of our dependencies labels Nov 10, 2022
@zanieb
Copy link
Contributor

zanieb commented Nov 10, 2022

Thanks for the issue! I originally thought this was an upstream issue (see pydantic/pydantic#2678) but with further thought this does look to be caused by the way we are reading type hints to generate a model. We'll probably need to look into how Pydantic solves this problem, which is perhaps discussed in the linked issue.

@filpano
Copy link

filpano commented May 12, 2023

I ran into this exact same issue yesterday which causes me a lot of debugging head-scratching :).

This also seems to happen with other types, e.g. pendulum.Date. Removing the from __future__ import annotations import does indeed make the issue go away.

Interestingly, this bug seemed to have caused my flow orchestration to go haywire, as my flow runs were crashing with an infrastructure failure and all I could see was:

Prefect 2.8.5 scheduled flow run (flow run logs):

Encountered exception during execution:
Traceback (most recent call last):
  File "/home/prefect/.local/lib/python3.10/site-packages/prefect/engine.py", line 665, in orchestrate_flow_run
    result = await run_sync(flow_call)
  File "/home/prefect/.local/lib/python3.10/site-packages/prefect/utilities/asyncutils.py", line 154, in run_sync_in_interruptible_worker_thread
    async with anyio.create_task_group() as tg:
  File "/home/prefect/.local/lib/python3.10/site-packages/anyio/_backends/_asyncio.py", line 662, in __aexit__
    raise exceptions[0]
  File "/home/prefect/.local/lib/python3.10/site-packages/anyio/to_thread.py", line 31, in run_sync
    return await get_asynclib().run_sync_in_worker_thread(
  File "/home/prefect/.local/lib/python3.10/site-packages/anyio/_backends/_asyncio.py", line 937, in run_sync_in_worker_thread
    return await future
  File "/home/prefect/.local/lib/python3.10/site-packages/anyio/_backends/_asyncio.py", line 867, in run
    result = context.run(func, *args)
  File "/home/prefect/.local/lib/python3.10/site-packages/prefect/utilities/asyncutils.py", line 135, in capture_worker_thread_and_result
    result = __fn(*args, **kwargs)
  File "/tmp/tmp8qibmy3oprefect/flow_definitions/product_classification_preprocessing.py", line 115, in update_tables
    update_athena_tables(date=date, process_full_month=process_full_month)
  File "/home/prefect/.local/lib/python3.10/site-packages/prefect/flows.py", line 468, in __call__
    return enter_flow_run_engine_from_flow_call(
  File "/home/prefect/.local/lib/python3.10/site-packages/prefect/engine.py", line 179, in enter_flow_run_engine_from_flow_call
    return run_async_from_worker_thread(begin_run)
  File "/home/prefect/.local/lib/python3.10/site-packages/prefect/utilities/asyncutils.py", line 177, in run_async_from_worker_thread
    return anyio.from_thread.run(call)
  File "/home/prefect/.local/lib/python3.10/site-packages/anyio/from_thread.py", line 49, in run
    return asynclib.run_async_from_thread(func, *args)
  File "/home/prefect/.local/lib/python3.10/site-packages/anyio/_backends/_asyncio.py", line 970, in run_async_from_thread
    return f.result()
  File "/usr/lib/python3.10/concurrent/futures/_base.py", line 458, in result
    return self.__get_result()
  File "/usr/lib/python3.10/concurrent/futures/_base.py", line 403, in __get_result
    raise self._exception
  File "/home/prefect/.local/lib/python3.10/site-packages/prefect/client/utilities.py", line 47, in with_injected_client
    return await fn(*args, **kwargs)
  File "/home/prefect/.local/lib/python3.10/site-packages/prefect/engine.py", line 504, in create_and_begin_subflow_run
    result_factory = await ResultFactory.from_flow(
  File "/home/prefect/.local/lib/python3.10/site-packages/prefect/client/utilities.py", line 47, in with_injected_client
    return await fn(*args, **kwargs)
  File "/home/prefect/.local/lib/python3.10/site-packages/prefect/results.py", line 139, in from_flow
    result_storage=flow.result_storage or ctx.result_factory.storage_block,
AttributeError: 'Flow' object has no attribute 'result_storage'

Prefect 2.10.8 scheduled flow run (agent logs):

/usr/lib/python3.10/runpy.py:126: RuntimeWarning: 'prefect.engine' found in sys.modules after import of package 'prefect', but prior to execution of 'prefect.engine'; this may result in unpredictable behaviour
  warn(RuntimeWarning(msg))
Engine execution of flow run 'd2ae38bc-51b7-4d78-a1f0-2d4f30c10fdf' exited with unexpected exception
Traceback (most recent call last):
  File "/home/prefect/.local/lib/python3.10/site-packages/prefect/engine.py", line 2233, in <module>
    enter_flow_run_engine_from_subprocess(flow_run_id)
  File "/home/prefect/.local/lib/python3.10/site-packages/prefect/engine.py", line 202, in enter_flow_run_engine_from_subprocess
    return from_sync.wait_for_call_in_loop_thread(
  File "/home/prefect/.local/lib/python3.10/site-packages/prefect/_internal/concurrency/api.py", line 215, in wait_for_call_in_loop_thread
    return call.result()
  File "/home/prefect/.local/lib/python3.10/site-packages/prefect/_internal/concurrency/calls.py", line 173, in result
    return self.future.result(timeout=timeout)
  File "/usr/lib/python3.10/concurrent/futures/_base.py", line 451, in result
    return self.__get_result()
  File "/usr/lib/python3.10/concurrent/futures/_base.py", line 403, in __get_result
    raise self._exception
  File "/home/prefect/.local/lib/python3.10/site-packages/prefect/_internal/concurrency/calls.py", line 218, in _run_async
    result = await coro
  File "/home/prefect/.local/lib/python3.10/site-packages/prefect/client/utilities.py", line 40, in with_injected_client
    return await fn(*args, **kwargs)
  File "/home/prefect/.local/lib/python3.10/site-packages/prefect/engine.py", line 301, in retrieve_flow_then_begin_flow_run
    if flow.should_validate_parameters:
AttributeError: 'Flow' object has no attribute 'should_validate_parameters'
Process 'bright-dodo' exited with status code: 1

which doesn't seem to have anything to do with the issue at all (and was quite hard to debug)! I'm not a Python guru, but this smells like it might have something to do with dynamic imports...

@zanieb
Copy link
Contributor

zanieb commented May 12, 2023

Hm I thought perhaps you were encountering #8688 but a fix is out in 2.10.7

@zanieb zanieb added needs:research Blocked by investigation into feasibility and cause orchestration Related to Orchestration features labels May 12, 2023
@Andrew-S-Rosen
Copy link
Contributor

Andrew-S-Rosen commented Nov 9, 2023

@zanieb: I have some more details. This is still a significant issue.

In Prefect 2.14.3, Pydantic 2.4.2, Python 3.10.13 run the following:

from __future__ import annotations
from prefect import flow

class Test:
    pass

@flow
def foo(x: Test):
   print(x)

This yields a similar error as originally reported. Removing the from __future__ import annotations line gets rid of the bug. Concerningly, setting validate_parameters=False does not get rid of the bug, which is a major usability concern.

TypeError                                 Traceback (most recent call last)
Cell In[2], line 7
      3 class Test:
      4     pass
      6 @flow
----> 7 def foo(x: Test):
      8    print(x)

File ~/software/miniconda/envs/prefect/lib/python3.10/site-packages/prefect/flows.py:1339, 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)
   1237 """
   1238 Decorator to designate a function as a Prefect workflow.
   1239
   (...)
   1334     >>>     pass
   1335 """
   1336 if __fn:
   1337     return cast(
   1338         Flow[P, R],
-> 1339         Flow(
   1340             fn=__fn,
   1341             name=name,
   1342             version=version,
   1343             flow_run_name=flow_run_name,
   1344             task_runner=task_runner,
   1345             description=description,
   1346             timeout_seconds=timeout_seconds,
   1347             validate_parameters=validate_parameters,
   1348             retries=retries,
   1349             retry_delay_seconds=retry_delay_seconds,
   1350             persist_result=persist_result,
   1351             result_storage=result_storage,
   1352             result_serializer=result_serializer,
   1353             cache_result_in_memory=cache_result_in_memory,
   1354             log_prints=log_prints,
   1355             on_completion=on_completion,
   1356             on_failure=on_failure,
   1357             on_cancellation=on_cancellation,
   1358             on_crashed=on_crashed,
   1359         ),
   1360     )
   1361 else:
   1362     return cast(
   1363         Callable[[Callable[P, R]], Flow[P, R]],
   1364         partial(
   (...)
   1384         ),
   1385     )

File ~/software/miniconda/envs/prefect/lib/python3.10/site-packages/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 ~/software/miniconda/envs/prefect/lib/python3.10/site-packages/prefect/flows.py:299, 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)
    289 self.retries = (
    290     retries if retries is not None else PREFECT_FLOW_DEFAULT_RETRIES.value()
    291 )
    293 self.retry_delay_seconds = (
    294     retry_delay_seconds
    295     if retry_delay_seconds is not None
    296     else PREFECT_FLOW_DEFAULT_RETRY_DELAY_SECONDS.value()
    297 )
--> 299 self.parameters = parameter_schema(self.fn)
    300 self.should_validate_parameters = validate_parameters
    302 if self.should_validate_parameters:
    303     # Try to create the validated function now so that incompatibility can be
    304     # raised at declaration time rather than at runtime
    305     # We cannot, however, store the validated function on the flow because it
    306     # is not picklable in some environments

File ~/software/miniconda/envs/prefect/lib/python3.10/site-packages/prefect/utilities/callables.py:336, in parameter_schema(fn)
    333 # Generate a Pydantic model at each step so we can check if this parameter
    334 # type supports schema generation
    335 try:
--> 336     create_schema(
    337         "CheckParameter", model_cfg=ModelConfig, **{name: (type_, field)}
    338     )
    339 except ValueError:
    340     # This field's type is not valid for schema creation, update it to `Any`
    341     type_ = Any

File ~/software/miniconda/envs/prefect/lib/python3.10/site-packages/prefect/utilities/callables.py:296, in create_v1_schema(name_, model_cfg, **model_fields)
    292 def create_v1_schema(name_: str, model_cfg, **model_fields):
    293     model: "pydantic.BaseModel" = pydantic.create_model(
    294         name_, __config__=model_cfg, **model_fields
    295     )
--> 296     return model.schema(by_alias=True)

File ~/software/miniconda/envs/prefect/lib/python3.10/site-packages/pydantic/v1/main.py:664, in BaseModel.schema(cls, by_alias, ref_template)
    662 if cached is not None:
    663     return cached
--> 664 s = model_schema(cls, by_alias=by_alias, ref_template=ref_template)
    665 cls.__schema_cache__[(by_alias, ref_template)] = s
    666 return s

File ~/software/miniconda/envs/prefect/lib/python3.10/site-packages/pydantic/v1/schema.py:188, in model_schema(model, by_alias, ref_prefix, ref_template)
    186 model_name_map = get_model_name_map(flat_models)
    187 model_name = model_name_map[model]
--> 188 m_schema, m_definitions, nested_models = model_process_schema(
    189     model, by_alias=by_alias, model_name_map=model_name_map, ref_prefix=ref_prefix, ref_template=ref_template
    190 )
    191 if model_name in nested_models:
    192     # model_name is in Nested models, it has circular references
    193     m_definitions[model_name] = m_schema

File ~/software/miniconda/envs/prefect/lib/python3.10/site-packages/pydantic/v1/schema.py:582, in model_process_schema(model, by_alias, model_name_map, ref_prefix, ref_template, known_models, field)
    580     s['description'] = doc
    581 known_models.add(model)
--> 582 m_schema, m_definitions, nested_models = model_type_schema(
    583     model,
    584     by_alias=by_alias,
    585     model_name_map=model_name_map,
    586     ref_prefix=ref_prefix,
    587     ref_template=ref_template,
    588     known_models=known_models,
    589 )
    590 s.update(m_schema)
    591 schema_extra = model.__config__.schema_extra

File ~/software/miniconda/envs/prefect/lib/python3.10/site-packages/pydantic/v1/schema.py:623, in model_type_schema(model, by_alias, model_name_map, ref_template, ref_prefix, known_models)
    621 for k, f in model.__fields__.items():
    622     try:
--> 623         f_schema, f_definitions, f_nested_models = field_schema(
    624             f,
    625             by_alias=by_alias,
    626             model_name_map=model_name_map,
    627             ref_prefix=ref_prefix,
    628             ref_template=ref_template,
    629             known_models=known_models,
    630         )
    631     except SkipField as skip:
    632         warnings.warn(skip.message, UserWarning)

File ~/software/miniconda/envs/prefect/lib/python3.10/site-packages/pydantic/v1/schema.py:256, in field_schema(field, by_alias, model_name_map, ref_prefix, ref_template, known_models)
    253     s.update(validation_schema)
    254     schema_overrides = True
--> 256 f_schema, f_definitions, f_nested_models = field_type_schema(
    257     field,
    258     by_alias=by_alias,
    259     model_name_map=model_name_map,
    260     schema_overrides=schema_overrides,
    261     ref_prefix=ref_prefix,
    262     ref_template=ref_template,
    263     known_models=known_models or set(),
    264 )
    266 # $ref will only be returned when there are no schema_overrides
    267 if '$ref' in f_schema:

File ~/software/miniconda/envs/prefect/lib/python3.10/site-packages/pydantic/v1/schema.py:528, in field_type_schema(field, by_alias, model_name_map, ref_template, schema_overrides, ref_prefix, known_models)
    526 else:
    527     assert field.shape in {SHAPE_SINGLETON, SHAPE_GENERIC}, field.shape
--> 528     f_schema, f_definitions, f_nested_models = field_singleton_schema(
    529         field,
    530         by_alias=by_alias,
    531         model_name_map=model_name_map,
    532         schema_overrides=schema_overrides,
    533         ref_prefix=ref_prefix,
    534         ref_template=ref_template,
    535         known_models=known_models,
    536     )
    537     definitions.update(f_definitions)
    538     nested_models.update(f_nested_models)

File ~/software/miniconda/envs/prefect/lib/python3.10/site-packages/pydantic/v1/schema.py:927, in field_singleton_schema(field, by_alias, model_name_map, ref_template, schema_overrides, ref_prefix, known_models)
    924 if lenient_issubclass(getattr(field_type, '__pydantic_model__', None), BaseModel):
    925     field_type = field_type.__pydantic_model__
--> 927 if issubclass(field_type, BaseModel):
    928     model_name = model_name_map[field_type]
    929     if field_type not in known_models:

File ~/software/miniconda/envs/prefect/lib/python3.10/abc.py:123, in ABCMeta.__subclasscheck__(cls, subclass)
    121 def __subclasscheck__(cls, subclass):
    122     """Override for issubclass(subclass, cls)."""
--> 123     return _abc_subclasscheck(cls, subclass)

TypeError: issubclass() arg 1 must be a class

@zanieb
Copy link
Contributor

zanieb commented Nov 9, 2023

Thanks for the details Andrew — I'm not working on this project anymore perhaps @desertaxle can help instead.

@Andrew-S-Rosen
Copy link
Contributor

Andrew-S-Rosen commented Nov 9, 2023

Sorry about that! Hope you're enjoying the new adventures :) @desertaxle, if you need any additional details, don't hesitate to ping me.

@jamesdow21
Copy link

jamesdow21 commented Nov 22, 2023

When the from __future__ import annotations switch is used, the inspect.Parameter.annotation attribute becomes a string instead of a type

The two ways of parsing the parameters (for v1 or v2 of pydantic) are here and here

Instead of returning type_ directly after assigning it with type_ = Any if param.annotation is inspect._empty else param.annotation, you could check if it is a string and then manually un-stringize it (inspect.get_annotations is only available in Python 3.10+)

@Andrew-S-Rosen
Copy link
Contributor

@jamesdow21: Thanks, that is a great observation!! Are you interested in opening a PR for this? If not, I'll try to give it a go when I have some time since this is an important one for me (I'm not quite sure why more Prefect users aren't running into this...).

@jamesdow21
Copy link

@jamesdow21: Thanks, that is a great observation!! Are you interested in opening a PR for this? If not, I'll try to give it a go when I have some time since this is an important one for me (I'm not quite sure why more Prefect users aren't running into this...).

I can give it a shot in a couple days when I'll have some free time.
I'm hoping that it can be a pretty easy implementation by essentially copy-pasting the source for inspect.get_annotations into src/prefect/utilities/compat.py
The only trip up I'm worried about is if it is difficult to get the original globals and locals for the annotations after the handful of layers of passing around the decorated flow function

I'm also surprised that this isn't occurring more commonly, I always default to using the PEP563 annotations (at least until my minimum version will be 3.13 with PEP649 style annotations)

@Andrew-S-Rosen
Copy link
Contributor

If we don't want to copy source code, I suppose we could also just do a check if Python 3.10 is used and then call the inspect.get_annotations function directly.

@Andrew-S-Rosen
Copy link
Contributor

Andrew-S-Rosen commented Jan 7, 2024

@jamesdow21 -- FYI, I opened a PR in #11578.

Andrew-S-Rosen added a commit to Quantum-Accelerators/quacc that referenced this issue Jan 11, 2024
## Summary of Changes

Closes #1382.

TODO: 
- The `@subflow` decorator should ideally be doing a `gather`-type
operation like in Dask so nobody is iterating through a list to resolve
futures.
- Add documentation.

REQUIRES:
- PrefectHQ/prefect#7502
- PrefectHQ/prefect#11616

---------

Co-authored-by: deepsource-autofix[bot] <62050782+deepsource-autofix[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working great writeup This is a wonderful example of our standards needs:research Blocked by investigation into feasibility and cause orchestration Related to Orchestration features status:accepted We may work on this; we will accept work from external contributors
Projects
None yet
6 participants