Plugin

Pytest plugin entry point for pytest-item-dict.

This module registers :class:ItemDictPlugin with pytest's plugin manager, wires up the relevant hooks, and exposes two public objects:

  • plugin.collect_dict — a :class:~pytest_item_dict.collect_dict.CollectionDict populated after the collection phase.
  • plugin.test_dict — a :class:~pytest_item_dict.test_dict.TestDict populated incrementally during the run and finalised (with aggregated counts) at session end.

Module-level helpers :func:write_json_file and :func:write_xml_file can serialise either hierarchy to disk on demand.

Attributes:
  • ITEM_DICT_PLUGIN_NAME (str) –

    Canonical name used to register and look up the plugin instance.

ITEM_DICT_PLUGIN_NAME module-attribute

ITEM_DICT_PLUGIN_NAME: Final[str] = 'item_dict'

str : Canonical name used to register and look up the plugin instance.

ItemDictPlugin

Core pytest plugin that builds and maintains the item-dict hierarchies.

Two hierarchy dicts are managed throughout the session:

  • :attr:collect_dict — populated once during collection and optionally annotated with markers.
  • :attr:test_dict — a deep copy of the collection dict, updated incrementally with outcomes/durations as each test runs, then finalised (with aggregated counts) in pytest_sessionfinish.
Parameters:
  • config (Config) –
    The active pytest configuration object.
    
Attributes:
  • config (Config) –

    Stored reference to the pytest Config object.

  • collect_dict (CollectionDict) –

    Hierarchy built from collected items.

  • test_dict (TestDict) –

    Hierarchy extended with outcomes, durations, markers, and aggregated counts.

  • _suite_start_time (float) –

    Wall-clock timestamp captured at plugin instantiation, used to compute total suite duration.

