scipy-doctest 1.1__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.
- scipy_doctest/__init__.py +12 -0
- scipy_doctest/__main__.py +6 -0
- scipy_doctest/conftest.py +5 -0
- scipy_doctest/frontend.py +468 -0
- scipy_doctest/impl.py +506 -0
- scipy_doctest/plugin.py +336 -0
- scipy_doctest/tests/__init__.py +0 -0
- scipy_doctest/tests/failure_cases.py +17 -0
- scipy_doctest/tests/failure_cases_2.py +27 -0
- scipy_doctest/tests/finder_cases.py +65 -0
- scipy_doctest/tests/local_file.txt +0 -0
- scipy_doctest/tests/local_file_cases.py +37 -0
- scipy_doctest/tests/module_cases.py +192 -0
- scipy_doctest/tests/octave_a.mat +0 -0
- scipy_doctest/tests/scipy_ndimage_tutorial_clone.rst +2018 -0
- scipy_doctest/tests/stopwords_cases.py +9 -0
- scipy_doctest/tests/test_finder.py +170 -0
- scipy_doctest/tests/test_parser.py +36 -0
- scipy_doctest/tests/test_pytest_configuration.py +95 -0
- scipy_doctest/tests/test_runner.py +105 -0
- scipy_doctest/tests/test_skipmarkers.py +202 -0
- scipy_doctest/tests/test_testfile.py +24 -0
- scipy_doctest/tests/test_testmod.py +158 -0
- scipy_doctest/util.py +279 -0
- scipy_doctest-1.1.dist-info/LICENSE +29 -0
- scipy_doctest-1.1.dist-info/METADATA +390 -0
- scipy_doctest-1.1.dist-info/RECORD +29 -0
- scipy_doctest-1.1.dist-info/WHEEL +4 -0
- scipy_doctest-1.1.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import doctest
|
|
3
|
+
|
|
4
|
+
from contextlib import redirect_stderr
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
import matplotlib.pyplot as plt # noqa
|
|
10
|
+
HAVE_MATPLOTLIB = True
|
|
11
|
+
except Exception:
|
|
12
|
+
HAVE_MATPLOTLIB = False
|
|
13
|
+
|
|
14
|
+
import pytest
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
import scipy # noqa
|
|
18
|
+
HAVE_SCIPY = True
|
|
19
|
+
except Exception:
|
|
20
|
+
HAVE_SCIPY = False
|
|
21
|
+
|
|
22
|
+
from . import (module_cases as module,
|
|
23
|
+
stopwords_cases as stopwords,
|
|
24
|
+
finder_cases,
|
|
25
|
+
failure_cases,
|
|
26
|
+
failure_cases_2,
|
|
27
|
+
local_file_cases)
|
|
28
|
+
from ..frontend import testmod as _testmod, run_docstring_examples
|
|
29
|
+
from ..util import warnings_errors
|
|
30
|
+
from ..impl import DTConfig
|
|
31
|
+
|
|
32
|
+
_VERBOSE = 2
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@pytest.mark.skipif(not HAVE_SCIPY, reason='need scipy')
|
|
36
|
+
def test_module():
|
|
37
|
+
res, _ = _testmod(module, verbose=_VERBOSE)
|
|
38
|
+
assert res.failed == 0
|
|
39
|
+
assert res.attempted != 0
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@pytest.mark.skipif(not HAVE_SCIPY, reason='need scipy')
|
|
43
|
+
def test_module_vanilla_dtfinder():
|
|
44
|
+
config = DTConfig()
|
|
45
|
+
config.stopwords = []
|
|
46
|
+
res, _ = _testmod(module, verbose=_VERBOSE, config=config)
|
|
47
|
+
assert res.failed == 0
|
|
48
|
+
assert res.attempted != 0
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@pytest.mark.skipif(not HAVE_MATPLOTLIB, reason='need matplotlib')
|
|
52
|
+
def test_stopwords():
|
|
53
|
+
res, _ = _testmod(stopwords, verbose=_VERBOSE)
|
|
54
|
+
assert res.failed == 0
|
|
55
|
+
assert res.attempted != 0
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@pytest.mark.skipif(not HAVE_SCIPY, reason='need scipy')
|
|
59
|
+
def test_public_obj_discovery():
|
|
60
|
+
res, _ = _testmod(module, verbose=_VERBOSE, strategy='api')
|
|
61
|
+
assert res.failed == 0
|
|
62
|
+
assert res.attempted != 0
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_run_docstring_examples():
|
|
66
|
+
f = finder_cases.Klass
|
|
67
|
+
res1 = run_docstring_examples(f)
|
|
68
|
+
res2 = _testmod(finder_cases, strategy=[finder_cases.Klass])
|
|
69
|
+
assert res1 == res2
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_global_state():
|
|
73
|
+
# Make sure doctesting does not alter the global state, as much as reasonable
|
|
74
|
+
opts = np.get_printoptions()
|
|
75
|
+
_testmod(module)
|
|
76
|
+
new_opts = np.get_printoptions()
|
|
77
|
+
assert new_opts == opts
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_module_debugrunner():
|
|
81
|
+
with pytest.raises((doctest.UnexpectedException, doctest.DocTestFailure)):
|
|
82
|
+
_testmod(failure_cases, raise_on_error=True)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_verbosity_1():
|
|
86
|
+
# smoke test that verbose=1 works
|
|
87
|
+
stream = io.StringIO()
|
|
88
|
+
with redirect_stderr(stream):
|
|
89
|
+
_testmod(failure_cases, verbose=1, report=False)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_user_context():
|
|
93
|
+
# use a user context to turn warnings to errors : test that it raises
|
|
94
|
+
config = DTConfig()
|
|
95
|
+
config.user_context_mgr = warnings_errors
|
|
96
|
+
with pytest.raises(doctest.UnexpectedException):
|
|
97
|
+
_testmod(failure_cases_2,
|
|
98
|
+
raise_on_error=True, strategy=[failure_cases_2.func_depr],
|
|
99
|
+
config=config)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class TestLocalFiles:
|
|
103
|
+
def test_local_files(self):
|
|
104
|
+
# A doctest tries to open a local file. Test that it works
|
|
105
|
+
# (internally, the file will need to be copied).
|
|
106
|
+
config = DTConfig()
|
|
107
|
+
config.local_resources = {'scipy_doctest.tests.local_file_cases.local_files':
|
|
108
|
+
['local_file.txt']}
|
|
109
|
+
res, _ = _testmod(local_file_cases, config=config,
|
|
110
|
+
strategy=[local_file_cases.local_files],
|
|
111
|
+
verbose=_VERBOSE)
|
|
112
|
+
|
|
113
|
+
assert res.failed == 0
|
|
114
|
+
assert res.attempted != 0
|
|
115
|
+
|
|
116
|
+
@pytest.mark.skipif(not HAVE_SCIPY, reason='need scipy')
|
|
117
|
+
def test_sio_octave(self):
|
|
118
|
+
# scipy/tutorial/io.rst : octave_a.mat file
|
|
119
|
+
config = DTConfig()
|
|
120
|
+
config.local_resources = {'scipy_doctest.tests.local_file_cases.sio':
|
|
121
|
+
['octave_a.mat']}
|
|
122
|
+
res, _ = _testmod(local_file_cases, config=config,
|
|
123
|
+
strategy=[local_file_cases.sio],
|
|
124
|
+
verbose=_VERBOSE)
|
|
125
|
+
|
|
126
|
+
assert res.failed == 0
|
|
127
|
+
assert res.attempted != 0
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class TestNameErrorAfterException:
|
|
131
|
+
def test_name_error_after_exception(self):
|
|
132
|
+
# After an example fails, subsequent examples may emit NameErrors.
|
|
133
|
+
# Check that they are suppressed.
|
|
134
|
+
# This first came in in https://github.com/scipy/scipy/pull/13116
|
|
135
|
+
stream = io.StringIO()
|
|
136
|
+
with redirect_stderr(stream):
|
|
137
|
+
_testmod(failure_cases_2,
|
|
138
|
+
strategy=[failure_cases_2.func_name_error])
|
|
139
|
+
|
|
140
|
+
stream.seek(0)
|
|
141
|
+
output = stream.read()
|
|
142
|
+
|
|
143
|
+
assert "ValueError:" in output # the original exception
|
|
144
|
+
assert "NameError:" not in output # the follow-up NameError
|
|
145
|
+
|
|
146
|
+
def test_name_error_after_exception_off(self):
|
|
147
|
+
# show NameErrors
|
|
148
|
+
config = DTConfig(nameerror_after_exception=True)
|
|
149
|
+
stream = io.StringIO()
|
|
150
|
+
with redirect_stderr(stream):
|
|
151
|
+
_testmod(failure_cases_2,
|
|
152
|
+
strategy=[failure_cases_2.func_name_error], config=config)
|
|
153
|
+
|
|
154
|
+
stream.seek(0)
|
|
155
|
+
output = stream.read()
|
|
156
|
+
|
|
157
|
+
assert "ValueError:" in output # the original exception
|
|
158
|
+
assert "NameError:" in output # the follow-up NameError
|
scipy_doctest/util.py
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Assorted utilities.
|
|
3
|
+
"""
|
|
4
|
+
import os
|
|
5
|
+
import warnings
|
|
6
|
+
import operator
|
|
7
|
+
import shutil
|
|
8
|
+
import copy
|
|
9
|
+
import tempfile
|
|
10
|
+
import inspect
|
|
11
|
+
from contextlib import contextmanager
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@contextmanager
|
|
15
|
+
def matplotlib_make_nongui():
|
|
16
|
+
""" Temporarily make the matplotlib backend non-GUI; close all figures on exit.
|
|
17
|
+
"""
|
|
18
|
+
try:
|
|
19
|
+
import matplotlib
|
|
20
|
+
import matplotlib.pyplot as plt
|
|
21
|
+
backend = matplotlib.get_backend()
|
|
22
|
+
plt.close('all')
|
|
23
|
+
matplotlib.use('Agg')
|
|
24
|
+
except ImportError:
|
|
25
|
+
backend = None
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
# Matplotlib issues UserWarnings on plt.show() with a non-GUI backend,
|
|
29
|
+
# Filter them out.
|
|
30
|
+
# UserWarning: FigureCanvasAgg is non-interactive, and thus cannot be shown
|
|
31
|
+
with warnings.catch_warnings():
|
|
32
|
+
warnings.filterwarnings("ignore", "FigureCanvasAgg", UserWarning) # MPL >= 3.8.x
|
|
33
|
+
warnings.filterwarnings("ignore", "Matplotlib", UserWarning) # MPL <= 3.7.x
|
|
34
|
+
yield backend
|
|
35
|
+
finally:
|
|
36
|
+
if backend:
|
|
37
|
+
import matplotlib.pyplot as plt
|
|
38
|
+
plt.close('all')
|
|
39
|
+
matplotlib.use(backend)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@contextmanager
|
|
43
|
+
def temp_cwd(test, local_resources=None):
|
|
44
|
+
"""Switch to a temp directory, clean up when done.
|
|
45
|
+
|
|
46
|
+
Copy local files, if requested.
|
|
47
|
+
|
|
48
|
+
Parameters
|
|
49
|
+
----------
|
|
50
|
+
test : doctest.DocTest instance
|
|
51
|
+
The current test instance
|
|
52
|
+
local_resources : dict, optional
|
|
53
|
+
If provided, maps the name of the test (`test.name` attribute) to
|
|
54
|
+
a list of filenames to copy to the tempdir. File names are relative
|
|
55
|
+
to the `test.filename`, which is, in most cases, the name of the
|
|
56
|
+
file the doctest has been extracted from.
|
|
57
|
+
"""
|
|
58
|
+
cwd = os.getcwd()
|
|
59
|
+
tmpdir = tempfile.mkdtemp()
|
|
60
|
+
|
|
61
|
+
if local_resources and test.name in local_resources:
|
|
62
|
+
# local files requested; copy the files
|
|
63
|
+
path, _ = os.path.split(test.filename)
|
|
64
|
+
for fname in local_resources[test.name]:
|
|
65
|
+
shutil.copy(os.path.join(path, fname), tmpdir)
|
|
66
|
+
try:
|
|
67
|
+
os.chdir(tmpdir)
|
|
68
|
+
yield tmpdir
|
|
69
|
+
finally:
|
|
70
|
+
os.chdir(cwd)
|
|
71
|
+
shutil.rmtree(tmpdir)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# Options for the usr_context_mgr : do nothing (default), and control the random
|
|
75
|
+
# state, in two flavors.
|
|
76
|
+
|
|
77
|
+
@contextmanager
|
|
78
|
+
def scipy_rndm_state():
|
|
79
|
+
"""Restore the `np.random` state when done."""
|
|
80
|
+
# FIXME: this matches the refguide-check behavior, but is a tad strange:
|
|
81
|
+
# makes sure that the seed the old-fashioned np.random* methods is *NOT* reproducible
|
|
82
|
+
# but the new-style `default_rng()` *IS*.
|
|
83
|
+
# Should these two be either both repro or both not repro?
|
|
84
|
+
from scipy._lib._util import _fixed_default_rng
|
|
85
|
+
import numpy as np
|
|
86
|
+
with _fixed_default_rng():
|
|
87
|
+
np.random.seed(None)
|
|
88
|
+
yield
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@contextmanager
|
|
92
|
+
def numpy_rndm_state():
|
|
93
|
+
"""Restore the `np.random` state when done."""
|
|
94
|
+
# Make sure that the seed the old-fashioned np.random* methods is *NOT* reproducible
|
|
95
|
+
import numpy as np
|
|
96
|
+
np.random.seed(None)
|
|
97
|
+
yield
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@contextmanager
|
|
101
|
+
def noop_context_mgr(test):
|
|
102
|
+
"""Do nothing.
|
|
103
|
+
|
|
104
|
+
This is a stub context manager to serve as a default for
|
|
105
|
+
``DTConfig().user_context_mgr``, for users to override.
|
|
106
|
+
"""
|
|
107
|
+
yield
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@contextmanager
|
|
111
|
+
def np_errstate():
|
|
112
|
+
"""A context manager to restore the numpy errstate and printoptions when done."""
|
|
113
|
+
import numpy as np
|
|
114
|
+
with np.errstate():
|
|
115
|
+
with np.printoptions():
|
|
116
|
+
yield
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@contextmanager
|
|
120
|
+
def warnings_errors(test):
|
|
121
|
+
"""Temporarily turn all warnings to errors."""
|
|
122
|
+
with warnings.catch_warnings():
|
|
123
|
+
warnings.simplefilter('error', Warning)
|
|
124
|
+
yield
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _map_verbosity(level):
|
|
128
|
+
"""A helper for validating/constructing the verbosity level.
|
|
129
|
+
|
|
130
|
+
Validate that $ 0 <= level <= 2 $ and map the boolean flag for the
|
|
131
|
+
`doctest.DocTestFinder` et al:
|
|
132
|
+
0, 1 -> False, 2 -> True
|
|
133
|
+
See the `testmod` docstring for details.
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
Parameters
|
|
137
|
+
----------
|
|
138
|
+
level : int or None
|
|
139
|
+
Allowed values are 0, 1 or 2. None mean 0.
|
|
140
|
+
|
|
141
|
+
Returns
|
|
142
|
+
-------
|
|
143
|
+
level : int
|
|
144
|
+
dtverbose : bool
|
|
145
|
+
|
|
146
|
+
"""
|
|
147
|
+
if level is None:
|
|
148
|
+
level = 0
|
|
149
|
+
level = operator.index(level)
|
|
150
|
+
if level not in [0, 1, 2]:
|
|
151
|
+
raise ValueError("Unknown verbosity setting : level = %s " % level)
|
|
152
|
+
dtverbose = True if level == 2 else False
|
|
153
|
+
return level, dtverbose
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
### Object / Doctest selection helpers ###
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def is_deprecated(f):
|
|
160
|
+
""" Check if an item is deprecated.
|
|
161
|
+
"""
|
|
162
|
+
with warnings.catch_warnings(record=True):
|
|
163
|
+
warnings.simplefilter("error")
|
|
164
|
+
try:
|
|
165
|
+
f(**{"not a kwarg": None})
|
|
166
|
+
except DeprecationWarning:
|
|
167
|
+
return True
|
|
168
|
+
except Exception:
|
|
169
|
+
pass
|
|
170
|
+
return False
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def get_all_list(module):
|
|
174
|
+
"""Return a copy of the __all__ list with irrelevant items removed.
|
|
175
|
+
The __all__list explicitly specifies which objects should be considered public.
|
|
176
|
+
- If strategy="api" and __all__ is missing, return an empty list.
|
|
177
|
+
- Also return a list of deprecated items and "other" items, which failed
|
|
178
|
+
to classify.
|
|
179
|
+
"""
|
|
180
|
+
if hasattr(module, "__all__"):
|
|
181
|
+
all_list = copy.deepcopy(module.__all__)
|
|
182
|
+
else:
|
|
183
|
+
all_list = []
|
|
184
|
+
for name in ['absolute_import', 'division', 'print_function']:
|
|
185
|
+
try:
|
|
186
|
+
all_list.remove(name)
|
|
187
|
+
except ValueError:
|
|
188
|
+
pass
|
|
189
|
+
|
|
190
|
+
# Modules are almost always private; real submodules need a separate
|
|
191
|
+
# run of refguide_check.
|
|
192
|
+
all_list = [name for name in all_list
|
|
193
|
+
if not inspect.ismodule(getattr(module, name, None))]
|
|
194
|
+
|
|
195
|
+
deprecated = []
|
|
196
|
+
not_deprecated = []
|
|
197
|
+
for name in all_list:
|
|
198
|
+
f = getattr(module, name, None)
|
|
199
|
+
if callable(f) and is_deprecated(f):
|
|
200
|
+
deprecated.append(name)
|
|
201
|
+
else:
|
|
202
|
+
not_deprecated.append(name)
|
|
203
|
+
|
|
204
|
+
others = set(dir(module)).difference(set(deprecated)).difference(set(not_deprecated))
|
|
205
|
+
|
|
206
|
+
return not_deprecated, deprecated, others
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def get_public_objects(module, skiplist=None):
|
|
210
|
+
"""Return a list of public objects in a module.
|
|
211
|
+
|
|
212
|
+
Parameters
|
|
213
|
+
----------
|
|
214
|
+
module :
|
|
215
|
+
A module look for objects
|
|
216
|
+
skiplist : list
|
|
217
|
+
The list of names to skip
|
|
218
|
+
|
|
219
|
+
Returns
|
|
220
|
+
-------
|
|
221
|
+
(items, names) : a tuple of two lists
|
|
222
|
+
`items` is a list of public objects in the module
|
|
223
|
+
`names` is a list of names of these objects. Each entry of this list
|
|
224
|
+
is nothing but `item.__name__` *if the latter exists* :
|
|
225
|
+
`name == item.__name__ if item.__name__`.
|
|
226
|
+
Otherwise, the name is taken from the `__all__` list of the module.
|
|
227
|
+
failures : list
|
|
228
|
+
a list of names which failed to be found in the module
|
|
229
|
+
|
|
230
|
+
"""
|
|
231
|
+
if skiplist is None:
|
|
232
|
+
skiplist = set()
|
|
233
|
+
|
|
234
|
+
all_list, _, _ = get_all_list(module)
|
|
235
|
+
|
|
236
|
+
items, names, failures = [], [], []
|
|
237
|
+
|
|
238
|
+
for name in all_list:
|
|
239
|
+
full_name = module.__name__ + '.' + name
|
|
240
|
+
|
|
241
|
+
if full_name in skiplist:
|
|
242
|
+
continue
|
|
243
|
+
|
|
244
|
+
try:
|
|
245
|
+
obj = getattr(module, name)
|
|
246
|
+
items.append(obj)
|
|
247
|
+
names.append(name)
|
|
248
|
+
except AttributeError:
|
|
249
|
+
import traceback
|
|
250
|
+
failures.append((full_name, False,
|
|
251
|
+
"Missing item!\n" +
|
|
252
|
+
traceback.format_exc()))
|
|
253
|
+
continue
|
|
254
|
+
|
|
255
|
+
return (items, names), failures
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# XXX: not used ATM
|
|
259
|
+
modules = []
|
|
260
|
+
def generate_log(module, test):
|
|
261
|
+
"""
|
|
262
|
+
Generate a log of the doctested items.
|
|
263
|
+
|
|
264
|
+
This function logs the items being doctested to a file named 'doctest.log'.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
module (module): The module being doctested.
|
|
268
|
+
test (str): The name of the doctest item.
|
|
269
|
+
"""
|
|
270
|
+
with open('doctest.log', 'a') as LOGFILE:
|
|
271
|
+
try:
|
|
272
|
+
if module.__name__ not in modules:
|
|
273
|
+
LOGFILE.write("\n" + module.__name__ + "\n")
|
|
274
|
+
LOGFILE.write("="*len(module.__name__) + "\n")
|
|
275
|
+
modules.append(module.__name__)
|
|
276
|
+
LOGFILE.write(f"{test}\n")
|
|
277
|
+
except AttributeError:
|
|
278
|
+
LOGFILE.write(f"{test}\n")
|
|
279
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022, Evgeni Burovski
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
17
|
+
contributors may be used to endorse or promote products derived from
|
|
18
|
+
this software without specific prior written permission.
|
|
19
|
+
|
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
23
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
24
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
25
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
26
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
27
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
28
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|