• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

chanzuckerberg / miniwdl / 19203665972

09 Nov 2025 05:01AM UTC coverage: 95.191% (-0.02%) from 95.214%
19203665972

Pull #820

github

web-flow
Merge 0b5f0eb52 into beaa9a968
Pull Request #820: [WDL 1.2] relax struct-to-struct type coercion rule

7 of 7 new or added lines in 1 file covered. (100.0%)

3 existing lines in 2 files now uncovered.

7483 of 7861 relevant lines covered (95.19%)

0.95 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.76
/WDL/Error.py
1
from typing import (
1✔
2
    List,
3
    Optional,
4
    Union,
5
    Iterable,
6
    Generator,
7
    Callable,
8
    Any,
9
    Dict,
10
    NamedTuple,
11
)
12
from functools import total_ordering
1✔
13
from contextlib import contextmanager
1✔
14

15
from . import Type
1✔
16

17

18
class SourcePosition(
1✔
19
    NamedTuple(
20
        "SourcePosition",
21
        [
22
            ("uri", str),
23
            ("abspath", str),
24
            ("line", int),
25
            ("column", int),
26
            ("end_line", int),
27
            ("end_column", int),
28
        ],
29
    )
30
):
31
    """
32
    Source position attached to AST nodes and exceptions; NamedTuple of ``uri`` the filename/URI
33
    passed to :func:`WDL.load` or a WDL import statement, which may be relative; ``abspath`` the
34
    absolute filename/URI; and one-based int positions ``line`` ``end_line`` ``column``
35
    ``end_column``
36
    """
37

38

39
class SyntaxError(Exception):
1✔
40
    """Failure to lex/parse a WDL document"""
41

42
    pos: SourcePosition
1✔
43
    wdl_version: str
1✔
44
    declared_wdl_version: Optional[str]
1✔
45

46
    def __init__(
1✔
47
        self, pos: SourcePosition, msg: str, wdl_version: str, declared_wdl_version: Optional[str]
48
    ) -> None:
49
        super().__init__(msg)
1✔
50
        self.pos = pos
1✔
51
        self.wdl_version = wdl_version
1✔
52
        self.declared_wdl_version = declared_wdl_version
1✔
53

54

55
class _BadCharacterEncoding(Exception):
1✔
56
    """"""
57

58
    # Invalid escape sequence in a string literal; this is used internally, eventually resurfaced
59
    # as a SyntaxError.
60
    pos: SourcePosition
1✔
61

62
    def __init__(self, pos: SourcePosition):
1✔
63
        self.pos = pos
1✔
64

65

66
class ImportError(Exception):
1✔
67
    """Failure to open/retrieve an imported WDL document
68

69
    The ``__cause__`` attribute may hold the inner error object."""
70

71
    pos: SourcePosition
1✔
72

73
    def __init__(self, pos: SourcePosition, import_uri: str, message: Optional[str] = None) -> None:
1✔
74
        msg = "Failed to import " + import_uri
1✔
75
        if message:
1✔
76
            msg = msg + ", " + message
×
77
        super().__init__(msg)
1✔
78
        self.pos = pos
1✔
79

80

81
@total_ordering
1✔
82
class SourceNode:
1✔
83
    """Base class for an AST node, recording the source position"""
84

85
    pos: SourcePosition
1✔
86
    """
×
87
    :type: SourcePosition
88

89
    Source position for this AST node
90
    """
91

92
    parent: Optional["SourceNode"] = None
1✔
93
    """
×
94
    :type: Optional[SourceNode]
95

96
    Parent node in the AST, if any
97
    """
98

99
    def __init__(self, pos: SourcePosition) -> None:
1✔
100
        self.pos = pos
1✔
101

102
    def __lt__(self, rhs: "SourceNode") -> bool:
