toil 9.1.1__py3-none-any.whl → 9.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. toil/__init__.py +5 -9
  2. toil/batchSystems/abstractBatchSystem.py +23 -22
  3. toil/batchSystems/abstractGridEngineBatchSystem.py +17 -12
  4. toil/batchSystems/awsBatch.py +8 -8
  5. toil/batchSystems/cleanup_support.py +4 -4
  6. toil/batchSystems/contained_executor.py +3 -3
  7. toil/batchSystems/gridengine.py +3 -4
  8. toil/batchSystems/htcondor.py +5 -5
  9. toil/batchSystems/kubernetes.py +65 -63
  10. toil/batchSystems/local_support.py +2 -3
  11. toil/batchSystems/lsf.py +6 -7
  12. toil/batchSystems/mesos/batchSystem.py +11 -7
  13. toil/batchSystems/mesos/test/__init__.py +1 -2
  14. toil/batchSystems/options.py +9 -10
  15. toil/batchSystems/registry.py +3 -7
  16. toil/batchSystems/singleMachine.py +8 -11
  17. toil/batchSystems/slurm.py +49 -38
  18. toil/batchSystems/torque.py +3 -4
  19. toil/bus.py +36 -34
  20. toil/common.py +129 -89
  21. toil/cwl/cwltoil.py +857 -729
  22. toil/cwl/utils.py +44 -35
  23. toil/fileStores/__init__.py +3 -1
  24. toil/fileStores/abstractFileStore.py +28 -30
  25. toil/fileStores/cachingFileStore.py +8 -8
  26. toil/fileStores/nonCachingFileStore.py +10 -21
  27. toil/job.py +159 -158
  28. toil/jobStores/abstractJobStore.py +68 -69
  29. toil/jobStores/aws/jobStore.py +249 -213
  30. toil/jobStores/aws/utils.py +13 -24
  31. toil/jobStores/fileJobStore.py +28 -22
  32. toil/jobStores/googleJobStore.py +21 -17
  33. toil/jobStores/utils.py +3 -7
  34. toil/leader.py +17 -22
  35. toil/lib/accelerators.py +6 -4
  36. toil/lib/aws/__init__.py +9 -10
  37. toil/lib/aws/ami.py +33 -19
  38. toil/lib/aws/iam.py +6 -6
  39. toil/lib/aws/s3.py +259 -157
  40. toil/lib/aws/session.py +76 -76
  41. toil/lib/aws/utils.py +51 -43
  42. toil/lib/checksum.py +19 -15
  43. toil/lib/compatibility.py +3 -2
  44. toil/lib/conversions.py +45 -18
  45. toil/lib/directory.py +29 -26
  46. toil/lib/docker.py +93 -99
  47. toil/lib/dockstore.py +77 -50
  48. toil/lib/ec2.py +39 -38
  49. toil/lib/ec2nodes.py +11 -4
  50. toil/lib/exceptions.py +8 -5
  51. toil/lib/ftp_utils.py +9 -14
  52. toil/lib/generatedEC2Lists.py +161 -20
  53. toil/lib/history.py +141 -97
  54. toil/lib/history_submission.py +163 -72
  55. toil/lib/io.py +27 -17
  56. toil/lib/memoize.py +2 -1
  57. toil/lib/misc.py +15 -11
  58. toil/lib/pipes.py +40 -25
  59. toil/lib/plugins.py +12 -8
  60. toil/lib/resources.py +1 -0
  61. toil/lib/retry.py +32 -38
  62. toil/lib/threading.py +12 -12
  63. toil/lib/throttle.py +1 -2
  64. toil/lib/trs.py +113 -51
  65. toil/lib/url.py +14 -23
  66. toil/lib/web.py +7 -2
  67. toil/options/common.py +18 -15
  68. toil/options/cwl.py +2 -2
  69. toil/options/runner.py +9 -5
  70. toil/options/wdl.py +1 -3
  71. toil/provisioners/__init__.py +9 -9
  72. toil/provisioners/abstractProvisioner.py +22 -20
  73. toil/provisioners/aws/__init__.py +20 -14
  74. toil/provisioners/aws/awsProvisioner.py +10 -8
  75. toil/provisioners/clusterScaler.py +19 -18
  76. toil/provisioners/gceProvisioner.py +2 -3
  77. toil/provisioners/node.py +11 -13
  78. toil/realtimeLogger.py +4 -4
  79. toil/resource.py +5 -5
  80. toil/server/app.py +2 -2
  81. toil/server/cli/wes_cwl_runner.py +11 -11
  82. toil/server/utils.py +18 -21
  83. toil/server/wes/abstract_backend.py +9 -8
  84. toil/server/wes/amazon_wes_utils.py +3 -3
  85. toil/server/wes/tasks.py +3 -5
  86. toil/server/wes/toil_backend.py +17 -21
  87. toil/server/wsgi_app.py +3 -3
  88. toil/serviceManager.py +3 -4
  89. toil/statsAndLogging.py +12 -13
  90. toil/test/__init__.py +33 -24
  91. toil/test/batchSystems/batchSystemTest.py +12 -11
  92. toil/test/batchSystems/batch_system_plugin_test.py +3 -5
  93. toil/test/batchSystems/test_slurm.py +38 -24
  94. toil/test/cwl/conftest.py +5 -6
  95. toil/test/cwl/cwlTest.py +194 -78
  96. toil/test/cwl/download_file_uri.json +6 -0
  97. toil/test/cwl/download_file_uri_no_hostname.json +6 -0
  98. toil/test/docs/scripts/tutorial_staging.py +1 -0
  99. toil/test/jobStores/jobStoreTest.py +9 -7
  100. toil/test/lib/aws/test_iam.py +1 -3
  101. toil/test/lib/aws/test_s3.py +1 -1
  102. toil/test/lib/dockerTest.py +9 -9
  103. toil/test/lib/test_ec2.py +12 -11
  104. toil/test/lib/test_history.py +4 -4
  105. toil/test/lib/test_trs.py +16 -14
  106. toil/test/lib/test_url.py +7 -6
  107. toil/test/lib/url_plugin_test.py +12 -18
  108. toil/test/provisioners/aws/awsProvisionerTest.py +10 -8
  109. toil/test/provisioners/clusterScalerTest.py +2 -5
  110. toil/test/provisioners/clusterTest.py +1 -3
  111. toil/test/server/serverTest.py +13 -4
  112. toil/test/sort/restart_sort.py +2 -6
  113. toil/test/sort/sort.py +3 -8
  114. toil/test/src/deferredFunctionTest.py +7 -7
  115. toil/test/src/environmentTest.py +1 -2
  116. toil/test/src/fileStoreTest.py +5 -5
  117. toil/test/src/importExportFileTest.py +5 -6
  118. toil/test/src/jobServiceTest.py +22 -14
  119. toil/test/src/jobTest.py +121 -25
  120. toil/test/src/miscTests.py +5 -7
  121. toil/test/src/promisedRequirementTest.py +8 -7
  122. toil/test/src/regularLogTest.py +2 -3
  123. toil/test/src/resourceTest.py +5 -8
  124. toil/test/src/restartDAGTest.py +5 -6
  125. toil/test/src/resumabilityTest.py +2 -2
  126. toil/test/src/retainTempDirTest.py +3 -3
  127. toil/test/src/systemTest.py +3 -3
  128. toil/test/src/threadingTest.py +1 -1
  129. toil/test/src/workerTest.py +1 -2
  130. toil/test/utils/toilDebugTest.py +6 -4
  131. toil/test/utils/toilKillTest.py +1 -1
  132. toil/test/utils/utilsTest.py +15 -14
  133. toil/test/wdl/wdltoil_test.py +247 -124
  134. toil/test/wdl/wdltoil_test_kubernetes.py +2 -2
  135. toil/toilState.py +2 -3
  136. toil/utils/toilDebugFile.py +3 -8
  137. toil/utils/toilDebugJob.py +1 -2
  138. toil/utils/toilLaunchCluster.py +1 -2
  139. toil/utils/toilSshCluster.py +2 -0
  140. toil/utils/toilStats.py +19 -24
  141. toil/utils/toilStatus.py +11 -14
  142. toil/version.py +10 -10
  143. toil/wdl/wdltoil.py +313 -209
  144. toil/worker.py +18 -12
  145. {toil-9.1.1.dist-info → toil-9.2.0.dist-info}/METADATA +11 -14
  146. {toil-9.1.1.dist-info → toil-9.2.0.dist-info}/RECORD +150 -153
  147. {toil-9.1.1.dist-info → toil-9.2.0.dist-info}/WHEEL +1 -1
  148. toil/test/cwl/staging_cat.cwl +0 -27
  149. toil/test/cwl/staging_make_file.cwl +0 -25
  150. toil/test/cwl/staging_workflow.cwl +0 -43
  151. toil/test/cwl/zero_default.cwl +0 -61
  152. toil/test/utils/ABCWorkflowDebug/ABC.txt +0 -1
  153. {toil-9.1.1.dist-info → toil-9.2.0.dist-info}/entry_points.txt +0 -0
  154. {toil-9.1.1.dist-info → toil-9.2.0.dist-info}/licenses/LICENSE +0 -0
  155. {toil-9.1.1.dist-info → toil-9.2.0.dist-info}/top_level.txt +0 -0
toil/job.py CHANGED
@@ -27,36 +27,29 @@ import time
27
27
  import uuid
28
28
  from abc import ABCMeta, abstractmethod
29
29
  from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser, Namespace
30
+ from collections.abc import Callable, Iterator, Mapping, Sequence
30
31
  from contextlib import contextmanager
31
32
  from io import BytesIO
32
33
  from typing import (
33
34
  TYPE_CHECKING,
34
35
  Any,
35
- Callable,
36
- Dict,
37
- Iterator,
38
- List,
39
- Mapping,
36
+ Literal,
40
37
  NamedTuple,
41
- Optional,
42
- Sequence,
43
- Tuple,
38
+ TypedDict,
44
39
  TypeVar,
45
40
  Union,
46
41
  cast,
47
42
  overload,
48
- TypedDict,
49
- Literal,
50
43
  )
51
44
  from urllib.error import HTTPError
52
- from urllib.parse import urlsplit, unquote, urljoin
45
+ from urllib.parse import unquote, urljoin, urlsplit
53
46
 
54
47
  import dill
55
48
  from configargparse import ArgParser
56
49
 
50
+ from toil.lib.io import is_remote_url
57
51
  from toil.lib.memoize import memoize
58
52
  from toil.lib.misc import StrPath
59
- from toil.lib.io import is_remote_url
60
53
 
61
54
  if sys.version_info < (3, 11):
62
55
  from typing_extensions import NotRequired
@@ -69,19 +62,16 @@ from toil.deferred import DeferredFunction
69
62
  from toil.fileStores import FileID
70
63
  from toil.lib.compatibility import deprecated
71
64
  from toil.lib.conversions import bytes2human, human2bytes
65
+ from toil.lib.exceptions import UnimplementedURLException
72
66
  from toil.lib.expando import Expando
73
67
  from toil.lib.resources import ResourceMonitor
74
68
  from toil.resource import ModuleDescriptor
75
69
  from toil.statsAndLogging import set_logging_from_options
76
70
 
77
- from toil.lib.exceptions import UnimplementedURLException
78
-
79
71
  if TYPE_CHECKING:
80
72
  from optparse import OptionParser
81
73
 
82
- from toil.batchSystems.abstractBatchSystem import (
83
- BatchJobExitReason
84
- )
74
+ from toil.batchSystems.abstractBatchSystem import BatchJobExitReason
85
75
  from toil.fileStores.abstractFileStore import AbstractFileStore
86
76
  from toil.jobStores.abstractJobStore import AbstractJobStore
87
77
 
@@ -95,9 +85,7 @@ class JobPromiseConstraintError(RuntimeError):
95
85
  (Due to the return value not yet been hit in the topological order of the job graph.)
96
86
  """
97
87
 
98
- def __init__(
99
- self, promisingJob: "Job", recipientJob: Optional["Job"] = None
100
- ) -> None:
88
+ def __init__(self, promisingJob: Job, recipientJob: Job | None = None) -> None:
101
89
  """
102
90
  Initialize this error.
103
91
 
@@ -122,7 +110,7 @@ class JobPromiseConstraintError(RuntimeError):
122
110
 
123
111
 
124
112
  class ConflictingPredecessorError(Exception):
125
- def __init__(self, predecessor: "Job", successor: "Job") -> None:
113
+ def __init__(self, predecessor: Job, successor: Job) -> None:
126
114
  super().__init__(
127
115
  f'The given job: "{predecessor.description}" is already a predecessor of job: "{successor.description}".'
128
116
  )
@@ -140,7 +128,7 @@ class FilesDownloadedStoppingPointReached(DebugStoppingPointReached):
140
128
  """
141
129
 
142
130
  def __init__(
143
- self, message: str, host_and_job_paths: Optional[list[tuple[str, str]]] = None
131
+ self, message: str, host_and_job_paths: list[tuple[str, str]] | None = None
144
132
  ) -> None:
145
133
  super().__init__(message)
146
134
 
@@ -222,7 +210,7 @@ class AcceleratorRequirement(TypedDict):
222
210
 
223
211
 
224
212
  def parse_accelerator(
225
- spec: Union[int, str, dict[str, Union[str, int]]]
213
+ spec: int | str | dict[str, str | int],
226
214
  ) -> AcceleratorRequirement:
227
215
  """
228
216
  Parse an AcceleratorRequirement specified by user code.
@@ -375,7 +363,7 @@ def accelerator_satisfies(
375
363
 
376
364
 
377
365
  def accelerators_fully_satisfy(
378
- candidates: Optional[list[AcceleratorRequirement]],
366
+ candidates: list[AcceleratorRequirement] | None,
379
367
  requirement: AcceleratorRequirement,
380
368
  ignore: list[str] = [],
381
369
  ) -> bool:
@@ -412,7 +400,7 @@ class RequirementsDict(TypedDict):
412
400
  Where requirement values are of different types depending on the requirement.
413
401
  """
414
402
 
415
- cores: NotRequired[Union[int, float]]
403
+ cores: NotRequired[int | float]
416
404
  memory: NotRequired[int]
417
405
  disk: NotRequired[int]
418
406
  accelerators: NotRequired[list[AcceleratorRequirement]]
@@ -472,7 +460,7 @@ class Requirer:
472
460
 
473
461
  # We can have a toil.common.Config assigned to fill in default values
474
462
  # for e.g. job requirements not explicitly specified.
475
- self._config: Optional[Config] = None
463
+ self._config: Config | None = None
476
464
 
477
465
  # Save requirements, parsing and validating anything that needs parsing
478
466
  # or validating. Don't save Nones.
@@ -502,7 +490,7 @@ class Requirer:
502
490
  state["_config"] = None
503
491
  return state
504
492
 
505
- def __copy__(self) -> "Requirer":
493
+ def __copy__(self) -> Requirer:
506
494
  """Return a semantically-shallow copy of the object, for :meth:`copy.copy`."""
507
495
  # The hide-the-method-and-call-the-copy-module approach from
508
496
  # <https://stackoverflow.com/a/40484215> doesn't seem to work for
@@ -519,7 +507,7 @@ class Requirer:
519
507
 
520
508
  return clone
521
509
 
522
- def __deepcopy__(self, memo: Any) -> "Requirer":
510
+ def __deepcopy__(self, memo: Any) -> Requirer:
523
511
  """Return a semantically-deep copy of the object, for :meth:`copy.deepcopy`."""
524
512
  # We used to use <https://stackoverflow.com/a/40484215> and
525
513
  # <https://stackoverflow.com/a/71125311> but that would result in
@@ -540,7 +528,7 @@ class Requirer:
540
528
  @overload
541
529
  @staticmethod
542
530
  def _parseResource(
543
- name: Union[Literal["memory"], Literal["disks"]],
531
+ name: Literal["memory"] | Literal["disks"],
544
532
  value: ParseableIndivisibleResource,
545
533
  ) -> int: ...
546
534
 
@@ -548,7 +536,7 @@ class Requirer:
548
536
  @staticmethod
549
537
  def _parseResource(
550
538
  name: Literal["cores"], value: ParseableDivisibleResource
551
- ) -> Union[int, float]: ...
539
+ ) -> int | float: ...
552
540
 
553
541
  @overload
554
542
  @staticmethod
@@ -566,8 +554,8 @@ class Requirer:
566
554
 
567
555
  @staticmethod
568
556
  def _parseResource(
569
- name: str, value: Optional[ParseableRequirement]
570
- ) -> Optional[ParsedRequirement]:
557
+ name: str, value: ParseableRequirement | None
558
+ ) -> ParsedRequirement | None:
571
559
  """
572
560
  Parse a Toil resource requirement value and apply resource-specific type checks.
573
561
 
@@ -650,7 +638,7 @@ class Requirer:
650
638
  # Anything else we just pass along without opinons
651
639
  return cast(ParsedRequirement, value)
652
640
 
653
- def _fetchRequirement(self, requirement: str) -> Optional[ParsedRequirement]:
641
+ def _fetchRequirement(self, requirement: str) -> ParsedRequirement | None:
654
642
  """
655
643
  Get the value of the specified requirement ('blah').
656
644
 
@@ -708,7 +696,7 @@ class Requirer:
708
696
  self._requirementOverrides["memory"] = Requirer._parseResource("memory", val)
709
697
 
710
698
  @property
711
- def cores(self) -> Union[int, float]:
699
+ def cores(self) -> int | float:
712
700
  """Get the number of CPU cores required."""
713
701
  return cast(Union[int, float], self._fetchRequirement("cores"))
714
702
 
@@ -746,7 +734,7 @@ class Requirer:
746
734
  "accelerators", val
747
735
  )
748
736
 
749
- def scale(self, requirement: str, factor: float) -> "Requirer":
737
+ def scale(self, requirement: str, factor: float) -> Requirer:
750
738
  """
751
739
  Return a copy of this object with the given requirement scaled up or down.
752
740
 
@@ -813,14 +801,15 @@ class JobDescription(Requirer):
813
801
  Subclassed into variants for checkpoint jobs and service jobs that have
814
802
  their specific parameters.
815
803
  """
804
+
816
805
  def __init__(
817
806
  self,
818
- requirements: Mapping[str, Union[int, str, float, bool, list]],
807
+ requirements: Mapping[str, int | str | float | bool | list],
819
808
  jobName: str,
820
- unitName: Optional[str] = "",
821
- displayName: Optional[str] = "",
822
- local: Optional[bool] = None,
823
- files: Optional[set[FileID]] = None,
809
+ unitName: str | None = "",
810
+ displayName: str | None = "",
811
+ local: bool | None = None,
812
+ files: set[FileID] | None = None,
824
813
  ) -> None:
825
814
  """
826
815
  Create a new JobDescription.
@@ -852,7 +841,7 @@ class JobDescription(Requirer):
852
841
  self.local: bool = local or False
853
842
 
854
843
  # Save names, making sure they are strings and not e.g. bytes or None.
855
- def makeString(x: Union[str, bytes, None]) -> str:
844
+ def makeString(x: str | bytes | None) -> str:
856
845
  if isinstance(x, bytes):
857
846
  return x.decode("utf-8", errors="replace")
858
847
  if x is None:
@@ -866,12 +855,12 @@ class JobDescription(Requirer):
866
855
  # Set properties that are not fully filled in on creation.
867
856
 
868
857
  # ID of this job description in the JobStore.
869
- self.jobStoreID: Union[str, TemporaryID] = TemporaryID()
858
+ self.jobStoreID: str | TemporaryID = TemporaryID()
870
859
 
871
860
  # Information that encodes how to find the Job body data that this
872
861
  # JobDescription describes, and the module(s) needed to unpickle it.
873
862
  # None if no body needs to run.
874
- self._body: Optional[JobBodyReference] = None
863
+ self._body: JobBodyReference | None = None
875
864
 
876
865
  # Set scheduling properties that the leader read to think about scheduling.
877
866
 
@@ -1091,7 +1080,7 @@ class JobDescription(Requirer):
1091
1080
  self._body.module_string
1092
1081
  )
1093
1082
 
1094
- def nextSuccessors(self) -> Optional[set[str]]:
1083
+ def nextSuccessors(self) -> set[str] | None:
1095
1084
  """
1096
1085
  Return the collection of job IDs for the successors of this job that are ready to run.
1097
1086
 
@@ -1150,7 +1139,7 @@ class JobDescription(Requirer):
1150
1139
  if k not in toRemove
1151
1140
  }
1152
1141
 
1153
- def clear_nonexistent_dependents(self, job_store: "AbstractJobStore") -> None:
1142
+ def clear_nonexistent_dependents(self, job_store: AbstractJobStore) -> None:
1154
1143
  """
1155
1144
  Remove all references to child, follow-on, and associated service jobs that do not exist.
1156
1145
 
@@ -1178,7 +1167,7 @@ class JobDescription(Requirer):
1178
1167
  not self.has_body() and next(self.successorsAndServiceHosts(), None) is None
1179
1168
  )
1180
1169
 
1181
- def replace(self, other: "JobDescription") -> None:
1170
+ def replace(self, other: JobDescription) -> None:
1182
1171
  """
1183
1172
  Take on the ID of another JobDescription, retaining our own state and type.
1184
1173
 
@@ -1238,7 +1227,7 @@ class JobDescription(Requirer):
1238
1227
  self._job_version = other._job_version
1239
1228
  self._job_version_writer = os.getpid()
1240
1229
 
1241
- def assert_is_not_newer_than(self, other: "JobDescription") -> None:
1230
+ def assert_is_not_newer_than(self, other: JobDescription) -> None:
1242
1231
  """
1243
1232
  Make sure this JobDescription is not newer than a prospective new version of the JobDescription.
1244
1233
  """
@@ -1247,7 +1236,7 @@ class JobDescription(Requirer):
1247
1236
  f"Cannot replace {self} from PID {self._job_version_writer} with older version {other} from PID {other._job_version_writer}"
1248
1237
  )
1249
1238
 
1250
- def is_updated_by(self, other: "JobDescription") -> bool:
1239
+ def is_updated_by(self, other: JobDescription) -> bool:
1251
1240
  """
1252
1241
  Return True if the passed JobDescription is a distinct, newer version of this one.
1253
1242
  """
@@ -1341,7 +1330,7 @@ class JobDescription(Requirer):
1341
1330
  """Notify the JobDescription that a predecessor has been added to its Job."""
1342
1331
  self.predecessorNumber += 1
1343
1332
 
1344
- def onRegistration(self, jobStore: "AbstractJobStore") -> None:
1333
+ def onRegistration(self, jobStore: AbstractJobStore) -> None:
1345
1334
  """
1346
1335
  Perform setup work that requires the JobStore.
1347
1336
 
@@ -1355,8 +1344,8 @@ class JobDescription(Requirer):
1355
1344
 
1356
1345
  def setupJobAfterFailure(
1357
1346
  self,
1358
- exit_status: Optional[int] = None,
1359
- exit_reason: Optional["BatchJobExitReason"] = None,
1347
+ exit_status: int | None = None,
1348
+ exit_reason: BatchJobExitReason | None = None,
1360
1349
  ) -> None:
1361
1350
  """
1362
1351
  Configure job after a failure.
@@ -1519,15 +1508,15 @@ class ServiceJobDescription(JobDescription):
1519
1508
 
1520
1509
  # An empty file in the jobStore which when deleted is used to signal that the service
1521
1510
  # should cease.
1522
- self.terminateJobStoreID: Optional[str] = None
1511
+ self.terminateJobStoreID: str | None = None
1523
1512
 
1524
1513
  # Similarly a empty file which when deleted is used to signal that the service is
1525
1514
  # established
1526
- self.startJobStoreID: Optional[str] = None
1515
+ self.startJobStoreID: str | None = None
1527
1516
 
1528
1517
  # An empty file in the jobStore which when deleted is used to signal that the service
1529
1518
  # should terminate signaling an error.
1530
- self.errorJobStoreID: Optional[str] = None
1519
+ self.errorJobStoreID: str | None = None
1531
1520
 
1532
1521
  def onRegistration(self, jobStore):
1533
1522
  """
@@ -1555,7 +1544,7 @@ class CheckpointJobDescription(JobDescription):
1555
1544
  # Set checkpoint-specific properties
1556
1545
 
1557
1546
  # None, or a copy of the original self._body used to reestablish the job after failure.
1558
- self.checkpoint: Optional[JobBodyReference] = None
1547
+ self.checkpoint: JobBodyReference | None = None
1559
1548
 
1560
1549
  # Files that can not be deleted until the job and its successors have completed
1561
1550
  self.checkpointFilesToDelete = []
@@ -1577,7 +1566,7 @@ class CheckpointJobDescription(JobDescription):
1577
1566
  raise RuntimeError(f"Cannot restore an empty checkpoint for a job {self}")
1578
1567
  self._body = self.checkpoint
1579
1568
 
1580
- def restartCheckpoint(self, jobStore: "AbstractJobStore") -> list[str]:
1569
+ def restartCheckpoint(self, jobStore: AbstractJobStore) -> list[str]:
1581
1570
  """
1582
1571
  Restart a checkpoint after the total failure of jobs in its subtree.
1583
1572
 
@@ -1648,18 +1637,18 @@ class Job:
1648
1637
 
1649
1638
  def __init__(
1650
1639
  self,
1651
- memory: Optional[ParseableIndivisibleResource] = None,
1652
- cores: Optional[ParseableDivisibleResource] = None,
1653
- disk: Optional[ParseableIndivisibleResource] = None,
1654
- accelerators: Optional[ParseableAcceleratorRequirement] = None,
1655
- preemptible: Optional[ParseableFlag] = None,
1656
- preemptable: Optional[ParseableFlag] = None,
1657
- unitName: Optional[str] = "",
1658
- checkpoint: Optional[bool] = False,
1659
- displayName: Optional[str] = "",
1660
- descriptionClass: Optional[type] = None,
1661
- local: Optional[bool] = None,
1662
- files: Optional[set[FileID]] = None,
1640
+ memory: ParseableIndivisibleResource | None = None,
1641
+ cores: ParseableDivisibleResource | None = None,
1642
+ disk: ParseableIndivisibleResource | None = None,
1643
+ accelerators: ParseableAcceleratorRequirement | None = None,
1644
+ preemptible: ParseableFlag | None = None,
1645
+ preemptable: ParseableFlag | None = None,
1646
+ unitName: str | None = "",
1647
+ checkpoint: bool | None = False,
1648
+ displayName: str | None = "",
1649
+ descriptionClass: type | None = None,
1650
+ local: bool | None = None,
1651
+ files: set[FileID] | None = None,
1663
1652
  ) -> None:
1664
1653
  """
1665
1654
  Job initializer.
@@ -1792,7 +1781,7 @@ class Job:
1792
1781
  )
1793
1782
 
1794
1783
  @property
1795
- def jobStoreID(self) -> Union[str, TemporaryID]:
1784
+ def jobStoreID(self) -> str | TemporaryID:
1796
1785
  """Get the ID of this Job."""
1797
1786
  # This is managed by the JobDescription.
1798
1787
  return self._description.jobStoreID
@@ -1824,7 +1813,7 @@ class Job:
1824
1813
  self.description.memory = val
1825
1814
 
1826
1815
  @property
1827
- def cores(self) -> Union[int, float]:
1816
+ def cores(self) -> int | float:
1828
1817
  """The number of CPU cores required."""
1829
1818
  return self.description.cores
1830
1819
 
@@ -1883,7 +1872,7 @@ class Job:
1883
1872
  """
1884
1873
  self.description.assignConfig(config)
1885
1874
 
1886
- def run(self, fileStore: "AbstractFileStore") -> Any:
1875
+ def run(self, fileStore: AbstractFileStore) -> Any:
1887
1876
  """
1888
1877
  Override this function to perform work and dynamically create successor jobs.
1889
1878
 
@@ -1894,7 +1883,7 @@ class Job:
1894
1883
  :func:`toil.job.Job.rv`.
1895
1884
  """
1896
1885
 
1897
- def _jobGraphsJoined(self, other: "Job") -> None:
1886
+ def _jobGraphsJoined(self, other: Job) -> None:
1898
1887
  """
1899
1888
  Called whenever the job graphs of this job and the other job may have been merged into one connected component.
1900
1889
 
@@ -1929,7 +1918,7 @@ class Job:
1929
1918
  # Point all their jobs at the new combined registry
1930
1919
  job._registry = self._registry
1931
1920
 
1932
- def addChild(self, childJob: "Job") -> "Job":
1921
+ def addChild(self, childJob: Job) -> Job:
1933
1922
  """
1934
1923
  Add a childJob to be run as child of this job.
1935
1924
 
@@ -1954,7 +1943,7 @@ class Job:
1954
1943
 
1955
1944
  return childJob
1956
1945
 
1957
- def hasChild(self, childJob: "Job") -> bool:
1946
+ def hasChild(self, childJob: Job) -> bool:
1958
1947
  """
1959
1948
  Check if childJob is already a child of this job.
1960
1949
 
@@ -1962,7 +1951,7 @@ class Job:
1962
1951
  """
1963
1952
  return self._description.hasChild(childJob.jobStoreID)
1964
1953
 
1965
- def addFollowOn(self, followOnJob: "Job") -> "Job":
1954
+ def addFollowOn(self, followOnJob: Job) -> Job:
1966
1955
  """
1967
1956
  Add a follow-on job.
1968
1957
 
@@ -1986,11 +1975,11 @@ class Job:
1986
1975
 
1987
1976
  return followOnJob
1988
1977
 
1989
- def hasPredecessor(self, job: "Job") -> bool:
1978
+ def hasPredecessor(self, job: Job) -> bool:
1990
1979
  """Check if a given job is already a predecessor of this job."""
1991
1980
  return job in self._directPredecessors
1992
1981
 
1993
- def hasFollowOn(self, followOnJob: "Job") -> bool:
1982
+ def hasFollowOn(self, followOnJob: Job) -> bool:
1994
1983
  """
1995
1984
  Check if given job is already a follow-on of this job.
1996
1985
 
@@ -1999,8 +1988,8 @@ class Job:
1999
1988
  return self._description.hasChild(followOnJob.jobStoreID)
2000
1989
 
2001
1990
  def addService(
2002
- self, service: "Job.Service", parentService: Optional["Job.Service"] = None
2003
- ) -> "Promise":
1991
+ self, service: Job.Service, parentService: Job.Service | None = None
1992
+ ) -> Promise:
2004
1993
  """
2005
1994
  Add a service.
2006
1995
 
@@ -2046,7 +2035,7 @@ class Job:
2046
2035
  # Return the promise for the service's startup result
2047
2036
  return hostingJob.rv()
2048
2037
 
2049
- def hasService(self, service: "Job.Service") -> bool:
2038
+ def hasService(self, service: Job.Service) -> bool:
2050
2039
  """Return True if the given Service is a service of this job, and False otherwise."""
2051
2040
  return service.hostID is None or self._description.hasServiceHostJob(
2052
2041
  service.hostID
@@ -2054,7 +2043,7 @@ class Job:
2054
2043
 
2055
2044
  # Convenience functions for creating jobs
2056
2045
 
2057
- def addChildFn(self, fn: Callable, *args, **kwargs) -> "FunctionWrappingJob":
2046
+ def addChildFn(self, fn: Callable, *args, **kwargs) -> FunctionWrappingJob:
2058
2047
  """
2059
2048
  Add a function as a child job.
2060
2049
 
@@ -2070,7 +2059,7 @@ class Job:
2070
2059
  else:
2071
2060
  return self.addChild(FunctionWrappingJob(fn, *args, **kwargs))
2072
2061
 
2073
- def addFollowOnFn(self, fn: Callable, *args, **kwargs) -> "FunctionWrappingJob":
2062
+ def addFollowOnFn(self, fn: Callable, *args, **kwargs) -> FunctionWrappingJob:
2074
2063
  """
2075
2064
  Add a function as a follow-on job.
2076
2065
 
@@ -2086,7 +2075,7 @@ class Job:
2086
2075
  else:
2087
2076
  return self.addFollowOn(FunctionWrappingJob(fn, *args, **kwargs))
2088
2077
 
2089
- def addChildJobFn(self, fn: Callable, *args, **kwargs) -> "FunctionWrappingJob":
2078
+ def addChildJobFn(self, fn: Callable, *args, **kwargs) -> FunctionWrappingJob:
2090
2079
  """
2091
2080
  Add a job function as a child job.
2092
2081
 
@@ -2104,7 +2093,7 @@ class Job:
2104
2093
  else:
2105
2094
  return self.addChild(JobFunctionWrappingJob(fn, *args, **kwargs))
2106
2095
 
2107
- def addFollowOnJobFn(self, fn: Callable, *args, **kwargs) -> "FunctionWrappingJob":
2096
+ def addFollowOnJobFn(self, fn: Callable, *args, **kwargs) -> FunctionWrappingJob:
2108
2097
  """
2109
2098
  Add a follow-on job function.
2110
2099
 
@@ -2139,7 +2128,7 @@ class Job:
2139
2128
  self._fileStore.log_to_leader(text, level)
2140
2129
 
2141
2130
  @staticmethod
2142
- def wrapFn(fn, *args, **kwargs) -> "FunctionWrappingJob":
2131
+ def wrapFn(fn, *args, **kwargs) -> FunctionWrappingJob:
2143
2132
  """
2144
2133
  Makes a Job out of a function.
2145
2134
 
@@ -2156,7 +2145,7 @@ class Job:
2156
2145
  return FunctionWrappingJob(fn, *args, **kwargs)
2157
2146
 
2158
2147
  @staticmethod
2159
- def wrapJobFn(fn, *args, **kwargs) -> "JobFunctionWrappingJob":
2148
+ def wrapJobFn(fn, *args, **kwargs) -> JobFunctionWrappingJob:
2160
2149
  """
2161
2150
  Makes a Job out of a job function.
2162
2151
 
@@ -2172,7 +2161,7 @@ class Job:
2172
2161
  else:
2173
2162
  return JobFunctionWrappingJob(fn, *args, **kwargs)
2174
2163
 
2175
- def encapsulate(self, name: Optional[str] = None) -> "EncapsulatedJob":
2164
+ def encapsulate(self, name: str | None = None) -> EncapsulatedJob:
2176
2165
  """
2177
2166
  Encapsulates the job, see :class:`toil.job.EncapsulatedJob`.
2178
2167
  Convenience function for constructor of :class:`toil.job.EncapsulatedJob`.
@@ -2188,7 +2177,7 @@ class Job:
2188
2177
  # job run functions
2189
2178
  ####################################################
2190
2179
 
2191
- def rv(self, *path) -> "Promise":
2180
+ def rv(self, *path) -> Promise:
2192
2181
  """
2193
2182
  Create a *promise* (:class:`toil.job.Promise`).
2194
2183
 
@@ -2230,7 +2219,7 @@ class Job:
2230
2219
  self._rvs[path].append(jobStoreFileID)
2231
2220
  return self._promiseJobStore.config.jobStore, jobStoreFileID
2232
2221
 
2233
- def prepareForPromiseRegistration(self, jobStore: "AbstractJobStore") -> None:
2222
+ def prepareForPromiseRegistration(self, jobStore: AbstractJobStore) -> None:
2234
2223
  """
2235
2224
  Set up to allow this job's promises to register themselves.
2236
2225
 
@@ -2275,7 +2264,7 @@ class Job:
2275
2264
  self.checkJobGraphAcylic()
2276
2265
  self.checkNewCheckpointsAreLeafVertices()
2277
2266
 
2278
- def getRootJobs(self) -> set["Job"]:
2267
+ def getRootJobs(self) -> set[Job]:
2279
2268
  """
2280
2269
  Return the set of root job objects that contain this job.
2281
2270
 
@@ -2363,7 +2352,7 @@ class Job:
2363
2352
  )
2364
2353
 
2365
2354
  @staticmethod