Source code in src/pytest_item_dict/plugin.py
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
class ItemDictPlugin:
	"""Core pytest plugin that builds and maintains the item-dict hierarchies.

	Two hierarchy dicts are managed throughout the session:

	* :attr:`collect_dict` — populated once during collection and optionally
	  annotated with markers.
	* :attr:`test_dict` — a deep copy of the collection dict, updated
	  incrementally with outcomes/durations as each test runs, then finalised
	  (with aggregated counts) in ``pytest_sessionfinish``.

	Parameters
	----------
	config : pytest.Config
		The active pytest configuration object.

	Attributes
	----------
	config : pytest.Config
		Stored reference to the pytest ``Config`` object.
	collect_dict : CollectionDict
		Hierarchy built from collected items.
	test_dict : TestDict
		Hierarchy extended with outcomes, durations, markers, and aggregated
		counts.
	_suite_start_time : float
		Wall-clock timestamp captured at plugin instantiation, used to compute
		total suite duration.
	"""

	def __init__(self, config: Config) -> None:
		self.config: Config = config
		self.collect_dict: CollectionDict = CollectionDict(config=config)
		self.test_dict: TestDict = TestDict(config=config)
		self._suite_start_time: float = time.time()
		self._class_first_items: frozenset[Item] = frozenset()
		self._class_last_items: frozenset[Item] = frozenset()

	def pytest_collection_modifyitems(
	    self,
	    session: Session,
	    config: Config,
	    items: list[Item],
	) -> None:
		"""Build both hierarchy dicts immediately after collection.

		Parameters
		----------
		session : pytest.Session
			The pytest session object.
		config : pytest.Config
			The pytest config object.
		items : list[pytest.Item]
			Ordered list of collected test items (may be filtered in place).
		"""
		for item in items:
			setattr(item, TestProperties.DURATION, 0.0)
			setattr(item, TestProperties.OUTCOME, "unexecuted")
		self.collect_dict.create_hierarchy_dict(items=items)

		self.test_dict.hierarchy = deepcopy(self.collect_dict.hierarchy)
		self.test_dict.items = items

		self.collect_dict.run_ini_options()
		self.test_dict.set_unexecuted_test_outcomes()

		if self.test_dict.set_setup_teardown:
			class_first: dict[type, Item] = {}
			class_last: dict[type, Item] = {}
			for item in items:
				item_cls = getattr(item, "cls", None)
				if item_cls is not None:
					if item_cls not in class_first:
						class_first[item_cls] = item
					class_last[item_cls] = item
			self._class_first_items = frozenset(class_first.values())
			self._class_last_items = frozenset(class_last.values())

	def pytest_collection_finish(self, session: Session) -> dict[Any, Any]:
		"""Return the collection hierarchy after all modifications are applied.

		Parameters
		----------
		session : pytest.Session
			The pytest session object.

		Returns
		-------
		dict[Any, Any]
			The root of the collection hierarchy dict.
		"""
		self.collect_dict._total_duration = time.time() - self._suite_start_time
		self.collect_dict.count_tests()
		# write_json_file(hierarchy=self.collect_dict.hierarchy)
		# write_xml_file(hierarchy=self.collect_dict.hierarchy)
		return self.collect_dict.hierarchy

	def pytest_sessionfinish(self, session: Session) -> None:
		"""Finalise the test-run hierarchy and compute aggregated metrics.

		Called after all tests have completed.  Writes per-item durations and
		markers (if enabled), then runs :meth:`~TestDict.aggregate_counts` to
		bubble outcome counts and total durations up through every parent node.

		Parameters
		----------
		session : pytest.Session
			The pytest session object.
		"""
		self.test_dict._total_duration = time.time() - self._suite_start_time
		self.test_dict.run_ini_options()
		if self.test_dict.set_setup_teardown:
			self.test_dict.run_setup_teardown(
			    class_first_items=self._class_first_items,
			    class_last_items=self._class_last_items,
			)
		if self.test_dict.set_hierarchy_outcomes or self.test_dict.set_hierarchy_durations:
			self.test_dict.aggregate_counts()

	@pytest.hookimpl(tryfirst=True, hookwrapper=True)
	def pytest_runtest_makereport(
	    self,
	    item: Item,
	    call: CallInfo,
	) -> Generator[None, Any, None]:
		"""Capture per-test outcomes and cumulative durations in real time.

		This hook wraps the default report-creation mechanism so it can inspect
		the :class:`~pytest.TestReport` for each test phase.  Only the
		``"call"`` phase updates the test outcome; all phases contribute to the
		cumulative item duration.

		Parameters
		----------
		item : pytest.Item
			The test item being reported.
		call : pytest.CallInfo
			Timing and exception information for the current test phase.

		Yields
		------
		None
			Delegation point; the wrapped hook produces the
			:class:`~pytest.TestReport`.
		"""
		outcome = yield
		report: TestReport = outcome.get_result()

		if hasattr(item, TestProperties.DURATION):
			prev_duration: float = getattr(item, TestProperties.DURATION)
			setattr(item, TestProperties.DURATION, prev_duration + report.duration)

		match report.when:

			case "call":
				if self.test_dict.set_outcomes:
					setattr(item, TestProperties.OUTCOME, report.outcome)
					if self.test_dict.update_on_test:
						self.test_dict.set_outcome_attribute(item=item)

			case "setup":
				if self.test_dict.set_setup_teardown:
					setattr(item, TestProperties.SETUP_OUTCOME, report.outcome)
					if self.test_dict.update_on_test:
						self.test_dict.set_setup_attribute(item=item)
						item_cls = getattr(item, "cls", None)
						if (item in self._class_first_items and item_cls is not None and hasattr(item_cls, "setup_class")):
							self.test_dict.set_class_setup_attribute(item=item)

			case "teardown":
				if self.test_dict.set_setup_teardown:
					setattr(item, TestProperties.TEARDOWN_OUTCOME, report.outcome)
					if self.test_dict.update_on_test:
						self.test_dict.set_teardown_attribute(item=item)
						item_cls = getattr(item, "cls", None)
						if (item in self._class_last_items and item_cls is not None and hasattr(item_cls, "teardown_class")):
							self.test_dict.set_class_teardown_attribute(item=item)

			case _:
				pass

pytest_collection_finish

pytest_collection_finish(session: Session) -> dict[Any, Any]

Return the collection hierarchy after all modifications are applied.

Parameters:
  • session (Session) –
    The pytest session object.
    
Returns:
  • dict[Any, Any]

    The root of the collection hierarchy dict.

Source code in src/pytest_item_dict/plugin.py
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
def pytest_collection_finish(self, session: Session) -> dict[Any, Any]:
	"""Return the collection hierarchy after all modifications are applied.

	Parameters
	----------
	session : pytest.Session
		The pytest session object.

	Returns
	-------
	dict[Any, Any]
		The root of the collection hierarchy dict.
	"""
	self.collect_dict._total_duration = time.time() - self._suite_start_time
	self.collect_dict.count_tests()
	# write_json_file(hierarchy=self.collect_dict.hierarchy)
	# write_xml_file(hierarchy=self.collect_dict.hierarchy)
	return self.collect_dict.hierarchy

pytest_collection_modifyitems

pytest_collection_modifyitems(session: Session, config: Config, items: list[Item]) -> None

Build both hierarchy dicts immediately after collection.

Parameters:
  • session (Session) –
    The pytest session object.
    
  • config (Config) –
    The pytest config object.
    
  • items (list[Item]) –
    Ordered list of collected test items (may be filtered in place).
    
Source code in src/pytest_item_dict/plugin.py
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
def pytest_collection_modifyitems(
    self,
    session: Session,
    config: Config,
    items: list[Item],
) -> None:
	"""Build both hierarchy dicts immediately after collection.

	Parameters
	----------
	session : pytest.Session
		The pytest session object.
	config : pytest.Config
		The pytest config object.
	items : list[pytest.Item]
		Ordered list of collected test items (may be filtered in place).
	"""
	for item in items:
		setattr(item, TestProperties.DURATION, 0.0)
		setattr(item, TestProperties.OUTCOME, "unexecuted")
	self.collect_dict.create_hierarchy_dict(items=items)

	self.test_dict.hierarchy = deepcopy(self.collect_dict.hierarchy)
	self.test_dict.items = items

	self.collect_dict.run_ini_options()
	self.test_dict.set_unexecuted_test_outcomes()

	if self.test_dict.set_setup_teardown:
		class_first: dict[type, Item] = {}
		class_last: dict[type, Item] = {}
		for item in items:
			item_cls = getattr(item, "cls", None)
			if item_cls is not None:
				if item_cls not in class_first:
					class_first[item_cls] = item
				class_last[item_cls] = item
		self._class_first_items = frozenset(class_first.values())
		self._class_last_items = frozenset(class_last.values())

pytest_runtest_makereport

pytest_runtest_makereport(item: Item, call: CallInfo) -> Generator[None, Any, None]

Capture per-test outcomes and cumulative durations in real time.

This hook wraps the default report-creation mechanism so it can inspect the :class:~pytest.TestReport for each test phase. Only the "call" phase updates the test outcome; all phases contribute to the cumulative item duration.

Parameters:
  • item (Item) –
    The test item being reported.
    
  • call (CallInfo) –
    Timing and exception information for the current test phase.
    
Yields:
  • None

    Delegation point; the wrapped hook produces the :class:~pytest.TestReport.

Source code in src/pytest_item_dict/plugin.py
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
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(
    self,
    item: Item,
    call: CallInfo,
) -> Generator[None, Any, None]:
	"""Capture per-test outcomes and cumulative durations in real time.

	This hook wraps the default report-creation mechanism so it can inspect
	the :class:`~pytest.TestReport` for each test phase.  Only the
	``"call"`` phase updates the test outcome; all phases contribute to the
	cumulative item duration.

	Parameters
	----------
	item : pytest.Item
		The test item being reported.
	call : pytest.CallInfo
		Timing and exception information for the current test phase.

	Yields
	------
	None
		Delegation point; the wrapped hook produces the
		:class:`~pytest.TestReport`.
	"""
	outcome = yield
	report: TestReport = outcome.get_result()

	if hasattr(item, TestProperties.DURATION):
		prev_duration: float = getattr(item, TestProperties.DURATION)
		setattr(item, TestProperties.DURATION, prev_duration + report.duration)

	match report.when:

		case "call":
			if self.test_dict.set_outcomes:
				setattr(item, TestProperties.OUTCOME, report.outcome)
				if self.test_dict.update_on_test:
					self.test_dict.set_outcome_attribute(item=item)

		case "setup":
			if self.test_dict.set_setup_teardown:
				setattr(item, TestProperties.SETUP_OUTCOME, report.outcome)
				if self.test_dict.update_on_test:
					self.test_dict.set_setup_attribute(item=item)
					item_cls = getattr(item, "cls", None)
					if (item in self._class_first_items and item_cls is not None and hasattr(item_cls, "setup_class")):
						self.test_dict.set_class_setup_attribute(item=item)

		case "teardown":
			if self.test_dict.set_setup_teardown:
				setattr(item, TestProperties.TEARDOWN_OUTCOME, report.outcome)
				if self.test_dict.update_on_test:
					self.test_dict.set_teardown_attribute(item=item)
					item_cls = getattr(item, "cls", None)
					if (item in self._class_last_items and item_cls is not None and hasattr(item_cls, "teardown_class")):
						self.test_dict.set_class_teardown_attribute(item=item)

		case _:
			pass

pytest_sessionfinish

pytest_sessionfinish(session: Session) -> None

Finalise the test-run hierarchy and compute aggregated metrics.

Called after all tests have completed. Writes per-item durations and markers (if enabled), then runs :meth:~TestDict.aggregate_counts to bubble outcome counts and total durations up through every parent node.

Parameters:
  • session (Session) –
    The pytest session object.
    
Source code in src/pytest_item_dict/plugin.py
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
def pytest_sessionfinish(self, session: Session) -> None:
	"""Finalise the test-run hierarchy and compute aggregated metrics.

	Called after all tests have completed.  Writes per-item durations and
	markers (if enabled), then runs :meth:`~TestDict.aggregate_counts` to
	bubble outcome counts and total durations up through every parent node.

	Parameters
	----------
	session : pytest.Session
		The pytest session object.
	"""
	self.test_dict._total_duration = time.time() - self._suite_start_time
	self.test_dict.run_ini_options()
	if self.test_dict.set_setup_teardown:
		self.test_dict.run_setup_teardown(
		    class_first_items=self._class_first_items,
		    class_last_items=self._class_last_items,
		)
	if self.test_dict.set_hierarchy_outcomes or self.test_dict.set_hierarchy_durations:
		self.test_dict.aggregate_counts()

pytest_addoption

pytest_addoption(parser: Parser) -> None

Register pytest ini options for the plugin.

Parameters:
  • parser (Parser) –
    The pytest option parser.
    
Source code in src/pytest_item_dict/plugin.py
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
def pytest_addoption(parser: Parser) -> None:
	"""Register pytest ini options for the plugin.

	Parameters
	----------
	parser : pytest.Parser
		The pytest option parser.
	"""
	group: pytest.OptionGroup = parser.getgroup(name=ITEM_DICT_PLUGIN_NAME)
	parser.addini(name=INIOptions.CREATE_ITEM_DICT, type='bool', default=True, help='create collection and test hierarchical dicts')
	parser.addini(name=INIOptions.SET_COLLECT_MARKERS, type='bool', default=False, help='set test markers in collection hierarchical dict')
	parser.addini(name=INIOptions.SET_TEST_MARKERS, type='bool', default=False, help='set test markers in test hierarchical dict')
	parser.addini(name=INIOptions.SET_TEST_OUTCOMES, type='bool', default=True, help='set test outcomes in test hierarchical dict')
	parser.addini(name=INIOptions.UPDATE_DICT_ON_TEST, type='bool', default=True, help='update the test outcomes after each test in test hierarchical dict')
	parser.addini(name=INIOptions.SET_TEST_DURATIONS, type='bool', default=False, help='set test durations in test hierarchical dict')
	parser.addini(name=INIOptions.SET_TEST_HIERARCHY_OUTCOMES, type='bool', default=False, help='count test outcomes in test hierarchical dict')
	parser.addini(name=INIOptions.SET_TEST_HIERARCHY_DURATIONS, type='bool', default=False, help='calculate test durations in test hierarchical dict')
	parser.addini(name=INIOptions.SET_SETUP_TEARDOWN, type='bool', default=False, help='record setup/teardown phase outcomes in test hierarchical dict')

pytest_configure

pytest_configure(config: Config) -> None

Register :class:ItemDictPlugin with pytest's plugin manager.

Parameters:
  • config (Config) –
    The active pytest configuration object.
    
Source code in src/pytest_item_dict/plugin.py
80
81
82
83
84
85
86
87
88
89
90
91
def pytest_configure(config: Config) -> None:
	"""Register :class:`ItemDictPlugin` with pytest's plugin manager.

	Parameters
	----------
	config : pytest.Config
		The active pytest configuration object.
	"""
	create_item_dict: bool = bool(config.getini(name=INIOptions.CREATE_ITEM_DICT))
	if create_item_dict:
		item_dict_plugin: ItemDictPlugin = ItemDictPlugin(config=config)
		config.pluginmanager.register(plugin=item_dict_plugin, name=ITEM_DICT_PLUGIN_NAME)

pytest_unconfigure

pytest_unconfigure(config: Config) -> None

Unregister :class:ItemDictPlugin from pytest's plugin manager.

Parameters:
  • config (Config) –
    The active pytest configuration object.
    
Source code in src/pytest_item_dict/plugin.py
 94
 95
 96
 97
 98
 99
100
101
102
103
104
def pytest_unconfigure(config: Config) -> None:
	"""Unregister :class:`ItemDictPlugin` from pytest's plugin manager.

	Parameters
	----------
	config : pytest.Config
		The active pytest configuration object.
	"""
	item_dict_plugin: object | None = config.pluginmanager.getplugin(name=ITEM_DICT_PLUGIN_NAME)
	if item_dict_plugin is not None:
		config.pluginmanager.unregister(plugin=item_dict_plugin)

write_json_file

write_json_file(hierarchy: dict[str, Any], prefix: str = 'collect', name: str = 'hierarchy') -> None

Serialise a hierarchy dict to a JSON file under output/reports/.

Parameters:
  • hierarchy (dict[str, Any]) –
    The hierarchical dict to serialise.
    
  • prefix (str, default: 'collect' ) –
    File-name prefix (default ``"collect"``).
    
  • name (str, default: 'hierarchy' ) –
    File-name stem (default ``"hierarchy"``).
    
Source code in src/pytest_item_dict/plugin.py
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
def write_json_file(
    hierarchy: dict[str, Any],
    prefix: str = "collect",
    name: str = "hierarchy",
) -> None:
	"""Serialise a hierarchy dict to a JSON file under ``output/reports/``.

	Parameters
	----------
	hierarchy : dict[str, Any]
		The hierarchical dict to serialise.
	prefix : str, optional
		File-name prefix (default ``"collect"``).
	name : str, optional
		File-name stem (default ``"hierarchy"``).
	"""
	output_file: str = Path(f"{__file__}/../../../output/reports/{prefix}_{name}.json").as_posix()
	Path(output_file).parent.mkdir(mode=0o764, parents=True, exist_ok=True)
	with open(file=output_file, mode="w+") as f:
		f.write(json.dumps(obj=hierarchy) + "\n")

write_xml_file

write_xml_file(hierarchy: dict[str, Any], prefix: str = 'collect', name: str = 'hierarchy') -> None

Serialise a hierarchy dict to an XML file under output/reports/.

Parameters:
  • hierarchy (dict[str, Any]) –
    The hierarchical dict to serialise.
    
  • prefix (str, default: 'collect' ) –
    File-name prefix (default ``"collect"``).
    
  • name (str, default: 'hierarchy' ) –
    File-name stem (default ``"hierarchy"``).
    
Source code in src/pytest_item_dict/plugin.py
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
def write_xml_file(
    hierarchy: dict[str, Any],
    prefix: str = "collect",
    name: str = "hierarchy",
) -> None:
	"""Serialise a hierarchy dict to an XML file under ``output/reports/``.

	Parameters
	----------
	hierarchy : dict[str, Any]
		The hierarchical dict to serialise.
	prefix : str, optional
		File-name prefix (default ``"collect"``).
	name : str, optional
		File-name stem (default ``"hierarchy"``).
	"""
	output_file: str = Path(f"{__file__}/../../../output/reports/{prefix}_{name}.xml").as_posix()
	xml: XMLConverter = XMLConverter(my_dict=hierarchy, root_node="pytest")
	Path(output_file).parent.mkdir(mode=0o764, parents=True, exist_ok=True)
	with open(file=output_file, mode="w+") as f:
		f.writelines(xml.formatted_xml)