locust 2.20.1.dev28__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.dev28.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.dev28.dist-info/RECORD +0 -102
  61. {locust-2.20.1.dev28.dist-info → locust-2.20.2.dist-info}/LICENSE +0 -0
  62. {locust-2.20.1.dev28.dist-info → locust-2.20.2.dist-info}/WHEEL +0 -0
  63. {locust-2.20.1.dev28.dist-info → locust-2.20.2.dist-info}/entry_points.txt +0 -0
  64. {locust-2.20.1.dev28.dist-info → locust-2.20.2.dist-info}/top_level.txt +0 -0
locust/__init__.py CHANGED
@@ -15,15 +15,15 @@ from gevent import monkey
15
15
  monkey.patch_all()
16
16
 
17
17
  from ._version import version as __version__
18
- from .user.sequential_taskset import SequentialTaskSet
19
- from .user import wait_time
20
- from .user.task import task, tag, TaskSet
21
- from .user.users import HttpUser, User
22
18
  from .contrib.fasthttp import FastHttpUser
23
- from .user.wait_time import between, constant, constant_pacing, constant_throughput
24
- from .shape import LoadTestShape
25
19
  from .debug import run_single_user
26
20
  from .event import Events
21
+ from .shape import LoadTestShape
22
+ from .user import wait_time
23
+ from .user.sequential_taskset import SequentialTaskSet
24
+ from .user.task import TaskSet, tag, task
25
+ from .user.users import HttpUser, User
26
+ from .user.wait_time import between, constant, constant_pacing, constant_throughput
27
27
 
28
28
  events = Events()
29
29
 
@@ -46,5 +46,5 @@ __all__ = (
46
46
  )
47
47
 
48
48
  # Used for raising a DeprecationWarning if old Locust/HttpLocust is used
49
- from .util.deprecation import DeprecatedLocustClass as Locust
50
49
  from .util.deprecation import DeprecatedHttpLocustClass as HttpLocust
50
+ from .util.deprecation import DeprecatedLocustClass as Locust
locust/_version.py CHANGED
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '2.20.1.dev28'
16
- __version_tuple__ = version_tuple = (2, 20, 1, 'dev28')
15
+ __version__ = version = '2.20.2'
16
+ __version_tuple__ = version_tuple = (2, 20, 2)
locust/argument_parser.py CHANGED
@@ -1,11 +1,14 @@
1
+ from __future__ import annotations
2
+
3
+ import locust
4
+
1
5
  import os
2
6
  import platform
3
7
  import sys
4
8
  import textwrap
5
- from typing import Dict, List, NamedTuple, Optional, Any
6
- import configargparse
9
+ from typing import Any, NamedTuple
7
10
 
8
- import locust
11
+ import configargparse
9
12
 
10
13
  version = locust.__version__
11
14
 
@@ -38,11 +41,11 @@ class LocustArgumentParser(configargparse.ArgumentParser):
38
41
  return action
39
42
 
40
43
  @property
41
- def args_included_in_web_ui(self) -> Dict[str, configargparse.Action]:
44
+ def args_included_in_web_ui(self) -> dict[str, configargparse.Action]:
42
45
  return {a.dest: a for a in self._actions if hasattr(a, "include_in_web_ui") and a.include_in_web_ui}
43
46
 
44
47
  @property
45
- def secret_args_included_in_web_ui(self) -> Dict[str, configargparse.Action]:
48
+ def secret_args_included_in_web_ui(self) -> dict[str, configargparse.Action]:
46
49
  return {
47
50
  a.dest: a
48
51
  for a in self._actions
@@ -91,7 +94,7 @@ def find_locustfile(locustfile):
91
94
  # Implicit 'return None' if nothing was found
92
95
 
93
96
 
94
- def find_locustfiles(locustfiles: List[str], is_directory: bool) -> List[str]:
97
+ def find_locustfiles(locustfiles: list[str], is_directory: bool) -> list[str]:
95
98
  """
96
99
  Returns a list of relative file paths for the Locustfile Picker. If is_directory is True,
97
100
  locustfiles is expected to have a single index which is a directory that will be searched for
@@ -176,7 +179,7 @@ See documentation for more details, including how to set options using a file or
176
179
  return parser
177
180
 
178
181
 
179
- def parse_locustfile_option(args=None) -> List[str]:
182
+ def parse_locustfile_option(args=None) -> list[str]:
180
183
  """
181
184
  Construct a command line parser that is only used to parse the -f argument so that we can
182
185
  import the test scripts in case any of them adds additional command line arguments to the
@@ -304,6 +307,13 @@ def setup_parser_arguments(parser):
304
307
  dest="list_commands",
305
308
  help="Show list of possible User classes and exit",
306
309
  )
310
+ parser.add_argument(
311
+ "--config-users",
312
+ type=str,
313
+ nargs="*",
314
+ help="User configuration as a JSON string or file. A list of arguments or an Array of JSON configuration may be provided",
315
+ env_var="LOCUST_CONFIG_USERS",
316
+ )
307
317
 
308
318
  web_ui_group = parser.add_argument_group("Web UI options")
309
319
  web_ui_group.add_argument(
@@ -355,9 +365,16 @@ def setup_parser_arguments(parser):
355
365
  dest="web_auth",
356
366
  metavar="<username:password>",
357
367
  default=None,
358
- help="DEPRECATED. See https://github.com/locustio/locust/issues/2517 Turn on Basic Auth for the web interface. Should be supplied in the following format: username:password ",
368
+ help=configargparse.SUPPRESS,
359
369
  env_var="LOCUST_WEB_AUTH",
360
370
  )
371
+ web_ui_group.add_argument(
372
+ "--web-login",
373
+ default=False,
374
+ action="store_true",
375
+ help="Protects the web interface with a login page. See https://docs.locust.io/en/stable/extending-locust.html#authentication",
376
+ env_var="LOCUST_WEB_LOGIN",
377
+ )
361
378
  web_ui_group.add_argument(
362
379
  "--tls-cert",
363
380
  default="",
@@ -429,6 +446,13 @@ def setup_parser_arguments(parser):
429
446
  help="How long should the master wait for workers to connect before giving up. Defaults to wait forever",
430
447
  env_var="LOCUST_EXPECT_WORKERS_MAX_WAIT",
431
448
  )
449
+ master_group.add_argument(
450
+ "--enable-rebalancing",
451
+ action="store_true",
452
+ default=False,
453
+ dest="enable_rebalancing",
454
+ help="Re-distribute users if new workers are added or removed during a test run. Experimental.",
455
+ )
432
456
  master_group.add_argument(
433
457
  "--expect-slaves",
434
458
  action="store_true",
@@ -611,13 +635,6 @@ Typically ONLY these options (and --locustfile) need to be specified on workers,
611
635
  dest="equal_weights",
612
636
  help="Use equally distributed task weights, overriding the weights specified in the locustfile.",
613
637
  )
614
- other_group.add_argument(
615
- "--enable-rebalancing",
616
- action="store_true",
617
- default=False,
618
- dest="enable_rebalancing",
619
- help="Allow to automatically rebalance users if new workers are added or removed during a test run.",
620
- )
621
638
 
622
639
  user_classes_group = parser.add_argument_group("User classes")
