Skip to content

Commit 2a9d727

Browse files
Add wsi/model endpoint tests (#1275)
* Add wsi/model endpoint tests Signed-off-by: Sachidanand Alle <[email protected]> * Fix object instantiate Signed-off-by: Sachidanand Alle <[email protected]> --------- Signed-off-by: Sachidanand Alle <[email protected]>
1 parent 4a96ae7 commit 2a9d727

File tree

13 files changed

+149
-40
lines changed

13 files changed

+149
-40
lines changed

monailabel/interfaces/utils/app.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
import json
1414
import logging
1515
import os
16-
import sys
1716
from typing import Any, Dict
1817

1918
from monailabel.config import settings
@@ -97,9 +96,6 @@ def run_main():
9796
logger.debug(f"++ APP_DIR: {app_dir}")
9897
logger.debug(f"++ STUDIES: {studies}")
9998

100-
sys.path.append(app_dir)
101-
sys.path.append(os.path.join(app_dir, "lib"))
102-
10399
logging.basicConfig(
104100
level=(logging.DEBUG if args.debug else logging.INFO),
105101
format="[%(asctime)s] [%(threadName)s] [%(levelname)s] (%(name)s:%(lineno)d) - %(message)s",

monailabel/main.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -295,8 +295,6 @@ def start_server_init_settings(self, args):
295295
if not os.path.exists(d):
296296
os.makedirs(d)
297297

298-
sys.path.append(args.app)
299-
sys.path.append(os.path.join(args.app, "lib"))
300298
os.environ["PATH"] += os.pathsep + os.path.join(args.app, "bin")
301299

302300
if args.dryrun:

monailabel/tasks/infer/bundle.py

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import json
1313
import logging
1414
import os
15+
import sys
1516
from typing import Any, Callable, Dict, Optional, Sequence, Union
1617

1718
from monai.bundle import ConfigItem, ConfigParser
@@ -22,6 +23,7 @@
2223
from monailabel.tasks.infer.basic_infer import BasicInferTask
2324
from monailabel.transform.post import Restored
2425
from monailabel.transform.pre import LoadImageTensord
26+
from monailabel.utils.others.class_utils import unload_module
2527
from monailabel.utils.others.generic import strtobool
2628

2729
logger = logging.getLogger(__name__)
@@ -96,6 +98,10 @@ def __init__(
9698
logger.warning(f"Ignore {path} as there is no infer config {self.const.configs()} exists")
9799
return
98100

101+
self.bundle_path = path
102+
sys.path.insert(0, self.bundle_path)
103+
unload_module("scripts")
104+
99105
self.bundle_config = ConfigParser()
100106
self.bundle_config.read_config(os.path.join(path, "configs", config_paths[0]))
101107
self.bundle_config.config.update({self.const.key_bundle_root(): path}) # type: ignore
@@ -112,6 +118,7 @@ def __init__(
112118
logger.warning(
113119
f"Ignore {path} as neither {self.const.model_pytorch()} nor {self.const.model_torchscript()} exists"
114120
)
121+
sys.path.remove(self.bundle_path)
115122
return
116123

117124
# https://docs.monai.io/en/latest/mb_specification.html#metadata-json-file
@@ -142,6 +149,7 @@ def __init__(
142149
)
143150
self.valid = True
144151
self.version = metadata.get("version")
152+
sys.path.remove(self.bundle_path)
145153

146154
def is_valid(self) -> bool:
147155
return self.valid
@@ -152,6 +160,8 @@ def info(self) -> Dict[str, Any]:
152160
return i
153161

154162
def pre_transforms(self, data=None) -> Sequence[Callable]:
163+
sys.path.insert(0, self.bundle_path)
164+
unload_module("scripts")
155165
self._update_device(data)
156166

157167
pre = []
@@ -169,30 +179,47 @@ def pre_transforms(self, data=None) -> Sequence[Callable]:
169179
else:
170180
res.append(t)
171181
pre = res
182+
183+
sys.path.remove(self.bundle_path)
172184
return pre
173185

174186
def inferer(self, data=None) -> Inferer:
187+
sys.path.insert(0, self.bundle_path)
188+
unload_module("scripts")
175189
self._update_device(data)
176190

191+
i = None
177192
for k in self.const.key_inferer():
178193
if self.bundle_config.get(k):
179-
return self.bundle_config.get_parsed_content(k, instantiate=True) # type: ignore
180-
return SimpleInferer()
194+
i = self.bundle_config.get_parsed_content(k, instantiate=True) # type: ignore
195+
break
196+
197+
sys.path.remove(self.bundle_path)
198+
return i if i is not None else SimpleInferer()
181199

182200
def detector(self, data=None) -> Optional[Callable]:
201+
sys.path.insert(0, self.bundle_path)
202+
unload_module("scripts")
183203
self._update_device(data)
184204

205+
d = None
185206
for k in self.const.key_detector():
186207
if self.bundle_config.get(k):
187208
detector = self.bundle_config.get_parsed_content(k, instantiate=True) # type: ignore
188209
for k in self.const.key_detector_ops():
189210
self.bundle_config.get_parsed_content(k, instantiate=True)
211+
190212
if detector is None or callable(detector):
191-
return detector # type: ignore
213+
d = detector # type: ignore
214+
break
192215
raise ValueError("Invalid Detector type; It's not callable")
193-
return None
216+
217+
sys.path.remove(self.bundle_path)
218+
return d
194219

195220
def post_transforms(self, data=None) -> Sequence[Callable]:
221+
sys.path.insert(0, self.bundle_path)
222+
unload_module("scripts")
196223
self._update_device(data)
197224

198225
post = []
@@ -204,6 +231,8 @@ def post_transforms(self, data=None) -> Sequence[Callable]:
204231

205232
if self.add_post_restore:
206233
post.append(Restored(keys=self.key_pred, ref_image=self.key_image))
234+
235+
sys.path.remove(self.bundle_path)
207236
return post
208237

209238
def _get_type(self, name, type):

monailabel/tasks/train/bundle.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import logging
1414
import os
1515
import subprocess
16+
import sys
1617
from typing import Dict, Optional, Sequence
1718

1819
import monai.bundle
@@ -24,6 +25,7 @@
2425
from monailabel.config import settings
2526
from monailabel.interfaces.datastore import Datastore
2627
from monailabel.interfaces.tasks.train import TrainTask
28+
from monailabel.utils.others.class_utils import unload_module
2729

2830
logger = logging.getLogger(__name__)
2931

@@ -266,6 +268,9 @@ def __call__(self, request, datastore: Datastore):
266268
self.bundle_config.set(v, k)
267269
ConfigParser.export_config_file(self.bundle_config.config, train_path, indent=2) # type: ignore
268270

271+
sys.path.insert(0, self.bundle_path)
272+
unload_module("scripts")
273+
269274
env = os.environ.copy()
270275
env["CUDA_VISIBLE_DEVICES"] = ",".join([str(g) for g in gpus])
271276
logger.info(f"Using CUDA_VISIBLE_DEVICES: {env['CUDA_VISIBLE_DEVICES']}")
@@ -293,8 +298,13 @@ def __call__(self, request, datastore: Datastore):
293298

294299
self.run_multi_gpu(request, cmd, env)
295300
else:
301+
sys.path.insert(0, self.bundle_path)
302+
unload_module("scripts")
303+
296304
self.run_single_gpu(request, overrides)
297305

306+
sys.path.remove(self.bundle_path)
307+
298308
logger.info("Training Finished....")
299309
return {}
300310

monailabel/utils/others/class_utils.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,37 @@
1414
import inspect
1515
import logging
1616
import os
17+
import sys
18+
from distutils.util import strtobool
1719
from typing import List
1820

1921
from monailabel.interfaces.exception import MONAILabelError, MONAILabelException
2022

2123
logger = logging.getLogger(__name__)
2224

2325

26+
def unload_module(name):
27+
modules = []
28+
for m in sorted(sys.modules):
29+
if m == name or m.startswith(f"{name}."):
30+
modules.append(m)
31+
32+
if modules and strtobool(os.environ.get("MONAI_LABEL_RELOAD_APP_LIB", "true")):
33+
logger.info(f"Remove/Reload previous Modules: {modules}")
34+
for m in modules:
35+
del sys.modules[m]
36+
37+
2438
def module_from_file(module_name, file_path):
39+
app_dir = os.path.dirname(file_path)
40+
sys.path.insert(0, app_dir)
41+
unload_module("lib")
42+
2543
spec = importlib.util.spec_from_file_location(module_name, file_path)
2644
module = importlib.util.module_from_spec(spec)
2745
spec.loader.exec_module(module)
46+
47+
sys.path.remove(app_dir)
2848
logger.debug(f"module: {module}")
2949
return module
3050

monailabel/utils/others/generic.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
import pathlib
1919
import shutil
2020
import subprocess
21-
import sys
2221
import time
2322
from typing import Dict
2423

@@ -308,7 +307,6 @@ def get_bundle_models(app_dir, conf, conf_key="models"):
308307
download(name=name, version=version, bundle_dir=model_dir, source=zoo_source, repo=zoo_repo)
309308
if version:
310309
shutil.move(os.path.join(model_dir, name), p)
311-
sys.path.append(p)
312310

313311
bundles[k] = p
314312

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ exclude = '''
4646

4747
[tool.pytest.ini_options]
4848
log_cli = true
49-
log_cli_level = "ERROR"
49+
log_cli_level = "INFO"
5050
log_cli_format = "[%(threadName)s] [%(levelname)s] (%(name)s:%(lineno)d) %(message)s"
5151

5252
#log_file = "pytest.log"

sample-apps/pathology/lib/activelearning/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@
99
# See the License for the specific language governing permissions and
1010
# limitations under the License.
1111

12-
from .random import Random
12+
from .random import WSIRandom

tests/unit/endpoints/context.py

Lines changed: 13 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import logging
1313
import os
1414
import random
15-
import sys
15+
import time
1616
import unittest
1717

1818
from fastapi.testclient import TestClient
@@ -54,7 +54,9 @@ def create_client(app_dir, studies, data_dir, conf=None):
5454
from monailabel.interfaces.utils.app import clear_cache
5555

5656
clear_cache()
57-
return TestClient(app)
57+
c = TestClient(app)
58+
time.sleep(1)
59+
return c
5860

5961

6062
class BasicEndpointTestSuite(unittest.TestCase):
@@ -68,12 +70,11 @@ class BasicEndpointTestSuite(unittest.TestCase):
6870

6971
@classmethod
7072
def setUpClass(cls) -> None:
71-
sys.path.append(cls.app_dir)
7273
cls.client = create_client(cls.app_dir, cls.studies, cls.data_dir)
7374

7475
@classmethod
7576
def tearDownClass(cls) -> None:
76-
sys.path.remove(cls.app_dir)
77+
pass
7778

7879

7980
class DICOMWebEndpointTestSuite(unittest.TestCase):
@@ -88,12 +89,11 @@ class DICOMWebEndpointTestSuite(unittest.TestCase):
8889

8990
@classmethod
9091
def setUpClass(cls) -> None:
91-
sys.path.append(cls.app_dir)
9292
cls.client = create_client(cls.app_dir, cls.studies, cls.data_dir)
9393

9494
@classmethod
9595
def tearDownClass(cls) -> None:
96-
sys.path.remove(cls.app_dir)
96+
pass
9797

9898

9999
class BasicEndpointV2TestSuite(unittest.TestCase):
@@ -107,14 +107,13 @@ class BasicEndpointV2TestSuite(unittest.TestCase):
107107

108108
@classmethod
109109
def setUpClass(cls) -> None:
110-
sys.path.append(cls.app_dir)
111110
cls.client = create_client(
112111
cls.app_dir, cls.studies, cls.data_dir, {"models": "deepgrow_2d,deepgrow_3d,segmentation_spleen,deepedit"}
113112
)
114113

115114
@classmethod
116115
def tearDownClass(cls) -> None:
117-
sys.path.remove(cls.app_dir)
116+
pass
118117

119118

120119
class BasicEndpointV3TestSuite(unittest.TestCase):
@@ -135,12 +134,11 @@ def setUpClass(cls) -> None:
135134
"skip_scoring": "false",
136135
"models": "segmentation_spleen",
137136
}
138-
sys.path.append(cls.app_dir)
139137
cls.client = create_client(cls.app_dir, cls.studies, cls.data_dir, conf=conf)
140138

141139
@classmethod
142140
def tearDownClass(cls) -> None:
143-
sys.path.remove(cls.app_dir)
141+
pass
144142

145143

146144
class BasicEndpointV4TestSuite(unittest.TestCase):
@@ -153,21 +151,15 @@ class BasicEndpointV4TestSuite(unittest.TestCase):
153151

154152
@classmethod
155153
def setUpClass(cls) -> None:
156-
sys.path.append(cls.app_dir)
157-
# sys.path.append(os.path.join(cls.app_dir, "lib", "activelearning", "random.py"))
158-
# sys.path.append(os.path.join(cls.app_dir, "lib", "infers", "nuclick.py"))
159-
160-
cls.client = create_client(
161-
cls.app_dir, cls.studies, cls.data_dir, {"models": "segmentation_nuclei,nuclick,classification_nuclei"}
162-
)
154+
cls.client = create_client(cls.app_dir, cls.studies, cls.data_dir, {"models": "segmentation_nuclei"})
163155
response = cls.client.get("/info/")
164156
# check if following fields exist in the response
165157
res = response.json()
166158
print(res)
167159

168160
@classmethod
169161
def tearDownClass(cls) -> None:
170-
sys.path.remove(cls.app_dir)
162+
pass
171163

172164

173165
class BasicDetectionBundleTestSuite(unittest.TestCase):
@@ -181,14 +173,13 @@ class BasicDetectionBundleTestSuite(unittest.TestCase):
181173

182174
@classmethod
183175
def setUpClass(cls) -> None:
184-
sys.path.append(cls.app_dir)
185176
cls.client = create_client(
186177
cls.app_dir, cls.studies, cls.data_dir, {"models": "lung_nodule_ct_detection", "tracking": False}
187178
)
188179

189180
@classmethod
190181
def tearDownClass(cls) -> None:
191-
sys.path.remove(cls.app_dir)
182+
pass
192183

193184

194185
class BasicBundleTestSuite(unittest.TestCase):
@@ -202,12 +193,11 @@ class BasicBundleTestSuite(unittest.TestCase):
202193

203194
@classmethod
204195
def setUpClass(cls) -> None:
205-
sys.path.append(cls.app_dir)
206196
cls.client = create_client(cls.app_dir, cls.studies, cls.data_dir, {"models": "spleen_ct_segmentation"})
207197

208198
@classmethod
209199
def tearDownClass(cls) -> None:
210-
sys.path.remove(cls.app_dir)
200+
pass
211201

212202

213203
class BasicBundleV2TestSuite(unittest.TestCase):
@@ -228,9 +218,8 @@ def setUpClass(cls) -> None:
228218
"epistemic_dropout": 0.2,
229219
"models": "spleen_ct_segmentation",
230220
}
231-
sys.path.append(cls.app_dir)
232221
cls.client = create_client(cls.app_dir, cls.studies, cls.data_dir, conf=conf)
233222

234223
@classmethod
235224
def tearDownClass(cls) -> None:
236-
sys.path.remove(cls.app_dir)
225+
pass

0 commit comments

Comments
 (0)