Repository: jina-ai/serve
Pull request: feat: allow Executors as dataclass
Goals:

- ...
- ...
- [ ] check and update documentation. See [guide](https://github.com/jina-ai/jina/CONTRIBUTING.md#documentation-guidelines) and ask the team.


Problem statement:
Support Executor dataclass besides classic init
The way of writing this is getting old:

```python
class GLID3Diffusion(Executor):
    def __init__(self, glid3_path: str, steps: int, **kwargs):
        super().__init__(**kwargs)
        self.glid3_path = glid3_path
        self.diffusion_steps = steps
```

We need to be more modern:

```python
@datalcass
class GLID3Diffusion(Executor):
     glid3_path: str
     steps: int
```

Or even "auto-dataclass" (@alaeddine-13 we did such thing in docarray)

```python
class GLID3Diffusion(Executor):
     glid3_path: str
     steps: int

assert dataclass.isdataclass(GLID3Diffusion)   # True
```

Not easy? Probably. But here are the rationales:

- I think it is necessary to keep the UX modern and avoid the "old feeling" of 2y product. As a long-term heavy user of Jina I do find the current way lengthy and old, which slows me down on building solution. Other Jina power-users may feel the same.
- @alaeddine-13 has rich experience in docarray dataclass design, DocArray dataclass is much harder than this. If we can manage that, he can do this work beautifully. 
- All other debates on the usefulness of dataclass is out of Jina's scope: if Python introduces it since 3.7 there must be a good reason.




Sub tasks:
- [ ] Add support to `@dataclass` on top of existing init.
- [ ] All docs mentions Executor use `@dataclass` instead of init
- [ ] Note sure if normalizer gets affected, but maybe check.
- [ ] Release a minor.

Hints from issue discussion:

Commit message: test: add integration test

Patch to review:
diff --git a/jina/jaml/parsers/executor/legacy.py b/jina/jaml/parsers/executor/legacy.py
index 67145fc342a50..639217849ef42 100644
--- a/jina/jaml/parsers/executor/legacy.py
+++ b/jina/jaml/parsers/executor/legacy.py
@@ -1,3 +1,4 @@
+import dataclasses
 import inspect
 from functools import reduce
 from typing import Any, Dict, Optional, Set, Type
@@ -72,12 +73,24 @@ def parse(
 
         cls._init_from_yaml = True
         # tmp_p = {kk: expand_env_var(vv) for kk, vv in data.get('with', {}).items()}
-        obj = cls(
-            **data.get('with', {}),
-            metas=data.get('metas', {}),
-            requests=data.get('requests', {}),
-            runtime_args=runtime_args,
-        )
+        if dataclasses.is_dataclass(cls):
+            obj = cls(
+                **data.get('with', {}),
+            )
+            cls.__bases__[0].__init__(
+                obj,
+                **data.get('with', {}),
+                metas=data.get('metas', {}),
+                requests=data.get('requests', {}),
+                runtime_args=runtime_args,
+            )
+        else:
+            obj = cls(
+                **data.get('with', {}),
+                metas=data.get('metas', {}),
+                requests=data.get('requests', {}),
+                runtime_args=runtime_args,
+            )
         cls._init_from_yaml = False
 
         # check if the yaml file used to instanciate 'cls' has arguments that are not in 'cls'
diff --git a/tests/integration/flow-dry-run/__init__.py b/tests/integration/dataclass_executor/__init__.py
similarity index 100%
rename from tests/integration/flow-dry-run/__init__.py
rename to tests/integration/dataclass_executor/__init__.py
diff --git a/tests/integration/dataclass_executor/test_dataclass_executor.py b/tests/integration/dataclass_executor/test_dataclass_executor.py
new file mode 100644
index 0000000000000..faebd7203f6db
--- /dev/null
+++ b/tests/integration/dataclass_executor/test_dataclass_executor.py
@@ -0,0 +1,29 @@
+import dataclasses
+
+from docarray import DocumentArray
+from jina import Executor, Flow, requests
+
+
+def test_executor_dataclass():
+    @dataclasses.dataclass()
+    class MyDataClassExecutor(Executor):
+        my_field: str = ''
+
+        @requests(on=['/search'])
+        def baz(self, docs, **kwargs):
+            for doc in docs:
+                doc.tags['metas_name'] = self.metas.name
+                doc.tags['my_field'] = self.my_field
+
+    f = Flow().add(
+        uses=MyDataClassExecutor,
+        uses_with={'my_field': 'this is my field'},
+        uses_metas={'name': 'test-name-updated'},
+        uses_requests={'/foo': 'baz'},
+    )
+    with f:
+        res = f.post(on='/foo', inputs=DocumentArray.empty(2))
+    assert len(res) == 2
+    for r in res:
+        assert r.tags['metas_name'] == 'test-name-updated'
+        assert r.tags['my_field'] == 'this is my field'
diff --git a/tests/integration/flow_dry_run/__init__.py b/tests/integration/flow_dry_run/__init__.py
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/tests/integration/flow-dry-run/test_flow_dry_run.py b/tests/integration/flow_dry_run/test_flow_dry_run.py
similarity index 100%
rename from tests/integration/flow-dry-run/test_flow_dry_run.py
rename to tests/integration/flow_dry_run/test_flow_dry_run.py
diff --git a/tests/integration/monitoring/__init__.py b/tests/integration/monitoring/__init__.py
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/tests/unit/jaml/test_type_parse.py b/tests/unit/jaml/test_type_parse.py
index 0f66525f2e22c..aa749f8a3f562 100644
--- a/tests/unit/jaml/test_type_parse.py
+++ b/tests/unit/jaml/test_type_parse.py
@@ -1,3 +1,4 @@
+import dataclasses
 import os
 
 import pytest
@@ -13,6 +14,15 @@ class MyExecutor(BaseExecutor):
     pass
 
 
+@dataclasses.dataclass()
+class MyDataClassExecutor(BaseExecutor):
+    my_field: str = ''
+
+    @requests
+    def baz(self, **kwargs):
+        pass
+
+
 def test_non_empty_reg_tags():
     assert JAML.registered_tags()
     assert __default_executor__ in JAML.registered_tags()
@@ -175,7 +185,6 @@ def test_parsing_brackets_in_envvar():
             'VAR1': '{"1": "2"}',
         }
     ):
-
         b = JAML.load(flow_yaml, substitute=True)
         assert b['executors'][0]['env']['var1'] == '{"1": "2"}'
         assert b['executors'][0]['env']['var2'] == 'a'
@@ -213,3 +222,28 @@ def test_jtype(tmpdir):
 
     assert type(BaseExecutor.load_config(exec_path)) == BaseExecutor
     assert type(Flow.load_config(flow_path)) == Flow
+
+    (
+        'metas',
+        {'name': 'test-name-updated', 'workspace': 'test-work-space-updated'},
+    ),
+    ('requests', {'/foo': 'baz'}),
+
+
+def test_load_dataclass_executor():
+    executor_yaml = '''
+        jtype: MyDataClassExecutor
+        with:
+            my_field: this is my field
+        metas:
+            name: test-name-updated
+            workspace: test-work-space-updated
+        requests:
+            /foo: baz
+        '''
+
+    exec = BaseExecutor.load_config(executor_yaml)
+    assert exec.my_field == 'this is my field'
+    assert exec.requests['/foo'] == MyDataClassExecutor.baz
+    assert exec.metas.name == 'test-name-updated'
+    assert exec.metas.workspace == 'test-work-space-updated'


Code review comments:
diff_hunk: @@ -13,6 +14,15 @@ class MyExecutor(BaseExecutor):
     pass
 
 
+@dataclasses.dataclass()
path: tests/unit/jaml/test_type_parse.py
text: @author:
```suggestion
@user1.dataclass
```