locust 2.32.2.dev44__py3-none-any.whl → 2.32.3__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.
locust/_version.py CHANGED
@@ -14,7 +14,7 @@ __version_tuple__: VERSION_TUPLE
14
14
  version_tuple: VERSION_TUPLE
15
15
 
16
16
 
17
- __version__ = "2.32.2.dev44"
17
+ __version__ = "2.32.3"
18
18
  version = __version__
19
- __version_tuple__ = (2, 32, 2, "dev44")
19
+ __version_tuple__ = (2, 32, 3)
20
20
  version_tuple = __version_tuple__
locust/argument_parser.py CHANGED
@@ -59,15 +59,18 @@ class LocustArgumentParser(configargparse.ArgumentParser):
59
59
  Arguments:
60
60
  include_in_web_ui: If True (default), the argument will show in the UI.
61
61
  is_secret: If True (default is False) and include_in_web_ui is True, the argument will show in the UI with a password masked text input.
62
+ is_required: If True (default is False) and include_in_web_ui is True, the argument will show in the UI as a required form field.
62
63
 
63
64
  Returns:
64
65
  argparse.Action: the new argparse action
65
66
  """
66
67
  include_in_web_ui = kwargs.pop("include_in_web_ui", True)
67
68
  is_secret = kwargs.pop("is_secret", False)
69
+ is_required = kwargs.pop("is_required", False)
68
70
  action = super().add_argument(*args, **kwargs)
69
71
  action.include_in_web_ui = include_in_web_ui
70
72
  action.is_secret = is_secret
73
+ action.is_required = is_required
71
74
  return action
72
75
 
73
76
  @property
@@ -82,6 +85,14 @@ class LocustArgumentParser(configargparse.ArgumentParser):
82
85
  if a.dest in self.args_included_in_web_ui and hasattr(a, "is_secret") and a.is_secret
83
86
  }
84
87
 
88
+ @property
89
+ def required_args_included_in_web_ui(self) -> dict[str, configargparse.Action]:
90
+ return {
91
+ a.dest: a
92
+ for a in self._actions
93
+ if a.dest in self.args_included_in_web_ui and hasattr(a, "is_required") and a.is_required
94
+ }
95
+
85
96
 
86
97
  class LocustTomlConfigParser(configargparse.TomlConfigParser):
87
98
  def parse(self, stream):
@@ -151,14 +162,18 @@ def download_locustfile_from_url(url: str) -> str:
151
162
  """
152
163
  try:
153
164
  response = requests.get(url)
154
- # Check if response is valid python code
155
- ast.parse(response.text)
156
165
  except requests.exceptions.RequestException as e:
157
166
  sys.stderr.write(f"Failed to get locustfile from: {url}. Exception: {e}")
158
167
  sys.exit(1)
159
- except SyntaxError:
160
- sys.stderr.write(f"Failed to get locustfile from: {url}. Response is not valid python code.")
161
- sys.exit(1)
168
+ else:
169
+ try:
170
+ # Check if response is valid python code
171
+ ast.parse(response.text)
172
+ except SyntaxError:
173
+ sys.stderr.write(
174
+ f"Failed to get locustfile from: {url}. Response was not valid python code: '{response.text[:100]}'"
175
+ )
176
+ sys.exit(1)
162
177
 
163
178
  with open(os.path.join(tempfile.gettempdir(), urlparse(url).path.split("/")[-1]), "w") as locustfile:
164
179
  locustfile.write(response.text)
@@ -798,6 +813,7 @@ def default_args_dict() -> dict:
798
813
  class UIExtraArgOptions(NamedTuple):
799
814
  default_value: str
800
815
  is_secret: bool
816
+ is_required: bool
801
817
  help_text: str
802
818
  choices: list[str] | None = None
803
819
 
@@ -813,6 +829,7 @@ def ui_extra_args_dict(args=None) -> dict[str, dict[str, Any]]:
813
829
  k: UIExtraArgOptions(
814
830
  default_value=v,
815
831
  is_secret=k in parser.secret_args_included_in_web_ui,
832
+ is_required=k in parser.required_args_included_in_web_ui,
816
833
  help_text=parser.args_included_in_web_ui[k].help,
817
834
  choices=parser.args_included_in_web_ui[k].choices,
818
835
  )._asdict()
locust/env.py CHANGED
@@ -295,8 +295,7 @@ class Environment:
295
295
  def _validate_shape_class_instance(self):
296
296
  if self.shape_class is not None and not isinstance(self.shape_class, LoadTestShape):
297
297
  raise ValueError(
298
- "shape_class should be instance of LoadTestShape or subclass LoadTestShape, but got: %s"
299
- % self.shape_class
298
+ f"shape_class should be instance of LoadTestShape or subclass LoadTestShape, but got: {self.shape_class}"
300
299
  )
