TestDict

Test-run hierarchy dictionary with outcome tracking and metric aggregation.

This module provides :class:TestDict, a sub-class of :class:~pytest_item_dict.collect_dict.CollectionDict that extends the collection-time hierarchy with per-test outcomes, durations, and markers, then aggregates those metrics upward through the hierarchy so that every parent node (class, module, folder) exposes summarised counts and total duration.

TestDict

Bases: CollectionDict

Extends :class:CollectionDict with per-test metrics and aggregation.

After a test session completes, calling :meth:aggregate_counts populates every non-leaf node with @counts (outcome breakdown) and, when durations are enabled, @total_duration (summed child durations in seconds).

Parameters:
  • config (Config) –
    Active pytest configuration object.
    
Attributes:
  • UNEXECUTED (str) –

    Sentinel outcome value ("unexecuted") assigned to tests that were collected but not yet executed.

  • EXECUTED (str) –

    Key used inside @counts for the number of tests that ran (total - unexecuted).

  • _set_outcomes (bool) –

    Whether to record individual test outcomes in the hierarchy.

  • _set_durations (bool) –

    Whether to record individual and aggregated durations in the hierarchy.

  • _update_on_test (bool) –

    Whether to update the hierarchy dict after each individual test (real-time) rather than only at session end.

  • _set_hierarchy_outcomes (bool) –

    Whether to aggregate outcome counts (@counts) at every non-leaf node.

  • _set_hierarchy_durations (bool) –

    Whether to aggregate total durations (@total_duration) at every non-leaf node.

Source code in src/pytest_item_dict/test_dict.py
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
class TestDict(CollectionDict):
	"""Extends :class:`CollectionDict` with per-test metrics and aggregation.

	After a test session completes, calling :meth:`aggregate_counts` populates
	every non-leaf node with ``@counts`` (outcome breakdown) and, when
	durations are enabled, ``@total_duration`` (summed child durations in
	seconds).

	Parameters
	----------
	config : pytest.Config
		Active pytest configuration object.

	Attributes
	----------
	UNEXECUTED : str
		Sentinel outcome value (``"unexecuted"``) assigned to tests that were
		collected but not yet executed.
	EXECUTED : str
		Key used inside ``@counts`` for the number of tests that ran
		(``total - unexecuted``).
	_set_outcomes : bool
		Whether to record individual test outcomes in the hierarchy.
	_set_durations : bool
		Whether to record individual and aggregated durations in the hierarchy.
	_update_on_test : bool
		Whether to update the hierarchy dict after each individual test
		(real-time) rather than only at session end.
	_set_hierarchy_outcomes : bool
		Whether to aggregate outcome counts (``@counts``) at every non-leaf node.
	_set_hierarchy_durations : bool
		Whether to aggregate total durations (``@total_duration``) at every
		non-leaf node.
	"""

	__test__: bool = False  # Prevent pytest from collecting this class as a test case

	UNEXECUTED: Final[str] = "unexecuted"
	EXECUTED: Final[str] = "executed"

	def __init__(self, config: Config) -> None:
		super().__init__(config=config)
		self._add_markers: bool = bool(config.getini(name=INIOptions.SET_TEST_MARKERS))
		self._set_durations: bool = bool(config.getini(name=INIOptions.SET_TEST_DURATIONS))
		self._set_outcomes: bool = bool(config.getini(name=INIOptions.SET_TEST_OUTCOMES))
		self._update_on_test: bool = bool(config.getini(name=INIOptions.UPDATE_DICT_ON_TEST))
		self._set_hierarchy_outcomes: bool = bool(config.getini(name=INIOptions.SET_TEST_HIERARCHY_OUTCOMES))
		self._set_hierarchy_durations: bool = bool(config.getini(name=INIOptions.SET_TEST_HIERARCHY_DURATIONS))
		self._set_setup_teardown: bool = bool(config.getini(name=INIOptions.SET_SETUP_TEARDOWN))

	# ------------------------------------------------------------------
	# Properties
	# ------------------------------------------------------------------

	@property
	def set_outcomes(self) -> bool:
		"""Whether to record individual test outcomes.

		Returns
		-------
		bool
			Value of the ``set_test_dict_outcomes`` ini option.
		"""
		return self._set_outcomes

	@property
	def set_durations(self) -> bool:
		"""Whether to record individual and aggregated durations.

		Returns
		-------
		bool
			Value of the ``set_test_dict_durations`` ini option.
		"""
		return self._set_durations

	@property
	def update_on_test(self) -> bool:
		"""Whether to update the hierarchy after every individual test.

		Returns
		-------
		bool
			Value of the ``update_dict_on_test`` ini option.
		"""
		return self._update_on_test

	@property
	def set_hierarchy_outcomes(self) -> bool:
		"""Whether to aggregate outcome counts at every non-leaf hierarchy node.

		Returns
		-------
		bool
			Value of the ``set_test_hierarchy_dict_outcomes`` ini option.
		"""
		return self._set_hierarchy_outcomes

	@property
	def set_hierarchy_durations(self) -> bool:
		"""Whether to aggregate total durations at every non-leaf hierarchy node.

		Returns
		-------
		bool
			Value of the ``set_test_hierarchy_dict_durations`` ini option.
		"""
		return self._set_hierarchy_durations

	@property
	def set_setup_teardown(self) -> bool:
		"""Whether to record setup/teardown phase outcomes in the hierarchy.

		Returns
		-------
		bool
			Value of the ``set_test_dict_setup_teardown`` ini option.
		"""
		return self._set_setup_teardown

	# ------------------------------------------------------------------
	# Outcome tracking
	# ------------------------------------------------------------------

	def set_unexecuted_test_outcomes(self) -> None:
		"""Initialise every item's ``@outcome`` attribute to ``"unexecuted"``.

		Called immediately after collection so that tests that are never
		reached (e.g. due to a session-abort) retain a meaningful outcome
		rather than being absent from the hierarchy.
		"""
		if self._set_outcomes:
			for item in self.items:
				setattr(item, TestProperties.OUTCOME, self.UNEXECUTED)
				self.set_outcome_attribute(item=item)

	def set_test_outcomes(self) -> None:
		"""Write each item's current ``outcome`` attribute into the hierarchy.

		Iterates over all items and calls :meth:`set_outcome_attribute` for
		each.  Useful for a final one-shot sync at session end.
		"""
		if self._set_outcomes:
			for item in self.items:
				self.set_outcome_attribute(item=item)

	def set_outcome_attribute(self, item: Item) -> None:
		"""Write *item*'s ``outcome`` attribute into the test-dict hierarchy.

		Parameters
		----------
		item : pytest.Item
			The test item whose outcome to persist.
		"""
		if self._set_outcomes and hasattr(item, TestProperties.OUTCOME):
			key_path: list[str] = self.get_key_path(path=item.nodeid)
			self.set_attribute(
			    key_path=key_path,
			    key=TestProperties.OUTCOME,
			    value=getattr(item, TestProperties.OUTCOME),
			)

	# ------------------------------------------------------------------
	# Duration tracking
	# ------------------------------------------------------------------

	def set_duration_attribute(self, item: Item) -> None:
		"""Write *item*'s accumulated duration (seconds) into the hierarchy.

		The duration is stored as a plain :class:`float` (seconds) to
		facilitate numeric aggregation in :meth:`aggregate_counts`.

		Parameters
		----------
		item : pytest.Item
			The test item whose ``duration`` attribute to persist.
		"""
		if self._set_durations and hasattr(item, TestProperties.DURATION):
			key_path: list[str] = self.get_key_path(path=item.nodeid)
			duration_seconds: float = float(getattr(item, TestProperties.DURATION))
			self.set_attribute(
			    key_path=key_path,
			    key=TestProperties.DURATION,
			    value=duration_seconds,
			)

	# ------------------------------------------------------------------
	# Setup / teardown tracking
	# ------------------------------------------------------------------

	def set_setup_attribute(self, item: Item) -> None:
		"""Write *item*'s setup-phase outcome into the hierarchy.

		Stored as a child node of the test node under the key
		``setup_method`` (class-based tests) or ``setup_function``
		(module-level functions).  The child dict contains a single
		``@outcome`` attribute.  Because the key is listed in
		:data:`~pytest_item_dict.item_dict_enums.SETUP_TEARDOWN_KEYS` it is
		never counted by :meth:`_aggregate_node`.

		Parameters
		----------
		item : pytest.Item
			The test item whose ``setup_outcome`` attribute to persist.
		"""
		if not self._set_setup_teardown or not hasattr(item, TestProperties.SETUP_OUTCOME):
			return
		key_path: list[str] = self.get_key_path(path=item.nodeid)
		setup_key: str = "setup_method" if getattr(item, "cls", None) is not None else "setup_function"
		self._set_new_value(
		    key_path=key_path + [setup_key, f"@{TestProperties.OUTCOME}"],
		    value=getattr(item, TestProperties.SETUP_OUTCOME),
		)

	def set_teardown_attribute(self, item: Item) -> None:
		"""Write *item*'s teardown-phase outcome into the hierarchy.

		Stored as a child node of the test node under the key
		``teardown_method`` (class-based tests) or ``teardown_function``
		(module-level functions).  Because the key is listed in
		:data:`~pytest_item_dict.item_dict_enums.SETUP_TEARDOWN_KEYS` it is
		never counted by :meth:`_aggregate_node`.

		Parameters
		----------
		item : pytest.Item
			The test item whose ``teardown_outcome`` attribute to persist.
		"""
		if not self._set_setup_teardown or not hasattr(item, TestProperties.TEARDOWN_OUTCOME):
			return
		key_path: list[str] = self.get_key_path(path=item.nodeid)
		teardown_key: str = "teardown_method" if getattr(item, "cls", None) is not None else "teardown_function"
		self._set_new_value(
		    key_path=key_path + [teardown_key, f"@{TestProperties.OUTCOME}"],
		    value=getattr(item, TestProperties.TEARDOWN_OUTCOME),
		)

	def set_class_setup_attribute(self, item: Item) -> None:
		"""Write the ``setup_class`` outcome to the parent class node.

		Called for the first test item in a class when that class defines
		``setup_class``.  The outcome recorded is the setup-phase result of
		that first item (which includes the ``setup_class`` call).  Stored
		under the key ``setup_class`` at the class-level hierarchy node so
		that it appears as a sibling to the test methods.

		Parameters
		----------
		item : pytest.Item
			The first test item in the class.
		"""
		if not self._set_setup_teardown or not hasattr(item, TestProperties.SETUP_OUTCOME) or getattr(item, "cls", None) is None:
			return
		key_path: list[str] = self.get_key_path(path=item.nodeid)
		class_path: list[str] = key_path[:-1]  # strip the test function name
		self._set_new_value(
		    key_path=class_path + ["setup_class", f"@{TestProperties.OUTCOME}"],
		    value=getattr(item, TestProperties.SETUP_OUTCOME),
		)

	def set_class_teardown_attribute(self, item: Item) -> None:
		"""Write the ``teardown_class`` outcome to the parent class node.

		Called for the last test item in a class when that class defines
		``teardown_class``.  The outcome recorded is the teardown-phase
		result of that last item (which includes the ``teardown_class``
		call).  Stored under the key ``teardown_class`` at the class-level
		hierarchy node.

		Parameters
		----------
		item : pytest.Item
			The last test item in the class.
		"""
		if not self._set_setup_teardown or not hasattr(item, TestProperties.TEARDOWN_OUTCOME) or getattr(item, "cls", None) is None:
			return
		key_path: list[str] = self.get_key_path(path=item.nodeid)
		class_path: list[str] = key_path[:-1]  # strip the test function name
		self._set_new_value(
		    key_path=class_path + ["teardown_class", f"@{TestProperties.OUTCOME}"],
		    value=getattr(item, TestProperties.TEARDOWN_OUTCOME),
		)

	# ------------------------------------------------------------------
	# Aggregation
	# ------------------------------------------------------------------

	def aggregate_counts(self) -> None:
		"""Recursively aggregate outcome counts and durations through the hierarchy.

		For every non-leaf node (folder, module, class) computes and stores
		``@counts``, a mapping of outcome label to test count.  When
		``set_test_dict_durations`` is enabled, also stores ``@total_duration``
		(float, seconds) at every non-leaf node.

		Leaf nodes are detected structurally: a node is a leaf when it contains
		no child keys whose values are plain dicts (``@``-prefixed attribute
		keys are ignored).

		Notes
		-----
		The method mutates ``_hierarchy`` in place and is idempotent.  It
		should be called once, after all individual outcomes and durations have
		been recorded (i.e. from ``pytest_sessionfinish``).

		Examples
		--------
		Given a two-test module hierarchy after the run::

			{
				"test_mod.py": {
					"test_pass": {"@outcome": "passed"},
					"test_fail": {"@outcome": "failed"},
				}
			}

		After ``aggregate_counts()``::

			{
				"@counts": {"passed": 1, "failed": 1, "skipped": 0,
							"unexecuted": 0, "total": 2},
				"test_mod.py": {
					"@counts": {"passed": 1, "failed": 1, "skipped": 0,
								"unexecuted": 0, "total": 2},
					"test_pass": {"@outcome": "passed"},
					"test_fail": {"@outcome": "failed"},
				}
			}
		"""
		self._aggregate_node(self._hierarchy)

	def _aggregate_node(
	    self,
	    node: dict[str, Any],
	) -> tuple[dict[str, int], float]:
		"""Recursively aggregate a single hierarchy node.

		Parameters
		----------
		node : dict[str, Any]
			A node from the test-dict hierarchy.  Attribute keys are prefixed
			with ``"@"``; child nodes are plain keys whose values are ``dict``
			instances.

		Returns
		-------
		tuple[dict[str, int], float]
			A 2-tuple of:

			counts : dict[str, int]
				Outcome label → count for all leaf tests in this subtree, plus
				``"total"`` for the grand total.
			total_duration : float
				Sum of all ``@duration`` values (seconds) in this subtree.
		"""
		outcome_key: str = f"@{TestProperties.OUTCOME}"
		duration_key: str = f"@{TestProperties.DURATION}"

		# Structural leaf detection: no non-attribute dict children.
		child_keys: list[str] = [k for k, v in node.items() if not k.startswith("@") and isinstance(v, dict) and k not in SETUP_TEARDOWN_KEYS]

		if not child_keys:
			# ── Leaf node ──────────────────────────────────────────────
			outcome: str = node.get(outcome_key, self.UNEXECUTED)
			duration: float = float(node.get(duration_key, 0.0))
			return {outcome: 1, "total": 1}, duration

		# ── Parent node: aggregate children ────────────────────────────
		counts: defaultdict[str, int] = defaultdict(int)
		total_duration: float = 0.0

		for key in child_keys:
			child_counts, child_duration = self._aggregate_node(node[key])
			for label, count in child_counts.items():
				counts[label] += count
			total_duration += child_duration

		# Guarantee all standard outcome keys are present even when zero.
		for outcome in ("passed", "failed", "skipped", self.UNEXECUTED):
			counts.setdefault(outcome, 0)
		counts[self.EXECUTED] = counts["total"] - counts[self.UNEXECUTED]

		if self._set_hierarchy_outcomes:
			node[f"@{TestProperties.COUNTS}"] = dict(counts)
		if self._set_hierarchy_durations:
			node[f"@{TestProperties.TOTAL_DURATION}"] = total_duration

		return dict(counts), total_duration

	# ------------------------------------------------------------------
	# Session-end runners
	# ------------------------------------------------------------------

	def run_ini_options(self) -> None:
		"""Execute all ini-option-driven metric recording for the test run.

		Iterates over :attr:`items` and calls :meth:`set_marker_attribute`
		and :meth:`set_duration_attribute` for each item when the respective
		ini options are enabled.
		"""
		if self._add_markers or self._set_durations:
			for item in self.items:
				self.set_marker_attribute(item=item)
				self.set_duration_attribute(item=item)

	def run_setup_teardown(
	    self,
	    class_first_items: frozenset[Item],
	    class_last_items: frozenset[Item],
	) -> None:
		"""Write all setup/teardown outcomes into the hierarchy at session end.

		For every collected item the per-test setup and teardown outcomes are
		written (``setup_method`` / ``teardown_method`` for class-based tests,
		``setup_function`` / ``teardown_function`` for module-level tests).
		For the first item in each class that defines ``setup_class``, the
		class-level ``setup_class`` outcome is written at the class node.
		Similarly for ``teardown_class`` at the last item.

		This method is always called at session end so that outcomes are
		persisted regardless of the ``update_dict_on_test`` setting.

		Parameters
		----------
		class_first_items : frozenset[pytest.Item]
			The first collected item in each test class.
		class_last_items : frozenset[pytest.Item]
			The last collected item in each test class.
		"""
		if not self._set_setup_teardown:
			return
		for item in self.items:
			self.set_setup_attribute(item=item)
			self.set_teardown_attribute(item=item)
		for item in class_first_items:
			item_cls = getattr(item, "cls", None)
			if item_cls is not None and hasattr(item_cls, "setup_class"):
				self.set_class_setup_attribute(item=item)
		for item in class_last_items:
			item_cls = getattr(item, "cls", None)
			if item_cls is not None and hasattr(item_cls, "teardown_class"):
				self.set_class_teardown_attribute(item=item)

set_durations property

set_durations: bool

Whether to record individual and aggregated durations.

Returns:
  • bool

    Value of the set_test_dict_durations ini option.

set_hierarchy_durations property

set_hierarchy_durations: bool

Whether to aggregate total durations at every non-leaf hierarchy node.

Returns:
  • bool

    Value of the set_test_hierarchy_dict_durations ini option.

set_hierarchy_outcomes property

set_hierarchy_outcomes: bool

Whether to aggregate outcome counts at every non-leaf hierarchy node.

Returns:
  • bool

    Value of the set_test_hierarchy_dict_outcomes ini option.

set_outcomes property

set_outcomes: bool

Whether to record individual test outcomes.

Returns:
  • bool

    Value of the set_test_dict_outcomes ini option.

set_setup_teardown property

set_setup_teardown: bool

Whether to record setup/teardown phase outcomes in the hierarchy.

Returns:
  • bool

    Value of the set_test_dict_setup_teardown ini option.

update_on_test property

update_on_test: bool

Whether to update the hierarchy after every individual test.

Returns:
  • bool

    Value of the update_dict_on_test ini option.

aggregate_counts

aggregate_counts() -> None

Recursively aggregate outcome counts and durations through the hierarchy.

For every non-leaf node (folder, module, class) computes and stores @counts, a mapping of outcome label to test count. When set_test_dict_durations is enabled, also stores @total_duration (float, seconds) at every non-leaf node.

Leaf nodes are detected structurally: a node is a leaf when it contains no child keys whose values are plain dicts (@-prefixed attribute keys are ignored).

Notes

The method mutates _hierarchy in place and is idempotent. It should be called once, after all individual outcomes and durations have been recorded (i.e. from pytest_sessionfinish).

Examples:

Given a two-test module hierarchy after the run::

    {
            "test_mod.py": {
                    "test_pass": {"@outcome": "passed"},
                    "test_fail": {"@outcome": "failed"},
            }
    }

After aggregate_counts()::

    {
            "@counts": {"passed": 1, "failed": 1, "skipped": 0,
                                    "unexecuted": 0, "total": 2},
            "test_mod.py": {
                    "@counts": {"passed": 1, "failed": 1, "skipped": 0,
                                            "unexecuted": 0, "total": 2},
                    "test_pass": {"@outcome": "passed"},
                    "test_fail": {"@outcome": "failed"},
            }
    }
Source code in src/pytest_item_dict/test_dict.py
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
def aggregate_counts(self) -> None:
	"""Recursively aggregate outcome counts and durations through the hierarchy.

	For every non-leaf node (folder, module, class) computes and stores
	``@counts``, a mapping of outcome label to test count.  When
	``set_test_dict_durations`` is enabled, also stores ``@total_duration``
	(float, seconds) at every non-leaf node.

	Leaf nodes are detected structurally: a node is a leaf when it contains
	no child keys whose values are plain dicts (``@``-prefixed attribute
	keys are ignored).

	Notes
	-----
	The method mutates ``_hierarchy`` in place and is idempotent.  It
	should be called once, after all individual outcomes and durations have
	been recorded (i.e. from ``pytest_sessionfinish``).

	Examples
	--------
	Given a two-test module hierarchy after the run::

		{
			"test_mod.py": {
				"test_pass": {"@outcome": "passed"},
				"test_fail": {"@outcome": "failed"},
			}
		}

	After ``aggregate_counts()``::

		{
			"@counts": {"passed": 1, "failed": 1, "skipped": 0,
						"unexecuted": 0, "total": 2},
			"test_mod.py": {
				"@counts": {"passed": 1, "failed": 1, "skipped": 0,
							"unexecuted": 0, "total": 2},
				"test_pass": {"@outcome": "passed"},
				"test_fail": {"@outcome": "failed"},
			}
		}
	"""
	self._aggregate_node(self._hierarchy)

run_ini_options

run_ini_options() -> None

Execute all ini-option-driven metric recording for the test run.

Iterates over :attr:items and calls :meth:set_marker_attribute and :meth:set_duration_attribute for each item when the respective ini options are enabled.

Source code in src/pytest_item_dict/test_dict.py
421
422
423
424
425
426
427
428
429
430
431
def run_ini_options(self) -> None:
	"""Execute all ini-option-driven metric recording for the test run.

	Iterates over :attr:`items` and calls :meth:`set_marker_attribute`
	and :meth:`set_duration_attribute` for each item when the respective
	ini options are enabled.
	"""
	if self._add_markers or self._set_durations:
		for item in self.items:
			self.set_marker_attribute(item=item)
			self.set_duration_attribute(item=item)

run_setup_teardown

run_setup_teardown(class_first_items: frozenset[Item], class_last_items: frozenset[Item]) -> None

Write all setup/teardown outcomes into the hierarchy at session end.

For every collected item the per-test setup and teardown outcomes are written (setup_method / teardown_method for class-based tests, setup_function / teardown_function for module-level tests). For the first item in each class that defines setup_class, the class-level setup_class outcome is written at the class node. Similarly for teardown_class at the last item.

This method is always called at session end so that outcomes are persisted regardless of the update_dict_on_test setting.

Parameters:
  • class_first_items (frozenset[Item]) –
    The first collected item in each test class.
    
  • class_last_items (frozenset[Item]) –
    The last collected item in each test class.
    
Source code in src/pytest_item_dict/test_dict.py
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
def run_setup_teardown(
    self,
    class_first_items: frozenset[Item],
    class_last_items: frozenset[Item],
) -> None:
	"""Write all setup/teardown outcomes into the hierarchy at session end.

	For every collected item the per-test setup and teardown outcomes are
	written (``setup_method`` / ``teardown_method`` for class-based tests,
	``setup_function`` / ``teardown_function`` for module-level tests).
	For the first item in each class that defines ``setup_class``, the
	class-level ``setup_class`` outcome is written at the class node.
	Similarly for ``teardown_class`` at the last item.

	This method is always called at session end so that outcomes are
	persisted regardless of the ``update_dict_on_test`` setting.

	Parameters
	----------
	class_first_items : frozenset[pytest.Item]
		The first collected item in each test class.
	class_last_items : frozenset[pytest.Item]
		The last collected item in each test class.
	"""
	if not self._set_setup_teardown:
		return
	for item in self.items:
		self.set_setup_attribute(item=item)
		self.set_teardown_attribute(item=item)
	for item in class_first_items:
		item_cls = getattr(item, "cls", None)
		if item_cls is not None and hasattr(item_cls, "setup_class"):
			self.set_class_setup_attribute(item=item)
	for item in class_last_items:
		item_cls = getattr(item, "cls", None)
		if item_cls is not None and hasattr(item_cls, "teardown_class"):
			self.set_class_teardown_attribute(item=item)

set_class_setup_attribute

set_class_setup_attribute(item: Item) -> None

Write the setup_class outcome to the parent class node.

Called for the first test item in a class when that class defines setup_class. The outcome recorded is the setup-phase result of that first item (which includes the setup_class call). Stored under the key setup_class at the class-level hierarchy node so that it appears as a sibling to the test methods.

Parameters:
  • item (Item) –
    The first test item in the class.
    
Source code in src/pytest_item_dict/test_dict.py
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
def set_class_setup_attribute(self, item: Item) -> None:
	"""Write the ``setup_class`` outcome to the parent class node.

	Called for the first test item in a class when that class defines
	``setup_class``.  The outcome recorded is the setup-phase result of
	that first item (which includes the ``setup_class`` call).  Stored
	under the key ``setup_class`` at the class-level hierarchy node so
	that it appears as a sibling to the test methods.

	Parameters
	----------
	item : pytest.Item
		The first test item in the class.
	"""
	if not self._set_setup_teardown or not hasattr(item, TestProperties.SETUP_OUTCOME) or getattr(item, "cls", None) is None:
		return
	key_path: list[str] = self.get_key_path(path=item.nodeid)
	class_path: list[str] = key_path[:-1]  # strip the test function name
	self._set_new_value(
	    key_path=class_path + ["setup_class", f"@{TestProperties.OUTCOME}"],
	    value=getattr(item, TestProperties.SETUP_OUTCOME),
	)

set_class_teardown_attribute

set_class_teardown_attribute(item: Item) -> None

Write the teardown_class outcome to the parent class node.

Called for the last test item in a class when that class defines teardown_class. The outcome recorded is the teardown-phase result of that last item (which includes the teardown_class call). Stored under the key teardown_class at the class-level hierarchy node.

Parameters:
  • item (Item) –
    The last test item in the class.
    
Source code in src/pytest_item_dict/test_dict.py
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
def set_class_teardown_attribute(self, item: Item) -> None:
	"""Write the ``teardown_class`` outcome to the parent class node.

	Called for the last test item in a class when that class defines
	``teardown_class``.  The outcome recorded is the teardown-phase
	result of that last item (which includes the ``teardown_class``
	call).  Stored under the key ``teardown_class`` at the class-level
	hierarchy node.

	Parameters
	----------
	item : pytest.Item
		The last test item in the class.
	"""
	if not self._set_setup_teardown or not hasattr(item, TestProperties.TEARDOWN_OUTCOME) or getattr(item, "cls", None) is None:
		return
	key_path: list[str] = self.get_key_path(path=item.nodeid)
	class_path: list[str] = key_path[:-1]  # strip the test function name
	self._set_new_value(
	    key_path=class_path + ["teardown_class", f"@{TestProperties.OUTCOME}"],
	    value=getattr(item, TestProperties.TEARDOWN_OUTCOME),
	)

set_duration_attribute

set_duration_attribute(item: Item) -> None

Write item's accumulated duration (seconds) into the hierarchy.

The duration is stored as a plain :class:float (seconds) to facilitate numeric aggregation in :meth:aggregate_counts.

Parameters:
  • item (Item) –
    The test item whose ``duration`` attribute to persist.
    
Source code in src/pytest_item_dict/test_dict.py
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
def set_duration_attribute(self, item: Item) -> None:
	"""Write *item*'s accumulated duration (seconds) into the hierarchy.

	The duration is stored as a plain :class:`float` (seconds) to
	facilitate numeric aggregation in :meth:`aggregate_counts`.

	Parameters
	----------
	item : pytest.Item
		The test item whose ``duration`` attribute to persist.
	"""
	if self._set_durations and hasattr(item, TestProperties.DURATION):
		key_path: list[str] = self.get_key_path(path=item.nodeid)
		duration_seconds: float = float(getattr(item, TestProperties.DURATION))
		self.set_attribute(
		    key_path=key_path,
		    key=TestProperties.DURATION,
		    value=duration_seconds,
		)

set_outcome_attribute

set_outcome_attribute(item: Item) -> None

Write item's outcome attribute into the test-dict hierarchy.

Parameters:
  • item (Item) –
    The test item whose outcome to persist.
    
Source code in src/pytest_item_dict/test_dict.py
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
def set_outcome_attribute(self, item: Item) -> None:
	"""Write *item*'s ``outcome`` attribute into the test-dict hierarchy.

	Parameters
	----------
	item : pytest.Item
		The test item whose outcome to persist.
	"""
	if self._set_outcomes and hasattr(item, TestProperties.OUTCOME):
		key_path: list[str] = self.get_key_path(path=item.nodeid)
		self.set_attribute(
		    key_path=key_path,
		    key=TestProperties.OUTCOME,
		    value=getattr(item, TestProperties.OUTCOME),
		)

set_setup_attribute

set_setup_attribute(item: Item) -> None

Write item's setup-phase outcome into the hierarchy.

Stored as a child node of the test node under the key setup_method (class-based tests) or setup_function (module-level functions). The child dict contains a single @outcome attribute. Because the key is listed in :data:~pytest_item_dict.item_dict_enums.SETUP_TEARDOWN_KEYS it is never counted by :meth:_aggregate_node.

Parameters:
  • item (Item) –
    The test item whose ``setup_outcome`` attribute to persist.
    
Source code in src/pytest_item_dict/test_dict.py
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
def set_setup_attribute(self, item: Item) -> None:
	"""Write *item*'s setup-phase outcome into the hierarchy.

	Stored as a child node of the test node under the key
	``setup_method`` (class-based tests) or ``setup_function``
	(module-level functions).  The child dict contains a single
	``@outcome`` attribute.  Because the key is listed in
	:data:`~pytest_item_dict.item_dict_enums.SETUP_TEARDOWN_KEYS` it is
	never counted by :meth:`_aggregate_node`.

	Parameters
	----------
	item : pytest.Item
		The test item whose ``setup_outcome`` attribute to persist.
	"""
	if not self._set_setup_teardown or not hasattr(item, TestProperties.SETUP_OUTCOME):
		return
	key_path: list[str] = self.get_key_path(path=item.nodeid)
	setup_key: str = "setup_method" if getattr(item, "cls", None) is not None else "setup_function"
	self._set_new_value(
	    key_path=key_path + [setup_key, f"@{TestProperties.OUTCOME}"],
	    value=getattr(item, TestProperties.SETUP_OUTCOME),
	)

set_teardown_attribute

set_teardown_attribute(item: Item) -> None

Write item's teardown-phase outcome into the hierarchy.

Stored as a child node of the test node under the key teardown_method (class-based tests) or teardown_function (module-level functions). Because the key is listed in :data:~pytest_item_dict.item_dict_enums.SETUP_TEARDOWN_KEYS it is never counted by :meth:_aggregate_node.

Parameters:
  • item (Item) –
    The test item whose ``teardown_outcome`` attribute to persist.
    
Source code in src/pytest_item_dict/test_dict.py
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
def set_teardown_attribute(self, item: Item) -> None:
	"""Write *item*'s teardown-phase outcome into the hierarchy.

	Stored as a child node of the test node under the key
	``teardown_method`` (class-based tests) or ``teardown_function``
	(module-level functions).  Because the key is listed in
	:data:`~pytest_item_dict.item_dict_enums.SETUP_TEARDOWN_KEYS` it is
	never counted by :meth:`_aggregate_node`.

	Parameters
	----------
	item : pytest.Item
		The test item whose ``teardown_outcome`` attribute to persist.
	"""
	if not self._set_setup_teardown or not hasattr(item, TestProperties.TEARDOWN_OUTCOME):
		return
	key_path: list[str] = self.get_key_path(path=item.nodeid)
	teardown_key: str = "teardown_method" if getattr(item, "cls", None) is not None else "teardown_function"
	self._set_new_value(
	    key_path=key_path + [teardown_key, f"@{TestProperties.OUTCOME}"],
	    value=getattr(item, TestProperties.TEARDOWN_OUTCOME),
	)

set_test_outcomes

set_test_outcomes() -> None

Write each item's current outcome attribute into the hierarchy.

Iterates over all items and calls :meth:set_outcome_attribute for each. Useful for a final one-shot sync at session end.

Source code in src/pytest_item_dict/test_dict.py
164
165
166
167
168
169
170
171
172
def set_test_outcomes(self) -> None:
	"""Write each item's current ``outcome`` attribute into the hierarchy.

	Iterates over all items and calls :meth:`set_outcome_attribute` for
	each.  Useful for a final one-shot sync at session end.
	"""
	if self._set_outcomes:
		for item in self.items:
			self.set_outcome_attribute(item=item)

set_unexecuted_test_outcomes

set_unexecuted_test_outcomes() -> None

Initialise every item's @outcome attribute to "unexecuted".

Called immediately after collection so that tests that are never reached (e.g. due to a session-abort) retain a meaningful outcome rather than being absent from the hierarchy.

Source code in src/pytest_item_dict/test_dict.py
152
153
154
155
156
157
158
159
160
161
162
def set_unexecuted_test_outcomes(self) -> None:
	"""Initialise every item's ``@outcome`` attribute to ``"unexecuted"``.

	Called immediately after collection so that tests that are never
	reached (e.g. due to a session-abort) retain a meaningful outcome
	rather than being absent from the hierarchy.
	"""
	if self._set_outcomes:
		for item in self.items:
			setattr(item, TestProperties.OUTCOME, self.UNEXECUTED)
			self.set_outcome_attribute(item=item)