salabim 24.0.12__tar.gz → 24.0.13__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
Files changed (26) hide show
  1. {salabim-24.0.12 → salabim-24.0.13}/PKG-INFO +1 -1
  2. {salabim-24.0.12 → salabim-24.0.13}/pyproject.toml +1 -1
  3. {salabim-24.0.12 → salabim-24.0.13}/salabim/salabim.py +270 -44
  4. {salabim-24.0.12 → salabim-24.0.13}/salabim.egg-info/PKG-INFO +1 -1
  5. {salabim-24.0.12 → salabim-24.0.13}/tests/test_componentgenerator.py +24 -2
  6. {salabim-24.0.12 → salabim-24.0.13}/README.md +0 -0
  7. {salabim-24.0.12 → salabim-24.0.13}/salabim/DejaVuSansMono.ttf +0 -0
  8. {salabim-24.0.12 → salabim-24.0.13}/salabim/LICENSE.txt +0 -0
  9. {salabim-24.0.12 → salabim-24.0.13}/salabim/__init__.py +0 -0
  10. {salabim-24.0.12 → salabim-24.0.13}/salabim/calibri.ttf +0 -0
  11. {salabim-24.0.12 → salabim-24.0.13}/salabim/mplus-1m-regular.ttf +0 -0
  12. {salabim-24.0.12 → salabim-24.0.13}/salabim.egg-info/SOURCES.txt +0 -0
  13. {salabim-24.0.12 → salabim-24.0.13}/salabim.egg-info/dependency_links.txt +0 -0
  14. {salabim-24.0.12 → salabim-24.0.13}/salabim.egg-info/top_level.txt +0 -0
  15. {salabim-24.0.12 → salabim-24.0.13}/setup.cfg +0 -0
  16. {salabim-24.0.12 → salabim-24.0.13}/tests/test salabim.py +0 -0
  17. {salabim-24.0.12 → salabim-24.0.13}/tests/test_cap_now.py +0 -0
  18. {salabim-24.0.12 → salabim-24.0.13}/tests/test_datetime.py +0 -0
  19. {salabim-24.0.12 → salabim-24.0.13}/tests/test_distributions.py +0 -0
  20. {salabim-24.0.12 → salabim-24.0.13}/tests/test_misc.py +0 -0
  21. {salabim-24.0.12 → salabim-24.0.13}/tests/test_monitor.py +0 -0
  22. {salabim-24.0.12 → salabim-24.0.13}/tests/test_process.py +0 -0
  23. {salabim-24.0.12 → salabim-24.0.13}/tests/test_queue.py +0 -0
  24. {salabim-24.0.12 → salabim-24.0.13}/tests/test_state.py +0 -0
  25. {salabim-24.0.12 → salabim-24.0.13}/tests/test_store.py +0 -0
  26. {salabim-24.0.12 → salabim-24.0.13}/tests/test_timeunit.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: salabim
3
- Version: 24.0.12
3
+ Version: 24.0.13
4
4
  Summary: salabim - discrete event simulation in Python
5
5
  Author-email: Ruud van der Ham <rt.van.der.ham@gmail.com>
6
6
  Project-URL: Homepage, https://salabim.org
@@ -8,7 +8,7 @@ authors = [
8
8
  {name = "Ruud van der Ham", email = "rt.van.der.ham@gmail.com"}
9
9
  ]
10
10
  description = "salabim - discrete event simulation in Python"
11
- version = "24.0.12"
11
+ version = "24.0.13"
12
12
  readme = "README.md"
13
13
  requires-python = ">=3.7"
14
14
  dependencies = [
@@ -1,13 +1,13 @@
1
- # _ _ _ ____ _ _ ___ _ ____
2
- # ___ __ _ | | __ _ | |__ (_) _ __ ___ |___ \ | || | / _ \ / ||___ \
3
- # / __| / _` || | / _` || '_ \ | || '_ ` _ \ __) || || |_ | | | | | | __) |
4
- # \__ \| (_| || || (_| || |_) || || | | | | | / __/ |__ _| _ | |_| | _ | | / __/
5
- # |___/ \__,_||_| \__,_||_.__/ |_||_| |_| |_| |_____| |_| (_) \___/ (_)|_||_____|
1
+ # _ _ _ ____ _ _ ___ _ _____
2
+ # ___ __ _ | | __ _ | |__ (_) _ __ ___ |___ \ | || | / _ \ / ||___ /
3
+ # / __| / _` || | / _` || '_ \ | || '_ ` _ \ __) || || |_ | | | | | | |_ \
4
+ # \__ \| (_| || || (_| || |_) || || | | | | | / __/ |__ _| _ | |_| | _ | | ___) |
5
+ # |___/ \__,_||_| \__,_||_.__/ |_||_| |_| |_| |_____| |_| (_) \___/ (_)|_||____/
6
6
  # discrete event simulation
7
7
  #
8
8
  # see www.salabim.org for more information, the documentation and license information
9
9
 
10
- __version__ = "24.0.12"
10
+ __version__ = "24.0.13"
11
11
 
12
12
  import heapq
13
13
  import random
@@ -60,7 +60,7 @@ PyDroid = sys.platform == "linux" and any("pydroid" in v for v in os.environ.val
60
60
  PyPy = platform.python_implementation() == "PyPy"
61
61
  Chromebook = "penguin" in platform.uname()
62
62
  PythonInExcel = not ("__file__" in globals())
63
- AnacondaCode = sys.platform=="emscripten"
63
+ AnacondaCode = sys.platform == "emscripten"
64
64
 
65
65
 
66
66
  def a_log(*args):
@@ -154,8 +154,9 @@ nan = float("nan")
154
154
  if Pythonista or AnacondaCode or PythonInExcel:
155
155
  _yieldless = False
156
156
  else:
157
- _yieldless=True
158
-
157
+ _yieldless = True
158
+
159
+
159
160
  class QueueFullError(Exception):
160
161
  pass
161
162
 
@@ -4144,7 +4145,7 @@ if Pythonista:
4144
4145
  try:
4145
4146
  ims = scene.load_pil_image(capture_image)
4146
4147
  except SystemError:
4147
- im_file = "temp.png" # hack for Pythonista 3.4 ***
4148
+ im_file = "temp.png" # hack for Pythonista 3.4
4148
4149
  capture_image.save(im_file, "PNG")
4149
4150
  ims = scene.load_image_file(im_file)
4150
4151
  scene.image(ims, 0, 0, *capture_image.size)
@@ -5717,7 +5718,7 @@ class Queue:
5717
5718
 
5718
5719
  class Store(Queue):
5719
5720
  def __init__(self, name: str = None, monitor: Any = True, fill: Iterable = None, capacity: float = inf, env: "Environment" = None, *args, **kwargs) -> None:
5720
- super().__init__(name=name, monitor=monitor,fill=None,capacity=capacity, env=env, *args, **kwargs)
5721
+ super().__init__(name=name, monitor=monitor, fill=None, capacity=capacity, env=env, *args, **kwargs)
5721
5722
 
5722
5723
  with self.env.suppress_trace():
5723
5724
  self._to_store_requesters = Queue(f"{name}.to_store_requesters", env=env)
@@ -7981,6 +7982,10 @@ by adding:
7981
7982
  ----
7982
7983
  Only if yieldless is False: if to be used for the current component, use ``yield self.cancel()``.
7983
7984
  """
7985
+ if self.status.value == data:
7986
+ if self.env._trace:
7987
+ self.env.print_trace("", "", "cancel (on data component) " + self.name() + " " + self._modetxt())
7988
+ return
7984
7989
  if self.status.value != current:
7985
7990
  self._checkisnotdata()
7986
7991
  self._remove()
@@ -9048,10 +9053,8 @@ by adding:
9048
9053
  self.status._value = waiting
9049
9054
  self._reschedule(scheduled_time, schedule_priority, urgent, "wait", cap_now)
9050
9055
  else:
9051
- return # ***
9052
- if self.env._yieldless:
9053
- if self is self.env._current_component:
9054
- self.env._glet.switch()
9056
+ return
9057
+
9055
9058
 
9056
9059
  def _trywait(self):
9057
9060
  if self.status.value == interrupted:
@@ -10000,6 +10003,211 @@ by adding:
10000
10003
  return None
10001
10004
 
10002
10005
 
10006
+ class Event(Component):
10007
+ """
10008
+ Event object
10009
+
10010
+ An event object is a specialized Component that is usually not subclassed.
10011
+
10012
+ Apart from the usual Component parameters it has an action parameter, to specifies what should
10013
+ happen after becoming active. This action is usually a lambda function.
10014
+
10015
+ Parameters
10016
+ ----------
10017
+ action : callable
10018
+ function called when the component becomes current.
10019
+
10020
+ action_string : str
10021
+ string to be printed in trace when action gets executed (default: "action")
10022
+
10023
+ name : str
10024
+ name of the component.
10025
+
10026
+ if the name ends with a period (.),
10027
+ auto serializing will be applied
10028
+
10029
+ if the name end with a comma,
10030
+ auto serializing starting at 1 will be applied
10031
+
10032
+ if omitted, the name will be derived from the class
10033
+ it is defined in (lowercased)
10034
+
10035
+ at : float or distribution
10036
+ schedule time
10037
+
10038
+ if omitted, now is used
10039
+
10040
+ if distribution, the distribution is sampled
10041
+
10042
+ delay : float or distributiom
10043
+ schedule with a delay
10044
+
10045
+ if omitted, no delay
10046
+
10047
+ if distribution, the distribution is sampled
10048
+
10049
+ priority : float
10050
+ priority
10051
+
10052
+ default: 0
10053
+
10054
+ if a component has the same time on the event list, this component is sorted accoring to
10055
+ the priority.
10056
+
10057
+ urgent : bool
10058
+ urgency indicator
10059
+
10060
+ if False (default), the component will be scheduled
10061
+ behind all other components scheduled
10062
+ for the same time and priority
10063
+
10064
+ if True, the component will be scheduled
10065
+ in front of all components scheduled
10066
+ for the same time and priority
10067
+
10068
+ suppress_trace : bool
10069
+ suppress_trace indicator
10070
+
10071
+ if True, this component will be excluded from the trace
10072
+
10073
+ If False (default), the component will be traced
10074
+
10075
+ Can be queried or set later with the suppress_trace method.
10076
+
10077
+ suppress_pause_at_step : bool
10078
+ suppress_pause_at_step indicator
10079
+
10080
+ if True, if this component becomes current, do not pause when stepping
10081
+
10082
+ If False (default), the component will be paused when stepping
10083
+
10084
+ Can be queried or set later with the suppress_pause_at_step method.
10085
+
10086
+ skip_standby : bool
10087
+ skip_standby indicator
10088
+
10089
+ if True, after this component became current, do not activate standby components
10090
+
10091
+ If False (default), after the component became current activate standby components
10092
+
10093
+ Can be queried or set later with the skip_standby method.
10094
+
10095
+ mode : str preferred
10096
+ mode
10097
+
10098
+ will be used in trace and can be used in animations
10099
+
10100
+ if omitted, the mode will be "".
10101
+
10102
+ also mode_time will be set to now.
10103
+
10104
+ cap_now : bool
10105
+ indicator whether times (at, delay) in the past are allowed. If, so now() will be used.
10106
+ default: sys.default_cap_now(), usualy False
10107
+
10108
+ env : Environment
10109
+ environment where the component is defined
10110
+
10111
+ if omitted, default_env will be used
10112
+ """
10113
+
10114
+ def __init__(
10115
+ self,
10116
+ action: Callable,
10117
+ action_string="action",
10118
+ name: str = None,
10119
+ at: Union[float, Callable] = None,
10120
+ delay: Union[float, Callable] = None,
10121
+ priority: float = None,
10122
+ urgent: bool = None,
10123
+ suppress_trace: bool = False,
10124
+ suppress_pause_at_step: bool = False,
10125
+ skip_standby: bool = False,
10126
+ mode: str = "",
10127
+ cap_now: bool = None,
10128
+ env: "Environment" = None,
10129
+ **kwargs,
10130
+ ):
10131
+ self._action = action
10132
+ self._action_string = action_string
10133
+ self._action_taken = False
10134
+ if env is None:
10135
+ env = g.default_env
10136
+ super().__init__(
10137
+ name=name,
10138
+ at=at,
10139
+ delay=delay,
10140
+ priority=priority,
10141
+ urgent=urgent,
10142
+ suppress_trace=suppress_trace,
10143
+ suppress_pause_at_step=suppress_pause_at_step,
10144
+ skip_standby=skip_standby,
10145
+ mode=mode,
10146
+ cap_now=cap_now,
10147
+ env=env,
10148
+ process="process" if env._yieldless else "process_yield",
10149
+ **kwargs,
10150
+ )
10151
+
10152
+ def process_yield(self):
10153
+ self.env.print_trace("", "", self._action_string, "")
10154
+ self._action()
10155
+ self._action_taken = True
10156
+ return
10157
+ yield 1 # just to make it a generator
10158
+
10159
+ def process(self):
10160
+ self.env.print_trace("", "", self._action_string, "")
10161
+ self._action()
10162
+ self._action_taken = True
10163
+
10164
+ def action(self, value=None):
10165
+ """
10166
+ action
10167
+
10168
+ Parameters
10169
+ ----------
10170
+ value : callable
10171
+ new action callable
10172
+
10173
+ Returns
10174
+ -------
10175
+ current action : callable
10176
+ """
10177
+ if value is not None:
10178
+ self._action = value
10179
+ return self._action
10180
+
10181
+ def action_string(self, value=None):
10182
+ """
10183
+ action_string
10184
+
10185
+ Parameters
10186
+ ----------
10187
+ value : string
10188
+ new action_string
10189
+
10190
+ Returns
10191
+ -------
10192
+ current action_string : string
10193
+ """
10194
+
10195
+ if value is not None:
10196
+ self._action_string = value
10197
+ return self._action_string
10198
+
10199
+ def action_taken(self):
10200
+ """
10201
+ action_taken
10202
+
10203
+ Returns
10204
+ -------
10205
+ action taken: bool
10206
+ True if action has been taken, False if not
10207
+ """
10208
+ return self._action_taken
10209
+
10210
+
10003
10211
  class Environment:
10004
10212
  """
10005
10213
  environment object
@@ -10791,7 +10999,7 @@ class Environment:
10791
10999
  if c.overridden_lineno:
10792
11000
  self.print_trace(self.time_to_str(self._now - self._offset), c.name(), "current", s0=un_na(c.overridden_lineno))
10793
11001
  else:
10794
- self.print_trace(self.time_to_str(self._now - self._offset), c.name(), "current", s0=un_na(c.lineno_txt())) # ***
11002
+ self.print_trace(self.time_to_str(self._now - self._offset), c.name(), "current", s0=un_na(c.lineno_txt()))
10795
11003
  if c == self._main:
10796
11004
  self.running = False
10797
11005
  return
@@ -11506,7 +11714,7 @@ class Environment:
11506
11714
  if self._video_pingpong:
11507
11715
  self._images.extend(self._images[::-1])
11508
11716
  if self._video_repeat == 1: # in case of repeat == 1, loop should not be specified (otherwise, it might show twice)
11509
- if PythonInExcel or AnacondaCode: # ***
11717
+ if PythonInExcel or AnacondaCode:
11510
11718
  with b64_file_handler(self._video_name, mode="b", result=_pie_result) as f:
11511
11719
  self._images[0].save(
11512
11720
  f,
@@ -11527,7 +11735,7 @@ class Environment:
11527
11735
  optimize=False,
11528
11736
  )
11529
11737
  else:
11530
- if PythonInExcel or AnacondaCode: # ***
11738
+ if PythonInExcel or AnacondaCode:
11531
11739
  with b64_file_handler(self._video_name, mode="b", result=_pie_result) as f:
11532
11740
  self._images[0].save(
11533
11741
  f,
@@ -15312,8 +15520,6 @@ class Animate2dBase(DynamicClass):
15312
15520
 
15313
15521
  if not self.screen_coordinates:
15314
15522
  fontsize = fontsize * self.env._scale
15315
- # offsetx = offsetx * self.env._scale # ***
15316
- # offsety = offsety * self.env._scale # ***
15317
15523
  text_anchor = self.text_anchor(t)
15318
15524
 
15319
15525
  if self.attached_to:
@@ -15347,6 +15553,7 @@ class Animate2dBase(DynamicClass):
15347
15553
  qx = (x - self.env._x0) * self.env._scale
15348
15554
  qy = (y - self.env._y0) * self.env._scale
15349
15555
  max_lines = self.max_lines(t)
15556
+
15350
15557
  self._image_ident = (text, fontname, fontsize, angle, textcolor, max_lines)
15351
15558
  if self._image_ident != self._image_ident_prev:
15352
15559
  font, heightA = getfont(fontname, fontsize)
@@ -19822,6 +20029,13 @@ class ComponentGenerator(Component):
19822
20029
 
19823
20030
  e.g. env.main().activate()
19824
20031
 
20032
+ moments : iterable
20033
+ specifies the moments when the components have to be generated. It is not required that these are sorted.
20034
+
20035
+ note that the moments are specified in the current time unit
20036
+
20037
+ cannot be used together with at, delay, till, duration, number, iat,force_at, force_till, disturbance or equidistant
20038
+
19825
20039
  env : Environment
19826
20040
  environment where the component is defined
19827
20041
 
@@ -19850,6 +20064,7 @@ class ComponentGenerator(Component):
19850
20064
  disturbance: Callable = None,
19851
20065
  equidistant: bool = False,
19852
20066
  at_end: Callable = None,
20067
+ moments: Iterable = None,
19853
20068
  env: "Environment" = None,
19854
20069
  **kwargs,
19855
20070
  ):
@@ -19867,6 +20082,15 @@ class ComponentGenerator(Component):
19867
20082
 
19868
20083
  if not callable(component_class):
19869
20084
  raise ValueError("component_class must be a callable")
20085
+ if moments is not None:
20086
+ if any(prop for prop in (at, delay, till, duration, number, iat, force_at, force_till, disturbance, equidistant)):
20087
+ raise ValueError(
20088
+ "specifying at, delay, till,duration, number, iat,force_at, force_till, disturbance or equidistant is not allowed, if moments is specified"
20089
+ )
20090
+ if callable(moments):
20091
+ moments = moments()
20092
+ moments = sorted([env.spec_to_time(moment) for moment in moments])
20093
+
19870
20094
  self.component_class = component_class
19871
20095
  self.iat = iat
19872
20096
  self.disturbance = disturbance
@@ -19909,25 +20133,26 @@ class ComponentGenerator(Component):
19909
20133
  at = None
19910
20134
  process = ""
19911
20135
  else:
19912
- if self.iat is None and not equidistant:
19913
- if till == inf or self.number == inf:
19914
- raise ValueError("iat not specified --> till and number need to be specified")
19915
- if disturbance is not None:
19916
- raise ValueError("iat not specified --> disturbance not allowed")
19917
-
19918
- samples = sorted([Uniform(at, till)() for _ in range(self.number)])
19919
- if force_at or force_till:
19920
- if number == 1:
19921
- if force_at and force_till:
19922
- raise ValueError("force_at and force_till does not allow number=1")
19923
- samples = [at] if force_at else [till]
19924
- else:
19925
- v_at = at if force_at else samples[0]
19926
- v_till = till if force_till else samples[-1]
19927
- min_sample = samples[0]
19928
- max_sample = samples[-1]
19929
- samples = [interpolate(sample, min_sample, max_sample, v_at, v_till) for sample in samples]
19930
- self.intervals = [t1 - t0 for t0, t1 in zip([0] + samples, samples)]
20136
+ if (self.iat is None and not equidistant) or moments:
20137
+ if not moments:
20138
+ if till == inf or self.number == inf:
20139
+ raise ValueError("iat not specified --> till and number need to be specified")
20140
+ if disturbance is not None:
20141
+ raise ValueError("iat not specified --> disturbance not allowed")
20142
+
20143
+ moments = sorted([Uniform(at, till)() for _ in range(self.number)])
20144
+ if force_at or force_till:
20145
+ if number == 1:
20146
+ if force_at and force_till:
20147
+ raise ValueError("force_at and force_till does not allow number=1")
20148
+ moments = [at] if force_at else [till]
20149
+ else:
20150
+ v_at = at if force_at else moments[0]
20151
+ v_till = till if force_till else moments[-1]
20152
+ min_moment = moments[0]
20153
+ max_moment = moments[-1]
20154
+ moments = [interpolate(moment, min_moment, max_moment, v_at, v_till) for moment in moments]
20155
+ self.intervals = [t1 - t0 for t0, t1 in zip([0] + moments, moments)]
19931
20156
  at = self.intervals[0]
19932
20157
  self.intervals[0] = 0
19933
20158
  process = "do_spread_yieldless" if env._yieldless else "do_spread"
@@ -26316,7 +26541,7 @@ def fonts():
26316
26541
 
26317
26542
  for dir, recursive in dir_recursives:
26318
26543
  for file_path in dir.glob("**/*.*" if recursive else "*.*"):
26319
- if file_path.suffix.lower() == ".ttf": # ***
26544
+ if file_path.suffix.lower() == ".ttf":
26320
26545
  file = str(file_path)
26321
26546
  fn = os.path.basename(file).split(".")[0]
26322
26547
  if "_std_fonts" in globals() and fn in _std_fonts(): # test for availabiitly, because of minimized version
@@ -26376,7 +26601,8 @@ def getfont(fontname, fontsize):
26376
26601
  return getfont.lookup[(fontname, fontsize)]
26377
26602
  else:
26378
26603
  getfont.lookup = {}
26379
-
26604
+ if fontname=="":
26605
+ a=1
26380
26606
  if isinstance(fontname, str):
26381
26607
  fontlist1 = [fontname]
26382
26608
  else:
@@ -26409,7 +26635,8 @@ def getfont(fontname, fontsize):
26409
26635
  result = ImageFont.truetype(filename, size=int(fontsize))
26410
26636
  else:
26411
26637
  # refer to https://github.com/python-pillow/Pillow/issues/3730 for explanation (in order to load >= 500 fonts)
26412
- result = ImageFont.truetype(font=io.BytesIO(open(filename, "rb").read()), size=int(fontsize))
26638
+ with open(filename, "rb") as f:
26639
+ result = ImageFont.truetype(font=io.BytesIO(f.read()), size=int(fontsize))
26413
26640
  break
26414
26641
  except Exception:
26415
26642
  raise
@@ -26948,7 +27175,7 @@ def reset() -> None:
26948
27175
  g.tkinter_loaded = "?"
26949
27176
  g.image_container_cache = {}
26950
27177
  g._default_cap_now = False
26951
- g._captured_stdout=[]
27178
+ g._captured_stdout = []
26952
27179
 
26953
27180
  random_seed() # always start with seed 1234567
26954
27181
 
@@ -27181,7 +27408,6 @@ reset()
27181
27408
  set_environment_aliases()
27182
27409
 
27183
27410
  if __name__ == "__main__":
27184
-
27185
27411
  sys.path.insert(0, str(Path(__file__).parent / ".." / "misc"))
27186
27412
  try:
27187
27413
  import salabim_exp
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: salabim
3
- Version: 24.0.12
3
+ Version: 24.0.13
4
4
  Summary: salabim - discrete event simulation in Python
5
5
  Author-email: Ruud van der Ham <rt.van.der.ham@gmail.com>
6
6
  Project-URL: Homepage, https://salabim.org
@@ -1,5 +1,15 @@
1
- import salabim as sim
2
1
  import pytest
2
+ from pathlib import Path
3
+ import sys
4
+ import os
5
+
6
+ if __name__ == "__main__":
7
+ file_folder = Path(__file__).parent
8
+ top_folder = (file_folder / "..").resolve()
9
+ sys.path.insert(0, str(top_folder))
10
+ os.chdir(file_folder)
11
+
12
+ import salabim as sim
3
13
 
4
14
 
5
15
  class X(sim.Component):
@@ -190,9 +200,21 @@ def test_dis():
190
200
  for component in components:
191
201
  names.tally(component.name().split(".")[0])
192
202
 
203
+ def test_moments():
204
+ moments=(1,2,3,4,100,5)
205
+ components = exp(X, moments=moments)
206
+ assert components[0].enter_time(components) == 1
207
+ assert components[1].enter_time(components) == 2
208
+ assert components[2].enter_time(components) == 3
209
+ assert components[3].enter_time(components) == 4
210
+ assert components[4].enter_time(components) == 5
211
+ assert components[5].enter_time(components) == 100
212
+
213
+
193
214
 
194
215
  # names.print_histogram(values=True, sort_on_weight=True)
195
216
 
196
217
  if __name__ == "__main__":
197
- pytest.main(["-vv", "-s", __file__])
218
+ pytest.main(["-vv", "-s",__file__])
219
+
198
220
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes