robotcode-robot 2.6.0__tar.gz → 2.6.1__tar.gz

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.
Files changed (48) hide show
  1. {robotcode_robot-2.6.0 → robotcode_robot-2.6.1}/PKG-INFO +2 -2
  2. {robotcode_robot-2.6.0 → robotcode_robot-2.6.1}/pyproject.toml +1 -1
  3. robotcode_robot-2.6.1/src/robotcode/robot/__version__.py +1 -0
  4. {robotcode_robot-2.6.0 → robotcode_robot-2.6.1}/src/robotcode/robot/diagnostics/data_cache.py +174 -55
  5. {robotcode_robot-2.6.0 → robotcode_robot-2.6.1}/src/robotcode/robot/diagnostics/document_cache_helper.py +17 -0
  6. robotcode_robot-2.6.0/src/robotcode/robot/__version__.py +0 -1
  7. {robotcode_robot-2.6.0 → robotcode_robot-2.6.1}/.gitignore +0 -0
  8. {robotcode_robot-2.6.0 → robotcode_robot-2.6.1}/README.md +0 -0
  9. {robotcode_robot-2.6.0 → robotcode_robot-2.6.1}/src/robotcode/robot/__init__.py +0 -0
  10. {robotcode_robot-2.6.0 → robotcode_robot-2.6.1}/src/robotcode/robot/config/__init__.py +0 -0
  11. {robotcode_robot-2.6.0 → robotcode_robot-2.6.1}/src/robotcode/robot/config/loader.py +0 -0
  12. {robotcode_robot-2.6.0 → robotcode_robot-2.6.1}/src/robotcode/robot/config/model.py +0 -0
  13. {robotcode_robot-2.6.0 → robotcode_robot-2.6.1}/src/robotcode/robot/config/utils.py +0 -0
  14. {robotcode_robot-2.6.0 → robotcode_robot-2.6.1}/src/robotcode/robot/diagnostics/__init__.py +0 -0
  15. {robotcode_robot-2.6.0 → robotcode_robot-2.6.1}/src/robotcode/robot/diagnostics/analyzer_result.py +0 -0
  16. {robotcode_robot-2.6.0 → robotcode_robot-2.6.1}/src/robotcode/robot/diagnostics/diagnostic_rules.py +0 -0
  17. {robotcode_robot-2.6.0 → robotcode_robot-2.6.1}/src/robotcode/robot/diagnostics/diagnostics_modifier.py +0 -0
  18. {robotcode_robot-2.6.0 → robotcode_robot-2.6.1}/src/robotcode/robot/diagnostics/entities.py +0 -0
  19. {robotcode_robot-2.6.0 → robotcode_robot-2.6.1}/src/robotcode/robot/diagnostics/errors.py +0 -0
  20. {robotcode_robot-2.6.0 → robotcode_robot-2.6.1}/src/robotcode/robot/diagnostics/import_resolver.py +0 -0
  21. {robotcode_robot-2.6.0 → robotcode_robot-2.6.1}/src/robotcode/robot/diagnostics/imports_manager.py +0 -0
  22. {robotcode_robot-2.6.0 → robotcode_robot-2.6.1}/src/robotcode/robot/diagnostics/keyword_finder.py +0 -0
  23. {robotcode_robot-2.6.0 → robotcode_robot-2.6.1}/src/robotcode/robot/diagnostics/library_doc.py +0 -0
  24. {robotcode_robot-2.6.0 → robotcode_robot-2.6.1}/src/robotcode/robot/diagnostics/model_helper.py +0 -0
  25. {robotcode_robot-2.6.0 → robotcode_robot-2.6.1}/src/robotcode/robot/diagnostics/namespace.py +0 -0
  26. {robotcode_robot-2.6.0 → robotcode_robot-2.6.1}/src/robotcode/robot/diagnostics/namespace_analyzer.py +0 -0
  27. {robotcode_robot-2.6.0 → robotcode_robot-2.6.1}/src/robotcode/robot/diagnostics/project_index.py +0 -0
  28. {robotcode_robot-2.6.0 → robotcode_robot-2.6.1}/src/robotcode/robot/diagnostics/scope_tree.py +0 -0
  29. {robotcode_robot-2.6.0 → robotcode_robot-2.6.1}/src/robotcode/robot/diagnostics/semantic_analyzer/__init__.py +0 -0
  30. {robotcode_robot-2.6.0 → robotcode_robot-2.6.1}/src/robotcode/robot/diagnostics/semantic_analyzer/analyzer.py +0 -0
  31. {robotcode_robot-2.6.0 → robotcode_robot-2.6.1}/src/robotcode/robot/diagnostics/semantic_analyzer/enums.py +0 -0
  32. {robotcode_robot-2.6.0 → robotcode_robot-2.6.1}/src/robotcode/robot/diagnostics/semantic_analyzer/model.py +0 -0
  33. {robotcode_robot-2.6.0 → robotcode_robot-2.6.1}/src/robotcode/robot/diagnostics/semantic_analyzer/nodes.py +0 -0
  34. {robotcode_robot-2.6.0 → robotcode_robot-2.6.1}/src/robotcode/robot/diagnostics/semantic_analyzer/run_keyword.py +0 -0
  35. {robotcode_robot-2.6.0 → robotcode_robot-2.6.1}/src/robotcode/robot/diagnostics/semantic_analyzer/serialization.py +0 -0
  36. {robotcode_robot-2.6.0 → robotcode_robot-2.6.1}/src/robotcode/robot/diagnostics/semantic_analyzer/variable_tokenizer.py +0 -0
  37. {robotcode_robot-2.6.0 → robotcode_robot-2.6.1}/src/robotcode/robot/diagnostics/variable_scope.py +0 -0
  38. {robotcode_robot-2.6.0 → robotcode_robot-2.6.1}/src/robotcode/robot/diagnostics/workspace_config.py +0 -0
  39. {robotcode_robot-2.6.0 → robotcode_robot-2.6.1}/src/robotcode/robot/py.typed +0 -0
  40. {robotcode_robot-2.6.0 → robotcode_robot-2.6.1}/src/robotcode/robot/utils/__init__.py +0 -0
  41. {robotcode_robot-2.6.0 → robotcode_robot-2.6.1}/src/robotcode/robot/utils/ast.py +0 -0
  42. {robotcode_robot-2.6.0 → robotcode_robot-2.6.1}/src/robotcode/robot/utils/markdownformatter.py +0 -0
  43. {robotcode_robot-2.6.0 → robotcode_robot-2.6.1}/src/robotcode/robot/utils/match.py +0 -0
  44. {robotcode_robot-2.6.0 → robotcode_robot-2.6.1}/src/robotcode/robot/utils/robot_patching.py +0 -0
  45. {robotcode_robot-2.6.0 → robotcode_robot-2.6.1}/src/robotcode/robot/utils/robot_path.py +0 -0
  46. {robotcode_robot-2.6.0 → robotcode_robot-2.6.1}/src/robotcode/robot/utils/stubs.py +0 -0
  47. {robotcode_robot-2.6.0 → robotcode_robot-2.6.1}/src/robotcode/robot/utils/variables.py +0 -0
  48. {robotcode_robot-2.6.0 → robotcode_robot-2.6.1}/src/robotcode/robot/utils/visitor.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: robotcode-robot
