Skip to content

Commit eb8b739

Browse files
jan-janssengoogle-labs-jules[bot]pre-commit-ci[bot]
authored
[Fix] Add dataclass fallback for ResourceDictValidation when pydantic is missing (#939)
* feat: add dataclass fallback for ResourceDictValidation Modified src/executorlib/standalone/validate.py to use a Python dataclass as a fallback when pydantic is not installed. The class definition is shared between both cases. Updated tests to verify the fallback behavior. Co-authored-by: jan-janssen <3854739+jan-janssen@users.noreply.github.com> * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * mypy fix * add additional unit test * clean up * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> Co-authored-by: jan-janssen <3854739+jan-janssen@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 693ca9e commit eb8b739

5 files changed

Lines changed: 76 additions & 82 deletions

File tree

src/executorlib/executor/flux.py

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,23 +13,16 @@
1313
check_wait_on_shutdown,
1414
validate_number_of_cores,
1515
)
16+
from executorlib.standalone.validate import (
17+
validate_resource_dict,
18+
validate_resource_dict_with_optional_keys,
19+
)
1620
from executorlib.task_scheduler.interactive.blockallocation import (
1721
BlockAllocationTaskScheduler,
1822
)
1923
from executorlib.task_scheduler.interactive.dependency import DependencyTaskScheduler
2024
from executorlib.task_scheduler.interactive.onetoone import OneProcessTaskScheduler
2125

22-
try:
23-
from executorlib.standalone.validate import (
24-
validate_resource_dict,
25-
validate_resource_dict_with_optional_keys,
26-
)
27-
except ImportError:
28-
from executorlib.task_scheduler.base import validate_resource_dict
29-
from executorlib.task_scheduler.base import (
30-
validate_resource_dict as validate_resource_dict_with_optional_keys,
31-
)
32-
3326

3427
class FluxJobExecutor(BaseExecutor):
3528
"""

src/executorlib/executor/single.py

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,23 +12,16 @@
1212
validate_number_of_cores,
1313
)
1414
from executorlib.standalone.interactive.spawner import MpiExecSpawner
15+
from executorlib.standalone.validate import (
16+
validate_resource_dict,
17+
validate_resource_dict_with_optional_keys,
18+
)
1519
from executorlib.task_scheduler.interactive.blockallocation import (
1620
BlockAllocationTaskScheduler,
1721
)
1822
from executorlib.task_scheduler.interactive.dependency import DependencyTaskScheduler
1923
from executorlib.task_scheduler.interactive.onetoone import OneProcessTaskScheduler
2024

21-
try:
22-
from executorlib.standalone.validate import (
23-
validate_resource_dict,
24-
validate_resource_dict_with_optional_keys,
25-
)
26-
except ImportError:
27-
from executorlib.task_scheduler.base import validate_resource_dict
28-
from executorlib.task_scheduler.base import (
29-
validate_resource_dict as validate_resource_dict_with_optional_keys,
30-
)
31-
3225

3326
class SingleNodeExecutor(BaseExecutor):
3427
"""

src/executorlib/executor/slurm.py

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
check_wait_on_shutdown,
1111
validate_number_of_cores,
1212
)
13+
from executorlib.standalone.validate import (
14+
validate_resource_dict,
15+
validate_resource_dict_with_optional_keys,
16+
)
1317
from executorlib.task_scheduler.interactive.blockallocation import (
1418
BlockAllocationTaskScheduler,
1519
)
@@ -20,17 +24,6 @@
2024
validate_max_workers,
2125
)
2226

23-
try:
24-
from executorlib.standalone.validate import (
25-
validate_resource_dict,
26-
validate_resource_dict_with_optional_keys,
27-
)
28-
except ImportError:
29-
from executorlib.task_scheduler.base import validate_resource_dict
30-
from executorlib.task_scheduler.base import (
31-
validate_resource_dict as validate_resource_dict_with_optional_keys,
32-
)
33-
3427