2366
- def _getImpliedEdges(roots) -> dict["Job", list["Job"]]:
2355
+ def _getImpliedEdges(roots) -> dict[Job, list[Job]]:
2367
2356
  """
2368
2357
  Gets the set of implied edges (between children and follow-ons of a common job).
2369
2358
 
@@ -2501,7 +2490,7 @@ class Job:
2501
2490
 
2502
2491
  @staticmethod
2503
2492
  def getDefaultOptions(
2504
- jobStore: Optional[StrPath] = None, jobstore_as_flag: bool = False
2493
+ jobStore: StrPath | None = None, jobstore_as_flag: bool = False
2505
2494
  ) -> Namespace:
2506
2495
  """
2507
2496
  Get default options for a toil workflow.
@@ -2529,7 +2518,7 @@ class Job:
2529
2518
 
2530
2519
  @staticmethod
2531
2520
  def addToilOptions(
2532
- parser: Union["OptionParser", ArgumentParser],
2521
+ parser: OptionParser | ArgumentParser,
2533
2522
  jobstore_as_flag: bool = False,
2534
2523
  ) -> None:
2535
2524
  """
@@ -2549,7 +2538,7 @@ class Job:
2549
2538
  addOptions(parser, jobstore_as_flag=jobstore_as_flag)
2550
2539
 
2551
2540
  @staticmethod
2552
- def startToil(job: "Job", options) -> Any:
2541
+ def startToil(job: Job, options) -> Any:
2553
2542
  """
2554
2543
  Run the toil workflow using the given options.
2555
2544
 
@@ -2580,12 +2569,12 @@ class Job:
2580
2569
 
2581
2570
  def __init__(
2582
2571
  self,
2583
- memory: Optional[ParseableIndivisibleResource] = None,
2584
- cores: Optional[ParseableDivisibleResource] = None,
2585
- disk: Optional[ParseableIndivisibleResource] = None,
2586
- accelerators: Optional[ParseableAcceleratorRequirement] = None,
2587
- preemptible: Optional[ParseableFlag] = None,
2588
- unitName: Optional[str] = "",
2572
+ memory: ParseableIndivisibleResource | None = None,
2573
+ cores: ParseableDivisibleResource | None = None,
2574
+ disk: ParseableIndivisibleResource | None = None,
2575
+ accelerators: ParseableAcceleratorRequirement | None = None,
2576
+ preemptible: ParseableFlag | None = None,
2577
+ unitName: str | None = "",
2589
2578
  ) -> None:
2590
2579
  """
2591
2580
  Memory, core and disk requirements are specified identically to as in \
@@ -2612,7 +2601,7 @@ class Job:
2612
2601
  self.hostID = None
2613
2602
 
2614
2603
  @abstractmethod
2615
- def start(self, job: "ServiceHostJob") -> Any:
2604
+ def start(self, job: ServiceHostJob) -> Any:
2616
2605
  """
2617
2606
  Start the service.
2618
2607
 
@@ -2625,7 +2614,7 @@ class Job:
2625
2614
  """
2626
2615
 
2627
2616
  @abstractmethod
2628
- def stop(self, job: "ServiceHostJob") -> None:
2617
+ def stop(self, job: ServiceHostJob) -> None:
2629
2618
  """
2630
2619
  Stops the service. Function can block until complete.
2631
2620
 
@@ -2781,7 +2770,7 @@ class Job:
2781
2770
  # We added this successor locally
2782
2771
  todo.append(self._registry[successorID])
2783
2772
 
2784
- def getTopologicalOrderingOfJobs(self) -> list["Job"]:
2773
+ def getTopologicalOrderingOfJobs(self) -> list[Job]:
2785
2774
  """
2786
2775
  :returns: a list of jobs such that for all pairs of indices i, j for which i < j, \
2787
2776
  the job at index i can be run before the job at index j.
@@ -2872,7 +2861,7 @@ class Job:
2872
2861
  # Do renames in the description
2873
2862
  self._description.renameReferences(renames)
2874
2863
 
2875
- def saveBody(self, jobStore: "AbstractJobStore") -> None:
2864
+ def saveBody(self, jobStore: AbstractJobStore) -> None:
2876
2865
  """
2877
2866
  Save the execution data for just this job to the JobStore, and fill in
2878
2867
  the JobDescription with the information needed to retrieve it.
@@ -2943,7 +2932,7 @@ class Job:
2943
2932
 
2944
2933
  def _saveJobGraph(
2945
2934
  self,
2946
- jobStore: "AbstractJobStore",
2935
+ jobStore: AbstractJobStore,
2947
2936
  saveSelf: bool = False,
2948
2937
  returnValues: bool = None,
2949
2938
  ):
@@ -3050,7 +3039,7 @@ class Job:
3050
3039
  if job != self or saveSelf:
3051
3040
  jobStore.create_job(job.description)
3052
3041
 
3053
- def saveAsRootJob(self, jobStore: "AbstractJobStore") -> JobDescription:
3042
+ def saveAsRootJob(self, jobStore: AbstractJobStore) -> JobDescription:
3054
3043
  """
3055
3044
  Save this job to the given jobStore as the root job of the workflow.
3056
3045
 
@@ -3079,8 +3068,8 @@ class Job:
3079
3068
 
3080
3069
  @classmethod
3081
3070
  def loadJob(
3082
- cls, job_store: "AbstractJobStore", job_description: JobDescription
3083
- ) -> "Job":
3071
+ cls, job_store: AbstractJobStore, job_description: JobDescription
3072
+ ) -> Job:
3084
3073
  """
3085
3074
  Retrieves a :class:`toil.job.Job` instance from a JobStore
3086
3075
 
@@ -3155,7 +3144,9 @@ class Job:
3155
3144
 
3156
3145
  if "download_only" in self._debug_flags:
3157
3146
  # We should stop right away
3158
- logger.debug("Job did not stop itself after downloading files; stopping.")
3147
+ logger.debug(
3148
+ "Job did not stop itself after downloading files; stopping."
3149
+ )
3159
3150
  raise DebugStoppingPointReached()
3160
3151
 
3161
3152
  # If the job is not a checkpoint job, add the promise files to delete
@@ -3178,7 +3169,7 @@ class Job:
3178
3169
  # Change dir back to cwd dir, if changed by job (this is a safety issue)
3179
3170
  if os.getcwd() != baseDir:
3180
3171
  os.chdir(baseDir)
3181
-
3172
+
3182
3173
  totalCpuTime, total_memory_kib = (
3183
3174
  ResourceMonitor.get_total_cpu_time_and_memory_usage()
3184
3175
  )
@@ -3196,7 +3187,7 @@ class Job:
3196
3187
  f"CPU than the requested {self.cores} cores. Consider "
3197
3188
  f"increasing the job's required CPU cores or limiting the "
3198
3189
  f"number of processes/threads launched.",
3199
- level=logging.WARNING
3190
+ level=logging.WARNING,
3200
3191
  )
3201
3192
 
3202
3193
  # Finish up the stats
@@ -3210,7 +3201,9 @@ class Job:
3210
3201
  clock=str(job_cpu_time),
3211
3202
  class_name=self._jobName(),
3212
3203
  memory=str(total_memory_kib),
3213
- requested_cores=str(self.cores), # TODO: Isn't this really consumed cores?
3204
+ requested_cores=str(
3205
+ self.cores
3206
+ ), # TODO: Isn't this really consumed cores?
3214
3207
  disk=str(fileStore.get_disk_usage()),
3215
3208
  succeeded=str(succeeded),
3216
3209
  )
@@ -3218,8 +3211,8 @@ class Job:
3218
3211
 
3219
3212
  def _runner(
3220
3213
  self,
3221
- jobStore: "AbstractJobStore",
3222
- fileStore: "AbstractFileStore",
3214
+ jobStore: AbstractJobStore,
3215
+ fileStore: AbstractFileStore,
3223
3216
  defer: Callable[[Any], None],
3224
3217
  **kwargs,
3225
3218
  ) -> None:
@@ -3286,7 +3279,7 @@ class Job:
3286
3279
  return flag in self._debug_flags
3287
3280
 
3288
3281
  def files_downloaded_hook(
3289
- self, host_and_job_paths: Optional[list[tuple[str, str]]] = None
3282
+ self, host_and_job_paths: list[tuple[str, str]] | None = None
3290
3283
  ) -> None:
3291
3284
  """
3292
3285
  Function that subclasses can call when they have downloaded their input files.
@@ -3354,9 +3347,7 @@ class FunctionWrappingJob(Job):
3354
3347
  list(zip(argSpec.args[-len(argSpec.defaults) :], argSpec.defaults))
3355
3348
  )
3356
3349
 
3357
- def resolve(
3358
- key, default: Optional[Any] = None, dehumanize: bool = False
3359
- ) -> Any:
3350
+ def resolve(key, default: Any | None = None, dehumanize: bool = False) -> Any:
3360
3351
  try:
3361
3352
  # First, try constructor arguments, ...
3362
3353
  value = kwargs.pop(key)
@@ -3399,7 +3390,7 @@ class FunctionWrappingJob(Job):
3399
3390
  userFunctionModule = self._loadUserModule(self.userFunctionModule)
3400
3391
  return getattr(userFunctionModule, self.userFunctionName)
3401
3392
 
3402
- def run(self, fileStore: "AbstractFileStore") -> Any:
3393
+ def run(self, fileStore: AbstractFileStore) -> Any:
3403
3394
  userFunction = self._getUserFunction()
3404
3395
  return userFunction(*self._args, **self._kwargs)
3405
3396
 
@@ -3444,10 +3435,10 @@ class JobFunctionWrappingJob(FunctionWrappingJob):
3444
3435
  """
3445
3436
 
3446
3437
  @property
3447
- def fileStore(self) -> "AbstractFileStore":
3438
+ def fileStore(self) -> AbstractFileStore:
3448
3439
  return self._fileStore
3449
3440
 
3450
- def run(self, fileStore: "AbstractFileStore") -> Any:
3441
+ def run(self, fileStore: AbstractFileStore) -> Any:
3451
3442
  userFunction = self._getUserFunction()
3452
3443
  rValue = userFunction(*((self,) + tuple(self._args)), **self._kwargs)
3453
3444
  return rValue
@@ -3543,7 +3534,7 @@ class EncapsulatedJob(Job):
3543
3534
  the same value after A or A.encapsulate() has been run.
3544
3535
  """
3545
3536
 
3546
- def __init__(self, job: Optional[Job], unitName: Optional[str] = None) -> None:
3537
+ def __init__(self, job: Job | None, unitName: str | None = None) -> None:
3547
3538
  """
3548
3539
  :param toil.job.Job job: the job to encapsulate.
3549
3540
  :param str unitName: human-readable name to identify this job instance.
@@ -3600,7 +3591,7 @@ class EncapsulatedJob(Job):
3600
3591
  )
3601
3592
  return Job.addFollowOn(self.encapsulatedFollowOn, followOnJob)
3602
3593
 
3603
- def rv(self, *path) -> "Promise":
3594
+ def rv(self, *path) -> Promise:
3604
3595
  if self.encapsulatedJob is None:
3605
3596
  raise RuntimeError("The encapsulated job was not set.")
3606
3597
  return self.encapsulatedJob.rv(*path)
@@ -3824,14 +3815,14 @@ class FileMetadata(NamedTuple):
3824
3815
 
3825
3816
  source: str
3826
3817
  parent_dir: str
3827
- size: Optional[int]
3818
+ size: int | None
3828
3819
 
3829
3820
 
3830
3821
  def potential_absolute_uris(
3831
3822
  uri: str,
3832
3823
  path: list[str],
3833
- importer: Optional[str] = None,
3834
- execution_dir: Optional[str] = None,
3824
+ importer: str | None = None,
3825
+ execution_dir: str | None = None,
3835
3826
  ) -> Iterator[str]:
3836
3827
  """
3837
3828
  Get potential absolute URIs to check for an imported file.
@@ -3903,12 +3894,12 @@ def potential_absolute_uris(
3903
3894
 
3904
3895
 
3905
3896
  def get_file_sizes(
3906
- filenames: List[str],
3897
+ filenames: list[str],
3907
3898
  file_source: AbstractJobStore,
3908
- search_paths: Optional[List[str]] = None,
3899
+ search_paths: list[str] | None = None,
3909
3900
  include_remote_files: bool = True,
3910
- execution_dir: Optional[str] = None,
3911
- ) -> Dict[str, FileMetadata]:
3901
+ execution_dir: str | None = None,
3902
+ ) -> dict[str, FileMetadata]:
3912
3903
  """
3913
3904
  Resolve relative-URI files in the given environment and turn them into absolute normalized URIs. Returns a dictionary of the *string values* from the WDL file values
3914
3905
  to a tuple of the normalized URI, parent directory ID, and size of the file. The size of the file may be None, which means unknown size.
