locust 2.20.1.dev26__py3-none-any.whl → 2.20.2__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 (64) hide show
  1. locust/__init__.py +7 -7
  2. locust/_version.py +2 -2
  3. locust/argument_parser.py +35 -18
  4. locust/clients.py +7 -6
  5. locust/contrib/fasthttp.py +42 -37
  6. locust/debug.py +14 -8
  7. locust/dispatch.py +25 -25
  8. locust/env.py +45 -37
  9. locust/event.py +13 -10
  10. locust/exception.py +0 -9
  11. locust/html.py +9 -8
  12. locust/input_events.py +7 -5
  13. locust/main.py +95 -47
  14. locust/rpc/protocol.py +4 -2
  15. locust/rpc/zmqrpc.py +6 -4
  16. locust/runners.py +69 -76
  17. locust/shape.py +7 -4
  18. locust/stats.py +73 -71
  19. locust/templates/index.html +8 -23
  20. locust/test/mock_logging.py +7 -5
  21. locust/test/test_debugging.py +4 -4
  22. locust/test/test_dispatch.py +10 -9
  23. locust/test/test_env.py +30 -1
  24. locust/test/test_fasthttp.py +9 -8
  25. locust/test/test_http.py +4 -3
  26. locust/test/test_interruptable_task.py +4 -4
  27. locust/test/test_load_locustfile.py +6 -5
  28. locust/test/test_locust_class.py +11 -5
  29. locust/test/test_log.py +5 -4
  30. locust/test/test_main.py +37 -7
  31. locust/test/test_parser.py +11 -15
  32. locust/test/test_runners.py +22 -21
  33. locust/test/test_sequential_taskset.py +3 -2
  34. locust/test/test_stats.py +16 -18
  35. locust/test/test_tags.py +3 -2
  36. locust/test/test_taskratio.py +3 -3
  37. locust/test/test_users.py +3 -3
  38. locust/test/test_util.py +3 -2
  39. locust/test/test_wait_time.py +3 -3
  40. locust/test/test_web.py +142 -27
  41. locust/test/test_zmqrpc.py +5 -3
  42. locust/test/testcases.py +7 -7
  43. locust/test/util.py +6 -7
  44. locust/user/__init__.py +1 -1
  45. locust/user/inspectuser.py +6 -5
  46. locust/user/sequential_taskset.py +3 -1
  47. locust/user/task.py +22 -27
  48. locust/user/users.py +17 -7
  49. locust/util/deprecation.py +0 -1
  50. locust/util/exception_handler.py +1 -1
  51. locust/util/load_locustfile.py +4 -2
  52. locust/web.py +102 -53
  53. locust/webui/dist/assets/auth-5e21717c.js.map +1 -0
  54. locust/webui/dist/assets/{index-01afe4fa.js → index-a83a5dd9.js} +85 -84
  55. locust/webui/dist/assets/index-a83a5dd9.js.map +1 -0
  56. locust/webui/dist/auth.html +20 -0
  57. locust/webui/dist/index.html +1 -1
  58. {locust-2.20.1.dev26.dist-info → locust-2.20.2.dist-info}/METADATA +2 -2
  59. locust-2.20.2.dist-info/RECORD +105 -0
  60. locust-2.20.1.dev26.dist-info/RECORD +0 -102
  61. {locust-2.20.1.dev26.dist-info → locust-2.20.2.dist-info}/LICENSE +0 -0
  62. {locust-2.20.1.dev26.dist-info → locust-2.20.2.dist-info}/WHEEL +0 -0
  63. {locust-2.20.1.dev26.dist-info → locust-2.20.2.dist-info}/entry_points.txt +0 -0
  64. {locust-2.20.1.dev26.dist-info → locust-2.20.2.dist-info}/top_level.txt +0 -0
locust/runners.py CHANGED
@@ -1,4 +1,9 @@
1
+ from __future__ import annotations
2
+
3
+ from locust import __version__
4
+
1
5
  import functools
6
+ import inspect
2
7
  import json
3
8
  import logging
4
9
  import os