623
640
  user_classes_group.add_argument(
@@ -660,10 +677,10 @@ class UIExtraArgOptions(NamedTuple):
660
677
  default_value: str
661
678
  is_secret: bool
662
679
  help_text: str
663
- choices: Optional[List[str]] = None
680
+ choices: list[str] | None = None
664
681
 
665
682
 
666
- def ui_extra_args_dict(args=None) -> Dict[str, Dict[str, Any]]:
683
+ def ui_extra_args_dict(args=None) -> dict[str, dict[str, Any]]:
667
684
  """Get all the UI visible arguments"""
668
685
  locust_args = default_args_dict()
669
686
 
@@ -684,7 +701,7 @@ def ui_extra_args_dict(args=None) -> Dict[str, Dict[str, Any]]:
684
701
  return extra_args
685
702
 
686
703
 
687
- def locustfile_is_directory(locustfiles: List[str]) -> bool:
704
+ def locustfile_is_directory(locustfiles: list[str]) -> bool:
688
705
  """
689
706
  If a user passes in a locustfile without a file extension and there is a directory with the same name,
690
707
  this function defaults to using the file and will raise a warning.
locust/clients.py CHANGED
@@ -1,8 +1,9 @@
1
1
  from __future__ import annotations
2
+
2
3
  import re
3
4
  import time
4
5
  from contextlib import contextmanager
5
- from typing import Generator, Optional
6
+ from typing import Generator
6
7
  from urllib.parse import urlparse, urlunparse
7
8
 
8
9
  import requests
@@ -48,7 +49,7 @@ class HttpSession(requests.Session):
48
49
  and then mark it as successful even if the response code was not (i.e 500 or 404).
49
50
  """
50
51
 
51
- def __init__(self, base_url, request_event, user, *args, pool_manager: Optional[PoolManager] = None, **kwargs):
52
+ def __init__(self, base_url, request_event, user, *args, pool_manager: PoolManager | None = None, **kwargs):
52
53
  super().__init__(*args, **kwargs)
53
54
 
54
55
  self.base_url = base_url
@@ -57,7 +58,7 @@ class HttpSession(requests.Session):
57
58
 
58
59
  # User can group name, or use the group context manager to gather performance statistics under a specific name
59
60
  # This is an alternative to passing in the "name" parameter to the requests function
60
- self.request_name: Optional[str] = None
61
+ self.request_name: str | None = None
61
62
 
62
63
  # Check for basic authentication
63
64
  parsed_url = urlparse(self.base_url)
@@ -301,7 +302,7 @@ class ResponseContextManager(LocustResponse):
301
302
 
302
303
 
303
304
  class LocustHttpAdapter(HTTPAdapter):
304
- def __init__(self, pool_manager: Optional[PoolManager], *args, **kwargs):
305
+ def __init__(self, pool_manager: PoolManager | None, *args, **kwargs):
305
306
  self.poolmanager = pool_manager
306
307
  super().__init__(*args, **kwargs)
307
308
 
@@ -323,5 +324,5 @@ def _failure(self):
323
324
  )
324
325
 
325
326
 
326
- Response.success = _success
327
- Response.failure = _failure
327
+ Response.success = _success # type: ignore[attr-defined]
328
+ Response.failure = _failure # type: ignore[attr-defined]
@@ -1,35 +1,35 @@
1
1
  from __future__ import annotations
2
- import re
3
- import socket
2
+
3
+ from locust.env import Environment
4
+ from locust.exception import CatchResponseError, LocustError, ResponseError
5
+ from locust.user import User
6
+ from locust.util.deprecation import DeprecatedFastHttpLocustClass as FastHttpLocust
7
+
4
8
  import json
5
9
  import json as unshadowed_json # some methods take a named parameter called json
10
+ import re
11
+ import socket
12
+ import time
13
+ import traceback
6
14
  from base64 import b64encode
7
15
  from contextlib import contextmanager
16
+ from http.cookiejar import CookieJar
8
17
  from json.decoder import JSONDecodeError
9
- from urllib.parse import urlparse, urlunparse
10
18
  from ssl import SSLError
11
- import time
12
- import traceback
13
- from typing import Callable, Optional, Tuple, Dict, Any, Generator, cast
14
-
15
- from http.cookiejar import CookieJar
19
+ from typing import Any, Callable, Generator, cast
20
+ from urllib.parse import urlparse, urlunparse
16
21
 
17
22
  import gevent
23
+ from charset_normalizer import detect
18
24
  from gevent.timeout import Timeout
19
25
  from geventhttpclient._parser import HTTPParseError
20
26
  from geventhttpclient.client import HTTPClientPool
21
- from geventhttpclient.useragent import UserAgent, CompatRequest, CompatResponse, ConnectionError
22
- from geventhttpclient.response import HTTPConnectionClosed, HTTPSocketPoolResponse
23
27
  from geventhttpclient.header import Headers
28
+ from geventhttpclient.response import HTTPConnectionClosed, HTTPSocketPoolResponse
29
+ from geventhttpclient.useragent import CompatRequest, CompatResponse, ConnectionError, UserAgent
24
30
 
25
31
  # borrow requests's content-type header parsing
26
32
  from requests.utils import get_encoding_from_headers
27
- from charset_normalizer import detect
28
-
29
- from locust.user import User
30
- from locust.exception import LocustError, CatchResponseError, ResponseError
31
- from locust.env import Environment
32
- from locust.util.deprecation import DeprecatedFastHttpLocustClass as FastHttpLocust
33
33
 