1✔
103
        return isinstance(rhs, SourceNode) and (
×
104
            self.pos.abspath,
105
            self.pos.line,
106
            self.pos.column,
107
            self.pos.end_line,
108
            self.pos.end_column,
109
        ) < (
110
            rhs.pos.abspath,
111
            rhs.pos.line,
112
            rhs.pos.column,
113
            rhs.pos.end_line,
114
            rhs.pos.end_column,
115
        )
116

117
    def __eq__(self, rhs: Any) -> bool:
1✔
118
        return isinstance(rhs, SourceNode) and self.pos == rhs.pos
1✔
119

120
    @property
1✔
121
    def children(self) -> Iterable["SourceNode"]:
1✔
122
        """
123
        :type: Iterable[SourceNode]
124

125
        Yield all child nodes
126
        """
127
        return []
1✔
128

129

130
class ValidationError(Exception):
1✔
131
    """
132
    Base class for a WDL validation error (when the document loads and parses, but fails typechecking or other static
133
    validity tests)
134
    """
135

136
    pos: SourcePosition
1✔
137
    """:type: SourcePosition"""
×
138

139
    node: Optional[SourceNode] = None
1✔
140
    """:type: Optional[SourceNode]"""
×
141

142
    source_text: Optional[str] = None
1✔
143
    """:type: Optional[str]
×
144

145
    The complete source text of the WDL document (if available)"""
146

147
    declared_wdl_version: Optional[str] = None
1✔
148

149
    def __init__(self, node: Union[SourceNode, SourcePosition], message: str) -> None:
1✔
150
        if isinstance(node, SourceNode):
1✔
151
            self.node = node
1✔
152
            self.pos = node.pos
1✔
153
        else:
154
            self.pos = node
1✔
155
        super().__init__(message)
1✔
156

157

158
class InvalidType(ValidationError):
1✔
159
    pass
1✔
160

161

162
class IndeterminateType(ValidationError):
1✔
163
    pass
1✔
164

165

166
class NoSuchTask(ValidationError):
1✔
167
    def __init__(self, node: Union[SourceNode, SourcePosition], name: str) -> None:
1✔
168
        super().__init__(node, "No such task/workflow: " + name)
1✔
169

170

171
class NoSuchCall(ValidationError):
1✔
172
    def __init__(self, node: Union[SourceNode, SourcePosition], name: str) -> None:
1✔
173
        super().__init__(node, "No such call in this workflow: " + name)
1✔
174

175

176
class NoSuchFunction(ValidationError):
1✔
177
    def __init__(self, node: Union[SourceNode, SourcePosition], name: str) -> None:
1✔
178
        super().__init__(node, "No such function: " + name)
1✔
179

180

181
class WrongArity(ValidationError):
1✔
182
    def __init__(self, node: Union[SourceNode, SourcePosition], expected: int) -> None:
1✔
183
        # avoiding circular dep:
184
        # assert isinstance(node, WDL.Expr.Apply)
185
        msg = "{} expects {} argument(s)".format(getattr(node, "function_name"), expected)
1✔
186
        super().__init__(node, msg)
1✔
187

188

189
class NotAnArray(ValidationError):
1✔
190
    def __init__(self, node: Union[SourceNode, SourcePosition]) -> None:
1✔
191
        super().__init__(node, "Not an array")
1✔
192

193

194
class NoSuchMember(ValidationError):
1✔
195
    def __init__(self, node: Union[SourceNode, SourcePosition], member: str) -> None:
1✔
196
        super().__init__(node, "No such member '{}'".format(member))
1✔
197

198

199
class StaticTypeMismatch(ValidationError):
1✔
200
    message: str
1✔
201

202
    def __init__(
1✔
203
        self, node: SourceNode, expected: Type.Base, actual: Type.Base, message: str = ""
204
    ) -> None:
205
        self.expected = expected
1✔
206
        self.actual = actual
1✔
207
        self.message = message
1✔
208
        super().__init__(node, message)
1✔
209

210
    def __str__(self) -> str:
1✔
211
        msg = f"Expected {self.expected} instead of {self.actual}"
1✔
212
        if self.message:
1✔
213
            msg += "; " + self.message
1✔
UNCOV
214
        elif isinstance(self.expected, Type.Int) and isinstance(self.actual, Type.Float):
×
215
            msg += "; perhaps try floor() or round()"
×
UNCOV
216
        elif str(self.actual).replace("?", "") == str(self.expected):
×
217
            msg += (
×
218
                " -- to coerce T? X into T, try select_first([X,defaultValue])"
219
                " or select_first([X]) (which might fail at runtime);"
220
                " to coerce Array[T?] X into Array[T], try select_all(X)"
221
            )
222
        return msg
1✔
223

224

225
class IncompatibleOperand(ValidationError):
1✔
226
    def __init__(self, node: SourceNode, message: str) -> None:
1✔
227
        super().__init__(node, message)
1✔
228

229

230
class UnknownIdentifier(ValidationError):
1✔
231
    def __init__(self, node: SourceNode, message: Optional[str] = None) -> None:
1✔
232
        # avoiding circular dep:
233
        # assert isinstance(node, WDL.Expr.Ident)
234
        if not message:
1✔
235
            message = "Unknown identifier " + str(node)
1✔
236
        super().__init__(node, message)
1✔
237

238

239
class NoSuchInput(ValidationError):
1✔
240
    def __init__(self, node: SourceNode, name: str) -> None:
1✔
241
        super().__init__(node, "No such input " + name)
1✔
242

243

244
class UncallableWorkflow(ValidationError):
1✔
245
    def __init__(self, node: SourceNode, name: str) -> None:
1✔
246
        super().__init__(
1✔
247
            node,
248
            (
249
                "Cannot call subworkflow {} because its own calls have missing required inputs, "
250
                "and/or it lacks an output section"
251
            ).format(name),
252
        )
253

254

255
class MultipleDefinitions(ValidationError):
1✔
256
    pass
1✔
257

258

259
class StrayInputDeclaration(ValidationError):
1✔
260
    pass
1✔
261

262

263
class CircularDependencies(ValidationError):
1✔
264
    def __init__(self, node: SourceNode) -> None:
1✔
265
        msg = "circular dependencies"
1✔
266
        nm = next(
1✔
267
            (getattr(node, attr) for attr in ("name", "workflow_node_id") if hasattr(node, attr)),
268
            None,
269
        )
270
        if nm:
1✔
271
            nm += " involving " + nm
1✔
272
        super().__init__(node, msg)
1✔
273

274

275
class MultipleValidationErrors(Exception):
1✔
276
    """Propagates several validation/typechecking errors"""
277

278
    exceptions: List[ValidationError]
1✔
279
    """:type: List[ValidationError]"""
×
280

281
    source_text: Optional[str] = None
1✔
282

283
    declared_wdl_version: Optional[str] = None
1✔
284

285
    def __init__(
1✔
286
        self, *exceptions: List[Union[ValidationError, "MultipleValidationErrors"]]
287
    ) -> None:
288
        super().__init__()
1✔
289
        self.exceptions = []
1✔
290
        for exn in exceptions:
1✔
291
            if isinstance(exn, ValidationError):
1✔
292
                self.exceptions.append(exn)
1✔
293
            elif isinstance(exn, MultipleValidationErrors):
1✔
294
                self.exceptions.extend(exn.exceptions)
1✔
295
            else:
296
                assert False
×
297
        assert self.exceptions
1✔
298
        self.exceptions = sorted(self.exceptions, key=lambda exn: getattr(exn, "pos"))
1✔
299

300

301
class _MultiContext:
1✔
302
    """"""
303

304
    _exceptions: List[Union[ValidationError, MultipleValidationErrors]]
1✔
305

306
    def __init__(self) -> None:
1✔
307
        self._exceptions = []