@@ -17,30 +22,23 @@ from operator import (
17
22
  from types import TracebackType
18
23
  from typing import (
19
24
  TYPE_CHECKING,
20
- Dict,
25
+ Any,
26
+ Callable,
21
27
  Iterator,
22
- List,
23
28
  NoReturn,
29
+ TypedDict,
24
30
  ValuesView,
25
- Set,
26
- Optional,
27
- Tuple,
28
- Type,
29
- Any,
30
31
  cast,
31
- Callable,
32
- TypedDict,
33
32
  )
34
33
  from uuid import uuid4
34
+
35
35
  import gevent
36
36
  import greenlet
37
37
  import psutil
38
38
  from gevent.event import Event
39
39
  from gevent.pool import Group
40
- import inspect
41
40
 
42
- from . import User
43
- from locust import __version__
41
+ from . import argument_parser
44
42
  from .dispatch import UsersDispatcher
45
43
  from .exception import RPCError, RPCReceiveError, RPCSendError
46
44
  from .log import greenlet_exception_logger
@@ -53,9 +51,9 @@ from .stats import (
53
51
  StatsError,
54
52
  setup_distributed_stats_event_listeners,
55
53
  )
56
- from . import argument_parser
57
54
 
58
55
  if TYPE_CHECKING:
56
+ from . import User
59
57
  from .env import Environment
60
58
 
61
59
  logger = logging.getLogger(__name__)
@@ -87,7 +85,7 @@ class ExceptionDict(TypedDict):
87
85
  count: int
88
86
  msg: str
89
87
  traceback: str
90
- nodes: Set[str]
88
+ nodes: set[str]
91
89
 
92
90
 
93
91
  class Runner:
@@ -101,29 +99,29 @@ class Runner:
101
99
  desired type.
102
100
  """
103
101
 
104
- def __init__(self, environment: "Environment") -> None:
102
+ def __init__(self, environment: Environment) -> None:
105
103
  self.environment = environment
106
104
  self.user_greenlets = Group()
107
105
  self.greenlet = Group()
108
106
  self.state = STATE_INIT
109
- self.spawning_greenlet: Optional[gevent.Greenlet] = None
110
- self.shape_greenlet: Optional[gevent.Greenlet] = None
111
- self.shape_last_tick: Tuple[int, float] | Tuple[int, float, Optional[List[Type[User]]]] | None = None
107
+ self.spawning_greenlet: gevent.Greenlet | None = None
108
+ self.shape_greenlet: gevent.Greenlet | None = None
109
+ self.shape_last_tick: tuple[int, float] | tuple[int, float, list[type[User]] | None] | None = None
112
110
  self.current_cpu_usage: int = 0
113
111
  self.cpu_warning_emitted: bool = False
114
112
  self.worker_cpu_warning_emitted: bool = False
115
113
  self.current_memory_usage: int = 0
116
114
  self.greenlet.spawn(self.monitor_cpu_and_memory).link_exception(greenlet_exception_handler)
117
- self.exceptions: Dict[int, ExceptionDict] = {}
115
+ self.exceptions: dict[int, ExceptionDict] = {}
118
116
  # Because of the way the ramp-up/ramp-down is implemented, target_user_classes_count
119
117
  # is only updated at the end of the ramp-up/ramp-down.
120
118
  # See https://github.com/locustio/locust/issues/1883#issuecomment-919239824 for context.
121
- self.target_user_classes_count: Dict[str, int] = {}
119
+ self.target_user_classes_count: dict[str, int] = {}
122
120
  # target_user_count is set before the ramp-up/ramp-down occurs.
123
121
  self.target_user_count: int = 0
124
- self.custom_messages: Dict[str, Callable] = {}
122
+ self.custom_messages: dict[str, Callable] = {}
125
123
 
126
- self._users_dispatcher: Optional[UsersDispatcher] = None
124
+ self._users_dispatcher: UsersDispatcher | None = None
127
125
 
128
126
  # set up event listeners for recording requests
129
127
  def on_request(request_type, name, response_time, response_length, exception=None, **_kwargs):
@@ -134,7 +132,7 @@ class Runner:
134
132
  self.environment.events.request.add_listener(on_request)
135
133
 
136
134
  self.connection_broken = False
137
- self.final_user_classes_count: Dict[str, int] = {} # just for the ratio report, fills before runner stops
135
+ self.final_user_classes_count: dict[str, int] = {} # just for the ratio report, fills before runner stops
138
136
 
139
137
  # register listener that resets stats when spawning is complete
140
138
  def on_spawning_complete(user_count: int) -> None:
@@ -151,11 +149,11 @@ class Runner:
151
149
  self.greenlet.kill(block=False)
152
150
 
153
151
  @property
154
- def user_classes(self) -> List[Type[User]]:
152
+ def user_classes(self) -> list[type[User]]:
155
153
  return self.environment.user_classes
156
154
 
157
155
  @property
158
- def user_classes_by_name(self) -> Dict[str, Type[User]]:
156
+ def user_classes_by_name(self) -> dict[str, type[User]]:
159
157
  return self.environment.user_classes_by_name
160
158
 
161
159
  @property
@@ -163,7 +161,7 @@ class Runner:
163
161
  return self.environment.stats
164
162
 
165
163
  @property
166
- def errors(self) -> Dict[str, StatsError]:
164
+ def errors(self) -> dict[str, StatsError]:
167
165
  return self.stats.errors
168
166
 
169
167
  @property
@@ -174,7 +172,7 @@ class Runner:
174
172
  return len(self.user_greenlets)
175
173
 
176
174
  @property
177
- def user_classes_count(self) -> Dict[str, int]:
175
+ def user_classes_count(self) -> dict[str, int]:
178
176
  """
179
177
  :returns: Number of currently running users for each user class
180
178
  """
@@ -214,18 +212,17 @@ class Runner:
214
212
  )
215
213
  return self.cpu_warning_emitted
216
214
 
217
- def spawn_users(self, user_classes_spawn_count: Dict[str, int], wait: bool = False):
215
+ def spawn_users(self, user_classes_spawn_count: dict[str, int], wait: bool = False):
218
216
  if self.state == STATE_INIT or self.state == STATE_STOPPED:
219
217
  self.update_state(STATE_SPAWNING)
220
218
 
221
219
  logger.debug(
222
- "Spawning additional %s (%s already running)..."
223
- % (json.dumps(user_classes_spawn_count), json.dumps(self.user_classes_count))
220
+ f"Spawning additional {json.dumps(user_classes_spawn_count)} ({json.dumps(self.user_classes_count)} already running)..."
224
221
  )
225
222
 
226
- def spawn(user_class: str, spawn_count: int) -> List[User]:
223
+ def spawn(user_class: str, spawn_count: int) -> list[User]:
227
224
  n = 0
228
- new_users: List[User] = []
225
+ new_users: list[User] = []
229
226
  while n < spawn_count:
230
227
  new_user = self.user_classes_by_name[user_class](self.environment)
231
228
  new_user.start(self.user_greenlets)
@@ -236,7 +233,7 @@ class Runner:
236
233
  logger.debug("All users of class %s spawned" % user_class)
237
234
  return new_users
238
235
 
239
- new_users: List[User] = []
236
+ new_users: list[User] = []
240
237
  for user_class, spawn_count in user_classes_spawn_count.items():
241
238
  new_users += spawn(user_class, spawn_count)
242
239
 
@@ -245,7 +242,7 @@ class Runner:
245
242
  logger.info("All users stopped\n")
246
243
  return new_users
247
244
 
248
- def stop_users(self, user_classes_stop_count: Dict[str, int]) -> None:
245
+ def stop_users(self, user_classes_stop_count: dict[str, int]) -> None:
249
246
  async_calls_to_stop = Group()
250
247
  stop_group = Group()
251
248
 
@@ -253,7 +250,7 @@ class Runner:
253
250
  if self.user_classes_count[user_class] == 0:
254
251
  continue
255
252
 
256
- to_stop: List[greenlet.greenlet] = []
253
+ to_stop: list[greenlet.greenlet] = []
257
254
  for user_greenlet in self.user_greenlets:
258
255
  if len(to_stop) == stop_count:
259
256
  break
@@ -313,12 +310,12 @@ class Runner:
313
310
 
314
311
  @abstractmethod
315
312
  def start(
316
- self, user_count: int, spawn_rate: float, wait: bool = False, user_classes: Optional[List[Type[User]]] = None
313
+ self, user_count: int, spawn_rate: float, wait: bool = False, user_classes: list[type[User]] | None = None
317
314
  ) -> None:
318
315
  ...
319
316
 
320
317
  @abstractmethod
321
- def send_message(self, msg_type: str, data: Optional[Any] = None, client_id: Optional[str] = None) -> None:
318
+ def send_message(self, msg_type: str, data: Any | None = None, client_id: str | None = None) -> None:
322
319
  ...
323
320
 
324
321
  def start_shape(self) -> None:
@@ -462,9 +459,7 @@ class LocalRunner(Runner):
462
459
 
463
460
  self.environment.events.user_error.add_listener(on_user_error)
464
461
 
465
- def _start(
466
- self, user_count: int, spawn_rate: float, wait: bool = False, user_classes: Optional[list] = None
467
- ) -> None:
462
+ def _start(self, user_count: int, spawn_rate: float, wait: bool = False, user_classes: list | None = None) -> None:
468
463
  """
469
464
  Start running a load test
470
465
 
@@ -507,8 +502,8 @@ class LocalRunner(Runner):
507
502
 
508
503
  try:
509
504
  for dispatched_users in self._users_dispatcher:
510
- user_classes_spawn_count: Dict[str, int] = {}
511
- user_classes_stop_count: Dict[str, int] = {}
505
+ user_classes_spawn_count: dict[str, int] = {}
506
+ user_classes_stop_count: dict[str, int] = {}
512
507
  user_classes_count = dispatched_users[self._local_worker_node.id]
513
508
  logger.debug("Ramping to %s" % _format_user_classes_count_for_log(user_classes_count))
514
509
  for user_class_name, user_class_count in user_classes_count.items():
@@ -546,7 +541,7 @@ class LocalRunner(Runner):
546
541
  self.environment.events.spawning_complete.fire(user_count=sum(self.target_user_classes_count.values()))
547
542
 
548
543
  def start(
549
- self, user_count: int, spawn_rate: float, wait: bool = False, user_classes: Optional[List[Type[User]]] = None
544
+ self, user_count: int, spawn_rate: float, wait: bool = False, user_classes: list[type[User]] | None = None
550
545
  ) -> None:
551
546
  if spawn_rate > 100:
552
547
  logger.warning(
@@ -566,7 +561,7 @@ class LocalRunner(Runner):
566
561
  return
567
562
  super().stop()
568
563
 
569
- def send_message(self, msg_type: str, data: Optional[Any] = None, client_id: Optional[str] = None) -> None:
564
+ def send_message(self, msg_type: str, data: Any | None = None, client_id: str | None = None) -> None:
570
565
  """
571
566
  Emulates internodal messaging by calling registered listeners
572
567
 
@@ -597,7 +592,7 @@ class WorkerNode:
597
592
  self.cpu_warning_emitted = False
598
593
  self.memory_usage: int = 0
599
594
  # The reported users running on the worker
600
- self.user_classes_count: Dict[str, int] = {}
595
+ self.user_classes_count: dict[str, int] = {}
601
596
 
602
597
  @property
603
598
  def user_count(self) -> int:
@@ -606,9 +601,9 @@ class WorkerNode:
606
601
 
607
602
  class WorkerNodes(MutableMapping):
608
603
  def __init__(self):
609
- self._worker_nodes: Dict[str, WorkerNode] = {}
604
+ self._worker_nodes: dict[str, WorkerNode] = {}
610
605
 
611
- def get_by_state(self, state) -> List[WorkerNode]:
606
+ def get_by_state(self, state) -> list[WorkerNode]:
612
607
  return [c for c in self.values() if c.state == state]
613
608
 
614
609
  @property
@@ -616,19 +611,19 @@ class WorkerNodes(MutableMapping):
616
611
  return self.values()
617
612
 
618
613
  @property
619
- def ready(self) -> List[WorkerNode]:
614
+ def ready(self) -> list[WorkerNode]:
620
615
  return self.get_by_state(STATE_INIT)
621
616
 
622
617
  @property
623
- def spawning(self) -> List[WorkerNode]:
618
+ def spawning(self) -> list[WorkerNode]:
624
619
  return self.get_by_state(STATE_SPAWNING)
625
620
 
626
621
  @property
627
- def running(self) -> List[WorkerNode]:
622
+ def running(self) -> list[WorkerNode]:
628
623
  return self.get_by_state(STATE_RUNNING)
629
624
 
630
625
  @property
631
- def missing(self) -> List[WorkerNode]:
626
+ def missing(self) -> list[WorkerNode]:
632
627
  return self.get_by_state(STATE_MISSING)
633
628
 
634
629
  def __setitem__(self, k: str, v: WorkerNode) -> None:
@@ -687,13 +682,13 @@ class MasterRunner(DistributedRunner):
687
682
  else:
688
683
  raise
689
684
 
690
- self._users_dispatcher: Optional[UsersDispatcher] = None
685
+ self._users_dispatcher: UsersDispatcher | None = None
691
686
 
692
687
  self.greenlet.spawn(self.heartbeat_worker).link_exception(greenlet_exception_handler)
693
688
  self.greenlet.spawn(self.client_listener).link_exception(greenlet_exception_handler)
694
689
 
695
690
  # listener that gathers info on how many users the worker has spawned
696
- def on_worker_report(client_id: str, data: Dict[str, Any]) -> None:
691
+ def on_worker_report(client_id: str, data: dict[str, Any]) -> None:
697
692
  if client_id not in self.clients:
698
693
  logger.info("Discarded report from unrecognized worker %s", client_id)
699
694
  return
@@ -702,7 +697,7 @@ class MasterRunner(DistributedRunner):
702
697
  self.environment.events.worker_report.add_listener(on_worker_report)
703
698
 
704
699
  # register listener that sends quit message to worker nodes
705
- def on_quitting(environment: "Environment", **kw):
700
+ def on_quitting(environment: Environment, **kw):
706
701
  self.quit()
707
702
 
708
703
  self.environment.events.quitting.add_listener(on_quitting)
@@ -737,7 +732,7 @@ class MasterRunner(DistributedRunner):
737
732
  return warning_emitted
738
733
 
739
734
  def start(
740
- self, user_count: int, spawn_rate: float, wait=False, user_classes: Optional[List[Type[User]]] = None
735
+ self, user_count: int, spawn_rate: float, wait=False, user_classes: list[type[User]] | None = None
741
736
  ) -> None:
742
737
  self.spawning_completed = False
743
738
 
@@ -908,7 +903,7 @@ class MasterRunner(DistributedRunner):
908
903
  self.stop(send_stop_to_client=False)
909
904
  logger.debug("Quitting...")
910
905
  for client in self.clients.all:
911
- logger.debug("Sending quit message to worker %s (index %s)" % (client.id, self.get_worker_index(client.id)))
906
+ logger.debug(f"Sending quit message to worker {client.id} (index {self.get_worker_index(client.id)})")
912
907
  self.server.send_to_client(Message("quit", None, client.id))
913
908
  gevent.sleep(0.5) # wait for final stats report from all workers
914
909
  self.greenlet.kill(block=True)
@@ -1128,14 +1123,14 @@ class MasterRunner(DistributedRunner):
1128
1123
  return len(self.clients.ready) + len(self.clients.spawning) + len(self.clients.running)
1129
1124
 
1130
1125
  @property
1131
- def reported_user_classes_count(self) -> Dict[str, int]:
1132
- reported_user_classes_count: Dict[str, int] = defaultdict(lambda: 0)
1126
+ def reported_user_classes_count(self) -> dict[str, int]:
1127
+ reported_user_classes_count: dict[str, int] = defaultdict(int)
1133
1128
  for client in self.clients.ready + self.clients.spawning + self.clients.running:
1134
1129
  for name, count in client.user_classes_count.items():
1135
1130
  reported_user_classes_count[name] += count
1136
1131
  return reported_user_classes_count
1137
1132
 
1138
- def send_message(self, msg_type: str, data: Optional[Dict[str, Any]] = None, client_id: Optional[str] = None):
1133
+ def send_message(self, msg_type: str, data: dict[str, Any] | None = None, client_id: str | None = None):
1139
1134
  """
1140
1135
  Sends a message to attached worker node(s)
1141
1136
 
@@ -1145,11 +1140,11 @@ class MasterRunner(DistributedRunner):
1145
1140
  If None, will send to all attached workers
1146
1141
  """
1147
1142
  if client_id:
1148
- logger.debug("Sending %s message to worker %s" % (msg_type, client_id))
1143
+ logger.debug(f"Sending {msg_type} message to worker {client_id}")
1149
1144
  self.server.send_to_client(Message(msg_type, data, client_id))
1150
1145
  else:
1151
1146
  for client in self.clients.all:
1152
- logger.debug("Sending %s message to worker %s" % (msg_type, client.id))
1147
+ logger.debug(f"Sending {msg_type} message to worker {client.id}")
1153
1148
  self.server.send_to_client(Message(msg_type, data, client.id))
1154
1149
 
1155
1150
 
@@ -1165,7 +1160,7 @@ class WorkerRunner(DistributedRunner):
1165
1160
  # the worker index is set on ACK, if master provided it (masters <= 2.10.2 do not provide it)
1166
1161
  worker_index = -1
1167
1162
 
1168
- def __init__(self, environment: "Environment", master_host: str, master_port: int) -> None:
1163
+ def __init__(self, environment: Environment, master_host: str, master_port: int) -> None:
1169
1164
  """
1170
1165
  :param environment: Environment instance
1171
1166
  :param master_host: Host/IP to use for connection to the master
@@ -1174,14 +1169,14 @@ class WorkerRunner(DistributedRunner):
1174
1169
  super().__init__(environment)
1175
1170
  self.retry = 0
1176
1171
  self.connected = False
1177
- self.last_heartbeat_timestamp: Optional[float] = None
1172
+ self.last_heartbeat_timestamp: float | None = None
1178
1173
  self.connection_event = Event()
1179
1174
  self.worker_state = STATE_INIT
1180
1175
  self.client_id = socket.gethostname() + "_" + uuid4().hex
1181
1176
  self.master_host = master_host
1182
1177
  self.master_port = master_port
1183
1178
  self.worker_cpu_warning_emitted = False
1184
- self._users_dispatcher: Optional[UsersDispatcher] = None
1179
+ self._users_dispatcher: UsersDispatcher | None = None
1185
1180
  self.client = rpc.Client(master_host, master_port, self.client_id)
1186
1181
  self.greenlet.spawn(self.worker).link_exception(greenlet_exception_handler)
1187
1182
  self.connect_to_master()
@@ -1204,14 +1199,14 @@ class WorkerRunner(DistributedRunner):
1204
1199
  self.environment.events.spawning_complete.add_listener(on_spawning_complete)
1205
1200
 
1206
1201
  # register listener that adds the current number of spawned users to the report that is sent to the master node
1207
- def on_report_to_master(client_id: str, data: Dict[str, Any]):
1202
+ def on_report_to_master(client_id: str, data: dict[str, Any]):
1208
1203
  data["user_classes_count"] = self.user_classes_count
1209
1204
  data["user_count"] = self.user_count
1210
1205
 
1211
1206
  self.environment.events.report_to_master.add_listener(on_report_to_master)
1212
1207
 
1213
1208
  # register listener that sends quit message to master
1214
- def on_quitting(environment: "Environment", **kw) -> None:
1209
+ def on_quitting(environment: Environment, **kw) -> None:
1215
1210
  self.client.send(Message("quit", None, self.client_id))
1216
1211
 
1217
1212
  self.environment.events.quitting.add_listener(on_quitting)
@@ -1224,11 +1219,11 @@ class WorkerRunner(DistributedRunner):
1224
1219
  self.environment.events.user_error.add_listener(on_user_error)
1225
1220
 
1226
1221
  def start(
1227
- self, user_count: int, spawn_rate: float, wait: bool = False, user_classes: Optional[List[Type[User]]] = None
1222
+ self, user_count: int, spawn_rate: float, wait: bool = False, user_classes: list[type[User]] | None = None
1228
1223
  ) -> None:
1229
1224
  raise NotImplementedError("use start_worker")
1230
1225
 
1231
- def start_worker(self, user_classes_count: Dict[str, int], **kwargs) -> None:
1226
+ def start_worker(self, user_classes_count: dict[str, int], **kwargs) -> None:
1232
1227
  """
1233
1228
  Start running a load test as a worker
1234
1229
 
@@ -1241,8 +1236,8 @@ class WorkerRunner(DistributedRunner):
1241
1236
  if self.environment.host:
1242
1237
  user_class.host = self.environment.host
1243
1238
 
1244
- user_classes_spawn_count: Dict[str, int] = {}
1245
- user_classes_stop_count: Dict[str, int] = {}
1239
+ user_classes_spawn_count: dict[str, int] = {}
1240
+ user_classes_stop_count: dict[str, int] = {}
1246
1241
 
1247
1242
  for user_class_name, user_class_count in user_classes_count.items():
1248
1243
  if self.user_classes_count[user_class_name] > user_class_count:
@@ -1378,9 +1373,7 @@ class WorkerRunner(DistributedRunner):
1378
1373
  logger.error(f"Temporary connection lost to master server: {e}, will retry later.")
1379
1374
  gevent.sleep(WORKER_REPORT_INTERVAL)
1380
1375
 
1381
- def send_message(
1382
- self, msg_type: str, data: Optional[Dict[str, Any]] = None, client_id: Optional[str] = None
1383
- ) -> None:
1376
+ def send_message(self, msg_type: str, data: dict[str, Any] | None = None, client_id: str | None = None) -> None:
1384
1377
  """
1385
1378
  Sends a message to master node
1386
1379
 
@@ -1392,7 +1385,7 @@ class WorkerRunner(DistributedRunner):
1392
1385
  self.client.send(Message(msg_type, data, self.client_id))
1393
1386
 
1394
1387
  def _send_stats(self) -> None:
1395
- data: Dict[str, Any] = {}
1388
+ data: dict[str, Any] = {}
1396
1389
  self.environment.events.report_to_master.fire(client_id=self.client_id, data=data)
1397
1390
  self.client.send(Message("stats", data, self.client_id))
1398
1391
 
@@ -1419,14 +1412,14 @@ class WorkerRunner(DistributedRunner):
1419
1412
  self.connected = True
1420
1413
 
1421
1414
 
1422
- def _format_user_classes_count_for_log(user_classes_count: Dict[str, int]) -> str:
1415
+ def _format_user_classes_count_for_log(user_classes_count: dict[str, int]) -> str:
1423
1416
  return "{} ({} total users)".format(
1424
1417
  json.dumps(dict(sorted(user_classes_count.items(), key=itemgetter(0)))),
1425
1418
  sum(user_classes_count.values()),
1426
1419
  )
1427
1420
 
1428
1421
 
1429
- def _aggregate_dispatched_users(d: Dict[str, Dict[str, int]]) -> Dict[str, int]:
1422
+ def _aggregate_dispatched_users(d: dict[str, dict[str, int]]) -> dict[str, int]:
1430
1423
  # TODO: Test it
1431
1424
  user_classes = list(next(iter(d.values())).keys())
1432
1425
  return {u: sum(d[u] for d in d.values()) for u in user_classes}
locust/shape.py CHANGED
@@ -1,11 +1,14 @@
1
1
  from __future__ import annotations
2
+
2
3
  import time
3
- from typing import ClassVar, Optional, Tuple, List, Type
4
4
  from abc import ABCMeta, abstractmethod
5
+ from typing import TYPE_CHECKING, ClassVar
5
6
 
6
- from . import User
7
7
  from .runners import Runner
8
8
 
9
+ if TYPE_CHECKING:
10
+ from . import User
11
+
9
12
 
10
13
  class LoadTestShapeMeta(ABCMeta):
11
14
  """
@@ -23,7 +26,7 @@ class LoadTestShape(metaclass=LoadTestShapeMeta):
23
26
  Base class for custom load shapes.
24
27
  """
25
28
 
26
- runner: Optional[Runner] = None
29
+ runner: Runner | None = None
27
30
  """Reference to the :class:`Runner <locust.runners.Runner>` instance"""
28
31
 
29
32
  abstract: ClassVar[bool] = True
@@ -52,7 +55,7 @@ class LoadTestShape(metaclass=LoadTestShapeMeta):
52
55
  return self.runner.user_count
53
56
 
54
57
  @abstractmethod
55
- def tick(self) -> Tuple[int, float] | Tuple[int, float, Optional[List[Type[User]]]] | None:
58
+ def tick(self) -> tuple[int, float] | tuple[int, float, list[type[User]] | None] | None:
56
59
  """
57
60
  Returns a tuple with 2 elements to control the running load test:
58
61