34
34
  # Monkey patch geventhttpclient.useragent.CompatRequest so that Cookiejar works with Python >= 3.3
35
35
  # More info: https://github.com/requests/requests/pull/871
@@ -79,10 +79,10 @@ class FastHttpSession:
79
79
  self,
80
80
  environment: Environment,
81
81
  base_url: str,
82
- user: Optional[User],
82
+ user: User | None,
83
83
  insecure=True,
84
- client_pool: Optional[HTTPClientPool] = None,
85
- ssl_context_factory: Optional[Callable] = None,
84
+ client_pool: HTTPClientPool | None = None,
85
+ ssl_context_factory: Callable | None = None,
86
86
  **kwargs,
87
87
  ):
88
88
  self.environment = environment
@@ -313,7 +313,7 @@ class FastHttpUser(User):
313
313
  insecure: bool = True
314
314
  """Parameter passed to FastHttpSession. Default True, meaning no SSL verification."""
315
315
 
316
- default_headers: Optional[dict] = None
316
+ default_headers: dict | None = None
317
317
  """Parameter passed to FastHttpSession. Adds the listed headers to every request."""
318
318
 
319
319
  concurrency: int = 10
@@ -321,10 +321,10 @@ class FastHttpUser(User):
321
321
  Note that setting this value has no effect when custom client_pool was given, and you need to spawn a your own gevent pool
322
322
  to use it (as Users only have one greenlet). See test_fasthttp.py / test_client_pool_concurrency for an example."""
323
323
 
324
- client_pool: Optional[HTTPClientPool] = None
324
+ client_pool: HTTPClientPool | None = None
325
325
  """HTTP client pool to use. If not given, a new pool is created per single user."""
326
326
 
327
- ssl_context_factory: Optional[Callable] = None
327
+ ssl_context_factory: Callable | None = None
328
328
  """A callable that return a SSLContext for overriding the default context created by the FastHttpSession."""
329
329
 
330
330
  abstract = True
@@ -360,7 +360,7 @@ class FastHttpUser(User):
360
360
 
361
361
  @contextmanager
362
362
  def rest(
363
- self, method, url, headers: Optional[dict] = None, **kwargs
363
+ self, method, url, headers: dict | None = None, **kwargs
364
364
  ) -> Generator[RestResponseContextManager, None, None]:
365
365
  """
366
366
  A wrapper for self.client.request that:
@@ -423,36 +423,36 @@ class FastHttpUser(User):
423
423
 
424
424
 
425
425
  class FastRequest(CompatRequest):
426
- payload: Optional[str] = None
426
+ payload: str | None = None
427
427
 
428
428
  @property
429
- def body(self) -> Optional[str]:
429
+ def body(self) -> str | None:
430
430
  return self.payload
431
431
 
432
432
 
433
433
  class FastResponse(CompatResponse):
434
- headers: Optional[Headers] = None
434
+ headers: Headers | None = None
435
435
  """Dict like object containing the response headers"""
436
436
 
437
- _response: Optional[HTTPSocketPoolResponse] = None
437
+ _response: HTTPSocketPoolResponse | None = None
438
438
 
439
- encoding: Optional[str] = None
439
+ encoding: str | None = None
440
440
  """In some cases setting the encoding explicitly is needed. If so, do it before calling .text"""
441
441
 
442
- request: Optional[FastRequest] = None
442
+ request: FastRequest | None = None
443
443
 
444
444
  def __init__(
445
445
  self,
446
446
  ghc_response: HTTPSocketPoolResponse,
447
- request: Optional[FastRequest] = None,
448
- sent_request: Optional[str] = None,
447
+ request: FastRequest | None = None,
448
+ sent_request: str | None = None,
449
449
  ):
450
450
  super().__init__(ghc_response, request, sent_request)
451
451
 
452
452
  self.request = request
453
453
 
454
454
  @property
455
- def text(self) -> Optional[str]:
455
+ def text(self) -> str | None:
456
456
  """
457
457
  Returns the text content of the response as a decoded string
458
458
  """
@@ -472,7 +472,7 @@ class FastResponse(CompatResponse):
472
472
  return str(self.content, str(self.encoding), errors="replace")
473
473
 
474
474
  @property
