3
3
import typing
4
4
from dataclasses import is_dataclass
5
5
from textwrap import dedent
6
- from types import UnionType
7
6
from typing import Any , Callable , Dict , Union , get_args , get_origin
8
7
9
8
import click
@@ -50,7 +49,6 @@ class UnionType:
50
49
TYPE_ANNOTATION_TYPES = (type , typing ._GenericAlias , UnionType ) # type: ignore[attr-defined]
51
50
52
51
53
-
54
52
def _generate_metadata (f : Callable ) -> LatchMetadata :
55
53
signature = inspect .signature (f )
56
54
metadata = LatchMetadata (f .__name__ , LatchAuthor ())
@@ -72,7 +70,7 @@ def _inject_metadata(f: Callable, metadata: LatchMetadata) -> None:
72
70
# so that when users call @workflow without any arguments or
73
71
# parentheses, the workflow still serializes as expected
74
72
def workflow (
75
- metadata : Union [LatchMetadata , Callable ]
73
+ metadata : Union [LatchMetadata , Callable ],
76
74
) -> Union [PythonFunctionWorkflow , Callable ]:
77
75
if isinstance (metadata , Callable ):
78
76
f = metadata
@@ -141,25 +139,33 @@ def decorator(f: Callable):
141
139
142
140
143
141
def _is_valid_samplesheet_parameter_type (parameter : inspect .Parameter ) -> bool :
144
- """
145
- Check if a parameter in the workflow function's signature is annotated with a valid type for a
146
- samplesheet LatchParameter.
142
+ """Check if a workflow parameter is hinted with a valid type for a samplesheet LatchParameter.
143
+
144
+ Currently, a samplesheet LatchParameter must be defined as a list of dataclasses, or as an
145
+ `Optional` list of dataclasses when the parameter is part of a `ForkBranch`.
146
+
147
+ Args:
148
+ parameter: A parameter from the workflow function's signature.
149
+
150
+ Returns:
151
+ True if the parameter is annotated as a list of dataclasses, or as an `Optional` list of
152
+ dataclasses.
153
+ False otherwise.
147
154
"""
148
155
annotation = parameter .annotation
149
156
150
157
# If the parameter did not have a type annotation, short-circuit and return False
151
158
if not _is_type_annotation (annotation ):
152
159
return False
153
160
154
- return (
155
- _is_list_of_dataclasses_type (annotation )
156
- or ( _is_optional_type ( annotation ) and _is_list_of_dataclasses_type (_unpack_optional_type (annotation ) ))
161
+ return _is_list_of_dataclasses_type ( annotation ) or (
162
+ _is_optional_type (annotation )
163
+ and _is_list_of_dataclasses_type (_unpack_optional_type (annotation ))
157
164
)
158
165
159
166
160
167
def _is_list_of_dataclasses_type (dtype : TypeAnnotation ) -> bool :
161
- """
162
- Check if the type is a list of dataclasses.
168
+ """Check if the type is a list of dataclasses.
163
169
164
170
Args:
165
171
dtype: A type.
@@ -169,10 +175,10 @@ def _is_list_of_dataclasses_type(dtype: TypeAnnotation) -> bool:
169
175
False otherwise.
170
176
171
177
Raises:
172
- TypeError: If the input is not a `type` .
178
+ TypeError: If the input is not a valid `TypeAnnotation` type (see above) .
173
179
"""
174
180
if not isinstance (dtype , TYPE_ANNOTATION_TYPES ):
175
- raise TypeError (f"Expected ` type` , got { type (dtype )} : { dtype } " )
181
+ raise TypeError (f"Expected type annotation , got { type (dtype )} : { dtype } " )
176
182
177
183
origin = get_origin (dtype )
178
184
args = get_args (dtype )
@@ -187,8 +193,7 @@ def _is_list_of_dataclasses_type(dtype: TypeAnnotation) -> bool:
187
193
188
194
189
195
def _is_optional_type (dtype : TypeAnnotation ) -> bool :
190
- """
191
- Check if a type is `Optional`.
196
+ """Check if a type is `Optional`.
192
197
193
198
An optional type may be declared using three syntaxes: `Optional[T]`, `Union[T, None]`, or `T |
194
199
None`. All of these syntaxes is supported by this function.
@@ -201,22 +206,25 @@ def _is_optional_type(dtype: TypeAnnotation) -> bool:
201
206
False otherwise.
202
207
203
208
Raises:
204
- TypeError: If the input is not a `type` .
209
+ TypeError: If the input is not a valid `TypeAnnotation` type (see above) .
205
210
"""
206
211
if not isinstance (dtype , TYPE_ANNOTATION_TYPES ):
207
- raise TypeError (f"Expected ` type` , got { type (dtype )} : { dtype } " )
212
+ raise TypeError (f"Expected type annotation , got { type (dtype )} : { dtype } " )
208
213
209
214
origin = get_origin (dtype )
210
215
args = get_args (dtype )
211
216
212
217
# Optional[T] has `typing.Union` as its origin, but PEP604 syntax (e.g. `int | None`) has
213
218
# `types.UnionType` as its origin.
214
- return (origin is Union or origin is UnionType ) and len (args ) == 2 and type (None ) in args
219
+ return (
220
+ (origin is Union or origin is UnionType )
221
+ and len (args ) == 2
222
+ and type (None ) in args
223
+ )
215
224
216
225
217
226
def _unpack_optional_type (dtype : TypeAnnotation ) -> type :
218
- """
219
- Given a type of `Optional[T]`, return `T`.
227
+ """Given a type of `Optional[T]`, return `T`.
220
228
221
229
Args:
222
230
dtype: A type of `Optional[T]`, `T | None`, or `Union[T, None]`.
@@ -225,14 +233,14 @@ def _unpack_optional_type(dtype: TypeAnnotation) -> type:
225
233
The type `T`.
226
234
227
235
Raises:
228
- TypeError: If the input is not a `type` .
236
+ TypeError: If the input is not a valid `TypeAnnotation` type (see above) .
229
237
ValueError: If the input type is not `Optional[T]`.
230
238
"""
231
239
if not isinstance (dtype , TYPE_ANNOTATION_TYPES ):
232
- raise TypeError (f"Expected ` type` , got { type (dtype )} : { dtype } " )
240
+ raise TypeError (f"Expected type annotation , got { type (dtype )} : { dtype } " )
233
241
234
242
if not _is_optional_type (dtype ):
235
- raise ValueError (f"Expected Optional[T], got { type (dtype )} : { dtype } " )
243
+ raise ValueError (f"Expected ` Optional[T]` , got { type (dtype )} : { dtype } " )
236
244
237
245
args = get_args (dtype )
238
246
@@ -245,26 +253,27 @@ def _unpack_optional_type(dtype: TypeAnnotation) -> type:
245
253
return base_type
246
254
247
255
256
+ # NB: `inspect.Parameter.annotation` is typed as `Any`, so here we narrow the type.
248
257
def _is_type_annotation (annotation : Any ) -> TypeGuard [TypeAnnotation ]:
249
- """
250
- Check if the annotation on an `inspect.Parameter` instance is a type annotation.
258
+ """Check if the annotation on an `inspect.Parameter` instance is a type annotation.
251
259
252
260
If the corresponding parameter **did not** have a type annotation, `annotation` is set to the
253
- special class variable `Parameter.empty`.
254
-
255
- NB: `Parameter.empty` itself is a subclass of `type`
256
- Otherwise, the annotation is assumed to be a type.
261
+ special class variable `inspect.Parameter.empty`. Otherwise, the annotation should be a valid
262
+ type annotation.
257
263
258
264
Args:
259
265
annotation: The annotation on an `inspect.Parameter` instance.
260
266
261
267
Returns:
262
- True if the annotation is not `Parameter.empty`.
268
+ True if the type annotation is not `inspect. Parameter.empty`.
263
269
False otherwise.
264
270
265
271
Raises:
266
- TypeError: If the annotation is neither a type nor `Parameter.empty`.
272
+ TypeError: If the annotation is neither a valid `TypeAnnotation` type (see above) nor
273
+ `inspect.Parameter.empty`.
267
274
"""
275
+ # NB: `inspect.Parameter.empty` is a subclass of `type`, so this check passes for unannotated
276
+ # parameters.
268
277
if not isinstance (annotation , TYPE_ANNOTATION_TYPES ):
269
278
raise TypeError (f"Annotation must be a type, not { type (annotation ).__name__ } " )
270
279
0 commit comments