Skip to content

Commit 35f2260

Browse files
jan-janssenclaude
andauthored
Docs: add and extend docstrings across executorlib (#992)
Add missing docstrings to ResourceDictValidation, FutureSelector, private helpers in task_scheduler/file/shared.py, and several inputcheck validators. Extend existing one-liners with Args/Returns/Raises sections throughout executor/base.py, task_scheduler/base.py, standalone/serialize.py, and standalone/interactive/communication.py. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent ecdee72 commit 35f2260

8 files changed

Lines changed: 239 additions & 2 deletions

File tree

src/executorlib/executor/base.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,22 @@ def __init__(self, executor: TaskSchedulerBase):
2525

2626
@property
2727
def max_workers(self) -> Optional[int]:
28+
"""
29+
Return the number of parallel workers configured for this executor.
30+
31+
Returns:
32+
Optional[int]: The maximum number of parallel workers, or None if unconstrained.
33+
"""
2834
return self._task_scheduler.max_workers
2935

3036
@max_workers.setter
3137
def max_workers(self, max_workers: int):
38+
"""
39+
Set the number of parallel workers on the underlying task scheduler.
40+
41+
Args:
42+
max_workers (int): New maximum number of parallel workers.
43+
"""
3244
self._task_scheduler.max_workers = max_workers
3345

3446
@property

src/executorlib/standalone/inputcheck.py

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,16 @@ def check_init_function(
141141
def check_max_workers_and_cores(
142142
max_workers: Optional[int], max_cores: Optional[int]
143143
) -> None:
144+
"""
145+
Check that neither max_workers nor max_cores is set when using a pysqa-based backend.
146+
147+
Args:
148+
max_workers (int, optional): Maximum number of workers.
149+
max_cores (int, optional): Maximum number of cores.
150+
151+
Raises:
152+
ValueError: If max_workers or max_cores is not None.
153+
"""
144154
if max_workers is not None:
145155
raise ValueError(
146156
"The number of workers cannot be controlled with the pysqa based backend."
@@ -152,20 +162,48 @@ def check_max_workers_and_cores(
152162

153163

154164
def check_hostname_localhost(hostname_localhost: Optional[bool]) -> None:
165+
"""
166+
Check that hostname_localhost is not set when using a pysqa-based backend.
167+
168+
Args:
169+
hostname_localhost (bool, optional): Flag to use localhost for ZMQ connections.
170+
171+
Raises:
172+
ValueError: If hostname_localhost is not None.
173+
"""
155174
if hostname_localhost is not None:
156175
raise ValueError(
157176
"The option to connect to hosts based on their hostname is not available with the pysqa based backend."
158177
)
159178

160179

161180
def check_restart_limit(restart_limit: int, block_allocation: bool = True) -> None:
181+
"""
182+
Check that restart_limit is only used together with block_allocation.
183+
184+
Args:
185+
restart_limit (int): Maximum number of times a worker process may be restarted.
186+
block_allocation (bool): Whether block allocation is enabled. Defaults to True.
187+
188+
Raises:
189+
ValueError: If restart_limit is non-zero and block_allocation is False.
190+
"""
162191
if not block_allocation and restart_limit != 0:
163192
raise ValueError(
164193
"The option to specify a restart limit for worker processes is only available with block_allocation=True."
165194
)
166195

167196

168197
def check_pmi_mode(pmi_mode: Optional[str]) -> None:
198+
"""
199+
Check that pmi_mode is not set on a local workstation without SLURM or flux.
200+
201+
Args:
202+
pmi_mode (str, optional): PMI interface name (e.g. "pmix", "pmi1", "pmi2").
203+
204+
Raises:
205+
ValueError: If pmi_mode is not None.
206+
"""
169207
if pmi_mode is not None:
170208
raise ValueError(
171209
"The option to specify the pmi mode is not available on a local workstation, it requires SLURM or flux."
@@ -199,7 +237,22 @@ def validate_number_of_cores(
199237
set_local_cores: bool = False,
200238
) -> int:
201239
"""
202-
Validate the number of cores and return the appropriate value.
240+
Validate the number of cores and return the number of workers to use.
241+
242+
Precedence: max_cores / cores_per_worker > max_workers > CPU count (with warning).
243+
244+
Args:
245+
max_cores (int, optional): Total number of cores available.
246+
max_workers (int, optional): Explicit number of parallel workers.
247+
cores_per_worker (int, optional): Number of cores allocated to each worker. Defaults to 1.
248+
set_local_cores (bool): When True, fall back to the local CPU count if neither
249+
max_cores nor max_workers is given instead of raising an error. Defaults to False.
250+
251+
Returns:
252+
int: Number of parallel workers to use.
253+
254+
Raises:
255+
ValueError: If neither max_cores nor max_workers is set and set_local_cores is False.
203256
"""
204257
if max_cores is not None and max_workers is None and cores_per_worker is not None:
205258
return int(max_cores / cores_per_worker)

src/executorlib/standalone/interactive/communication.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,9 @@ def interface_connect(host: str, port: str) -> tuple[zmq.Context, zmq.Socket]:
240240
Args:
241241
host (str): hostname of the host running the SocketInterface instance to connect to.
242242
port (str): port on the host the SocketInterface instance is running on.
243+
244+
Returns:
245+
Tuple[zmq.Context, zmq.Socket]: The ZMQ context and the connected PAIR socket.
243246
"""
244247
context = zmq.Context()
245248
socket = context.socket(zmq.PAIR)
@@ -265,6 +268,9 @@ def interface_receive(socket: Optional[zmq.Socket]) -> dict:
265268
266269
Args:
267270
socket (zmq.Socket): socket for the connection
271+
272+
Returns:
273+
dict: Deserialized dictionary received from the socket, or an empty dict if socket is None.
268274
"""
269275
if socket is not None:
270276
return cloudpickle.loads(socket.recv())

src/executorlib/standalone/select.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,51 @@
33

44

55
class FutureSelector(Future):
6+
"""
7+
A Future wrapper that returns a single element from a result that is a tuple, list, or dict.
8+
9+
This is used by split_future() and get_item_from_future() to give callers an individual
10+
Future-like object for each output of a function that returns a collection.
11+
12+
Args:
13+
future (Future): The underlying future whose result is a collection.
14+
selector (int | str): Index (for sequences) or key (for mappings) used to extract
15+
the desired element from the result.
16+
"""
17+
618
def __init__(self, future: Future, selector: int | str):
19+
"""
20+
Args:
21+
future (Future): The underlying future whose result is a collection.
22+
selector (int | str): Index or key used to extract an element from the result.
23+
"""
724
self._future = future
825
self._selector = selector
926
super().__init__()
1027

1128
def __getattr__(self, attr: str) -> Any:
29+
"""Delegate attribute access to the wrapped future."""
1230
return getattr(self._future, attr)
1331

1432
def __setattr__(self, name: str, value: Any):
33+
"""Set attributes on the wrapped future, except for the two private instance variables."""
1534
if name in ["_future", "_selector"]:
1635
super().__setattr__(name, value)
1736
else:
1837
setattr(self._future, name, value)
1938

2039
def result(self, timeout: Optional[float] = None) -> Any:
40+
"""
41+
Return the selected element from the underlying future's result.
42+
43+
Args:
44+
timeout (float, optional): Maximum seconds to wait for the result. Defaults to None
45+
(wait indefinitely).
46+
47+
Returns:
48+
Any: The element at position/key ``selector`` in the underlying result, or None if
49+
the underlying result is None.
50+
"""
2151
result = self._future.result(timeout=timeout)
2252
if result is not None:
2353
return result[self._selector]

src/executorlib/standalone/serialize.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,18 @@ def _get_hash(binary: bytes) -> str:
102102

103103

104104
def _get_function_name(fn: Callable) -> str:
105+
"""
106+
Return a human-readable name for a callable.
107+
108+
For regular functions and methods the ``__name__`` attribute is used. For callable objects
109+
(classes implementing ``__call__``) the class name is extracted from ``__class__``.
110+
111+
Args:
112+
fn (Callable): The callable whose name should be determined.
113+
114+
Returns:
115+
str: A short name string suitable for use as part of a cache key prefix.
116+
"""
105117
if hasattr(fn, "__name__"):
106118
return fn.__name__
107119
else:

src/executorlib/standalone/validate.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,25 @@
1414

1515

1616
class ResourceDictValidation(BaseModel):
17+
"""
18+
Pydantic (or dataclass fallback) model for validating resource dictionaries passed to task submissions.
19+
20+
Attributes:
21+
cores (int, optional): Number of MPI cores to be used for each function call.
22+
threads_per_core (int, optional): Number of OpenMP threads to be used for each function call.
23+
gpus_per_core (int, optional): Number of GPUs per worker.
24+
cwd (str, optional): Current working directory where the parallel python task is executed.
25+
cache_key (str, optional): External cache key to identify tasks on the file system.
26+
cache_directory (str, optional): The directory to store cache files.
27+
num_nodes (int, optional): Number of compute nodes used for the evaluation.
28+
exclusive (bool, optional): Reserve exclusive access to selected compute nodes.
29+
error_log_file (str, optional): Path to the error log file.
30+
run_time_max (int, optional): Maximum allowed execution time in seconds.
31+
priority (int, optional): Queuing system priority for the task.
32+
slurm_cmd_args (list[str], optional): Additional command line arguments for the srun call.
33+
submission_template (str, optional): Template for queuing system job submission scripts.
34+
"""
35+
1736
cores: Optional[int] = None
1837
threads_per_core: Optional[int] = None
1938
gpus_per_core: Optional[int] = None
@@ -39,6 +58,18 @@ class Config:
3958

4059

4160
def _get_accepted_keys(class_type) -> list[str]:
61+
"""
62+
Return a list of accepted field names from a Pydantic model or dataclass.
63+
64+
Args:
65+
class_type: A Pydantic BaseModel subclass or a dataclass.
66+
67+
Returns:
68+
list[str]: Field names declared on the class.
69+
70+
Raises:
71+
TypeError: If the class type is neither a Pydantic model nor a dataclass.
72+
"""
4273
if hasattr(class_type, "model_fields"):
4374
return list(class_type.model_fields.keys())
4475
elif hasattr(class_type, "__dataclass_fields__"):
@@ -47,10 +78,32 @@ def _get_accepted_keys(class_type) -> list[str]:
4778

4879

4980
def validate_resource_dict(resource_dict: dict) -> None:
81+
"""
82+
Validate a resource dictionary against the declared fields of ResourceDictValidation.
83+
84+
Args:
85+
resource_dict (dict): Dictionary of resource requirements to validate. Unknown keys raise
86+
a validation error when pydantic is available.
87+
88+
Raises:
89+
ValidationError: If any key/value pair violates the ResourceDictValidation schema.
90+
"""
5091
_ = ResourceDictValidation(**resource_dict)
5192

5293

5394
def validate_resource_dict_with_optional_keys(resource_dict: dict) -> None:
95+
"""
96+
Validate a resource dictionary, allowing unknown keys with a warning instead of an error.
97+
98+
Known keys are validated against ResourceDictValidation. Unknown keys are collected and
99+
emitted as a UserWarning so callers can detect unsupported options without hard failure.
100+
101+
Args:
102+
resource_dict (dict): Dictionary of resource requirements to validate.
103+
104+
Raises:
105+
ValidationError: If any known key/value pair violates the ResourceDictValidation schema.
106+
"""
54107
accepted_keys = _get_accepted_keys(class_type=ResourceDictValidation)
55108
optional_lst = [key for key in resource_dict if key not in accepted_keys]
56109
validate_dict = {

src/executorlib/task_scheduler/base.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@
1515

1616

1717
def validate_resource_dict(resource_dict: dict):
18+
"""
19+
No-op resource dict validator used as the default when no validation is required.
20+
21+
Args:
22+
resource_dict (dict): Dictionary of resource requirements (ignored).
23+
"""
1824
pass
1925

2026

@@ -32,7 +38,13 @@ def __init__(
3238
validator: Callable = validate_resource_dict,
3339
):
3440
"""
35-
Initialize the ExecutorBase class.
41+
Initialize the TaskSchedulerBase.
42+
43+
Args:
44+
max_cores (int, optional): Maximum number of cores available to the scheduler.
45+
Tasks requesting more cores than this will be rejected. Defaults to None (unlimited).
46+
validator (Callable): Function used to validate per-task resource dicts before
47+
submission. Defaults to the no-op validate_resource_dict.
3648
"""
3749
cloudpickle_register(ind=3)
3850
self._process_kwargs: dict = {}
@@ -43,10 +55,22 @@ def __init__(
4355

4456
@property
4557
def max_workers(self) -> Optional[int]:
58+
"""
59+
Return the configured number of parallel workers, or None if unconstrained.
60+
61+
Returns:
62+
Optional[int]: The max_workers value stored in process kwargs, or None.
63+
"""
4664
return self._process_kwargs.get("max_workers")
4765

4866
@max_workers.setter
4967
def max_workers(self, max_workers: int):
68+
"""
69+
Setting max_workers after construction is not supported by the base scheduler.
70+
71+
Raises:
72+
NotImplementedError: Always.
73+
"""
5074
raise NotImplementedError("The max_workers setter is not implemented.")
5175

5276
@property

0 commit comments

Comments
 (0)