475
- def url(self) -> Optional[str]:
475
+ def url(self) -> str | None:
476
476
  """
477
477
  Get "response" URL, which is the same as the request URL. This is a small deviation from HttpSession, which gets the final (possibly redirected) URL.
478
478
  """
@@ -500,6 +500,11 @@ class FastResponse(CompatResponse):
500
500
  """
501
501
  return self._response.get_code() if self._response is not None else 0
502
502
 
503
+ @property
504
+ def ok(self):
505
+ """Returns True if :attr:`status_code` is less than 400, False if not."""
506
+ return self.status_code < 400
507
+
503
508
  def _content(self):
504
509
  if self.headers is None:
505
510
  return None
@@ -522,11 +527,11 @@ class ErrorResponse:
522
527
  that doesn't have a real Response object attached. E.g. a socket error or similar
523
528
  """
524
529
 
525
- headers: Optional[Headers] = None
530
+ headers: Headers | None = None
526
531
  content = None
527
532
  status_code = 0
528
- error: Optional[Exception] = None
529
- text: Optional[str] = None
533
+ error: Exception | None = None
534
+ text: str | None = None
530
535
  request: CompatRequest
531
536
 
532
537
  def __init__(self, url: str, request: CompatRequest):
@@ -542,7 +547,7 @@ class LocustUserAgent(UserAgent):
542
547
  request_type = FastRequest
543
548
  valid_response_codes = frozenset([200, 201, 202, 203, 204, 205, 206, 207, 208, 226, 301, 302, 303, 304, 307])
544
549
 
545
- def __init__(self, client_pool: Optional[HTTPClientPool] = None, **kwargs):
550
+ def __init__(self, client_pool: HTTPClientPool | None = None, **kwargs):
546
551
  super().__init__(**kwargs)
547
552
 
548
553
  if client_pool is not None:
locust/debug.py CHANGED
@@ -1,13 +1,19 @@
1
- from datetime import datetime, timezone
2
- import os
3
- import inspect
1
+ from __future__ import annotations
2
+
4
3
  import locust
5
4
  import locust.log
6
- from locust import User, argument_parser
7
- from typing import Type, Optional
5
+ from locust import argument_parser
8
6
  from locust.env import Environment
9
7
  from locust.exception import CatchResponseError, RescheduleTask
10
8
 
9
+ import inspect
10
+ import os
11
+ from datetime import datetime, timezone
12
+ from typing import TYPE_CHECKING
13
+
14
+ if TYPE_CHECKING:
15
+ from locust import User
16
+
11
17
 
12
18
  def _print_t(s):
13
19
  """
@@ -94,16 +100,16 @@ class PrintListener:
94
100
  print()
95
101
 
96
102
 
97
- _env: Optional[Environment] = None # minimal Environment for debugging
103
+ _env: Environment | None = None # minimal Environment for debugging
98
104
 
99
105
 
100
106
  def run_single_user(
101
- user_class: Type[User],
107
+ user_class: type[User],
102
108
  include_length=False,
103
109
  include_time=False,
104
110
  include_context=False,
105
111
  include_payload=False,
106
- loglevel: Optional[str] = "WARNING",
112
+ loglevel: str | None = "WARNING",
107
113
  ):
108
114
  """
109
115
  Runs a single User. Useful when you want to run a debugger.
locust/dispatch.py CHANGED
@@ -1,19 +1,19 @@
1
- from collections import defaultdict
1
+ from __future__ import annotations
2
+
2
3
  import contextlib
3
4
  import itertools
4
5
  import math
5
6
  import time
7
+ from collections import defaultdict
6
8
  from collections.abc import Iterator
7
9
  from operator import attrgetter
8
- from typing import Dict, Generator, List, TYPE_CHECKING, Optional, Tuple, Type, Set
10
+ from typing import TYPE_CHECKING, Generator
9
11
 
10
12
  import gevent
11
-
12
13
  from roundrobin import smooth
13
14
 
14
- from locust import User
15
-
16
15
  if TYPE_CHECKING:
16
+ from locust import User
17
17
  from locust.runners import WorkerNode
18
18
 
19
19
 
@@ -49,7 +49,7 @@ class UsersDispatcher(Iterator):
49
49
  from 10 to 100.