3
- Version: 2.6.0
3
+ Version: 2.6.1
4
4
  Summary: Support classes for RobotCode for handling Robot Framework projects.
5
5
  Project-URL: Homepage, https://robotcode.io
6
6
  Project-URL: Donate, https://opencollective.com/robotcode
@@ -26,7 +26,7 @@ Classifier: Topic :: Utilities
26
26
  Classifier: Typing :: Typed
27
27
  Requires-Python: >=3.10
28
28
  Requires-Dist: platformdirs>=4.3
29
- Requires-Dist: robotcode-core==2.6.0
29
+ Requires-Dist: robotcode-core==2.6.1
30
30
  Requires-Dist: robotframework>=5.0.0
31
31
  Requires-Dist: tomli>=1.1.0; python_version < '3.11'
32
32
  Description-Content-Type: text/markdown
@@ -31,7 +31,7 @@ dependencies = [
31
31
  "robotframework>=5.0.0",
32
32
  "tomli>=1.1.0; python_version < '3.11'",
33
33
  "platformdirs>=4.3",
34
- "robotcode-core==2.6.0",
34
+ "robotcode-core==2.6.1",
35
35
  ]
36
36
  dynamic = ["version"]
37
37
 
@@ -0,0 +1 @@
1
+ __version__ = "2.6.1"
@@ -2,11 +2,14 @@ import os
2
2
  import pickle
3
3
  import sqlite3
4
4
  import sys
5
+ import threading
5
6
  from contextlib import contextmanager
6
7
  from dataclasses import dataclass
7
8
  from enum import Enum
8
9
  from pathlib import Path
9
- from typing import Any, Generic, Iterator, List, Optional, Tuple, Type, TypeVar, Union
10
+ from typing import Any, Callable, Generic, Iterator, List, Optional, Tuple, Type, TypeVar, Union
11
+
12
+ from robotcode.core.utils.logging import LoggingDescriptor
10
13
 
11
14
  from ..utils import get_robot_version_str
12
15
 
@@ -19,6 +22,7 @@ else:
19
22
 
20
23
  _M = TypeVar("_M")
21
24
  _D = TypeVar("_D")
25
+ _R = TypeVar("_R")
22
26
 
23
27
 
24
28
  class CacheSection(Enum):
@@ -38,14 +42,14 @@ class CacheEntry(Generic[_M, _D]):
38
42
 
39
43
  def __init__(
40
44
  self,
41
- conn: sqlite3.Connection,
45
+ cache: "SqliteDataCache",
42
46
  section: "CacheSection",
43
47
  entry_name: str,
44
48
  meta_blob: Optional[bytes],
45
49
  meta_type: Union[Type[_M], Tuple[Type[_M], ...]],
46
50
  data_type: Union[Type[_D], Tuple[Type[_D], ...]],
47
51
  ) -> None:
48
- self._conn = conn
52
+ self._cache = cache
49
53
  self._section = section
50
54
  self._entry_name = entry_name
51
55
  self._meta_blob = meta_blob
@@ -70,10 +74,7 @@ class CacheEntry(Generic[_M, _D]):
70
74
  @property
71
75
  def data(self) -> _D:
72
76
  if not self._data_loaded:
73
- row = self._conn.execute(
74
- f"SELECT data FROM {self._section.value} WHERE entry_name = ?",
75
- (self._entry_name,),
76
- ).fetchone()
77
+ row = self._cache._fetch_data(self._section, self._entry_name)
77
78
  if row is None:
78
79
  raise RuntimeError(f"Cache entry '{self._entry_name}' disappeared from DB")
79
80
  result = pickle.loads(row[0])
@@ -247,16 +248,50 @@ def build_cache_dir(base_path: Path) -> Path:
247
248
  )
248
249
 
249
250
 
251
+ # Primary SQLite result codes for an unusable database file.
252
+ _SQLITE_CORRUPT = 11
253
+ _SQLITE_NOTADB = 26
254
+
255
+ # Substring markers used to recognize corruption when no error code is available
256
+ # (Python < 3.11) or when the error is raised by the sqlite3 module itself rather
257
+ # than the engine (e.g. "Could not decode to UTF-8" on a corrupt TEXT column, which
258
+ # carries no error code on any version). SQLite error strings are fixed and not
259
+ # localized, so substring matching is safe.
260
+ _CORRUPTION_MESSAGE_MARKERS = ("malformed", "not a database", "disk image", "corrupt", "decode")
261
+
262
+
263
+ def _is_corruption(exc: sqlite3.DatabaseError) -> bool:
264
+ """Whether the error means the database file itself is corrupt/unusable.
265
+
266
+ A locked database, a closed connection (``ProgrammingError``) or a constraint
267
+ violation are *not* corruption and must not trigger a destructive rebuild.
268
+ """
269
+ code = getattr(exc, "sqlite_errorcode", None) # available since Python 3.11
270
+ if code is not None:
271
+ return (code & 0xFF) in (_SQLITE_CORRUPT, _SQLITE_NOTADB)
272
+ message = str(exc).lower()
273
+ return any(marker in message for marker in _CORRUPTION_MESSAGE_MARKERS)
274
+
275
+
250
276
  class SqliteDataCache:
