pytest-agent-digest 0.2.0__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.
@@ -0,0 +1,23 @@
1
+ """Top-level package for pytest_agent_digest.
2
+
3
+ The pytest11 entry point points to this package, so pytest hook functions
4
+ are imported here from ``plugin`` to make them discoverable.
5
+ """
6
+
7
+ from pytest_agent_digest.plugin import (
8
+ AgentDigestPlugin,
9
+ get_output_modes,
10
+ get_report_path,
11
+ pytest_addoption,
12
+ pytest_configure,
13
+ )
14
+
15
+ __version__ = "0.2.0"
16
+
17
+ __all__ = [
18
+ "AgentDigestPlugin",
19
+ "get_output_modes",
20
+ "get_report_path",
21
+ "pytest_addoption",
22
+ "pytest_configure",
23
+ ]
@@ -0,0 +1,194 @@
1
+ """Test result collector for pytest-agent-digest."""
2
+
3
+ import re
4
+ from collections import Counter
5
+ from dataclasses import dataclass
6
+
7
+ import pytest
8
+
9
+
10
+ def strip_ansi(text: str) -> str:
11
+ """
12
+ Remove ANSI escape sequences from *text*.
13
+
14
+ Args:
15
+ text: The string that may contain ANSI escape sequences.
16
+
17
+ Returns:
18
+ The input string with all ANSI color/style codes removed.
19
+ """
20
+ return re.sub(r"\x1b\[[0-9;]*m", "", text)
21
+
22
+
23
+ @dataclass
24
+ class TestResult:
25
+ """Represents a single test outcome captured during a pytest session.
26
+
27
+ Attributes:
28
+ node_id: The pytest node ID (e.g. `tests/test_foo.py::test_bar`).
29
+ outcome: One of `"passed"`, `"failed"`, `"skipped"`, `"xfailed"`, `"xpassed"`.
30
+ longrepr: Formatted traceback or reason string, stripped of ANSI codes.
31
+ `None` when not applicable.
32
+ duration: Test duration in seconds.
33
+ skip_reason: Human-readable skip reason for skipped tests; `None` otherwise.
34
+ """
35
+
36
+ node_id: str
37
+ outcome: str
38
+ longrepr: str | None
39
+ duration: float
40
+ skip_reason: str | None
41
+
42
+
43
+ class ReportCollector:
44
+ """
45
+ Accumulates the `TestResult` objects while a pytest session runs.
46
+
47
+ Wire this into `pytest_configure` (instantiate) and `pytest_runtest_logreport` (call `add`) so the renderer can
48
+ consume a fully populated collector at the session end.
49
+ """
50
+
51
+ def __init__(self) -> None:
52
+ """Initialize with an empty result list."""
53
+ self.results: list[TestResult] = []
54
+
55
+ def add(self, report: pytest.TestReport) -> None:
56
+ """
57
+ Classify *report* and append a `TestResult` class if it is a call phase.
58
+
59
+ ``report.when == "call"`` reports are stored for normal outcomes.
60
+ ``report.when == "setup"`` reports are stored only when skipped, because
61
+ ``@pytest.mark.skip`` raises a ``Skipped`` exception during the setup phase
62
+ and never produces a call-phase report.
63
+
64
+ Args:
65
+ report: The pytest test report to classify and store.
66
+ """
67
+ if report.when == "setup" and report.skipped:
68
+ outcome = "skipped"
69
+ longrepr = self._extract_longrepr(report)
70
+ skip_reason = self._extract_skip_reason(report, outcome)
71
+ self.results.append(
72
+ TestResult(
73
+ node_id=report.nodeid,
74
+ outcome=outcome,
75
+ longrepr=longrepr,
76
+ duration=report.duration,
77
+ skip_reason=skip_reason,
78
+ )
79
+ )
80
+ return
81
+
82
+ if report.when != "call":
83
+ return
84
+
85
+ outcome = self._classify(report)
86
+ longrepr = self._extract_longrepr(report)
87
+ skip_reason = self._extract_skip_reason(report, outcome)
88
+
89
+ self.results.append(
90
+ TestResult(
91
+ node_id=report.nodeid,
92
+ outcome=outcome,
93
+ longrepr=longrepr,
94
+ duration=report.duration,
95
+ skip_reason=skip_reason,
96
+ )
97
+ )
98
+
99
+ # ------------------------------------------------------------------
100
+ # Properties
101
+ # ------------------------------------------------------------------
102
+
103
+ @property
104
+ def counts(self) -> dict[str, int]:
105
+ """
106
+ Return a dict of `{outcome: count}` with zero-count keys omitted.
107
+
108
+ Returns:
109
+ Mapping from the outcome string to the number of results with that outcome.
110
+ """
111
+ return dict(Counter(tr.outcome for tr in self.results))
112
+
113
+ @property
114
+ def has_failures(self) -> bool:
115
+ """
116
+ Return `True` if any result is `"failed"` or `"xpassed"`.
117
+
118
+ Returns:
119
+ `True` when there is at least one failure or unexpected pass.
120
+ """
121
+ return any(tr.outcome in {"failed", "xpassed"} for tr in self.results)
122
+
123
+ # ------------------------------------------------------------------
124
+ # Private helpers
125
+ # ------------------------------------------------------------------
126
+
127
+ @staticmethod
128
+ def _classify(report: pytest.TestReport) -> str:
129
+ """
130
+ Return the outcome string for *report*.
131
+
132
+ Args:
133
+ report: The test report to classify.
134
+
135
+ Returns:
136
+ One of `"passed"`, `"failed"`, `"skipped"`, `"xfailed"`, `"xpassed"`.
137
+ """
138
+ has_xfail = hasattr(report, "wasxfail")
139
+ if report.passed:
140
+ return "xpassed" if has_xfail else "passed"
141
+ if report.skipped:
142
+ return "xfailed" if has_xfail else "skipped"
143
+ return "failed"
144
+
145
+ @staticmethod
146
+ def _longrepr_reason(longrepr: object) -> str:
147
+ """
148
+ Extract and ANSI-strip a reason string from a longrepr value.
149
+
150
+ Skipped reports store ``longrepr`` as a ``(filename, lineno, reason)`` tuple;
151
+ all other reports store it as a string-like object.
152
+
153
+ Args:
154
+ longrepr: The raw ``report.longrepr`` value (tuple or str-like).
155
+
156
+ Returns:
157
+ A plain-text reason string.
158
+ """
159
+ if isinstance(longrepr, tuple):
160
+ return strip_ansi(str(longrepr[2]))
161
+ return strip_ansi(str(longrepr))
162
+
163
+ @staticmethod
164
+ def _extract_longrepr(report: pytest.TestReport) -> str | None:
165
+ """
166
+ Extract and ANSI-strip the longrepr from *report*.
167
+
168
+ Args:
169
+ report: The test report whose longrepr should be extracted.
170
+
171
+ Returns:
172
+ A plain-text longrepr string, or `None` if absent.
173
+ """
174
+ if report.longrepr is None:
175
+ return None
176
+ return ReportCollector._longrepr_reason(report.longrepr)
177
+
178
+ @staticmethod
179
+ def _extract_skip_reason(report: pytest.TestReport, outcome: str) -> str | None:
180
+ """
181
+ Return the skip reason for skipped tests, else `None`.
182
+
183
+ Args:
184
+ report: The test report to inspect.
185
+ outcome: The already-classified outcome string.
186
+
187
+ Returns:
188
+ The skip reason string, or `None` for non-skipped outcomes.
189
+ """
190
+ if outcome != "skipped":
191
+ return None
192
+ if report.longrepr is None:
193
+ return None
194
+ return ReportCollector._longrepr_reason(report.longrepr)
@@ -0,0 +1,153 @@
1
+ """pytest plugin hooks for pytest-agent-digest."""
2
+
3
+ from pathlib import Path
4
+
5
+ import pytest
6
+
7
+ from pytest_agent_digest.collector import ReportCollector
8
+ from pytest_agent_digest.renderer import render_report
9
+
10
+ _PLUGIN_NAME = "agent_digest_plugin"
11
+
12
+
13
+ class AgentDigestPlugin:
14
+ """
15
+ Internal plugin class that holds the per-session state.
16
+
17
+ Registered by `pytest_configure` so that `pytest_runtest_logreport` and
18
+ `pytest_sessionfinish` share the same `~pytest_agent_digest.collector.ReportCollector`
19
+ without a global.
20
+ """
21
+
22
+ def __init__(self) -> None:
23
+ """Initialize the plugin with a fresh collector."""
24
+ self.collector: ReportCollector = ReportCollector()
25
+
26
+ def pytest_runtest_logreport(self, report: pytest.TestReport) -> None:
27
+ """
28
+ Forward each test report to the collector.
29
+
30
+ Args:
31
+ report: The test report for the current phase.
32
+ """
33
+ self.collector.add(report)
34
+
35
+ def pytest_sessionfinish(self, session: pytest.Session, exitstatus: int) -> None:
36
+ """
37
+ Finalize the digest at the end of the test session.
38
+
39
+ When ``term`` mode is active, renders the Markdown digest and prints it to stdout.
40
+ When ``file`` mode is active, writes the Markdown digest to disk and prints a confirmation line.
41
+ Both modes may be active simultaneously.
42
+
43
+ Args:
44
+ session: The pytest session object.
45
+ exitstatus: The exit status code for the session.
46
+ """
47
+ config = session.config
48
+ modes = get_output_modes(config)
49
+ if not modes:
50
+ return
51
+
52
+ report = render_report(self.collector, verbose=config.option.verbose, tb_style=config.option.tbstyle)
53
+
54
+ if "term" in modes:
55
+ print(report, end="")
56
+
57
+ if "file" in modes:
58
+ path = get_report_path(config)
59
+ path.parent.mkdir(parents=True, exist_ok=True)
60
+ path.write_text(report, encoding="utf-8")
61
+ print(f"Agent digest written to {path}")
62
+
63
+
64
+ def pytest_addoption(parser: pytest.Parser) -> None:
65
+ """
66
+ Register pytest-agent-digest command-line options.
67
+
68
+ Args:
69
+ parser: The pytest argument parser.
70
+ """
71
+ parser.addoption(
72
+ "--agent-digest",
73
+ action="append",
74
+ choices=["term", "file"],
75
+ default=None,
76
+ help=(
77
+ "Generate Agent-friendly Markdown digest. Use 'term' for stdout, "
78
+ "'file' for file output. Can be passed twice."
79
+ ),
80
+ )
81
+ parser.addoption(
82
+ "--agent-digest-file",
83
+ action="store",
84
+ default=None,
85
+ help="Path for the Markdown digest file (default: test-results.md).",
86
+ )
87
+ parser.addini(
88
+ "agent_digest_file",
89
+ default="test-results.md",
90
+ help="Default path for the Agent digest file.",
91
+ )
92
+
93
+
94
+ @pytest.hookimpl(trylast=True)
95
+ def pytest_configure(config: pytest.Config) -> None:
96
+ """
97
+ Configure the pytest-agent-digest plugin.
98
+
99
+ Instantiates the `AgentDigestPlugin` class and registers it so its hooks are active for the session.
100
+ When ``term`` mode is active, unregisters pytest's built-in terminal reporter so only the Markdown
101
+ digest appears on stdout.
102
+
103
+ Marked ``trylast=True`` so that the built-in terminal reporter is already registered by the time
104
+ this hook runs, making it safe to unregister.
105
+
106
+ Args:
107
+ config: The pytest configuration object.
108
+ """
109
+ plugin = AgentDigestPlugin()
110
+ config.pluginmanager.register(plugin, _PLUGIN_NAME)
111
+
112
+ if "term" in get_output_modes(config):
113
+ tr = config.pluginmanager.get_plugin("terminalreporter")
114
+ if tr is not None:
115
+ config.pluginmanager.unregister(tr)
116
+
117
+
118
+ def get_output_modes(config: pytest.Config) -> set[str]:
119
+ """
120
+ Return the set of output modes requested via --agent-digest.
121
+
122
+ Args:
123
+ config: The pytest configuration object.
124
+
125
+ Returns:
126
+ A set of mode strings (`"term"`, `"file"`), or an empty set if the flag was not passed.
127
+ """
128
+ modes = config.getoption("--agent-digest", default=None)
129
+ if not modes:
130
+ return set()
131
+ return set(modes)
132
+
133
+
134
+ def get_report_path(config: pytest.Config) -> Path:
135
+ """
136
+ Return the output path for the Markdown digest file.
137
+
138
+ Resolution order:
139
+
140
+ 1. `--agent-digest-file` CLI flag
141
+ 2. `agent_digest_file` ini option
142
+ 3. Hard-coded default `test-results.md`
143
+
144
+ Args:
145
+ config: The pytest configuration object.
146
+
147
+ Returns:
148
+ A `pathlib.Path` pointing to the desired report file location.
149
+ """
150
+ cli_value = config.getoption("--agent-digest-file", default=None)
151
+ if cli_value is not None:
152
+ return Path(cli_value)
153
+ return Path(config.getini("agent_digest_file"))
@@ -0,0 +1,93 @@
1
+ """Markdown renderer for pytest-agent-digest."""
2
+
3
+ from pytest_agent_digest.collector import ReportCollector, TestResult
4
+
5
+ _OUTCOME_ORDER = ["passed", "failed", "skipped", "xfailed", "xpassed"]
6
+
7
+ _STATUS_LABEL: dict[str, str] = {
8
+ "failed": "FAILED",
9
+ "xpassed": "XPASSED",
10
+ }
11
+
12
+
13
+ def render_report(collector: ReportCollector, verbose: int, tb_style: str) -> str:
14
+ """
15
+ Render a Markdown report from a populated collector.
16
+
17
+ Args:
18
+ collector: The populated ``ReportCollector`` instance.
19
+ verbose: If ``True``, include a ``## Passes`` section listing each passed test.
20
+ tb_style: The pytest ``--tb`` style value. When ``"no"``, traceback
21
+ code blocks are omitted from failure entries.
22
+
23
+ Returns:
24
+ A Markdown string suitable for Agent consumption. The document always
25
+ ends with a newline and contains no ANSI escape sequences.
26
+ """
27
+ sections: list[str] = []
28
+
29
+ # ------------------------------------------------------------------
30
+ # Summary line (always present, even when empty)
31
+ # ------------------------------------------------------------------
32
+ counts = collector.counts
33
+ summary_parts = [f"{counts[o]} {o}" for o in _OUTCOME_ORDER if o in counts]
34
+ sections.append(", ".join(summary_parts))
35
+
36
+ # ------------------------------------------------------------------
37
+ # Failures section (failed + xpassed)
38
+ # ------------------------------------------------------------------
39
+ failures = [r for r in collector.results if r.outcome in {"failed", "xpassed"}]
40
+ if failures:
41
+ failure_lines: list[str] = ["## Failures"]
42
+ for result in failures:
43
+ failure_lines.extend(_failure_entry_lines(result, tb_style))
44
+ sections.append("\n".join(failure_lines))
45
+
46
+ # ------------------------------------------------------------------
47
+ # Skipped section
48
+ # ------------------------------------------------------------------
49
+ skipped = [r for r in collector.results if r.outcome == "skipped"]
50
+ if skipped:
51
+ skipped_lines: list[str] = ["## Skipped", ""]
52
+ for result in skipped:
53
+ skipped_lines.append(f"- {result.node_id}: {result.skip_reason}")
54
+ sections.append("\n".join(skipped_lines))
55
+
56
+ # ------------------------------------------------------------------
57
+ # Passes section (verbose only)
58
+ # ------------------------------------------------------------------
59
+ if verbose:
60
+ passed = [r for r in collector.results if r.outcome == "passed"]
61
+ if passed:
62
+ passed_lines: list[str] = ["## Passes", ""]
63
+ for result in passed:
64
+ passed_lines.append(f"- {result.node_id}")
65
+ sections.append("\n".join(passed_lines))
66
+
67
+ return "\n\n".join(sections) + "\n"
68
+
69
+
70
+ def _failure_entry_lines(result: TestResult, tb_style: str) -> list[str]:
71
+ """
72
+ Build the lines for a single failure entry.
73
+
74
+ Args:
75
+ result: The ``TestResult`` to render (must be ``failed`` or ``xpassed``).
76
+ tb_style: The pytest ``--tb`` style; ``"no"`` suppresses the traceback block.
77
+
78
+ Returns:
79
+ A list of strings (without a trailing blank line) representing the entry.
80
+ """
81
+ lines = [
82
+ "",
83
+ f"### {result.node_id}",
84
+ "",
85
+ f"**Status:** {_STATUS_LABEL.get(result.outcome, result.outcome.upper())}",
86
+ f"**Duration:** {result.duration:.2f}s",
87
+ ]
88
+ if tb_style != "no" and result.longrepr:
89
+ lines.append("")
90
+ lines.append("```")
91
+ lines.extend(tb_line.rstrip() for tb_line in result.longrepr.splitlines())
92
+ lines.append("```")
93
+ return lines
@@ -0,0 +1,86 @@
1
+ Metadata-Version: 2.4
2
+ Name: pytest-agent-digest
3
+ Version: 0.2.0
4
+ Summary: A Pytest plugin to generate a Markdown report for AI Agents
5
+ Project-URL: Homepage, https://github.com/callowayproject/pytest-agent-digest
6
+ Project-URL: Documentation, https://callowayproject.github.io/pytest-agent-digest
7
+ Project-URL: Repository, https://github.com/callowayproject/pytest-agent-digest
8
+ Project-URL: Changelog, https://github.com/callowayproject/pytest-agent-digest/CHANGELOG.md
9
+ Author-email: Corey Oordt <coreyoordt@gmail.com>
10
+ License: BSD License
11
+
12
+ Copyright (c) 2026, Corey Oordt
13
+ All rights reserved.
14
+
15
+ Redistribution and use in source and binary forms, with or without modification,
16
+ are permitted provided that the following conditions are met:
17
+
18
+ * Redistributions of source code must retain the above copyright notice, this
19
+ list of conditions and the following disclaimer.
20
+
21
+ * Redistributions in binary form must reproduce the above copyright notice, this
22
+ list of conditions and the following disclaimer in the documentation and/or
23
+ other materials provided with the distribution.
24
+
25
+ * Neither the name of pytest-agent-digest nor the names of its
26
+ contributors may be used to endorse or promote products derived from this
27
+ software without specific prior written permission.
28
+
29
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
30
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
31
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
32
+ IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
33
+ INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
34
+ BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
35
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
36
+ OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
37
+ OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
38
+ OF THE POSSIBILITY OF SUCH DAMAGE.
39
+ License-File: LICENSE
40
+ Keywords: AI,LLM,agent,pytest,report
41
+ Classifier: Development Status :: 3 - Alpha
42
+ Classifier: Environment :: Plugins
43
+ Classifier: Framework :: Pytest
44
+ Classifier: Intended Audience :: Developers
45
+ Classifier: License :: OSI Approved :: BSD License
46
+ Classifier: Natural Language :: English
47
+ Classifier: Programming Language :: Python :: 3
48
+ Classifier: Programming Language :: Python :: 3.11
49
+ Classifier: Programming Language :: Python :: 3.12
50
+ Classifier: Programming Language :: Python :: 3.13
51
+ Classifier: Programming Language :: Python :: 3.14
52
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
53
+ Classifier: Topic :: Software Development :: Testing
54
+ Requires-Python: >=3.11
55
+ Requires-Dist: pytest>=7.0
56
+ Description-Content-Type: text/markdown
57
+
58
+ # pytest-agent-digest
59
+
60
+ A pytest plugin that generates compact Markdown digests designed for consumption by AI agents and LLMs. Instead of the standard terminal output, `pytest-agent-digest` produces structured Markdown that is typically **≥20% smaller in token count** than raw pytest output — keeping your context window budget under control.
61
+
62
+ [![PyPI](https://img.shields.io/pypi/v/pytest-agent-digest)](https://pypi.org/project/pytest-agent-digest/)
63
+ [![Python](https://img.shields.io/pypi/pyversions/pytest-agent-digest)](https://pypi.org/project/pytest-agent-digest/)
64
+
65
+ ## Quick start
66
+
67
+ ```bash
68
+ # Install
69
+ pip install pytest-agent-digest
70
+
71
+ # Print Markdown digest to stdout (replaces normal output)
72
+ pytest --agent-digest=term
73
+
74
+ # Write digest to test-results.md (normal output preserved)
75
+ pytest --agent-digest=file
76
+
77
+ # Both at once
78
+ pytest --agent-digest=term --agent-digest=file
79
+ ```
80
+
81
+ ## Documentation
82
+
83
+ Full documentation is available at **<https://callowayproject.github.io/pytest-agent-digest>**, including:
84
+
85
+ - [Quickstart guide](https://callowayproject.github.io/pytest-agent-digest/quickstart/)
86
+ - [Options reference](https://callowayproject.github.io/pytest-agent-digest/reference/options/)
@@ -0,0 +1,9 @@
1
+ pytest_agent_digest/__init__.py,sha256=Vw4ToGGT_PLIznVxIq1G0mnQUjVPQPcSqQno5FZ67Us,498
2
+ pytest_agent_digest/collector.py,sha256=zMvZxV3kcL0ZO6BZLUiWqk5EU2phmAuGJKcG4-yvyTE,6203
3
+ pytest_agent_digest/plugin.py,sha256=HwhCSHdNNiWY-EfLglPokpfAFJNMolI3golyKgp_fOg,4729
4
+ pytest_agent_digest/renderer.py,sha256=x139dj-Ro-o36mOB7NMUoopkT5rsy2lXstFE7VGByJY,3629
5
+ pytest_agent_digest-0.2.0.dist-info/METADATA,sha256=qlHNobAm-MMe_QQVrIwAUrepnb9s5O0va2Wwu9_WkUY,4216
6
+ pytest_agent_digest-0.2.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
7
+ pytest_agent_digest-0.2.0.dist-info/entry_points.txt,sha256=CrI63EMB63yVm0o2dXTSaPtWQUpeFaNuqnxAJgOb_Qw,46
8
+ pytest_agent_digest-0.2.0.dist-info/licenses/LICENSE,sha256=TVX9QAgAPr4aFy9DSBKeZMmySsYXDsFRMTOXOh3q7Xc,1501
9
+ pytest_agent_digest-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [pytest11]
2
+ agent_digest = pytest_agent_digest
@@ -0,0 +1,29 @@
1
+ BSD License
2
+
3
+ Copyright (c) 2026, Corey Oordt
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without modification,
7
+ are permitted provided that the following conditions are met:
8
+
9
+ * Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ * Redistributions in binary form must reproduce the above copyright notice, this
13
+ list of conditions and the following disclaimer in the documentation and/or
14
+ other materials provided with the distribution.
15
+
16
+ * Neither the name of pytest-agent-digest nor the names of its
17
+ contributors may be used to endorse or promote products derived from this
18
+ software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
21
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
22
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
23
+ IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
24
+ INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
25
+ BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
27
+ OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
28
+ OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
29
+ OF THE POSSIBILITY OF SUCH DAMAGE.