@@ -3975,9 +3966,7 @@ def get_file_sizes(
3975
3966
  if file_basename == "":
3976
3967
  # We can't have files with no basename because we need to
3977
3968
  # download them at that basename later in WDL.
3978
- raise RuntimeError(
3979
- f"File {candidate_uri} has no basename"
3980
- )
3969
+ raise RuntimeError(f"File {candidate_uri} has no basename")
3981
3970
 
3982
3971
  # Was actually found
3983
3972
  if is_remote_url(candidate_uri):
@@ -3985,7 +3974,7 @@ def get_file_sizes(
3985
3974
  # We need to make sure file URIs and local paths that point to
3986
3975
  # the same place are treated the same.
3987
3976
  parsed = urlsplit(candidate_uri)
3988
- if parsed.scheme == "file:":
3977
+ if parsed.scheme == "file":
3989
3978
  # This is a local file URI. Convert to a path for source directory tracking.
3990
3979
  parent_dir = os.path.dirname(unquote(parsed.path))
3991
3980
  else:
@@ -3995,7 +3984,7 @@ def get_file_sizes(
3995
3984
  # Must be a local path
3996
3985
  parent_dir = os.path.dirname(candidate_uri)
3997
3986
 
3998
- return cast(FileMetadata, (candidate_uri, parent_dir, filesize))
3987
+ return FileMetadata(candidate_uri, parent_dir, filesize)
3999
3988
  # Not found
4000
3989
  raise RuntimeError(
4001
3990
  f"Could not find {filename} at any of: {list(potential_absolute_uris(filename, search_paths if search_paths is not None else []))}"
@@ -4006,17 +3995,17 @@ def get_file_sizes(
4006
3995
 
4007
3996
  class CombineImportsJob(Job):
4008
3997
  """
4009
- Combine the outputs of multiple WorkerImportsJob into one promise
3998
+ Combine the outputs of multiple WorkerImportJobs into one promise
4010
3999
  """
4011
4000
 
4012
- def __init__(self, d: Sequence[Promised[Dict[str, FileID]]], **kwargs):
4001
+ def __init__(self, d: Sequence[Promised[dict[str, FileID]]], **kwargs):
4013
4002
  """
4014
4003
  :param d: Sequence of dictionaries to merge
4015
4004
  """
4016
4005
  self._d = d
4017
4006
  super().__init__(**kwargs)
4018
4007
 
4019
- def run(self, file_store: "AbstractFileStore") -> Promised[Dict[str, FileID]]:
4008
+ def run(self, file_store: AbstractFileStore) -> Promised[dict[str, FileID]]:
4020
4009
  """
4021
4010
  Merge the dicts
4022
4011
  """
@@ -4031,12 +4020,7 @@ class WorkerImportJob(Job):
4031
4020
  For the CWL/WDL runners, this class is only used when runImportsOnWorkers is enabled.
4032
4021
  """
4033
4022
 
4034
- def __init__(
4035
- self,
4036
- filenames: List[str],
4037
- local: bool = False,
4038
- **kwargs: Any
4039
- ):
4023
+ def __init__(self, filenames: list[str], local: bool = False, **kwargs: Any):
4040
4024
  """
4041
4025
  Setup importing files on a worker.
4042
4026
  :param filenames: List of file URIs to import
@@ -4047,23 +4031,29 @@ class WorkerImportJob(Job):
4047
4031
 
4048
4032
  @staticmethod
4049
4033
  def import_files(
4050
- files: List[str], file_source: "AbstractJobStore"
4051
- ) -> Dict[str, FileID]:
4034
+ files: list[str], file_source: AbstractJobStore, symlink: bool = True
4035
+ ) -> dict[str, FileID]:
4052
4036
  """
4053
4037
  Import a list of files into the jobstore. Returns a mapping of the filename to the associated FileIDs
4054
4038
 
4055
4039
  When stream is true but the import is not streamable, the worker will run out of
4056
4040
  disk space and run a new import job with enough disk space instead.
4041
+
4057
4042
  :param files: list of files to import
4043
+
4058
4044
  :param file_source: AbstractJobStore
4059
- :return: Dictionary mapping filenames to associated jobstore FileID
4045
+
4046
+ :param symlink: whether to allow symlinking the imported files
4047
+
4048
+ :return: Dictionary mapping filenames from files to associated jobstore
4049
+ FileID.
4060
4050
  """
4061
4051
  # todo: make the import ensure streaming is done instead of relying on running out of disk space
4062
4052
  path_to_fileid = {}
4063
4053
 
4064
4054
  @memoize
4065
- def import_filename(filename: str) -> Optional[FileID]:
4066
- return file_source.import_file(filename, symlink=True)
4055
+ def import_filename(filename: str) -> FileID | None:
4056
+ return file_source.import_file(filename, symlink=symlink)
4067
4057
 
4068
4058
  for file in files:
4069
4059
  imported = import_filename(file)
@@ -4071,7 +4061,7 @@ class WorkerImportJob(Job):
4071
4061
  path_to_fileid[file] = imported
4072
4062
  return path_to_fileid
4073
4063
 
4074
- def run(self, file_store: "AbstractFileStore") -> Promised[Dict[str, FileID]]:
4064
+ def run(self, file_store: AbstractFileStore) -> Promised[dict[str, FileID]]:
4075
4065
  """
4076
4066
  Import the workflow inputs and then create and run the workflow.
4077
4067
  :return: Promise of workflow outputs
@@ -4088,7 +4078,7 @@ class ImportsJob(Job):
4088
4078
 
4089
4079
  def __init__(
4090
4080
  self,
4091
- file_to_data: Dict[str, FileMetadata],
4081
+ file_to_data: dict[str, FileMetadata],
4092
4082
  max_batch_size: ParseableIndivisibleResource,
4093
4083
  import_worker_disk: ParseableIndivisibleResource,
4094
4084
  **kwargs: Any,
@@ -4107,11 +4097,20 @@ class ImportsJob(Job):
4107
4097
  self._import_worker_disk = import_worker_disk
4108
4098
 
4109
4099
  def run(
4110
- self, file_store: "AbstractFileStore"
4111
- ) -> Tuple[Promised[Dict[str, FileID]], Dict[str, FileMetadata]]:
4100
+ self, file_store: AbstractFileStore
4101
+ ) -> tuple[Promised[dict[str, FileID]], dict[str, FileMetadata]]:
4112
4102
  """
4113
4103
  Import the workflow inputs and then create and run the workflow.
4114
- :return: Tuple of a mapping from the candidate uri to the file id and a mapping of the source filenames to its metadata. The candidate uri is a field in the file metadata
4104
+
4105
+ The two parts of the return value must be used together and cannot be
4106
+ safely split apart. To look up a FileID from an original filename, you
4107
+ must first look up the FileMetadata in the second dict, extract its
4108
+ .source (the normalized candidate URI), and then look that up in the
4109
+ first dict.
4110
+
4111
+ :return: Tuple of (candidate URI to FileID mapping, original filename
4112
+ to FileMetadata mapping). The candidate URI is stored in
4113
+ FileMetadata.source.
4115
4114
  """
4116
4115
  max_batch_size = self._max_batch_size
4117
4116
  file_to_data = self._file_to_data
@@ -4150,7 +4149,9 @@ class ImportsJob(Job):
4150
4149
  # Create batch import jobs for each group of files
4151
4150
  for batch in file_batches:
4152
4151
  candidate_uris = [file_to_data[filename][0] for filename in batch]
4153
- import_jobs.append(WorkerImportJob(candidate_uris, disk=self._import_worker_disk))
4152
+ import_jobs.append(
4153
+ WorkerImportJob(candidate_uris, disk=self._import_worker_disk)
4154
+ )
4154
4155
 
4155
4156
  for job in import_jobs:
4156
4157
  self.addChild(job)
@@ -4178,7 +4179,7 @@ class Promise:
4178
4179
  run function has been executed.
4179
4180
  """
4180
4181
 
4181
- _jobstore: Optional["AbstractJobStore"] = None
4182
+ _jobstore: AbstractJobStore | None = None
4182
4183
  """
4183
4184
  Caches the job store instance used during unpickling to prevent it from being instantiated
4184
4185
  for each promise
@@ -4189,7 +4190,7 @@ class Promise:
4189
4190
  A set of IDs of files containing promised values when we know we won't need them anymore
4190
4191
  """
4191
4192
 
4192
- def __init__(self, job: "Job", path: Any):
4193
+ def __init__(self, job: Job, path: Any):
4193
4194
  """
4194
4195
  Initialize this promise.
4195
4196
 
@@ -4220,7 +4221,7 @@ class Promise:
4220
4221
  return self.__class__, (jobStoreLocator, jobStoreFileID)
4221
4222
 
4222
4223
  @staticmethod
4223
- def __new__(cls, *args) -> "Promise":
4224
+ def __new__(cls, *args) -> Promise:
4224
4225
  """Instantiate this Promise."""
4225
4226
  if len(args) != 2:
4226
4227
  raise RuntimeError(