251
277
  """Cache backend using a single SQLite database with per-section tables.
252
278
 
253
279
  Each CacheSection gets its own table with entry_name as PK, plus meta and data
254
280
  BLOB columns. An app_version is stored in a metadata table; on version mismatch
255
281
  all tables are dropped and recreated.
282
+
283
+ All access to the single shared connection is serialized through a lock, and a
284
+ corrupt database (``sqlite3.DatabaseError``, e.g. "database disk image is
285
+ malformed") is detected and rebuilt from scratch instead of propagating to
286
+ callers.
256
287
  """
257
288
 
289
+ _logger = LoggingDescriptor()
290
+
258
291
  def __init__(self, cache_dir: Path, app_version: str = "") -> None:
259
292
  self.cache_dir = cache_dir
293
+ self._app_version = app_version
294
+ self._lock = threading.Lock()
260
295
 
261
296
  if not cache_dir.exists():
262
297
  cache_dir.mkdir(parents=True)
@@ -267,25 +302,84 @@ class SqliteDataCache:
267
302
 
268
303
  self._lock_fd = _acquire_shared_lock(cache_dir)
269
304
 
270
- db_path = cache_dir / "cache.db"
271
- self._conn = sqlite3.connect(str(db_path), check_same_thread=False)
305
+ try:
306
+ self._open()
307
+ except sqlite3.DatabaseError as e:
308
+ if not _is_corruption(e):
309
+ raise
310
+ self._rebuild()
311
+
312
+ def _open(self, *, in_memory: bool = False) -> None:
313
+ """Open the connection, configure it, and ensure the schema exists."""
314
+ self._conn = sqlite3.connect(":memory:" if in_memory else str(self.db_path), check_same_thread=False)
272
315
  self._conn.execute("PRAGMA journal_mode=WAL")
273
316
  self._conn.execute("PRAGMA synchronous=NORMAL")
274
317
  self._conn.execute("PRAGMA cache_size=-8000")
275
- self._conn.execute("PRAGMA mmap_size=67108864")
276
-
277
- self._ensure_schema(app_version)
278
-
279
- def _ensure_schema(self, app_version: str) -> None:
318
+ self._conn.execute("PRAGMA busy_timeout=5000")
319
+ # Memory-mapped reads race with a concurrent writer extending the file on
320
+ # macOS/APFS and can persist a torn page ("database disk image is
321
+ # malformed"); keep mmap only where the unified page cache makes it safe.
322
+ self._conn.execute(f"PRAGMA mmap_size={0 if sys.platform == 'darwin' else 67108864}")
323
+ self._ensure_schema()
324
+
325
+ def _purge_db_files(self) -> None:
326
+ # Best-effort: on Windows another process (a second editor window) may hold
327
+ # cache.db open, so unlink can raise PermissionError. Failing to delete must
328
+ # not abort recovery - _rebuild falls back to an in-memory cache.
329
+ for suffix in ("", "-wal", "-shm"):
330
+ try:
331
+ (self.cache_dir / f"cache.db{suffix}").unlink(missing_ok=True)
332
+ except OSError:
333
+ pass
334
+
335
+ def _rebuild(self) -> None:
336
+ """Discard a corrupt database and reopen an empty one.
337
+
338
+ If the corrupt file cannot be removed or reopened (e.g. another process holds
339
+ it open), fall back to a transient in-memory database so the cache stays usable
340
+ for this session instead of taking down the language server.
341
+ """
342
+ self._logger.warning(lambda: f"Cache database {self.db_path} is corrupt, rebuilding it from scratch.")
343
+ conn = getattr(self, "_conn", None)
344
+ if conn is not None:
345
+ try:
346
+ conn.close()
347
+ except sqlite3.Error:
348
+ pass
349
+ self._purge_db_files()
350
+ try:
351
+ self._open()
352
+ except sqlite3.DatabaseError as e:
353
+ if not _is_corruption(e):
354
+ raise
355
+ self._logger.warning(
356
+ lambda: f"Could not rebuild cache database {self.db_path}; using a temporary in-memory cache."
357
+ )
358
+ self._open(in_memory=True)
359
+
360
+ def _run(self, operation: Callable[[], _R]) -> _R:
361
+ """Run a DB operation under the connection lock, rebuilding the cache once if it is corrupt."""
362
+ with self._lock:
363
+ try:
364
+ return operation()
365
+ except sqlite3.DatabaseError as e:
366
+ if not _is_corruption(e):
367
+ raise
368
+ self._rebuild()
369
+ return operation()
370
+
371
+ def _ensure_schema(self) -> None:
280
372
  self._conn.execute("CREATE TABLE IF NOT EXISTS _meta ( key TEXT PRIMARY KEY, value TEXT NOT NULL)")
281
373
 
282
374
  row = self._conn.execute("SELECT value FROM _meta WHERE key = 'app_version'").fetchone()
283
375
  stored_version = row[0] if row else None
284
376
 
285
- if stored_version != app_version:
377
+ if stored_version != self._app_version:
286
378
  for table in _TABLE_NAMES:
287
379
  self._conn.execute(f"DROP TABLE IF EXISTS {table}")
288
- self._conn.execute("INSERT OR REPLACE INTO _meta (key, value) VALUES ('app_version', ?)", (app_version,))
380
+ self._conn.execute(
381
+ "INSERT OR REPLACE INTO _meta (key, value) VALUES ('app_version', ?)", (self._app_version,)
382
+ )
289
383
 
290
384
  for table in _TABLE_NAMES:
291
385
  self._conn.execute(
@@ -305,15 +399,25 @@ class SqliteDataCache:
305
399
  meta_type: Union[Type[_M], Tuple[Type[_M], ...]],
306
400
  data_type: Union[Type[_D], Tuple[Type[_D], ...]],
307
401
  ) -> Optional[CacheEntry[_M, _D]]:
308
- row = self._conn.execute(
309
- f"SELECT meta FROM {section.value} WHERE entry_name = ?",
310
- (entry_name,),
311
- ).fetchone()
402
+ row = self._run(
403
+ lambda: self._conn.execute(
404
+ f"SELECT meta FROM {section.value} WHERE entry_name = ?",
405
+ (entry_name,),
406
+ ).fetchone()
407
+ )
312
408
 
313
409
  if row is None:
314
410
  return None
315
411
 
316
- return CacheEntry(self._conn, section, entry_name, row[0], meta_type, data_type)
412
+ return CacheEntry(self, section, entry_name, row[0], meta_type, data_type)
413
+
414
+ def _fetch_data(self, section: CacheSection, entry_name: str) -> Optional[Any]:
415
+ return self._run(
416
+ lambda: self._conn.execute(
417
+ f"SELECT data FROM {section.value} WHERE entry_name = ?",
418
+ (entry_name,),
419
+ ).fetchone()
420
+ )
317
421
 
318
422
  def save_entry(
319
423
  self,
@@ -324,19 +428,24 @@ class SqliteDataCache:
324
428
  ) -> None:
325
429
  meta_blob = pickle.dumps(meta, protocol=pickle.HIGHEST_PROTOCOL) if meta is not None else None
326
430
  data_blob = pickle.dumps(data, protocol=pickle.HIGHEST_PROTOCOL)
327
- self._conn.execute(
328
- f"INSERT INTO {section.value} (entry_name, meta, data)"
329
- f" VALUES (?, ?, ?)"
330
- f" ON CONFLICT(entry_name) DO UPDATE SET"
331
- f" meta = excluded.meta, data = excluded.data, modified_at = CURRENT_TIMESTAMP",
332
- (entry_name, meta_blob, data_blob),
333
- )
334
- self._conn.commit()
431
+
432
+ def op() -> None:
433
+ self._conn.execute(
434
+ f"INSERT INTO {section.value} (entry_name, meta, data)"
435
+ f" VALUES (?, ?, ?)"
436
+ f" ON CONFLICT(entry_name) DO UPDATE SET"
437
+ f" meta = excluded.meta, data = excluded.data, modified_at = CURRENT_TIMESTAMP",
438
+ (entry_name, meta_blob, data_blob),
439
+ )
440
+ self._conn.commit()
441
+
442
+ self._run(op)
335
443
 
336
444
  def close(self) -> None:
337
- self._conn.close()
338
- _release_lock(self._lock_fd)
339
- self._lock_fd = None
445
+ with self._lock:
446
+ self._conn.close()
447
+ fd, self._lock_fd = self._lock_fd, None
448
+ _release_lock(fd)
340
449
 
341
450
  @property
342
451
  def db_path(self) -> Path:
@@ -344,17 +453,19 @@ class SqliteDataCache:
344
453
 
345
454
  @property
346
455
  def app_version(self) -> Optional[str]:
347
- row = self._conn.execute("SELECT value FROM _meta WHERE key = 'app_version'").fetchone()
456
+ row = self._run(lambda: self._conn.execute("SELECT value FROM _meta WHERE key = 'app_version'").fetchone())
348
457
  return row[0] if row else None
349
458
 
350
459
  def get_section_stats(self, section: CacheSection) -> "SectionStats":
351
- row = self._conn.execute(
352
- f"SELECT COUNT(*),"
353
- f" COALESCE(SUM(LENGTH(meta) + LENGTH(data)), 0),"
354
- f" MIN(created_at),"
355
- f" MAX(modified_at)"
356
- f" FROM {section.value}",
357
- ).fetchone()
460
+ row = self._run(
461
+ lambda: self._conn.execute(
462
+ f"SELECT COUNT(*),"
463
+ f" COALESCE(SUM(LENGTH(meta) + LENGTH(data)), 0),"
464
+ f" MIN(created_at),"
465
+ f" MAX(modified_at)"
466
+ f" FROM {section.value}",
467
+ ).fetchone()
468
+ )
358
469
  assert row is not None
359
470
  return SectionStats(
360
471
  section=section,
@@ -365,12 +476,14 @@ class SqliteDataCache:
365
476
  )
366
477
 
367
478
  def list_entries(self, section: CacheSection) -> List["EntryInfo"]:
368
- rows = self._conn.execute(
369
- f"SELECT entry_name, created_at, modified_at,"
370
- f" LENGTH(meta), LENGTH(data)"
371
- f" FROM {section.value}"
372
- f" ORDER BY entry_name",
373
- ).fetchall()
479
+ rows = self._run(
480
+ lambda: self._conn.execute(
481
+ f"SELECT entry_name, created_at, modified_at,"
482
+ f" LENGTH(meta), LENGTH(data)"
483
+ f" FROM {section.value}"
484
+ f" ORDER BY entry_name",
485
+ ).fetchall()
486
+ )
374
487
  return [
375
488
  EntryInfo(
376
489
  entry_name=r[0],
@@ -383,17 +496,23 @@ class SqliteDataCache:
383
496
  ]
384
497
 
385
498
  def clear_section(self, section: CacheSection) -> int:
386
- cursor = self._conn.execute(f"DELETE FROM {section.value}")
387
- self._conn.commit()
388
- return cursor.rowcount
499
+ def op() -> int:
500
+ cursor = self._conn.execute(f"DELETE FROM {section.value}")
501
+ self._conn.commit()
502
+ return cursor.rowcount
503
+
504
+ return self._run(op)
389
505
 
390
506
  def clear_all(self) -> int:
391
- total = 0
392
- for table in _TABLE_NAMES:
393
- cursor = self._conn.execute(f"DELETE FROM {table}")
394
- total += cursor.rowcount
395
- self._conn.commit()
396
- return total
507
+ def op() -> int:
508
+ total = 0
509
+ for table in _TABLE_NAMES:
510
+ cursor = self._conn.execute(f"DELETE FROM {table}")
511
+ total += cursor.rowcount
512
+ self._conn.commit()
513
+ return total
514
+
515
+ return self._run(op)
397
516
 
398
517
 
399
518
  @dataclass
@@ -303,6 +303,23 @@ class DocumentsCacheHelper:
303
303
 
304
304
  return self.get_general_model(document)
305
305
 
306
+ def get_uncached_model(self, document: TextDocument) -> ast.AST:
307
+ """Build a fresh model that is never cached on the document.
308
+
309
+ The cached model returned by `get_model` is shared between all features.
310
+ Consumers that mutate the model in place (e.g. the Robocop formatter) must
311
+ use this instead, otherwise the mutations corrupt the cached model and break
312
+ subsequent operations.
313
+ """
314
+ document_type = self.get_document_type(document)
315
+
316
+ if document_type == DocumentType.INIT:
317
+ return self.__get_init_model(document)
318
+ if document_type == DocumentType.RESOURCE:
319
+ return self.__get_resource_model(document)
320
+
321
+ return self.__get_general_model(document)
322
+
306
323
  def __get_model(
307
324
  self,
308
325
  document: TextDocument,
@@ -1 +0,0 @@
1
- __version__ = "2.6.0"