3528
class SlurmClusterExecutor(BaseExecutor):
3629
"""

src/executorlib/standalone/validate.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
11
import warnings
22
from typing import Optional
33

4-
from pydantic import BaseModel, Extra
4+
try:
5+
from pydantic import BaseModel, Extra
6+
7+
HAS_PYDANTIC = True
8+
except ImportError:
9+
from dataclasses import dataclass
10+
11+
BaseModel = object
12+
Extra = None
13+
HAS_PYDANTIC = False
514

615

716
class ResourceDictValidation(BaseModel):
@@ -17,16 +26,30 @@ class ResourceDictValidation(BaseModel):
1726
priority: Optional[int] = None
1827
slurm_cmd_args: Optional[list[str]] = None
1928

20-
class Config:
21-
extra = Extra.forbid
29+
if HAS_PYDANTIC:
30+
31+
class Config:
32+
extra = Extra.forbid
33+
34+
35+
if not HAS_PYDANTIC:
36+
ResourceDictValidation = dataclass(ResourceDictValidation) # type: ignore
37+
38+
39+
def _get_accepted_keys(class_type) -> list[str]:
40+
if hasattr(class_type, "model_fields"):
41+
return list(class_type.model_fields.keys())
42+
elif hasattr(class_type, "__dataclass_fields__"):
43+
return list(class_type.__dataclass_fields__.keys())
44+
raise TypeError("Unsupported class type for validation")
2245

2346

2447
def validate_resource_dict(resource_dict: dict) -> None:
2548
_ = ResourceDictValidation(**resource_dict)
2649

2750

2851
def validate_resource_dict_with_optional_keys(resource_dict: dict) -> None:
29-
accepted_keys = ResourceDictValidation.model_fields.keys()
52+
accepted_keys = _get_accepted_keys(class_type=ResourceDictValidation)
3053
optional_lst = [key for key in resource_dict if key not in accepted_keys]
3154
validate_dict = {
3255
key: value for key, value in resource_dict.items() if key in accepted_keys

tests/unit/standalone/test_validate.py

Lines changed: 37 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -12,63 +12,55 @@
1212
skip_pydantic_test = True
1313

1414

15-
class TestValidateImport(unittest.TestCase):
16-
def test_single_node_executor(self):
15+
class TestValidateFallback(unittest.TestCase):
16+
def test_validate_resource_dict_fallback(self):
1717
with patch.dict('sys.modules', {'pydantic': None}):
1818
if 'executorlib.standalone.validate' in sys.modules:
1919
del sys.modules['executorlib.standalone.validate']
20-
if 'executorlib.executor.single' in sys.modules:
21-
del sys.modules['executorlib.executor.single']
2220

23-
import executorlib.executor.single
24-
importlib.reload(executorlib.executor.single)
21+
from executorlib.standalone.validate import validate_resource_dict, ResourceDictValidation
22+
from dataclasses import is_dataclass
2523

26-
from executorlib.executor.single import validate_resource_dict
27-
28-
source_file = inspect.getfile(validate_resource_dict)
29-
if os.name == 'nt':
30-
self.assertTrue(source_file.endswith('task_scheduler\\base.py'))
31-
else:
32-
self.assertTrue(source_file.endswith('task_scheduler/base.py'))
33-
self.assertIsNone(validate_resource_dict({"any": "thing"}))
24+
self.assertTrue(is_dataclass(ResourceDictValidation))
3425

35-
def test_flux_job_executor(self):
36-
with patch.dict('sys.modules', {'pydantic': None}):
37-
if 'executorlib.standalone.validate' in sys.modules:
38-
del sys.modules['executorlib.standalone.validate']
39-
if 'executorlib.executor.flux' in sys.modules:
40-
del sys.modules['executorlib.executor.flux']
26+
# Valid dict
27+
self.assertIsNone(validate_resource_dict({"cores": 1}))
4128

42-
import executorlib.executor.flux
43-
importlib.reload(executorlib.executor.flux)
29+
# Invalid dict (extra key)
30+
with self.assertRaises(TypeError):
31+
validate_resource_dict({"invalid_key": 1})
4432

45-
from executorlib.executor.flux import validate_resource_dict
46-
47-
source_file = inspect.getfile(validate_resource_dict)
48-
if os.name == 'nt':
49-
self.assertTrue(source_file.endswith('task_scheduler\\base.py'))
50-
else:
51-
self.assertTrue(source_file.endswith('task_scheduler/base.py'))
52-
self.assertIsNone(validate_resource_dict({"any": "thing"}))
53-
54-
def test_slurm_job_executor(self):
33+
def test_validate_resource_dict_with_optional_keys_fallback(self):
5534
with patch.dict('sys.modules', {'pydantic': None}):
5635
if 'executorlib.standalone.validate' in sys.modules:
5736
del sys.modules['executorlib.standalone.validate']
58-
if 'executorlib.executor.slurm' in sys.modules:
59-
del sys.modules['executorlib.executor.slurm']
6037

61-
import executorlib.executor.slurm
62-
importlib.reload(executorlib.executor.slurm)
38+
from executorlib.standalone.validate import validate_resource_dict_with_optional_keys
39+
40+
# Valid dict with optional keys
41+
with self.assertWarns(UserWarning):
42+
validate_resource_dict_with_optional_keys({"cores": 1, "optional_key": 2})
43+
44+
def test_get_accepted_keys(self):
45+
from executorlib.standalone.validate import _get_accepted_keys, ResourceDictValidation
6346

64-
from executorlib.executor.slurm import validate_resource_dict
65-
66-
source_file = inspect.getfile(validate_resource_dict)
67-
if os.name == 'nt':
68-
self.assertTrue(source_file.endswith('task_scheduler\\base.py'))
69-
else:
70-
self.assertTrue(source_file.endswith('task_scheduler/base.py'))
71-
self.assertIsNone(validate_resource_dict({"any": "thing"}))
47+
accepted_keys = _get_accepted_keys(ResourceDictValidation)
48+
expected_keys = [
49+
"cores",
50+
"threads_per_core",
51+
"gpus_per_core",
52+
"cwd",
53+
"cache_key",
54+
"num_nodes",
55+
"exclusive",
56+
"error_log_file",
57+
"run_time_limit",
58+
"priority",
59+
"slurm_cmd_args"
60+
]
61+
self.assertEqual(set(accepted_keys), set(expected_keys))
62+
with self.assertRaises(TypeError):
63+
_get_accepted_keys(int)
7264

7365

7466
@unittest.skipIf(skip_pydantic_test, "pydantic is not installed")
@@ -81,4 +73,4 @@ def dummy_function(i):
8173

8274
with SingleNodeExecutor() as exe:
8375
with self.assertRaises(ValidationError):
84-
exe.submit(dummy_function, 5, resource_dict={"any": "thing"})
76+
exe.submit(dummy_function, 5, resource_dict={"any": "thing"})

0 commit comments

Comments
 (0)