Repository: jina-ai/serve
Pull request: fix: avoid dump drivers
Removes `_drivers ` from the `__getstate__` method. This allows executors to be loaded with  updated `_drivers` information from `.yml` files. Before this commit the information would be serialised.

Problem statement:
Fix: avoid dumping _drivers in executor serialization
We found out that an app would not work 'as expected' when we indexed and then queried if we changed the `.yml` between index and query time. Essentially the change in the `.yml` file was not taken in consideration when it was loaded.

Hints from issue discussion:

Commit message: fix: modifications done by black

Patch to review:
diff --git a/jina/executors/__init__.py b/jina/executors/__init__.py
index e3e4beb3189e2..d78f7f2a6736c 100644
--- a/jina/executors/__init__.py
+++ b/jina/executors/__init__.py
@@ -7,7 +7,7 @@
 from datetime import datetime
 from pathlib import Path
 from types import SimpleNamespace
-from typing import Dict, TypeVar, Type, List
+from typing import Dict, TypeVar, Type, List, Optional
 
 from .decorators import (
     as_train_method,
@@ -27,29 +27,51 @@
     from ..peapods.runtimes.zmq.zed import ZEDRuntime
     from ..drivers import BaseDriver
 
-__all__ = ['BaseExecutor', 'AnyExecutor', 'ExecutorType']
+__all__ = ["BaseExecutor", "AnyExecutor", "ExecutorType"]
 
-AnyExecutor = TypeVar('AnyExecutor', bound='BaseExecutor')
+AnyExecutor = TypeVar("AnyExecutor", bound="BaseExecutor")
 
 # some variables may be self-referred and they must be resolved at here
 _ref_desolve_map = SimpleNamespace()
-_ref_desolve_map.__dict__['metas'] = SimpleNamespace()
-_ref_desolve_map.__dict__['metas'].__dict__['pea_id'] = 0
+_ref_desolve_map.__dict__["metas"] = SimpleNamespace()
+_ref_desolve_map.__dict__["metas"].__dict__["pea_id"] = 0
 
 
 class ExecutorType(type(JAMLCompatible), type):
     """The class of Executor type, which is the metaclass of :class:`BaseExecutor`."""
 
     def __new__(cls, *args, **kwargs):
+        """
+
+
+        # noqa: DAR201
+
+
+        # noqa: DAR101
+
+
+        # noqa: DAR102
+        """
         _cls = super().__new__(cls, *args, **kwargs)
         return cls.register_class(_cls)
 
     def __call__(cls, *args, **kwargs):
+        """
+
+
+        # noqa: DAR201
+
+
+        # noqa: DAR101
+
+
+        # noqa: DAR102
+        """
         # do _preload_package
-        getattr(cls, 'pre_init', lambda *x: None)()
+        getattr(cls, "pre_init", lambda *x: None)()
 
-        m = kwargs.pop('metas') if 'metas' in kwargs else {}
-        r = kwargs.pop('requests') if 'requests' in kwargs else {}
+        m = kwargs.pop("metas") if "metas" in kwargs else {}
+        r = kwargs.pop("requests") if "requests" in kwargs else {}
 
         obj = type.__call__(cls, *args, **kwargs)
 
@@ -57,7 +79,7 @@ def __call__(cls, *args, **kwargs):
         # metas in YAML > class attribute > default_jina_config
         # jina_config = expand_dict(jina_config)
 
-        getattr(obj, '_post_init_wrapper', lambda *x: None)(m, r)
+        getattr(obj, "_post_init_wrapper", lambda *x: None)(m, r)
         return obj
 
     @staticmethod
@@ -68,21 +90,21 @@ def register_class(cls):
         :param cls: The class.
         :return: The class, after being registered.
         """
-        update_funcs = ['train', 'add', 'delete', 'update']
-        train_funcs = ['train']
-        aggregate_funcs = ['evaluate']
+        update_funcs = ["train", "add", "delete", "update"]
+        train_funcs = ["train"]
+        aggregate_funcs = ["evaluate"]
 
-        reg_cls_set = getattr(cls, '_registered_class', set())
+        reg_cls_set = getattr(cls, "_registered_class", set())
 
-        cls_id = f'{cls.__module__}.{cls.__name__}'
-        if cls_id not in reg_cls_set or getattr(cls, 'force_register', False):
-            wrap_func(cls, ['__init__'], store_init_kwargs)
+        cls_id = f"{cls.__module__}.{cls.__name__}"
+        if cls_id not in reg_cls_set or getattr(cls, "force_register", False):
+            wrap_func(cls, ["__init__"], store_init_kwargs)
             wrap_func(cls, train_funcs, as_train_method)
             wrap_func(cls, update_funcs, as_update_method)
             wrap_func(cls, aggregate_funcs, as_aggregate_method)
 
             reg_cls_set.add(cls_id)
-            setattr(cls, '_registered_class', reg_cls_set)
+            setattr(cls, "_registered_class", reg_cls_set)
         return cls
 
 
@@ -127,21 +149,20 @@ def __init__(awesomeness = 5):
 
     store_args_kwargs = False  #: set this to ``True`` to save ``args`` (in a list) and ``kwargs`` (in a map) in YAML config
     exec_methods = [
-        'encode',
-        'add',
-        'query',
-        'craft',
-        'segment',
-        'score',
-        'evaluate',
-        'predict',
-        'query_by_key',
-        'delete',
-        'update',
+        "encode",
+        "add",
+        "query",
+        "craft",
+        "segment",
+        "score",
+        "evaluate",
+        "predict",
+        "query_by_key",
+        "delete",
+        "update",
     ]
 
     def __init__(self, *args, **kwargs):
-        """Constructor."""
         if isinstance(args, tuple) and len(args) > 0:
             self.args = args[0]
         else:
@@ -152,51 +173,59 @@ def __init__(self, *args, **kwargs):
         self._last_snapshot_ts = datetime.now()
 
     def _post_init_wrapper(
-        self, _metas: Dict = None, _requests: Dict = None, fill_in_metas: bool = True
+        self,
+        _metas: Optional[Dict] = None,
+        _requests: Optional[Dict] = None,
+        fill_in_metas: bool = True,
     ) -> None:
-        with TimeContext('post_init may take some time', self.logger):
+        with TimeContext("post_init may take some time", self.logger):
             if fill_in_metas:
                 if not _metas:
                     _metas = get_default_metas()
 
                 self._fill_metas(_metas)
+                self.fill_in_drivers(_requests)
 
-                from ..executors.requests import get_default_reqs
+            _before = set(list(vars(self).keys()))
+            self.post_init()
+            self._post_init_vars = {k for k in vars(self) if k not in _before}
 
-                default_requests = get_default_reqs(type.mro(self.__class__))
+    def fill_in_drivers(self, _requests: Optional[Dict]):
+        """
+        Fill in drivers in a BaseExecutor.
 
-                if not _requests:
-                    self._drivers = self._get_drivers_from_requests(default_requests)
-                else:
-                    parsed_drivers = self._get_drivers_from_requests(_requests)
+        :param _requests: Dict containing driver information.
+        """
+        from ..executors.requests import get_default_reqs
 
-                    if _requests.get('use_default', False):
-                        default_drivers = self._get_drivers_from_requests(
-                            default_requests
-                        )
+        default_requests = get_default_reqs(type.mro(self.__class__))
 
-                        for k, v in default_drivers.items():
-                            if k not in parsed_drivers:
-                                parsed_drivers[k] = v
+        if not _requests:
+            self._drivers = self._get_drivers_from_requests(default_requests)
+        else:
+            parsed_drivers = self._get_drivers_from_requests(_requests)
 
-                    self._drivers = parsed_drivers
+            if _requests.get("use_default", False):
+                default_drivers = self._get_drivers_from_requests(default_requests)
 
-            _before = set(list(vars(self).keys()))
-            self.post_init()
-            self._post_init_vars = {k for k in vars(self) if k not in _before}
+                for k, v in default_drivers.items():
+                    if k not in parsed_drivers:
+                        parsed_drivers[k] = v
+
+            self._drivers = parsed_drivers
 
     @staticmethod
     def _get_drivers_from_requests(_requests):
         _drivers = {}  # type: Dict[str, List['BaseDriver']]
 
-        if _requests and 'on' in _requests and isinstance(_requests['on'], dict):
+        if _requests and "on" in _requests and isinstance(_requests["on"], dict):
             # if control request is forget in YAML, then fill it
-            if 'ControlRequest' not in _requests['on']:
+            if "ControlRequest" not in _requests["on"]:
                 from ..drivers.control import ControlReqDriver
 
-                _requests['on']['ControlRequest'] = [ControlReqDriver()]
+                _requests["on"]["ControlRequest"] = [ControlReqDriver()]
 
-            for req_type, drivers_spec in _requests['on'].items():
+            for req_type, drivers_spec in _requests["on"].items():
                 if isinstance(req_type, str):
                     req_type = [req_type]
                 if isinstance(drivers_spec, list):
@@ -204,10 +233,10 @@ def _get_drivers_from_requests(_requests):
                     drivers = drivers_spec
                     common_kwargs = {}
                 elif isinstance(drivers_spec, dict):
-                    drivers = drivers_spec.get('drivers', [])
-                    common_kwargs = drivers_spec.get('with', {})
+                    drivers = drivers_spec.get("drivers", [])
+                    common_kwargs = drivers_spec.get("with", {})
                 else:
-                    raise TypeError(f'unsupported type of driver spec: {drivers_spec}')
+                    raise TypeError(f"unsupported type of driver spec: {drivers_spec}")
 
                 for r in req_type:
                     if r not in _drivers:
@@ -246,20 +275,20 @@ def _fill_metas(self, _metas):
                     setattr(self, k, v)
             elif type(getattr(self, k)) == type(v):
                 setattr(self, k, v)
-        if not getattr(self, 'name', None):
-            _id = random_identity().split('-')[0]
-            _name = f'{typename(self)}-{_id}'
-            if getattr(self, 'warn_unnamed', False):
+        if not getattr(self, "name", None):
+            _id = random_identity().split("-")[0]
+            _name = f"{typename(self)}-{_id}"
+            if getattr(self, "warn_unnamed", False):
                 self.logger.warning(
                     f'this executor is not named, i will call it "{_name}". '
-                    'naming is important as it provides an unique identifier when '
-                    'persisting this executor on disk.'
+                    "naming is important as it provides an unique identifier when "
+                    "persisting this executor on disk."
                 )
-            setattr(self, 'name', _name)
+            setattr(self, "name", _name)
         if unresolved_attr:
             _tmp = vars(self)
-            _tmp['metas'] = _metas
-            new_metas = JAML.expand_dict(_tmp, context=_ref_desolve_map)['metas']
+            _tmp["metas"] = _metas
+            new_metas = JAML.expand_dict(_tmp, context=_ref_desolve_map)["metas"]
 
             # set self values filtered by those non-exist, and non-expandable
             for k, v in new_metas.items():
@@ -271,7 +300,7 @@ def _fill_metas(self, _metas):
                             setattr(self, k, v)
                         else:
                             raise ValueError(
-                                f'{k}={v} is not substitutable or badly referred'
+                                f"{k}={v} is not substitutable or badly referred"
                             )
                     else:
                         setattr(self, k, v)
@@ -305,16 +334,18 @@ def save_abspath(self) -> str:
         """Get the file path of the binary serialized object
 
         The file name ends with `.bin`.
+
+        :return: the name of the file with `.bin`
         """
-        return self.get_file_from_workspace(f'{self.name}.bin')
+        return self.get_file_from_workspace(f"{self.name}.bin")
 
     @property
     def config_abspath(self) -> str:
         """Get the file path of the YAML config
 
-        The file name ends with `.yml`.
+        :return: The file name ends with `.yml`.
         """
-        return self.get_file_from_workspace(f'{self.name}.yml')
+        return self.get_file_from_workspace(f"{self.name}.yml")
 
     @staticmethod
     def get_shard_workspace(
@@ -326,18 +357,22 @@ def get_shard_workspace(
         :param workspace_folder: Folder of the workspace.
         :param workspace_name: Name of the workspace.
         :param pea_id: Id of the pea,
-        :return: Return the workspace of the shard of this Executor.
+
+        :return: returns the workspace of the shard of this Executor.
         """
         # TODO (Joan, Florian). We would prefer not to keep `pea_id` condition, but afraid many tests rely on this
         return (
-            os.path.join(workspace_folder, f'{workspace_name}-{pea_id}')
+            os.path.join(workspace_folder, f"{workspace_name}-{pea_id}")
             if (isinstance(pea_id, int) and pea_id > 0)
             else workspace_folder
         )
 
     @property
     def workspace_name(self):
-        """Get the name of the workspace."""
+        """Get the name of the workspace.
+
+        :return: returns the name of the executor
+        """
         return self.name
 
     @property
@@ -353,7 +388,7 @@ def _workspace(self):
 
         :return: returns the workspace property of the executor or default to './'
         """
-        return self.workspace or './'
+        return self.workspace or "./"
 
     @property
     def shard_workspace(self) -> str:
@@ -370,25 +405,30 @@ def get_file_from_workspace(self, name: str) -> str:
 
         :param name: the name of the file
 
-        :return file path
+        :return: file path
         """
         Path(self.shard_workspace).mkdir(parents=True, exist_ok=True)
         return os.path.join(self.shard_workspace, name)
 
     @property
     def physical_size(self) -> int:
-        """Return the size of the current workspace in bytes"""
+        """Return the size of the current workspace in bytes
+
+        :return: byte size of the current workspace
+        """
         root_directory = Path(self.shard_workspace)
-        return sum(f.stat().st_size for f in root_directory.glob('**/*') if f.is_file())
+        return sum(f.stat().st_size for f in root_directory.glob("**/*") if f.is_file())
 
     def __getstate__(self):
         d = dict(self.__dict__)
-        del d['logger']
+        del d["logger"]
         for k in self._post_init_vars:
             del d[k]
-        cached = [k for k in d.keys() if k.startswith('CACHED_')]
+        cached = [k for k in d.keys() if k.startswith("CACHED_")]
         for k in cached:
             del d[k]
+
+        d.pop("_drivers", None)
         return d
 
     def __setstate__(self, d):
@@ -398,7 +438,7 @@ def __setstate__(self, d):
             self._post_init_wrapper(fill_in_metas=False)
         except ModuleNotFoundError as ex:
             self.logger.warning(
-                f'{typename(ex)} is often caused by a missing component, '
+                f"{typename(ex)} is often caused by a missing component, "
                 f'which often can be solved by "pip install" relevant package: {ex!r}',
                 exc_info=True,
             )
@@ -406,6 +446,9 @@ def __setstate__(self, d):
     def train(self, *args, **kwargs) -> None:
         """
         Train this executor, need to be overrided
+
+        :param args: Additional arguments.
+        :param kwargs: Additional key word arguments.
         """
         pass
 
@@ -432,13 +475,12 @@ def save(self, filename: str = None):
         need to implement their own persistence strategy in the :func:`__getstate__`.
 
         :param filename: file path of the serialized file, if not given then :attr:`save_abspath` is used
-        :return: successfully persisted or not
         """
         if not self.read_only and self.is_updated:
             f = filename or self.save_abspath
             if not f:
                 f = tempfile.NamedTemporaryFile(
-                    'w', delete=False, dir=os.environ.get('JINA_EXECUTOR_WORKDIR', None)
+                    "w", delete=False, dir=os.environ.get("JINA_EXECUTOR_WORKDIR", None)
                 ).name
 
             if self.max_snapshot > 0 and os.path.exists(f):
@@ -452,17 +494,17 @@ def save(self, filename: str = None):
                     d_f = self._snapshot_files.pop(0)
                     if os.path.exists(d_f):
                         os.remove(d_f)
-            with open(f, 'wb') as fp:
+            with open(f, "wb") as fp:
                 pickle.dump(self, fp)
                 self._last_snapshot_ts = datetime.now()
             self.is_updated = False
             self.logger.success(
-                f'artifacts of this executor ({self.name}) is persisted to {f}'
+                f"artifacts of this executor ({self.name}) is persisted to {f}"
             )
         else:
             if not self.is_updated:
                 self.logger.info(
-                    f'no update since {self._last_snapshot_ts:%Y-%m-%d %H:%M:%S%z}, will not save. '
+                    f"no update since {self._last_snapshot_ts:%Y-%m-%d %H:%M:%S%z}, will not save. "
                     'If you really want to save it, call "touch()" before "save()" to force saving'
                 )
 
@@ -480,13 +522,16 @@ def inject_config(
         :param raw_config: raw config to work on
         :param pea_id: the id of the storage of this parallel pea
         :param read_only: if the executor should be readonly
+        :param args: Additional arguments.
+        :param kwargs: Additional key word arguments.
+
         :return: an executor object
         """
-        if 'metas' not in raw_config:
-            raw_config['metas'] = {}
+        if "metas" not in raw_config:
+            raw_config["metas"] = {}
         tmp = fill_metas_with_defaults(raw_config)
-        tmp['metas']['pea_id'] = pea_id
-        tmp['metas']['read_only'] = read_only
+        tmp["metas"]["pea_id"] = pea_id
+        tmp["metas"]["read_only"] = read_only
 
         return tmp
 
@@ -502,10 +547,10 @@ def load(filename: str = None) -> AnyExecutor:
         if not filename:
             raise FileNotFoundError
         try:
-            with open(filename, 'rb') as fp:
+            with open(filename, "rb") as fp:
                 return pickle.load(fp)
         except EOFError:
-            raise BadPersistantFile(f'broken file {filename} can not be loaded')
+            raise BadPersistantFile(f"broken file {filename} can not be loaded")
 
     def close(self) -> None:
         """
@@ -520,20 +565,35 @@ def __enter__(self):
     def __exit__(self, exc_type, exc_val, exc_tb):
         self.close()
 
-    def attach(self, runtime: 'ZEDRuntime', *args, **kwargs):
-        """Attach this executor to a :class:`jina.peapods.runtime.BasePea`.
+    def attach(self, runtime: "ZEDRuntime", *args, **kwargs):
+        """Attach this executor to a Basepea
 
         This is called inside the initializing of a :class:`jina.peapods.runtime.BasePea`.
+
+        :param runtime: Runtime procedure leveraging ZMQ.
+        :param args: Additional arguments.
+        :param kwargs: Additional key word arguments.
         """
         for v in self._drivers.values():
             for d in v:
                 d.attach(executor=self, runtime=runtime, *args, **kwargs)
 
         # replacing the logger to runtime's logger
-        if runtime and isinstance(getattr(runtime, 'logger', None), JinaLogger):
+        if runtime and isinstance(getattr(runtime, "logger", None), JinaLogger):
             self.logger = runtime.logger
 
     def __call__(self, req_type, *args, **kwargs):
+        """
+
+
+        # noqa: DAR201
+
+
+        # noqa: DAR101
+
+
+        # noqa: DAR102
+        """
         if req_type in self._drivers:
             for d in self._drivers[req_type]:
                 if d.attached:
@@ -541,7 +601,7 @@ def __call__(self, req_type, *args, **kwargs):
                 else:
                     raise UnattachedDriver(d)
         else:
-            raise NoDriverForRequest(f'{req_type} for {self}')
+            raise NoDriverForRequest(f"{req_type} for {self}")
 
     def __str__(self):
         return self.__class__.__name__
diff --git a/jina/jaml/parsers/executor/legacy.py b/jina/jaml/parsers/executor/legacy.py
index 800d40a56ba76..6b0f884ccd8c4 100644
--- a/jina/jaml/parsers/executor/legacy.py
+++ b/jina/jaml/parsers/executor/legacy.py
@@ -104,6 +104,8 @@ def parse(self, cls: Type['BaseExecutor'], data: Dict) -> 'BaseExecutor':
             workspace_in_dump = getattr(obj, 'workspace', None)
             if workspace_in_dump != workspace_loaded_from:
                 obj.workspace = workspace_loaded_from
+
+            obj.fill_in_drivers(data.get('requests', None))
             load_from_dump = True
         else:
             cls._init_from_yaml = True
diff --git a/tests/unit/executors/test_dump_executor_with_drivers.py b/tests/unit/executors/test_dump_executor_with_drivers.py
index a9231ff7b5e7e..212ee81ae6bb5 100644
--- a/tests/unit/executors/test_dump_executor_with_drivers.py
+++ b/tests/unit/executors/test_dump_executor_with_drivers.py
@@ -1,5 +1,6 @@
 import os
 import pickle
+import pytest
 
 from jina.drivers.control import RouteDriver
 from jina.executors import BaseExecutor
@@ -24,16 +25,31 @@ def test_dump_driver(tmpdir):
     assert not p.idle_dealer_ids
 
 
-def test_dump_excutor_with_drivers(tmpdir):
-    a = BaseExecutor.load_config(f'{cur_dir}/yaml/route.yml')
-    a.touch()
-    a._drivers['ControlRequest'][0].idle_dealer_ids = ('hello', 'there')
-    a.save(str(tmpdir / 'a.bin'))
+def test_dump_exegit_scutor_without_drivers(tmpdir):
+    # Create an executor from a yaml file and store it to disc
+    executor_a = BaseExecutor.load_config(f'{cur_dir}/yaml/route.yml')
+    executor_a.touch()
+    executor_a._drivers['ControlRequest'][0].idle_dealer_ids = ('hello', 'there')
+    executor_a.save(str(tmpdir / 'aux.bin'))
 
-    print(a._drivers)
+    # load the saved executor_a as executor_b
+    executor_b = BaseExecutor.load(str(tmpdir / 'aux.bin'))
+    assert hasattr(executor_b, '_drivers') is False
 
-    b = BaseExecutor.load(str(tmpdir / 'a.bin'))
-    print(b._drivers)
-    assert id(b._drivers['ControlRequest'][0]) != id(a._drivers['ControlRequest'][0])
 
-    assert not b._drivers['ControlRequest'][0].idle_dealer_ids
+@pytest.fixture
+def temp_workspace(tmpdir):
+    os.environ['JINA_TEST_LOAD_FROM_DUMP_WORKSPACE'] = str(tmpdir)
+    yield
+    del os.environ['JINA_TEST_LOAD_FROM_DUMP_WORKSPACE']
+
+
+def test_drivers_renewed_from_yml_when_loaded_from_dump(temp_workspace):
+    executor_a = BaseExecutor.load_config(f'{cur_dir}/yaml/example_1.yml')
+    assert executor_a._drivers['SearchRequest'][0]._is_update is True
+
+    with executor_a:
+        executor_a.touch()
+
+    executor_b = BaseExecutor.load_config(f'{cur_dir}/yaml/example_2.yml')
+    assert executor_b._drivers['SearchRequest'][0]._is_update is False
diff --git a/tests/unit/executors/yaml/example_1.yml b/tests/unit/executors/yaml/example_1.yml
new file mode 100644
index 0000000000000..803b8b5660188
--- /dev/null
+++ b/tests/unit/executors/yaml/example_1.yml
@@ -0,0 +1,30 @@
+!BinaryPbIndexer
+with:
+  index_filename: images.gz
+metas:
+  name: images_kv_idx  # a customized name
+  workspace: $JINA_TEST_LOAD_FROM_DUMP_WORKSPACE
+requests:
+  on:
+    IndexRequest:
+      - !FilterQL
+        with:
+          lookups: {'modality': 'image'}
+      - !Buffer2URI
+        with:
+          base64: True
+      - !ExcludeQL
+        with:
+          fields:
+            - chunks
+            - embedding
+            - buffer
+      - !KVIndexDriver
+        with:
+          executor: images_kv_idx
+    SearchRequest:
+      - !KVSearchDriver
+        with:
+          executor: images_kv_idx
+          traversal_paths: ['m']
+          is_update: True
\ No newline at end of file
diff --git a/tests/unit/executors/yaml/example_2.yml b/tests/unit/executors/yaml/example_2.yml
new file mode 100644
index 0000000000000..800c8a1c96ea1
--- /dev/null
+++ b/tests/unit/executors/yaml/example_2.yml
@@ -0,0 +1,30 @@
+!BinaryPbIndexer
+with:
+  index_filename: images.gz
+metas:
+  name: images_kv_idx  # a customized name
+  workspace: $JINA_TEST_LOAD_FROM_DUMP_WORKSPACE
+requests:
+  on:
+    IndexRequest:
+      - !FilterQL
+        with:
+          lookups: {'modality': 'image'}
+      - !Buffer2URI
+        with:
+          base64: True
+      - !ExcludeQL
+        with:
+          fields:
+            - chunks
+            - embedding
+            - buffer
+      - !KVIndexDriver
+        with:
+          executor: images_kv_idx
+    SearchRequest:
+      - !KVSearchDriver
+        with:
+          executor: images_kv_idx
+          traversal_paths: ['m']
+          is_update: False
\ No newline at end of file


Code review comments:
diff_hunk: @@ -27,37 +27,59 @@
     from ..peapods.runtimes.zmq.zed import ZEDRuntime
     from ..drivers import BaseDriver
 
-__all__ = ['BaseExecutor', 'AnyExecutor', 'ExecutorType']
+__all__ = ["BaseExecutor", "AnyExecutor", "ExecutorType"]
path: jina/executors/__init__.py
text: @user1:
we try to enforce single quote style

@author:
done