sphinx-lua-ls 0.0.4__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,694 @@
1
+ """
2
+ Wrapper around the Lua-LS executable; able to download lua-ls if it's not installed.
3
+
4
+ """
5
+
6
+ import datetime
7
+ import json
8
+ import os
9
+ import pathlib
10
+ import platform
11
+ import re
12
+ import shutil
13
+ import signal
14
+ import stat
15
+ import subprocess
16
+ import sys
17
+ import tempfile
18
+ import typing as _t
19
+
20
+ import github
21
+ import requests
22
+ import requests.adapters
23
+ import urllib3
24
+ from sphinx.util import logging
25
+ from sphinx.util.console import bold # type: ignore
26
+
27
+ _PathLike: _t.TypeAlias = str | os.PathLike[str]
28
+
29
+
30
+ _logger = logging.getLogger("sphinx_lua_ls")
31
+
32
+
33
+ class LuaLsError(Exception):
34
+ """
35
+ Raised when LuaLS is unavailable, or when installation fails.
36
+
37
+ """
38
+
39
+
40
+ class LuaLsRunError(LuaLsError, subprocess.CalledProcessError):
41
+ """
42
+ Raised when LuaLS process fails.
43
+
44
+ """
45
+
46
+ def __str__(self):
47
+ if self.returncode and self.returncode < 0:
48
+ try:
49
+ returncode = f"signal {signal.Signals(-self.returncode)}"
50
+ except ValueError:
51
+ returncode = f"unknown signal {self.returncode}"
52
+ else:
53
+ returncode = f"code {self.returncode}"
54
+
55
+ msg = f"LuaLS run failed with {returncode}"
56
+ stderr = self.stderr
57
+ if self.stderr:
58
+ if isinstance(stderr, bytes):
59
+ stderr = stderr.decode("utf-8", errors="replace")
60
+ msg += f"\n\nStderr:\n{stderr}"
61
+ stdout = self.stdout
62
+ if self.stdout:
63
+ if isinstance(stdout, bytes):
64
+ stdout = stdout.decode("utf-8", errors="replace")
65
+ msg += f"\n\nStdout:\n{stdout}"
66
+ return msg
67
+
68
+
69
+ @_t.final
70
+ class LuaLs:
71
+ """
72
+ Interface for a lua-language-server installation.
73
+
74
+ Do not create directly, use :func:`resolve` instead.
75
+
76
+ """
77
+
78
+ def __init__(
79
+ self,
80
+ *,
81
+ _lua_ls_path: pathlib.Path,
82
+ _path: str,
83
+ _quiet: bool = True,
84
+ _env: dict[str, str] | None = None,
85
+ _cwd: _PathLike | None = None,
86
+ ):
87
+ self._lua_ls_path = _lua_ls_path
88
+ self._path = _path
89
+ self._quiet = _quiet
90
+ self._env = _env
91
+ self._cwd = _cwd
92
+
93
+ def run(
94
+ self,
95
+ input_path: _PathLike,
96
+ *,
97
+ quiet: bool | None = None,
98
+ env: dict[str, str] | None = None,
99
+ cwd: _PathLike | None = None,
100
+ ) -> _t.Any:
101
+ """
102
+ Renter the given VHS file.
103
+
104
+ :param input_path:
105
+ path to the directory/file that needs documentation.
106
+ :param quiet:
107
+ redefine `quiet` for this invocation. (see :func:`resolve`).
108
+ :param env:
109
+ redefine `env` for this invocation. (see :func:`resolve`).
110
+ :param cwd:
111
+ redefine `cmd` for this invocation. (see :func:`resolve`).
112
+ :return:
113
+ parsed documentation.
114
+
115
+ :raises LuaLsRunError: VHS process failed with non-zero return code.
116
+
117
+ """
118
+
119
+ if quiet is None:
120
+ quiet = self._quiet
121
+
122
+ if env is None:
123
+ env = self._env
124
+ if env is None:
125
+ env = os.environ.copy()
126
+ else:
127
+ env = env.copy()
128
+ env["PATH"] = self._path
129
+
130
+ if cwd is None:
131
+ cwd = self._cwd
132
+
133
+ with tempfile.TemporaryDirectory() as output_path:
134
+ args: list[str | _PathLike] = [
135
+ self._lua_ls_path,
136
+ "--doc",
137
+ input_path,
138
+ "--doc_out_path",
139
+ output_path,
140
+ ]
141
+
142
+ try:
143
+ _logger.debug(
144
+ "running lua-language-server with args %r", args, type="lua-ls"
145
+ )
146
+ subprocess.run(
147
+ args,
148
+ capture_output=quiet,
149
+ env=env,
150
+ cwd=cwd,
151
+ check=True,
152
+ )
153
+ except subprocess.CalledProcessError as e:
154
+ raise LuaLsRunError(
155
+ e.returncode,
156
+ e.cmd,
157
+ e.output,
158
+ e.stderr,
159
+ ) from None
160
+
161
+ return json.loads(pathlib.Path(output_path, "doc.json").read_text())
162
+
163
+
164
+ class ProgressReporter:
165
+ """
166
+ Interface for reporting installation progress.
167
+
168
+ """
169
+
170
+ def start(self):
171
+ """
172
+ Called when installation starts.
173
+
174
+ """
175
+
176
+ def progress(self, desc: str, dl_size: int, total_size: int, speed: float, /):
177
+ """
178
+ Called to update current progress.
179
+
180
+ :param desc:
181
+ description of the currently performed operation.
182
+ :param dl_size:
183
+ when the installer downloads files, this number indicates
184
+ number of bytes downloaded so far. Otherwise, it is set to zero.
185
+ :param total_size:
186
+ when the installer downloads files, this number indicates
187
+ total number of bytes to download. Otherwise, it is set to zero.
188
+ :param speed:
189
+ when the installer downloads files, this number indicates
190
+ current downloading speed, in bytes per second. Otherwise,
191
+ it is set to zero.
192
+
193
+ """
194
+
195
+ def finish(self, exc_type, exc_val, exc_tb):
196
+ """
197
+ Called when installation finishes.
198
+
199
+ """
200
+
201
+
202
+ class DefaultProgressReporter(ProgressReporter):
203
+ """
204
+ Default reporter that prints progress to stderr.
205
+
206
+ """
207
+
208
+ _prev_len = 0
209
+
210
+ def __init__(self, stream: _t.TextIO | None = None):
211
+ self.stream = stream or sys.stderr
212
+
213
+ def progress(self, desc: str, dl_size: int, total_size: int, speed: float, /):
214
+ desc = self.format_desc(desc)
215
+
216
+ if total_size:
217
+ desc += self.format_progress(dl_size, total_size, speed)
218
+
219
+ self.write(desc.ljust(self._prev_len) + "\r")
220
+
221
+ self._prev_len = len(desc)
222
+
223
+ def finish(self, exc_type, exc_val, exc_tb):
224
+ if exc_val:
225
+ self.progress(f"lua_ls installation failed: {exc_val}", 0, 0, 0)
226
+ self.write("\n")
227
+ elif self._prev_len > 0:
228
+ self.progress(f"lua_ls installed", 0, 0, 0)
229
+ self.write("\n")
230
+
231
+ def format_desc(self, desc: str) -> str:
232
+ return desc
233
+
234
+ def format_progress(self, dl_size: int, total_size: int, speed: float) -> str:
235
+ dl_size_mb = dl_size / 1024**2
236
+ total_size_mb = total_size / 1024**2
237
+ speed_mb = speed / 1024**2
238
+
239
+ return f": {dl_size_mb:.1f}/{total_size_mb:.1f}MB - {speed_mb:.2f}MB/s"
240
+
241
+ def write(self, msg: str):
242
+ self.stream.write(msg)
243
+ self.stream.flush()
244
+
245
+
246
+ class SphinxProgressReporter(DefaultProgressReporter):
247
+ _prev_desc = None
248
+ _prev_len = 0
249
+
250
+ def __init__(self, verbosity: int):
251
+ super().__init__()
252
+
253
+ self._verbosity = verbosity
254
+
255
+ def progress(self, desc: str, dl_size: int, total_size: int, speed: float, /):
256
+ if self._verbosity:
257
+ if desc != self._prev_desc:
258
+ _logger.info("%s", desc, type="lua-ls")
259
+ else:
260
+ super().progress(desc, dl_size, total_size, speed)
261
+
262
+ self._prev_desc = desc
263
+
264
+ def format_desc(self, desc: str) -> str:
265
+ return bold(desc + "...")
266
+
267
+ def format_progress(self, dl_size: int, total_size: int, speed: float) -> str:
268
+ dl_size_mb = dl_size / 1024**2
269
+ total_size_mb = total_size / 1024**2
270
+ speed_mb = speed / 1024**2
271
+ progress = dl_size / total_size
272
+
273
+ return f" [{progress: >3.0%}] {dl_size_mb:.1f}/{total_size_mb:.1f}MB ({speed_mb:.1f}MB/s)"
274
+
275
+ def write(self, msg: str):
276
+ _logger.info(msg, nonl=True, type="lua-ls")
277
+
278
+
279
+ def resolve(
280
+ *,
281
+ cache_path: _PathLike | None = None,
282
+ min_version: str = "3.0.0",
283
+ quiet: bool = True,
284
+ env: dict[str, str] | None = None,
285
+ cwd: _PathLike | None = None,
286
+ install: bool = True,
287
+ reporter: ProgressReporter = ProgressReporter(),
288
+ timeout: int = 15,
289
+ retry: _t.Optional[urllib3.Retry] = None,
290
+ ):
291
+ """
292
+ Find a system LuaLS installation or download LuaLS from GitHub.
293
+
294
+ If LuaLS is not installed, or it's outdated, try to download it
295
+ and install it into `cache_path`.
296
+
297
+ Automatic download only works on 64-bit Linux.
298
+ MacOS users will be presented with an instruction to use `brew`,
299
+ and other systems users will get a link to LuaLS installation guide.
300
+
301
+ :param cache_path:
302
+ path where LuaLS binaries should be downloaded to.
303
+ :param min_version:
304
+ minimal LuaLS version required.
305
+ :param quiet:
306
+ if true (default), any output from the LuaLS binary is hidden.
307
+ :param env:
308
+ overrides environment variables for the LuaLS process.
309
+ :param cwd:
310
+ overrides current working directory for the LuaLS process.
311
+ :param install:
312
+ if false, disables installing LuaLS from GitHub.
313
+ :param reporter:
314
+ a hook that will be called to inform user about installation
315
+ progress. See :class:`ProgressReporter` for API documentation,
316
+ and :class:`DefaultProgressReporter` for an example.
317
+ :param timeout:
318
+ timeout in seconds for connecting to GitHub APIs.
319
+ :param retry:
320
+ retry policy for reading from GitHub and downloading releases.
321
+ The default retry polity uses exponential backoff
322
+ to avoid rate limiting.
323
+ :return:
324
+ resolved LuaLS installation.
325
+ :raises LuaLsError:
326
+ LuaLS not available or installation failed.
327
+
328
+ """
329
+
330
+ if cache_path is None:
331
+ cache_path = default_cache_path()
332
+ else:
333
+ cache_path = pathlib.Path(cache_path)
334
+ cache_path = cache_path.expanduser().resolve()
335
+
336
+ _logger.debug("using lua_ls cache path: %s", cache_path, type="lua-ls")
337
+
338
+ if retry is None:
339
+ retry = urllib3.Retry(10, backoff_factor=0.1)
340
+
341
+ reporter.start()
342
+ try:
343
+ lua_ls_path, path = _check_and_install(
344
+ min_version, cache_path, _get_path(env), install, reporter, timeout, retry
345
+ )
346
+ finally:
347
+ reporter.finish(*sys.exc_info())
348
+
349
+ return LuaLs(
350
+ _lua_ls_path=lua_ls_path,
351
+ _path=path,
352
+ _quiet=quiet,
353
+ _env=env,
354
+ _cwd=cwd,
355
+ )
356
+
357
+
358
+ def default_cache_path() -> pathlib.Path:
359
+ """
360
+ Return default path where LuaLS binaries should be downloaded to.
361
+
362
+ Currently it is equal to ``pathlib.Path(tempfile.gettempdir()) / "python_lua_ls_cache"``.
363
+
364
+ """
365
+
366
+ if path := os.environ.get("LUA_LS_CACHE_PATH", None):
367
+ return pathlib.Path(path)
368
+ else:
369
+ return pathlib.Path(tempfile.gettempdir()) / "python_lua_ls_cache"
370
+
371
+
372
+ def _get_path(env: dict[str, str] | None) -> str:
373
+ path = (env or {}).get("PATH", None)
374
+ if path is None:
375
+ path = os.environ.get("PATH", None)
376
+ if path is None:
377
+ try:
378
+ path = os.confstr("CS_PATH")
379
+ except (AttributeError, ValueError):
380
+ pass
381
+ if path is None:
382
+ path = os.defpath or ""
383
+ return path
384
+
385
+
386
+ def _check_version(
387
+ version: str, lua_ls_path: _PathLike
388
+ ) -> _t.Tuple[bool, _t.Optional[str]]:
389
+ version_tuple = tuple(int(c) for c in version.split("."))
390
+ try:
391
+ _logger.debug("checking version of %a", lua_ls_path, type="lua-ls")
392
+ system_version_text_b = subprocess.check_output([lua_ls_path, "--version"])
393
+ system_version_text = system_version_text_b.decode().strip()
394
+ if match := re.search(r"(\d+\.\d+\.\d+)", system_version_text):
395
+ system_version = match.group(1)
396
+ system_version_tuple = tuple(int(c) for c in system_version.split("."))
397
+ if system_version_tuple >= version_tuple:
398
+ return True, system_version
399
+ else:
400
+ _logger.debug(
401
+ "%s is outdated (got %s, required %s)",
402
+ lua_ls_path,
403
+ system_version,
404
+ version,
405
+ type="lua-ls",
406
+ )
407
+ return False, system_version
408
+ else:
409
+ _logger.debug(
410
+ "%s printed invalid version %r",
411
+ lua_ls_path,
412
+ system_version_text,
413
+ type="lua-ls",
414
+ )
415
+ except (subprocess.SubprocessError, OSError, UnicodeDecodeError):
416
+ _logger.debug(
417
+ "%s failed to print its version", lua_ls_path, exc_info=True, type="lua-ls"
418
+ )
419
+
420
+ return False, None
421
+
422
+
423
+ def _check_and_install(
424
+ version: str,
425
+ cache_path: pathlib.Path,
426
+ path: str,
427
+ install: bool,
428
+ reporter: ProgressReporter,
429
+ timeout: int,
430
+ retry: urllib3.Retry,
431
+ ) -> _t.Tuple[pathlib.Path, str]:
432
+ if version.startswith("v"):
433
+ version = version[1:]
434
+
435
+ # Check system lua_ls
436
+
437
+ system_lua_ls_path = shutil.which("lua-language-server", path=path)
438
+ system_version = None
439
+ if system_lua_ls_path:
440
+ can_use_system_lua_ls, system_version = _check_version(
441
+ version, system_lua_ls_path
442
+ )
443
+ if can_use_system_lua_ls:
444
+ _logger.debug(
445
+ "using pre-installed lua-language-server at %s",
446
+ system_lua_ls_path,
447
+ type="lua-ls",
448
+ )
449
+ return pathlib.Path(system_lua_ls_path).expanduser().resolve(), path
450
+ else:
451
+ _logger.debug("pre-installed lua-language-server not found", type="lua-ls")
452
+
453
+ machine = platform.machine().lower()
454
+ if "arm" in machine:
455
+ machine = "arm"
456
+
457
+ return _install(
458
+ version,
459
+ cache_path,
460
+ path,
461
+ install,
462
+ reporter,
463
+ timeout,
464
+ retry,
465
+ machine,
466
+ sys.platform,
467
+ system_lua_ls_path,
468
+ system_version,
469
+ )
470
+
471
+
472
+ def _install(
473
+ version: str,
474
+ cache_path: pathlib.Path,
475
+ path: str,
476
+ install: bool,
477
+ reporter: ProgressReporter,
478
+ timeout: int,
479
+ retry: urllib3.Retry,
480
+ machine: str,
481
+ platform: str,
482
+ system_lua_ls_path: str | None,
483
+ system_version: str | None,
484
+ verify: bool = False,
485
+ ):
486
+ # Check system compatibility.
487
+
488
+ release_names = {
489
+ ("darwin", "arm"): "-darwin-arm64.tar.gz",
490
+ ("darwin", "x86_64"): "-darwin-x64.tar.gz",
491
+ ("linux", "arm"): "-linux-arm64.tar.gz",
492
+ ("linux", "x86_64"): "-linux-x64.tar.gz",
493
+ ("win32", "amd64"): "-win32-x64.zip",
494
+ }
495
+
496
+ release_name = release_names.get((platform, machine), None)
497
+ if not install or not release_name:
498
+ if system_lua_ls_path:
499
+ raise LuaLsError(
500
+ f"you have lua-language-server {system_version}, "
501
+ f"but version {version} or newer is required; "
502
+ f"see upgrade instructions "
503
+ f"at https://lua_ls.github.io/#other-install"
504
+ )
505
+ else:
506
+ raise LuaLsError(
507
+ f"lua-language-server is not installed on your system; "
508
+ f"see installation instructions "
509
+ f"at https://lua_ls.github.io/#other-install"
510
+ )
511
+
512
+ # Check cached lua-ls
513
+
514
+ cache_path.mkdir(parents=True, exist_ok=True)
515
+
516
+ if platform == "win32":
517
+ bin_path = cache_path / "bin/lua-language-server.exe"
518
+ else:
519
+ bin_path = cache_path / "bin/lua-language-server"
520
+ if bin_path.exists():
521
+ bin_path.chmod(bin_path.stat().st_mode | stat.S_IEXEC)
522
+ can_use_cached_lua_ls, _ = _check_version(version, bin_path)
523
+ if can_use_cached_lua_ls:
524
+ _logger.debug("using cached lua-language-server", type="lua-ls")
525
+ return bin_path, path
526
+
527
+ # Download binary release.
528
+
529
+ api = github.Github(retry=retry, timeout=timeout)
530
+
531
+ _install_lua_ls(api, timeout, retry, cache_path, reporter, release_name, platform)
532
+
533
+ if verify:
534
+ can_use_cached_lua_ls, _ = _check_version(version, bin_path)
535
+ if not can_use_cached_lua_ls:
536
+ raise LuaLsError(
537
+ "downloaded latest lua-language-server is outdated; "
538
+ "are you sure min_lua_ls_version is correct?",
539
+ )
540
+ elif not bin_path.exists():
541
+ raise LuaLsError(
542
+ f"downloaded latest lua-language-server is broken: "
543
+ f"can't find {bin_path}",
544
+ )
545
+
546
+ return bin_path, path
547
+
548
+
549
+ def _install_lua_ls(
550
+ api: github.Github,
551
+ timeout: int,
552
+ retry: urllib3.Retry,
553
+ cache_path: pathlib.Path,
554
+ reporter: ProgressReporter,
555
+ release_name: str,
556
+ platform: str,
557
+ ):
558
+ filter = lambda name: name.endswith(release_name)
559
+
560
+ with tempfile.TemporaryDirectory() as tmp_dir_s:
561
+ tmp_dir = pathlib.Path(tmp_dir_s)
562
+
563
+ try:
564
+ tmp_file = _download_latest_release(
565
+ api,
566
+ timeout,
567
+ retry,
568
+ "lua-language-server",
569
+ "LuaLS/lua-language-server",
570
+ tmp_dir,
571
+ filter,
572
+ reporter,
573
+ )
574
+
575
+ reporter.progress(f"processing lua-language-server", 0, 0, 0)
576
+
577
+ _logger.debug("unpacking lua-language-server", type="lua-ls")
578
+
579
+ shutil.unpack_archive(tmp_file, cache_path)
580
+
581
+ if platform == "win32":
582
+ bin_path = cache_path / "bin/lua-language-server.exe"
583
+ else:
584
+ bin_path = cache_path / "bin/lua-language-server"
585
+ bin_path.chmod(bin_path.stat().st_mode | stat.S_IEXEC)
586
+ except Exception as e:
587
+ raise LuaLsError(f"lua-language-server install failed: {e}")
588
+
589
+
590
+ def _download_latest_release(
591
+ api: github.Github,
592
+ timeout: int,
593
+ retry: urllib3.Retry,
594
+ name: str,
595
+ repo_name: str,
596
+ dest: pathlib.Path,
597
+ filter: _t.Callable[[str], bool],
598
+ reporter: ProgressReporter,
599
+ ):
600
+ reporter.progress(f"resolving {name}", 0, 0, 0)
601
+
602
+ repo = api.get_repo(repo_name)
603
+
604
+ for release in repo.get_releases():
605
+ if release.draft or release.prerelease:
606
+ continue
607
+
608
+ _logger.debug("found %s release %s", name, release.tag_name, type="lua-ls")
609
+
610
+ for asset in release.assets:
611
+ _logger.debug("trying %s asset %s", name, asset.name, type="lua-ls")
612
+ if filter(asset.name):
613
+ _logger.debug("found %s asset %s", name, asset.name, type="lua-ls")
614
+ basename = asset.name
615
+ browser_download_url = asset.browser_download_url
616
+ break
617
+ else:
618
+ raise LuaLsError(
619
+ f"unable to find {name} release for platform {sys.platform}"
620
+ )
621
+
622
+ break
623
+ else:
624
+ raise LuaLsError(f"unable to find latest {name} release")
625
+
626
+ _logger.debug("downloading %s from %s", name, browser_download_url, type="lua-ls")
627
+
628
+ with requests.Session() as session:
629
+ adapter = requests.adapters.HTTPAdapter(max_retries=retry)
630
+ session.mount("https://", adapter)
631
+ session.mount("http://", adapter)
632
+
633
+ with requests.get(browser_download_url, stream=True, timeout=timeout) as stream:
634
+ stream.raise_for_status()
635
+
636
+ try:
637
+ size = int(stream.headers["content-length"])
638
+ except (KeyError, ValueError):
639
+ size = -1
640
+ downloaded = 0
641
+
642
+ reporter.progress(f"downloading {name}", downloaded, size, 0)
643
+
644
+ start = datetime.datetime.now()
645
+
646
+ with open(dest / basename, "wb") as dest_file:
647
+ for chunk in stream.iter_content(64 * 1024):
648
+ dest_file.write(chunk)
649
+ if size:
650
+ # note: this does not take content-encoding into account.
651
+ # our contents are not encoded, though, so this is fine.
652
+ time = (datetime.datetime.now() - start).total_seconds()
653
+ downloaded += len(chunk)
654
+ speed = downloaded / time if time else 0
655
+ reporter.progress(
656
+ f"downloading {name}", downloaded, size, speed
657
+ )
658
+
659
+ return dest / basename
660
+
661
+
662
+ if __name__ == "__main__":
663
+
664
+ def main():
665
+ import argparse
666
+
667
+ parser = argparse.ArgumentParser()
668
+ parser.add_argument("platform")
669
+ parser.add_argument("machine")
670
+ parser.add_argument("path", type=pathlib.Path)
671
+
672
+ _logger.setLevel("DEBUG")
673
+ _logger.logger.addHandler(
674
+ logging.NewLineStreamHandler(logging.SafeEncodingWriter(sys.stderr))
675
+ )
676
+
677
+ args = parser.parse_args()
678
+
679
+ _install(
680
+ "3.0.0",
681
+ args.path,
682
+ _get_path(None),
683
+ True,
684
+ DefaultProgressReporter(),
685
+ 15,
686
+ urllib3.Retry(10, backoff_factor=0.1),
687
+ args.machine,
688
+ args.platform,
689
+ None,
690
+ None,
691
+ False,
692
+ )
693
+
694
+ main()
sphinx_lua_ls/py.typed ADDED
File without changes
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2018 Tamika Nomara
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.