50
50
  """
51
51
 
52
- def __init__(self, worker_nodes: List["WorkerNode"], user_classes: List[Type[User]]):
52
+ def __init__(self, worker_nodes: list[WorkerNode], user_classes: list[type[User]]):
53
53
  """
54
54
  :param worker_nodes: List of worker nodes
55
55
  :param user_classes: The user classes
@@ -79,16 +79,16 @@ class UsersDispatcher(Iterator):
79
79
 
80
80
  self._current_user_count = self.get_current_user_count()
81
81
 
82
- self._dispatcher_generator: Generator[Dict[str, Dict[str, int]], None, None] = None
82
+ self._dispatcher_generator: Generator[dict[str, dict[str, int]], None, None] = None
83
83
 
84
84
  self._user_generator = self._user_gen()
85
85
 
86
86
  self._worker_node_generator = itertools.cycle(self._worker_nodes)
87
87
 
88
88
  # To keep track of how long it takes for each dispatch iteration to compute
89
- self._dispatch_iteration_durations: List[float] = []
89
+ self._dispatch_iteration_durations: list[float] = []
90
90
 
91
- self._active_users: List[Tuple[WorkerNode, str]] = []
91
+ self._active_users: list[tuple[WorkerNode, str]] = []
92
92
 
93
93
  # TODO: Test that attribute is set when dispatching and unset when done dispatching
94
94
  self._dispatch_in_progress = False
@@ -108,10 +108,10 @@ class UsersDispatcher(Iterator):
108
108
  return self._dispatch_in_progress
109
109
 
110
110
  @property
111
- def dispatch_iteration_durations(self) -> List[float]:
111
+ def dispatch_iteration_durations(self) -> list[float]:
112
112
  return self._dispatch_iteration_durations
113
113
 
114
- def __next__(self) -> Dict[str, Dict[str, int]]:
114
+ def __next__(self) -> dict[str, dict[str, int]]:
115
115
  users_on_workers = next(self._dispatcher_generator)
116
116
  # TODO: Is this necessary to copy the users_on_workers if we know
117
117
  # it won't be mutated by external code?
@@ -122,7 +122,7 @@ class UsersDispatcher(Iterator):
122
122
  worker_nodes_by_id = sorted(self._worker_nodes, key=lambda w: w.id)
123
123
 
124
124
  # Give every worker an index indicating how many workers came before it on that host
125
- workers_per_host = defaultdict(lambda: 0)
125
+ workers_per_host = defaultdict(int)
126
126
  for worker_node in worker_nodes_by_id:
127
127
  host = worker_node.id.split("_")[0]
128
128
  worker_node._index_within_host = workers_per_host[host]
@@ -131,7 +131,7 @@ class UsersDispatcher(Iterator):
131
131
  # Sort again, first by index within host, to ensure Users get started evenly across hosts
132
132
  self._worker_nodes = sorted(self._worker_nodes, key=lambda worker: (worker._index_within_host, worker.id))
133
133
 
134
- def _dispatcher(self) -> Generator[Dict[str, Dict[str, int]], None, None]:
134
+ def _dispatcher(self) -> Generator[dict[str, dict[str, int]], None, None]:
135
135
  self._dispatch_in_progress = True
136
136
 
137
137
  if self._rebalance:
@@ -164,7 +164,7 @@ class UsersDispatcher(Iterator):
164
164
 
165
165
  self._dispatch_in_progress = False
166
166
 
167
- def new_dispatch(self, target_user_count: int, spawn_rate: float, user_classes: Optional[List] = None) -> None:
167
+ def new_dispatch(self, target_user_count: int, spawn_rate: float, user_classes: list | None = None) -> None:
168
168
  """
169
169
  Initialize a new dispatch cycle.
170
170
 
@@ -194,7 +194,7 @@ class UsersDispatcher(Iterator):
194
194
 
195
195
  self._dispatch_iteration_durations.clear()
196
196
 
197
- def add_worker(self, worker_node: "WorkerNode") -> None:
197
+ def add_worker(self, worker_node: WorkerNode) -> None:
198
198
  """
199
199
  This method is to be called when a new worker connects to the master. When
200
200
  a new worker is added, the users dispatcher will flag that a rebalance is required
@@ -207,7 +207,7 @@ class UsersDispatcher(Iterator):
207
207
  self._sort_workers()
208
208
  self._prepare_rebalance()
