dagger / dagger

An engine to run your pipelines in containers
https://dagger.io
Apache License 2.0
10.89k stars 584 forks source link

🐞 Python: `dagger.Ignore` Annotation cannot be applied to `object_type` fields. #8285

Closed KGB33 closed 6 days ago

KGB33 commented 2 weeks ago

What is the issue?

When using the new dagger.Ignore annotation on object fields, I get a TypeError: unhashable type: 'list' error.

@object_type
class DaggerUnhashableTypeList:
    directory_arg: Annotated[dagger.Directory, DefaultPath("/"), Ignore([".venv"])] # Causes TypeError

Dagger version

dagger v0.12.6 (registry.dagger.io/engine:v0.12.6) linux/amd64

Steps to reproduce

Using the below module, run dagger functions.

from typing import Annotated, Any
import dagger
from dagger import dag, function, object_type, DefaultPath, Ignore

@object_type
class DaggerUnhashableTypeList:
    directory_arg: Annotated[dagger.Directory, DefaultPath("/"), Ignore([".venv"])]

    @function
    async def grep_dir(
        self,
        pattern: str,
    ) -> str:
        """Returns lines that match a pattern in the files of the provided Directory"""
        return await (
            dag.container()
            .from_("alpine:latest")
            .with_mounted_directory("/mnt", self.directory_arg)
            .with_workdir("/mnt")
            .with_exec(["grep", "-R", pattern, "."])
            .stdout()
        )

Log output

Logs ``` Error: input: module.withSource.initialize resolve: failed to initialize module: failed to call module "dagger-unhashable-type-list" to get functions: call constructor: process "/runtime" did not complete successfully: exit code: 1 Stderr: ╭───────────────────── Traceback (most recent call last) ──────────────────────╮ │ /runtime:8 in │ │ │ │ 5 from dagger.mod.cli import app │ │ 6 │ │ 7 if __name__ == "__main__": │ │ ❱ 8 │ sys.exit(app()) │ │ 9 │ │ │ │ /src/sdk/src/dagger/mod/cli.py:34 in app │ │ │ │ 31 │ │ ], │ │ 32 │ ) │ │ 33 │ try: │ │ ❱ 34 │ │ pymod = import_module() │ │ 35 │ │ mod = get_module(pymod).with_description(inspect.getdoc(pymod)) │ │ 36 │ │ mod() │ │ 37 │ except FatalError as e: │ │ │ │ /src/sdk/src/dagger/mod/cli.py:48 in import_module │ │ │ │ 45 │ """Import python module with given name.""" │ │ 46 │ # TODO: Allow configuring which package/module to use. │ │ 47 │ try: │ │ ❱ 48 │ │ return importlib.import_module(module_name) │ │ 49 │ except ModuleNotFoundError as e: │ │ 50 │ │ if e.name != module_name: │ │ 51 │ │ │ raise │ │ │ │ /usr/local/lib/python3.11/importlib/__init__.py:126 in import_module │ │ │ │ 123 │ │ │ if character != '.': │ │ 124 │ │ │ │ break │ │ 125 │ │ │ level += 1 │ │ ❱ 126 │ return _bootstrap._gcd_import(name[level:], package, level) │ │ 127 │ │ 128 │ │ 129 _RELOADING = {} │ │ in _gcd_import:1204 │ │ in _find_and_load:1176 │ │ in _find_and_load_unlocked:1147 │ │ in _load_unlocked:690 │ │ in exec_module:940 │ │ in _call_with_frames_removed:241 │ │ │ │ /src/src/main/__init__.py:6 in │ │ │ │ 3 from dagger import dag, function, object_type, DefaultPath, Ignore │ │ 4 │ │ 5 │ │ ❱ 6 @object_type │ │ 7 class DaggerUnhashableTypeList: │ │ 8 │ # directory_arg: dagger.Directory │ │ 9 │ directory_arg: Annotated[dagger.Directory, DefaultPath("/"), Ignore │ │ │ │ /src/sdk/src/dagger/mod/_module.py:500 in object_type │ │ │ │ 497 │ │ │ wrapped = dataclasses.dataclass(kw_only=True)(cls) │ │ 498 │ │ │ return self._process_type(wrapped) │ │ 499 │ │ │ │ ❱ 500 │ │ return wrapper(cls) if cls else wrapper │ │ 501 │ │ │ 502 │ def _process_type(self, cls: T) -> T: │ │ 503 │ │ types = typing.get_type_hints(cls) │ │ │ │ /src/sdk/src/dagger/mod/_module.py:498 in wrapper │ │ │ │ 495 │ │ │ │ raise UserError(msg) │ │ 496 │ │ │ │ │ 497 │ │ │ wrapped = dataclasses.dataclass(kw_only=True)(cls) │ │ ❱ 498 │ │ │ return self._process_type(wrapped) │ │ 499 │ │ │ │ 500 │ │ return wrapper(cls) if cls else wrapper │ │ 501 │ │ │ │ /src/sdk/src/dagger/mod/_module.py:529 in _process_type │ │ │ │ 526 │ │ # Include fields that are excluded from the constructor. │ │ 527 │ │ self._converter.register_unstructure_hook( │ │ 528 │ │ │ cls, │ │ ❱ 529 │ │ │ cattrs.gen.make_dict_unstructure_fn( │ │ 530 │ │ │ │ cls, │ │ 531 │ │ │ │ self._converter, │ │ 532 │ │ │ │ _cattrs_include_init_false=True, │ │ │ │ /usr/local/lib/python3.11/site-packages/cattrs/gen/__init__.py:279 in │ │ make_dict_unstructure_fn │ │ │ │ 276 │ working_set.add(cl) │ │ 277 │ │ │ 278 │ try: │ │ ❱ 279 │ │ return make_dict_unstructure_fn_from_attrs( │ │ 280 │ │ │ attrs, │ │ 281 │ │ │ cl, │ │ 282 │ │ │ converter, │ │ │ │ /usr/local/lib/python3.11/site-packages/cattrs/gen/__init__.py:154 in │ │ make_dict_unstructure_fn_from_attrs │ │ │ │ 151 │ │ │ │ │ │ # type of the default to dispatch on. │ │ 152 │ │ │ │ │ │ t = a.default.__class__ │ │ 153 │ │ │ │ │ try: │ │ ❱ 154 │ │ │ │ │ │ handler = converter.get_unstructure_hook(t, c │ │ 155 │ │ │ │ │ except RecursionError: │ │ 156 │ │ │ │ │ │ # There's a circular reference somewhere down │ │ 157 │ │ │ │ │ │ handler = converter.unstructure │ │ │ │ /usr/local/lib/python3.11/site-packages/cattrs/converters.py:424 in │ │ get_unstructure_hook │ │ │ │ 421 │ │ return ( │ │ 422 │ │ │ self._unstructure_func.dispatch(type) │ │ 423 │ │ │ if cache_result │ │ ❱ 424 │ │ │ else self._unstructure_func.dispatch_without_caching(type │ │ 425 │ │ ) │ │ 426 │ │ │ 427 │ @overload │ │ │ │ /usr/local/lib/python3.11/site-packages/cattrs/dispatch.py:130 in │ │ dispatch_without_caching │ │ │ │ 127 │ │ except Exception: # noqa: S110 │ │ 128 │ │ │ pass │ │ 129 │ │ │ │ ❱ 130 │ │ direct_dispatch = self._direct_dispatch.get(typ) │ │ 131 │ │ if direct_dispatch is not None: │ │ 132 │ │ │ return direct_dispatch │ │ 133 │ │ │ │ /usr/local/lib/python3.11/typing.py:2177 in __hash__ │ │ │ │ 2174 │ │ │ │ and self.__metadata__ == other.__metadata__) │ │ 2175 │ │ │ 2176 │ def __hash__(self): │ │ ❱ 2177 │ │ return hash((self.__origin__, self.__metadata__)) │ │ 2178 │ │ │ 2179 │ def __getattr__(self, attr): │ │ 2180 │ │ if attr in {'__name__', '__qualname__'}: │ │ in __hash__:3 │ ╰──────────────────────────────────────────────────────────────────────────────╯ TypeError: unhashable type: 'list' ```
KGB33 commented 2 weeks ago

As a workaround, I can annotate the constructor instead:

from typing import Annotated, Any
import dagger
from dagger import dag, function, object_type, DefaultPath, Ignore

@object_type
class DaggerUnhashableTypeList:
    directory_arg: dagger.Directory

    def __init__(
        self,
        directory_arg: Annotated[dagger.Directory, DefaultPath("/"), Ignore([".venv"])],
    ) -> None:
        self.directory_arg = directory_arg

    @function
    async def grep_dir(
        self,
        pattern: str,
    ) -> str:
        """Returns lines that match a pattern in the files of the provided Directory"""
        return await (
            dag.container()
            .from_("alpine:latest")
            .with_mounted_directory("/mnt", self.directory_arg)
            .with_workdir("/mnt")
            .with_exec(["grep", "-R", pattern, "."])
            .stdout()
        )