1✔
308

309
    def try1(self, fn: Callable[[], Any]) -> Optional[Any]:
1✔
310
        try:
1✔
311
            return fn()
1✔
312
        except (ValidationError, MultipleValidationErrors) as exn:
1✔
313
            self._exceptions.append(exn)
1✔
314
            return None
1✔
315

316
    def append(self, exn: Union[ValidationError, MultipleValidationErrors]) -> None:
1✔
317
        self._exceptions.append(exn)
1✔
318

319
    def maybe_raise(self) -> None:
1✔
320
        if len(self._exceptions) == 1:
1✔
321
            raise self._exceptions[0]
1✔
322
        if self._exceptions:
1✔
323
            raise MultipleValidationErrors(*self._exceptions) from self._exceptions[0]  # type: ignore
1✔
324

325

326
@contextmanager
1✔
327
def multi_context() -> Generator[_MultiContext, None, None]:
1✔
328
    """"""
329
    # Context manager to assist with catching and propagating multiple
330
    # validation/typechecking errors
331
    #
332
    # with WDL.Error.multi_context() as errors:
333
    #
334
    #    result = errors.try1(lambda: perform_validation())
335
    #    # Returns the result of invoking the lambda. If the lambda invocation
336
    #    # raises WDL.Error.ValidationError or
337
    #    # WDL.Error.MultipleValidationErrors, records the error and returns
338
    #    # None. (Other exceptions would halt execution and propagate
339
    #    # normally.)
340
    #
341
    #    errors.append(WDL.Error.NullValue())
342
    #    # errors.append() manually records one error.
343
    #
344
    # When the context closes, any exceptions recorded with errors.try1() or
345
    # errors.append() are raised at that point. Note that any exception raised
346
    # outside of errors.try1() will exit the context immediately and discard
347
    # any previously-recorded errors.
348
    #
349
    # Lastly, you can call errors.maybe_raise() to immediately propagate any
350
    # exceptions recorded so far, or if none, proceed with the remainder of
351
    # the context body.
352
    ctx = _MultiContext()
1✔
353
    yield ctx
1✔
354
    ctx.maybe_raise()
1✔
355

356

357
class RuntimeError(Exception):
1✔
358
    more_info: Dict[str, Any]
1✔
359
    """
×
360
    Backend-specific information about an error (for example, pointer to a centralized log system)
361
    """
362

363
    def __init__(self, *args, more_info: Optional[Dict[str, Any]] = None, **kwargs) -> None:
1✔
364
        super().__init__(*args, **kwargs)
1✔
365
        self.more_info = more_info if more_info else {}
1✔
366

367

368
class EvalError(RuntimeError):
1✔
369
    """Error evaluating a WDL expression or declaration"""
370

371
    pos: SourcePosition
1✔
372
    """:type: SourcePosition"""
×
373

374
    node: Optional[SourceNode] = None
1✔
375
    """:type: Optional[SourceNode]"""
×
376

377
    def __init__(self, node: Union[SourceNode, SourcePosition], message: str) -> None:
1✔
378
        if isinstance(node, SourceNode):
1✔
379
            self.node = node
1✔
380
            self.pos = node.pos
1✔
381
        else:
382
            self.pos = node
×
383
        super().__init__(message)
1✔
384

385

386
class OutOfBounds(EvalError):
1✔
387
    pass
1✔
388

389

390
class EmptyArray(EvalError):
1✔
391
    def __init__(self, node: SourceNode) -> None:
1✔
392
        super().__init__(node, "Empty array for Array+ input/declaration")
1✔
393

394

395
class NullValue(EvalError):
1✔
396
    def __init__(self, node: Union[SourceNode, SourcePosition]) -> None:
1✔
397
        super().__init__(node, "Null value")
1✔
398

399

400
class InputError(RuntimeError):
1✔
401
    """Error reading an input value/file"""
402

403
    pass
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc