33import typing
44from dataclasses import is_dataclass
55from textwrap import dedent
6- from types import UnionType
76from typing import Any , Callable , Dict , Union , get_args , get_origin
87
98import click
@@ -50,7 +49,6 @@ class UnionType:
5049TYPE_ANNOTATION_TYPES = (type , typing ._GenericAlias , UnionType ) # type: ignore[attr-defined]
5150
5251
53-
5452def _generate_metadata (f : Callable ) -> LatchMetadata :
5553 signature = inspect .signature (f )
5654 metadata = LatchMetadata (f .__name__ , LatchAuthor ())
@@ -72,7 +70,7 @@ def _inject_metadata(f: Callable, metadata: LatchMetadata) -> None:
7270# so that when users call @workflow without any arguments or
7371# parentheses, the workflow still serializes as expected
7472def workflow (
75- metadata : Union [LatchMetadata , Callable ]
73+ metadata : Union [LatchMetadata , Callable ],
7674) -> Union [PythonFunctionWorkflow , Callable ]:
7775 if isinstance (metadata , Callable ):
7876 f = metadata
@@ -141,25 +139,33 @@ def decorator(f: Callable):
141139
142140
143141def _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.
147154 """
148155 annotation = parameter .annotation
149156
150157 # If the parameter did not have a type annotation, short-circuit and return False
151158 if not _is_type_annotation (annotation ):
152159 return False
153160
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 ))
157164 )
158165
159166
160167def _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.
163169
164170 Args:
165171 dtype: A type.
@@ -169,10 +175,10 @@ def _is_list_of_dataclasses_type(dtype: TypeAnnotation) -> bool:
169175 False otherwise.
170176
171177 Raises:
172- TypeError: If the input is not a `type` .
178+ TypeError: If the input is not a valid `TypeAnnotation` type (see above) .
173179 """
174180 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 } " )
176182
177183 origin = get_origin (dtype )
178184 args = get_args (dtype )
@@ -187,8 +193,7 @@ def _is_list_of_dataclasses_type(dtype: TypeAnnotation) -> bool:
187193
188194
189195def _is_optional_type (dtype : TypeAnnotation ) -> bool :
190- """
191- Check if a type is `Optional`.
196+ """Check if a type is `Optional`.
192197
193198 An optional type may be declared using three syntaxes: `Optional[T]`, `Union[T, None]`, or `T |
194199 None`. All of these syntaxes is supported by this function.
@@ -201,22 +206,25 @@ def _is_optional_type(dtype: TypeAnnotation) -> bool:
201206 False otherwise.
202207
203208 Raises:
204- TypeError: If the input is not a `type` .
209+ TypeError: If the input is not a valid `TypeAnnotation` type (see above) .
205210 """
206211 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 } " )
208213
209214 origin = get_origin (dtype )
210215 args = get_args (dtype )
211216
212217 # Optional[T] has `typing.Union` as its origin, but PEP604 syntax (e.g. `int | None`) has
213218 # `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+ )
215224
216225
217226def _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`.
220228
221229 Args:
222230 dtype: A type of `Optional[T]`, `T | None`, or `Union[T, None]`.
@@ -225,14 +233,14 @@ def _unpack_optional_type(dtype: TypeAnnotation) -> type:
225233 The type `T`.
226234
227235 Raises:
228- TypeError: If the input is not a `type` .
236+ TypeError: If the input is not a valid `TypeAnnotation` type (see above) .
229237 ValueError: If the input type is not `Optional[T]`.
230238 """
231239 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 } " )
233241
234242 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 } " )
236244
237245 args = get_args (dtype )
238246
@@ -245,26 +253,27 @@ def _unpack_optional_type(dtype: TypeAnnotation) -> type:
245253 return base_type
246254
247255
256+ # NB: `inspect.Parameter.annotation` is typed as `Any`, so here we narrow the type.
248257def _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.
251259
252260 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.
257263
258264 Args:
259265 annotation: The annotation on an `inspect.Parameter` instance.
260266
261267 Returns:
262- True if the annotation is not `Parameter.empty`.
268+ True if the type annotation is not `inspect. Parameter.empty`.
263269 False otherwise.
264270
265271 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`.
267274 """
275+ # NB: `inspect.Parameter.empty` is a subclass of `type`, so this check passes for unannotated
276+ # parameters.
268277 if not isinstance (annotation , TYPE_ANNOTATION_TYPES ):
269278 raise TypeError (f"Annotation must be a type, not { type (annotation ).__name__ } " )
270279
0 commit comments