301
300
 
302
301
  @property
locust/log.py CHANGED
@@ -92,10 +92,10 @@ def greenlet_exception_logger(logger, level=logging.CRITICAL):
92
92
  """
93
93
 
94
94
  def exception_handler(greenlet):
95
- if greenlet.exc_info[0] == SystemExit:
95
+ if greenlet.exc_info[0] is SystemExit:
96
96
  logger.log(
97
97
  min(logging.INFO, level), # dont use higher than INFO for this, because it sounds way to urgent
98
- "sys.exit(%s) called (use log level DEBUG for callstack)" % greenlet.exc_info[1],
98
+ f"sys.exit({greenlet.exc_info[1]}) called (use log level DEBUG for callstack)",
99
99
  )
100
100
  logger.log(logging.DEBUG, "Unhandled exception in greenlet: %s", greenlet, exc_info=greenlet.exc_info)
101
101
  else:
locust/main.py CHANGED
@@ -554,7 +554,7 @@ See https://github.com/locustio/locust/wiki/Installation#increasing-maximum-numb
554
554
  logger.info("--run-time limit reached, stopping test")
555
555
  runner.stop()
556
556
  if options.autoquit != -1:
557
- logger.debug("Autoquit time limit set to %s seconds" % options.autoquit)
557
+ logger.debug(f"Autoquit time limit set to {options.autoquit} seconds")
558
558
  time.sleep(options.autoquit)
559
559
  logger.info("--autoquit time reached, shutting down")
560
560
  runner.quit()
@@ -587,11 +587,18 @@ See https://github.com/locustio/locust/wiki/Installation#increasing-maximum-numb
587
587
  logger.error("Gave up waiting for workers to connect")
588
588
  runner.quit()
589
589
  sys.exit(1)
590
- logging.info(
591
- "Waiting for workers to be ready, %s of %s connected",
592
- len(runner.clients.ready),
593
- options.expect_workers,
594
- )
590
+ if time.monotonic() - start_time > 5:
591
+ logging.info(
592
+ "Waiting for workers to be ready, %s of %s connected",
593
+ len(runner.clients.ready),
594
+ options.expect_workers,
595
+ )
596
+ else:
597
+ logging.debug(
598
+ "Waiting for workers to be ready, %s of %s connected",
599
+ len(runner.clients.ready),
600
+ options.expect_workers,
601
+ )
595
602
  # TODO: Handle KeyboardInterrupt and send quit signal to workers that are started.
596
603
  # Right now, if the user sends a ctrl+c, the master will not gracefully
597
604
  # shutdown resulting in all the already started workers to stay active.
locust/runners.py CHANGED
@@ -217,7 +217,7 @@ class Runner:
217
217
  n += 1
218
218
  if n % 10 == 0 or n == spawn_count:
219
219
  logger.debug("%i users spawned" % self.user_count)
220
- logger.debug("All users of class %s spawned" % user_class)
220
+ logger.debug(f"All users of class {user_class} spawned")
221
221
  return new_users
222
222
 
223
223
  new_users: list[User] = []
@@ -248,7 +248,7 @@ class Runner:
248
248
  "While stopping users, we encountered a user that didn't have proper args %s", user_greenlet
249
249
  )
250
250
  continue
251
- if type(user) == self.user_classes_by_name[user_class]:
251
+ if type(user) is self.user_classes_by_name[user_class]:
252
252
  to_stop.append(user)
253
253
 
254
254
  if not to_stop:
@@ -256,7 +256,7 @@ class Runner:
256
256
 
257
257
  while True:
258
258
  user_to_stop: User = to_stop.pop()
259
- logger.debug("Stopping %s" % user_to_stop.greenlet.name)
259
+ logger.debug(f"Stopping {user_to_stop.greenlet.name}")
260
260
  if user_to_stop.greenlet is greenlet.getcurrent():
261
261
  # User called runner.quit(), so don't block waiting for killing to finish
262
262
  user_to_stop.group.killone(user_to_stop.greenlet, block=False)
@@ -272,8 +272,7 @@ class Runner:
272
272
 
273
273
  if not stop_group.join(timeout=self.environment.stop_timeout):
274
274
  logger.info(
275
- "Not all users finished their tasks & terminated in %s seconds. Stopping them..."
276
- % self.environment.stop_timeout
275
+ f"Not all users finished their tasks & terminated in {self.environment.stop_timeout} seconds. Stopping them..."
277
276
  )
278
277
  stop_group.kill(block=True)
279
278
 
@@ -495,7 +494,7 @@ class LocalRunner(Runner):
495
494
  user_classes_spawn_count: dict[str, int] = {}
496
495
  user_classes_stop_count: dict[str, int] = {}
497
496
  user_classes_count = dispatched_users[self._local_worker_node.id]
498
- logger.debug("Ramping to %s" % _format_user_classes_count_for_log(user_classes_count))
497
+ logger.debug(f"Ramping to {_format_user_classes_count_for_log(user_classes_count)}")
499
498
  for user_class_name, user_class_count in user_classes_count.items():
500
499
  if self.user_classes_count[user_class_name] > user_class_count:
501
500
  user_classes_stop_count[user_class_name] = (
@@ -558,7 +557,7 @@ class LocalRunner(Runner):
558
557
  :param msg_type: The type of the message to emulate sending
559
558
  :param data: Optional data to include
560
559
  """
561
- logger.debug("Running locally: sending %s message to self" % msg_type)
560
+ logger.debug(f"Running locally: sending {msg_type} message to self")
562
561
  if msg_type in self.custom_messages:
563
562
  listener, concurrent = self.custom_messages[msg_type]
564
563
  msg = Message(msg_type, data, "local")
@@ -877,7 +876,7 @@ class MasterRunner(DistributedRunner):
877
876
 
878
877
  if send_stop_to_client:
879
878
  for client in self.clients.all:
880
- logger.debug("Sending stop message to worker %s" % client.id)
879
+ logger.debug(f"Sending stop message to worker {client.id}")
881
880
  self.server.send_to_client(Message("stop", None, client.id))
882
881
 
883
882
  # Give an additional 60s for all workers to stop
@@ -987,8 +986,7 @@ class MasterRunner(DistributedRunner):
987
986
  logger.error(f"RPCError: {e}. Will reset RPC server.")
988
987
  else:
989
988
  logger.debug(
990
- "RPCError when receiving from worker: %s (but no workers were expected to be connected anyway)"
991
- % (e)
989
+ f"RPCError when receiving from worker: {e} (but no workers were expected to be connected anyway)"
992
990
  )
993
991
  self.connection_broken = True
994
992
  gevent.sleep(FALLBACK_INTERVAL)
@@ -1027,7 +1025,9 @@ class MasterRunner(DistributedRunner):
1027
1025
  # if abs(time() - msg.data["time"]) > 5.0:
1028
1026
  # warnings.warn("The worker node's clock seem to be out of sync. For the statistics to be correct the different locust servers need to have synchronized clocks.")
1029
1027
  elif msg.type == "locustfile":
1030
- if msg.data["version"][0:4] == __version__[0:4]:
1028
+ if not msg.data["version"]:
1029
+ logger.error("A very old worker version requested locustfile. This probably won't work.")
1030
+ elif msg.data["version"][0:4] == __version__[0:4]:
1031
1031
  logger.debug(
1032
1032
  f"A worker ({msg.node_id}) running a different patch version ({msg.data['version']}) connected, master version is {__version__}"
1033
1033
  )
@@ -1410,7 +1410,7 @@ class WorkerRunner(DistributedRunner):
1410
1410
  # master says we have finished spawning (happens only once during a normal rampup)
1411
1411
  self.environment.events.spawning_complete.fire(user_count=msg.data["user_count"])
1412
1412
  elif msg.type in self.custom_messages:
1413
- logger.debug("Received %s message from master" % msg.type)
1413
+ logger.debug(f"Received {msg.type} message from master")
1414
1414
  listener, concurrent = self.custom_messages[msg.type]
1415
1415
  if not concurrent:
1416
1416
  listener(environment=self.environment, msg=msg)
@@ -1454,7 +1454,7 @@ class WorkerRunner(DistributedRunner):
1454
1454
  :param data: Optional data to send
1455
1455
  :param client_id: (unused)
1456
1456
  """
1457
- logger.debug("Sending %s message to master" % msg_type)
1457
+ logger.debug(f"Sending {msg_type} message to master")
1458
1458
  self.client.send(Message(msg_type, data, self.client_id))
1459
1459
 
1460
1460
  def _send_stats(self) -> None:
@@ -12,7 +12,7 @@ def check_for_deprecated_task_set_attribute(class_dict):
12
12
  if issubclass(task_set, TaskSet) and not hasattr(task_set, "locust_task_weight"):
13
13
  warnings.warn(
14
14
  "Usage of User.task_set is deprecated since version 1.0. Set the tasks attribute instead "
15
- "(tasks = [%s])" % task_set.__name__,
15
+ f"(tasks = [{task_set.__name__}])",
16
16
  DeprecationWarning,
17
17
  )
18
18
 
locust/web.py CHANGED
@@ -60,6 +60,7 @@ class InputField(TypedDict, total=False):
60
60
  default_value: bool | None
61
61
  choices: list[str] | None
62
62
  is_secret: bool | None
63
+ is_required: bool | None
63
64
 
64
65
 
65
66
  class CustomForm(TypedDict, total=False):