Repository: eWaterCycle/grpc4bmi

Issue:
Incompatibility with typeguard 3
Seems typeguard 3 was released which deprecates functions used in this repo. We should use the new way or pin typeguard to <3.

For example in ewatercycle test we got:

````
________________ ERROR collecting tests/forcing/test_default.py ________________
16
ImportError while importing test module '/home/runner/work/ewatercycle/ewatercycle/tests/forcing/test_default.py'.
17
Hint: make sure your test modules/packages have valid Python names.
18
Traceback:
19
/usr/share/miniconda3/envs/ewatercycle/lib/python3.9/importlib/__init__.py:127: in import_module
20
    return _bootstrap._gcd_import(name[level:], package, level)
21
tests/forcing/test_default.py:5: in <module>
22
    from ewatercycle.forcing import (
23
src/ewatercycle/forcing/__init__.py:8: in <module>
24
    from . import _hype, _lisflood, _marrmot, _pcrglobwb, _wflow
25
src/ewatercycle/forcing/_lisflood.py:18: in <module>
26
    from ._lisvap import create_lisvap_config, lisvap
27
src/ewatercycle/forcing/_lisvap.py:29: in <module>
28
    from ewatercycle.container import ContainerEngine
29
src/ewatercycle/container.py:7: in <module>
30
    from grpc4bmi.bmi_client_docker import BmiClientDocker
31
/usr/share/miniconda3/envs/ewatercycle/lib/python3.9/site-packages/grpc4bmi/bmi_client_docker.py:8: in <module>
32
    from typeguard import check_argument_types, qualified_name
33
E   ImportError: cannot import name 'check_argument_types' from 'typeguard' (/usr/share/miniconda3/envs/ewatercycle/lib/python3.9/site-packages/typeguard/__init__.py)

```

Found while running bmi2 branch.

Comments:

Patch:
diff --git a/grpc4bmi/bmi_client_apptainer.py b/grpc4bmi/bmi_client_apptainer.py
index 100a85b..a4169e2 100644
--- a/grpc4bmi/bmi_client_apptainer.py
+++ b/grpc4bmi/bmi_client_apptainer.py
@@ -8,7 +8,7 @@ from typing import Iterable
 
 from packaging.specifiers import SpecifierSet
 from packaging.version import Version
-from typeguard import check_argument_types, qualified_name
+from typeguard import typechecked
 
 from grpc4bmi.bmi_grpc_client import BmiClient
 from grpc4bmi.exceptions import ApptainerVersionException, DeadContainerException
@@ -194,13 +194,13 @@ class BmiClientApptainer(BmiClient):
 
     """
 
+    @typechecked
     def __init__(self, image: str, work_dir: str, input_dirs: Iterable[str] = tuple(), delay=0, timeout=None,
                  capture_logs=True,
                  ):
-        assert check_argument_types()
         if type(input_dirs) == str:
             msg = f'type of argument "input_dirs" must be collections.abc.Iterable; ' \
-                  f'got {qualified_name(input_dirs)} instead'
+                  f'got {type(input_dirs)} instead'
             raise TypeError(msg)
         check_apptainer_version()
         host = 'localhost'
diff --git a/grpc4bmi/bmi_client_docker.py b/grpc4bmi/bmi_client_docker.py
index d14fe67..328400a 100644
--- a/grpc4bmi/bmi_client_docker.py
+++ b/grpc4bmi/bmi_client_docker.py
@@ -5,7 +5,7 @@ from typing import Iterable
 
 import docker
 from docker.models.containers import Container
-from typeguard import check_argument_types, qualified_name
+from typeguard import typechecked
 
 from grpc4bmi.bmi_grpc_client import BmiClient
 from grpc4bmi.exceptions import DeadContainerException
@@ -58,14 +58,14 @@ class BmiClientDocker(BmiClient):
     See :py:class:`grpc4bmi.bmi_client_apptainer.BmiClientApptainer` for examples using `input_dirs` and `work_dir`.
     """
 
+    @typechecked
     def __init__(self, image: str, work_dir: str, image_port=50051, host=None,
                  input_dirs: Iterable[str] = tuple(),
                  user=os.getuid(), remove=False, delay=5,
                  timeout=None):
-        assert check_argument_types()
         if type(input_dirs) == str:
             msg = f'type of argument "input_dirs" must be collections.abc.Iterable; ' \
-                  f'got {qualified_name(input_dirs)} instead'
+                  f'got {type(input_dirs)} instead'
             raise TypeError(msg)
         port = BmiClient.get_unique_port()
         client = docker.from_env()
diff --git a/grpc4bmi/bmi_client_singularity.py b/grpc4bmi/bmi_client_singularity.py
index 3a49871..f2f1e4a 100644
--- a/grpc4bmi/bmi_client_singularity.py
+++ b/grpc4bmi/bmi_client_singularity.py
@@ -8,7 +8,7 @@ from typing import Iterable
 
 from packaging.specifiers import SpecifierSet
 from packaging.version import Version
-from typeguard import check_argument_types, qualified_name
+from typeguard import typechecked
 
 from grpc4bmi.bmi_grpc_client import BmiClient
 from grpc4bmi.exceptions import ApptainerVersionException, DeadContainerException, SingularityVersionException
@@ -197,13 +197,13 @@ class BmiClientSingularity(BmiClient):
 
     """
 
+    @typechecked
     def __init__(self, image: str, work_dir: str, input_dirs: Iterable[str] = tuple(), delay=0, timeout=None,
                  capture_logs=True,
                  ):
-        assert check_argument_types()
         if type(input_dirs) == str:
             msg = f'type of argument "input_dirs" must be collections.abc.Iterable; ' \
-                  f'got {qualified_name(input_dirs)} instead'
+                  f'got {type(input_dirs)} instead'
             raise TypeError(msg)
         check_singularity_version()
         host = 'localhost'
diff --git a/grpc4bmi/bmi_grpc_client.py b/grpc4bmi/bmi_grpc_client.py
index cacfa34..2db4cfe 100644
--- a/grpc4bmi/bmi_grpc_client.py
+++ b/grpc4bmi/bmi_grpc_client.py
@@ -9,7 +9,7 @@ import numpy as np
 from bmipy import Bmi
 import grpc
 import numpy
-from typeguard import check_argument_types
+from typeguard import typechecked
 
 from grpc_status import rpc_status
 from google.rpc import error_details_pb2
@@ -94,8 +94,8 @@ class BmiClient(Bmi):
             s.bind(("" if host is None else host, 0))
             return int(s.getsockname()[1])
 
+    @typechecked
     def initialize(self, filename: Optional[str]):
-        assert check_argument_types()
         fname = "" if filename is None else filename
         try:
             return self.stub.initialize(bmi_pb2.InitializeRequest(config_file=fname))
diff --git a/pyproject.toml b/pyproject.toml
index 4d4d9e8..de9cf52 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -28,7 +28,7 @@ dependencies = [
     "grpcio-reflection",
     "grpcio-status",
     "googleapis-common-protos>=1.5.5",
-    "protobuf",
+    "protobuf>=4,<5",
     "numpy",
     "docker",
     "bmipy",


Test patch:
diff --git a/test/test_apptainer.py b/test/test_apptainer.py
index 8dd570b..cd5ea5e 100644
--- a/test/test_apptainer.py
+++ b/test/test_apptainer.py
@@ -1,7 +1,8 @@
 import os
-from typing import Type, Union
+from typing import Type
 
 import pytest
+from typeguard import TypeCheckError
 
 from grpc4bmi.bmi_client_apptainer import SUPPORTED_APPTAINER_VERSIONS, BmiClientApptainer, check_apptainer_version_string
 from grpc4bmi.exceptions import ApptainerVersionException, DeadContainerException
@@ -64,17 +65,17 @@ class TestBmiClientApptainerBadDays:
             BmiClientApptainer(image=IMAGE_NAME, input_dirs=(some_dir,), work_dir=some_dir)
 
     def test_workdir_as_number(self):
-        with pytest.raises(TypeError, match='must be str'):
+        with pytest.raises(TypeCheckError, match='is not an instance of str'):
             BmiClientApptainer(image=IMAGE_NAME, work_dir=42)
 
     def test_inputdirs_as_str(self, tmp_path):
         some_dir = str(tmp_path)
-        with pytest.raises(TypeError, match='must be collections.abc.Iterable; got str instead'):
+        with pytest.raises(TypeError, match=' must be collections.abc.Iterable'):
             BmiClientApptainer(image=IMAGE_NAME, input_dirs='old type', work_dir=some_dir)
 
     def test_inputdirs_as_number(self, tmp_path):
         some_dir = str(tmp_path)
-        with pytest.raises(TypeError, match='must be collections.abc.Iterable; got int instead'):
+        with pytest.raises(TypeCheckError, match='is not an instance of collections.abc.Iterable'):
             BmiClientApptainer(image=IMAGE_NAME, input_dirs=42, work_dir=some_dir)
 
 
diff --git a/test/test_client.py b/test/test_client.py
index e1c643a..cf00c3b 100644
--- a/test/test_client.py
+++ b/test/test_client.py
@@ -9,6 +9,7 @@ from google.protobuf import any_pb2
 from google.rpc import error_details_pb2, status_pb2, code_pb2
 from grpc_status import rpc_status
 from heat import BmiHeat
+from typeguard import TypeCheckError
 
 from grpc4bmi.bmi_grpc_server import BmiServer
 from grpc4bmi.bmi_grpc_client import BmiClient, RemoteException, handle_error
@@ -108,7 +109,7 @@ def test_initialize():
 def test_initialize_with_nonstring():
     client, local = make_bmi_classes(False)
     assert client is not None
-    with pytest.raises(TypeError, match='got int instead'):
+    with pytest.raises(TypeCheckError, match='did not match any element in the union'):
         client.initialize(42)
     client.finalize()
     del client
diff --git a/test/test_docker.py b/test/test_docker.py
index 7b8fc65..812ec21 100644
--- a/test/test_docker.py
+++ b/test/test_docker.py
@@ -3,6 +3,7 @@ from io import BytesIO
 import docker
 import numpy as np
 import pytest
+from typeguard import TypeCheckError
 
 from grpc4bmi.bmi_client_docker import BmiClientDocker
 from grpc4bmi.exceptions import DeadContainerException
@@ -102,17 +103,17 @@ class TestBmiClientDocker:
             BmiClientDocker(image=walrus_docker_image, image_port=55555, input_dirs=(some_dir,), work_dir=some_dir)
 
     def test_workdir_as_number(self):
-        with pytest.raises(TypeError, match='must be str'):
+        with pytest.raises(TypeCheckError, match='is not an instance of str'):
             BmiClientDocker(image=walrus_docker_image, work_dir=42)
 
     def test_inputdirs_as_str(self, tmp_path):
         some_dir = str(tmp_path)
-        with pytest.raises(TypeError, match='must be collections.abc.Iterable; got str instead'):
+        with pytest.raises(TypeError, match='must be collections.abc.Iterable'):
             BmiClientDocker(image=walrus_docker_image, input_dirs='old type', work_dir=some_dir)
 
     def test_inputdirs_as_number(self, tmp_path):
         some_dir = str(tmp_path)
-        with pytest.raises(TypeError, match='must be collections.abc.Iterable; got int instead'):
+        with pytest.raises(TypeCheckError, match='is not an instance of collections.abc.Iterable'):
             BmiClientDocker(image=walrus_docker_image, input_dirs=42, work_dir=some_dir)
 
     def test_logs(self, walrus_model, capfd):
diff --git a/test/test_singularity.py b/test/test_singularity.py
index 41e4265..1f9d8b1 100644
--- a/test/test_singularity.py
+++ b/test/test_singularity.py
@@ -10,6 +10,7 @@ from grpc import RpcError
 from nbconvert.preprocessors import ExecutePreprocessor
 from nbformat.v4 import new_notebook, new_code_cell
 import numpy as np
+from typeguard import TypeCheckError
 
 from grpc4bmi.bmi_client_singularity import SUPPORTED_APPTAINER_VERSIONS, SUPPORTED_SINGULARITY_VERSIONS, BmiClientSingularity, check_singularity_version_string
 from grpc4bmi.exceptions import ApptainerVersionException, DeadContainerException, SingularityVersionException
@@ -167,17 +168,17 @@ class TestBmiClientSingularity:
         assert len(model.get_value('Q', np.zeros(1,))) == 1
 
     def test_workdir_as_number(self):
-        with pytest.raises(TypeError, match='must be str'):
+        with pytest.raises(TypeCheckError, match='is not an instance of str'):
             BmiClientSingularity(image=IMAGE_NAME, work_dir=42)
 
     def test_inputdirs_as_str(self, tmp_path):
         some_dir = str(tmp_path)
-        with pytest.raises(TypeError, match='must be collections.abc.Iterable; got str instead'):
+        with pytest.raises(TypeError, match='must be collections.abc.Iterable'):
             BmiClientSingularity(image=IMAGE_NAME, input_dirs='old type', work_dir=some_dir)
 
     def test_inputdirs_as_number(self, tmp_path):
         some_dir = str(tmp_path)
-        with pytest.raises(TypeError, match='must be collections.abc.Iterable; got int instead'):
+        with pytest.raises(TypeCheckError, match='is not an instance of collections.abc.Iterable'):
             BmiClientSingularity(image=IMAGE_NAME, input_dirs=42, work_dir=some_dir)