209
209
 
210
- def remove_worker(self, worker_node: "WorkerNode") -> None:
210
+ def remove_worker(self, worker_node: WorkerNode) -> None:
211
211
  """
212
212
  This method is similar to the above `add_worker`. When a worker disconnects
213
213
  (because of e.g. network failure, worker failure, etc.), this method will ensure that the next
@@ -268,7 +268,7 @@ class UsersDispatcher(Iterator):
268
268
  sleep_duration = max(0.0, self._wait_between_dispatch - delta)
269
269
  gevent.sleep(sleep_duration)
270
270
 
271
- def _add_users_on_workers(self) -> Dict[str, Dict[str, int]]:
271
+ def _add_users_on_workers(self) -> dict[str, dict[str, int]]:
272
272
  """Add users on the workers until the target number of users is reached for the current dispatch iteration
273
273
 
274
274
  :return: The users that we want to run on the workers
@@ -290,7 +290,7 @@ class UsersDispatcher(Iterator):
290
290
 
291
291
  return self._users_on_workers
292
292
 
293
- def _remove_users_from_workers(self) -> Dict[str, Dict[str, int]]:
293
+ def _remove_users_from_workers(self) -> dict[str, dict[str, int]]:
294
294
  """Remove users from the workers until the target number of users is reached for the current dispatch iteration
295
295
 
296
296
  :return: The users that we want to run on the workers
@@ -318,8 +318,8 @@ class UsersDispatcher(Iterator):
318
318
 
319
319
  def _distribute_users(
320
320
  self, target_user_count: int
321
- ) -> Tuple[
322
- Dict[str, Dict[str, int]], Generator[Optional[str], None, None], itertools.cycle, List[Tuple["WorkerNode", str]]
321
+ ) -> tuple[
322
+ dict[str, dict[str, int]], Generator[str | None, None, None], itertools.cycle, list[tuple[WorkerNode, str]]
323
323
  ]:
324
324
  """
325
325
  This function might take some time to complete if the `target_user_count` is a big number. A big number
@@ -349,7 +349,7 @@ class UsersDispatcher(Iterator):
349
349
 
350
350
  return users_on_workers, user_gen, worker_gen, active_users
351
351
 
352
- def _user_gen(self) -> Generator[Optional[str], None, None]:
352
+ def _user_gen(self) -> Generator[str | None, None, None]:
353
353
  """
354
354
  This method generates users according to their weights using
355
355
  a smooth weighted round-robin algorithm implemented by https://github.com/linnik/roundrobin.
@@ -361,7 +361,7 @@ class UsersDispatcher(Iterator):
361
361
  less accurate during ramp-up/down.
362
362
  """
363
363
 
364
- def infinite_cycle_gen(users: List[Tuple[Type[User], int]]) -> itertools.cycle:
364
+ def infinite_cycle_gen(users: list[tuple[type[User], int]]) -> itertools.cycle:
365
365
  if not users:
366
366
  return itertools.cycle([None])
367
367
 
@@ -401,9 +401,9 @@ class UsersDispatcher(Iterator):
401
401
  if self._try_dispatch_fixed:
402
402
  self._try_dispatch_fixed = False
403
403
  current_fixed_users_count = {u: self._get_user_current_count(u) for u in fixed_users}
404
- spawned_classes: Set[str] = set()
404
+ spawned_classes: set[str] = set()
405
405
  while len(spawned_classes) != len(fixed_users):
406
- user_name: Optional[str] = next(cycle_fixed_gen)
406
+ user_name: str | None = next(cycle_fixed_gen)
407
407
  if not user_name:
408
408
  break
409
409
 
@@ -422,7 +422,7 @@ class UsersDispatcher(Iterator):
422
422
  yield next(cycle_weighted_gen)
423
423
 
424
424
  @staticmethod
425
- def _fast_users_on_workers_copy(users_on_workers: Dict[str, Dict[str, int]]) -> Dict[str, Dict[str, int]]:
425
+ def _fast_users_on_workers_copy(users_on_workers: dict[str, dict[str, int]]) -> dict[str, dict[str, int]]:
426
426
  """deepcopy is too slow, so we use this custom copy function.
427
427
 
428
428
  The implementation was profiled and compared to other implementations such as dict-comprehensions