robotcode-robot 2.4.0__tar.gz → 2.5.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.
- {robotcode_robot-2.4.0 → robotcode_robot-2.5.1}/.gitignore +5 -1
- {robotcode_robot-2.4.0 → robotcode_robot-2.5.1}/PKG-INFO +1 -1
- robotcode_robot-2.5.1/src/robotcode/robot/__version__.py +1 -0
- robotcode_robot-2.5.1/src/robotcode/robot/diagnostics/data_cache.py +336 -0
- {robotcode_robot-2.4.0 → robotcode_robot-2.5.1}/src/robotcode/robot/diagnostics/document_cache_helper.py +240 -98
- {robotcode_robot-2.4.0 → robotcode_robot-2.5.1}/src/robotcode/robot/diagnostics/entities.py +78 -66
- robotcode_robot-2.5.1/src/robotcode/robot/diagnostics/import_resolver.py +656 -0
- {robotcode_robot-2.4.0 → robotcode_robot-2.5.1}/src/robotcode/robot/diagnostics/imports_manager.py +512 -312
- {robotcode_robot-2.4.0 → robotcode_robot-2.5.1}/src/robotcode/robot/diagnostics/keyword_finder.py +49 -40
- {robotcode_robot-2.4.0 → robotcode_robot-2.5.1}/src/robotcode/robot/diagnostics/library_doc.py +524 -283
- {robotcode_robot-2.4.0 → robotcode_robot-2.5.1}/src/robotcode/robot/diagnostics/model_helper.py +49 -82
- robotcode_robot-2.5.1/src/robotcode/robot/diagnostics/namespace.py +806 -0
- {robotcode_robot-2.4.0 → robotcode_robot-2.5.1}/src/robotcode/robot/diagnostics/namespace_analyzer.py +236 -148
- robotcode_robot-2.5.1/src/robotcode/robot/diagnostics/project_index.py +191 -0
- robotcode_robot-2.5.1/src/robotcode/robot/diagnostics/scope_tree.py +227 -0
- robotcode_robot-2.5.1/src/robotcode/robot/diagnostics/variable_scope.py +90 -0
- {robotcode_robot-2.4.0 → robotcode_robot-2.5.1}/src/robotcode/robot/diagnostics/workspace_config.py +1 -0
- {robotcode_robot-2.4.0 → robotcode_robot-2.5.1}/src/robotcode/robot/utils/__init__.py +3 -0
- {robotcode_robot-2.4.0 → robotcode_robot-2.5.1}/src/robotcode/robot/utils/ast.py +9 -16
- robotcode_robot-2.5.1/src/robotcode/robot/utils/robot_patching.py +46 -0
- robotcode_robot-2.5.1/src/robotcode/robot/utils/robot_path.py +82 -0
- {robotcode_robot-2.4.0 → robotcode_robot-2.5.1}/src/robotcode/robot/utils/variables.py +14 -7
- robotcode_robot-2.4.0/src/robotcode/robot/__version__.py +0 -1
- robotcode_robot-2.4.0/src/robotcode/robot/diagnostics/data_cache.py +0 -90
- robotcode_robot-2.4.0/src/robotcode/robot/diagnostics/namespace.py +0 -1863
- robotcode_robot-2.4.0/src/robotcode/robot/utils/robot_path.py +0 -69
- {robotcode_robot-2.4.0 → robotcode_robot-2.5.1}/README.md +0 -0
- {robotcode_robot-2.4.0 → robotcode_robot-2.5.1}/pyproject.toml +0 -0
- {robotcode_robot-2.4.0 → robotcode_robot-2.5.1}/src/robotcode/robot/__init__.py +0 -0
- {robotcode_robot-2.4.0 → robotcode_robot-2.5.1}/src/robotcode/robot/config/__init__.py +0 -0
- {robotcode_robot-2.4.0 → robotcode_robot-2.5.1}/src/robotcode/robot/config/loader.py +0 -0
- {robotcode_robot-2.4.0 → robotcode_robot-2.5.1}/src/robotcode/robot/config/model.py +0 -0
- {robotcode_robot-2.4.0 → robotcode_robot-2.5.1}/src/robotcode/robot/config/utils.py +0 -0
- {robotcode_robot-2.4.0 → robotcode_robot-2.5.1}/src/robotcode/robot/diagnostics/__init__.py +0 -0
- {robotcode_robot-2.4.0 → robotcode_robot-2.5.1}/src/robotcode/robot/diagnostics/diagnostics_modifier.py +0 -0
- {robotcode_robot-2.4.0 → robotcode_robot-2.5.1}/src/robotcode/robot/diagnostics/errors.py +0 -0
- {robotcode_robot-2.4.0 → robotcode_robot-2.5.1}/src/robotcode/robot/py.typed +0 -0
- {robotcode_robot-2.4.0 → robotcode_robot-2.5.1}/src/robotcode/robot/utils/markdownformatter.py +0 -0
- {robotcode_robot-2.4.0 → robotcode_robot-2.5.1}/src/robotcode/robot/utils/match.py +0 -0
- {robotcode_robot-2.4.0 → robotcode_robot-2.5.1}/src/robotcode/robot/utils/stubs.py +0 -0
- {robotcode_robot-2.4.0 → robotcode_robot-2.5.1}/src/robotcode/robot/utils/visitor.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.5.1"
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import pickle
|
|
3
|
+
import sqlite3
|
|
4
|
+
import sys
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Generic, Iterator, List, Optional, Tuple, Type, TypeVar, Union
|
|
10
|
+
|
|
11
|
+
from ..utils import get_robot_version_str
|
|
12
|
+
|
|
13
|
+
_M = TypeVar("_M")
|
|
14
|
+
_D = TypeVar("_D")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CacheSection(Enum):
|
|
18
|
+
LIBRARY = "libdoc"
|
|
19
|
+
VARIABLES = "variables"
|
|
20
|
+
RESOURCE = "resource"
|
|
21
|
+
NAMESPACE = "namespace"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CacheEntry(Generic[_M, _D]):
|
|
25
|
+
"""Lazy cache entry that defers both deserialization and data blob loading.
|
|
26
|
+
|
|
27
|
+
Only the meta blob is read from the DB initially. The data blob is fetched
|
|
28
|
+
lazily on first `.data` access, avoiding the transfer of large blobs when
|
|
29
|
+
only meta validation is needed (e.g. on cache misses).
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
conn: sqlite3.Connection,
|
|
35
|
+
section: "CacheSection",
|
|
36
|
+
entry_name: str,
|
|
37
|
+
meta_blob: Optional[bytes],
|
|
38
|
+
meta_type: Union[Type[_M], Tuple[Type[_M], ...]],
|
|
39
|
+
data_type: Union[Type[_D], Tuple[Type[_D], ...]],
|
|
40
|
+
) -> None:
|
|
41
|
+
self._conn = conn
|
|
42
|
+
self._section = section
|
|
43
|
+
self._entry_name = entry_name
|
|
44
|
+
self._meta_blob = meta_blob
|
|
45
|
+
self._meta_type = meta_type
|
|
46
|
+
self._data_type = data_type
|
|
47
|
+
self._meta_cache: Optional[_M] = None
|
|
48
|
+
self._data_cache: Optional[_D] = None
|
|
49
|
+
self._meta_loaded = False
|
|
50
|
+
self._data_loaded = False
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def meta(self) -> Optional[_M]:
|
|
54
|
+
if not self._meta_loaded:
|
|
55
|
+
if self._meta_blob is not None:
|
|
56
|
+
result = pickle.loads(self._meta_blob)
|
|
57
|
+
if not isinstance(result, self._meta_type):
|
|
58
|
+
raise TypeError(f"Expected {self._meta_type} but got {type(result)}")
|
|
59
|
+
self._meta_cache = result
|
|
60
|
+
self._meta_loaded = True
|
|
61
|
+
return self._meta_cache
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def data(self) -> _D:
|
|
65
|
+
if not self._data_loaded:
|
|
66
|
+
row = self._conn.execute(
|
|
67
|
+
f"SELECT data FROM {self._section.value} WHERE entry_name = ?",
|
|
68
|
+
(self._entry_name,),
|
|
69
|
+
).fetchone()
|
|
70
|
+
if row is None:
|
|
71
|
+
raise RuntimeError(f"Cache entry '{self._entry_name}' disappeared from DB")
|
|
72
|
+
result = pickle.loads(row[0])
|
|
73
|
+
if not isinstance(result, self._data_type):
|
|
74
|
+
raise TypeError(f"Expected {self._data_type} but got {type(result)}")
|
|
75
|
+
self._data_cache = result
|
|
76
|
+
self._data_loaded = True
|
|
77
|
+
|
|
78
|
+
assert self._data_cache is not None
|
|
79
|
+
return self._data_cache
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
_TABLE_NAMES = [s.value for s in CacheSection]
|
|
83
|
+
|
|
84
|
+
CACHE_DIR_NAME = ".robotcode_cache"
|
|
85
|
+
_LOCK_FILE_NAME = "cache.lock"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _acquire_shared_lock(cache_dir: Path) -> Optional[int]:
|
|
89
|
+
"""Acquire a shared advisory lock on the cache directory.
|
|
90
|
+
|
|
91
|
+
Returns the file descriptor on Unix, None on Windows
|
|
92
|
+
(Windows already prevents deletion of open files).
|
|
93
|
+
"""
|
|
94
|
+
if sys.platform == "win32":
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
import fcntl
|
|
98
|
+
|
|
99
|
+
lock_path = cache_dir / _LOCK_FILE_NAME
|
|
100
|
+
fd = os.open(str(lock_path), os.O_RDWR | os.O_CREAT, 0o666)
|
|
101
|
+
try:
|
|
102
|
+
fcntl.flock(fd, fcntl.LOCK_SH)
|
|
103
|
+
except Exception:
|
|
104
|
+
os.close(fd)
|
|
105
|
+
raise
|
|
106
|
+
return fd
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _release_lock(fd: Optional[int]) -> None:
|
|
110
|
+
"""Release an advisory lock acquired via _acquire_shared_lock."""
|
|
111
|
+
if fd is None:
|
|
112
|
+
return
|
|
113
|
+
try:
|
|
114
|
+
if sys.platform != "win32":
|
|
115
|
+
import fcntl
|
|
116
|
+
|
|
117
|
+
fcntl.flock(fd, fcntl.LOCK_UN)
|
|
118
|
+
finally:
|
|
119
|
+
os.close(fd)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@contextmanager
|
|
123
|
+
def exclusive_cache_lock(cache_dir: Path) -> Iterator[bool]:
|
|
124
|
+
"""Context manager that tries to acquire an exclusive lock on a cache directory.
|
|
125
|
+
|
|
126
|
+
Yields True if the lock was acquired (cache is not in use),
|
|
127
|
+
False if another process holds a shared lock (cache is in use).
|
|
128
|
+
The lock is held until the context manager exits.
|
|
129
|
+
"""
|
|
130
|
+
if sys.platform == "win32":
|
|
131
|
+
yield True
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
import fcntl
|
|
135
|
+
|
|
136
|
+
lock_path = cache_dir / _LOCK_FILE_NAME
|
|
137
|
+
if not lock_path.exists():
|
|
138
|
+
yield True
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
fd = os.open(str(lock_path), os.O_RDWR)
|
|
142
|
+
try:
|
|
143
|
+
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
144
|
+
yield True
|
|
145
|
+
except OSError:
|
|
146
|
+
yield False
|
|
147
|
+
finally:
|
|
148
|
+
try:
|
|
149
|
+
fcntl.flock(fd, fcntl.LOCK_UN)
|
|
150
|
+
except OSError:
|
|
151
|
+
pass
|
|
152
|
+
os.close(fd)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def resolve_cache_base_path(base_path: Path) -> Path:
|
|
156
|
+
"""Apply ROBOTCODE_CACHE_DIR env var override to a cache base path."""
|
|
157
|
+
env_cache_dir = os.environ.get("ROBOTCODE_CACHE_DIR")
|
|
158
|
+
if env_cache_dir:
|
|
159
|
+
return Path(env_cache_dir)
|
|
160
|
+
return base_path
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def build_cache_dir(base_path: Path) -> Path:
|
|
164
|
+
return (
|
|
165
|
+
base_path
|
|
166
|
+
/ CACHE_DIR_NAME
|
|
167
|
+
/ f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
|
|
168
|
+
/ get_robot_version_str()
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class SqliteDataCache:
|
|
173
|
+
"""Cache backend using a single SQLite database with per-section tables.
|
|
174
|
+
|
|
175
|
+
Each CacheSection gets its own table with entry_name as PK, plus meta and data
|
|
176
|
+
BLOB columns. An app_version is stored in a metadata table; on version mismatch
|
|
177
|
+
all tables are dropped and recreated.
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
def __init__(self, cache_dir: Path, app_version: str = "") -> None:
|
|
181
|
+
self.cache_dir = cache_dir
|
|
182
|
+
|
|
183
|
+
if not cache_dir.exists():
|
|
184
|
+
cache_dir.mkdir(parents=True)
|
|
185
|
+
(cache_dir / ".gitignore").write_text(
|
|
186
|
+
"# Created by robotcode\n*\n",
|
|
187
|
+
"utf-8",
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
self._lock_fd = _acquire_shared_lock(cache_dir)
|
|
191
|
+
|
|
192
|
+
db_path = cache_dir / "cache.db"
|
|
193
|
+
self._conn = sqlite3.connect(str(db_path), check_same_thread=False)
|
|
194
|
+
self._conn.execute("PRAGMA journal_mode=WAL")
|
|
195
|
+
self._conn.execute("PRAGMA synchronous=NORMAL")
|
|
196
|
+
self._conn.execute("PRAGMA cache_size=-8000")
|
|
197
|
+
self._conn.execute("PRAGMA mmap_size=67108864")
|
|
198
|
+
|
|
199
|
+
self._ensure_schema(app_version)
|
|
200
|
+
|
|
201
|
+
def _ensure_schema(self, app_version: str) -> None:
|
|
202
|
+
self._conn.execute("CREATE TABLE IF NOT EXISTS _meta ( key TEXT PRIMARY KEY, value TEXT NOT NULL)")
|
|
203
|
+
|
|
204
|
+
row = self._conn.execute("SELECT value FROM _meta WHERE key = 'app_version'").fetchone()
|
|
205
|
+
stored_version = row[0] if row else None
|
|
206
|
+
|
|
207
|
+
if stored_version != app_version:
|
|
208
|
+
for table in _TABLE_NAMES:
|
|
209
|
+
self._conn.execute(f"DROP TABLE IF EXISTS {table}")
|
|
210
|
+
self._conn.execute("INSERT OR REPLACE INTO _meta (key, value) VALUES ('app_version', ?)", (app_version,))
|
|
211
|
+
|
|
212
|
+
for table in _TABLE_NAMES:
|
|
213
|
+
self._conn.execute(
|
|
214
|
+
f"CREATE TABLE IF NOT EXISTS {table} ("
|
|
215
|
+
f" entry_name TEXT PRIMARY KEY,"
|
|
216
|
+
f" meta BLOB,"
|
|
217
|
+
f" data BLOB NOT NULL,"
|
|
218
|
+
f" created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,"
|
|
219
|
+
f" modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP)"
|
|
220
|
+
)
|
|
221
|
+
self._conn.commit()
|
|
222
|
+
|
|
223
|
+
def read_entry(
|
|
224
|
+
self,
|
|
225
|
+
section: CacheSection,
|
|
226
|
+
entry_name: str,
|
|
227
|
+
meta_type: Union[Type[_M], Tuple[Type[_M], ...]],
|
|
228
|
+
data_type: Union[Type[_D], Tuple[Type[_D], ...]],
|
|
229
|
+
) -> Optional[CacheEntry[_M, _D]]:
|
|
230
|
+
row = self._conn.execute(
|
|
231
|
+
f"SELECT meta FROM {section.value} WHERE entry_name = ?",
|
|
232
|
+
(entry_name,),
|
|
233
|
+
).fetchone()
|
|
234
|
+
|
|
235
|
+
if row is None:
|
|
236
|
+
return None
|
|
237
|
+
|
|
238
|
+
return CacheEntry(self._conn, section, entry_name, row[0], meta_type, data_type)
|
|
239
|
+
|
|
240
|
+
def save_entry(
|
|
241
|
+
self,
|
|
242
|
+
section: CacheSection,
|
|
243
|
+
entry_name: str,
|
|
244
|
+
meta: Any,
|
|
245
|
+
data: Any,
|
|
246
|
+
) -> None:
|
|
247
|
+
meta_blob = pickle.dumps(meta, protocol=pickle.HIGHEST_PROTOCOL) if meta is not None else None
|
|
248
|
+
data_blob = pickle.dumps(data, protocol=pickle.HIGHEST_PROTOCOL)
|
|
249
|
+
self._conn.execute(
|
|
250
|
+
f"INSERT INTO {section.value} (entry_name, meta, data)"
|
|
251
|
+
f" VALUES (?, ?, ?)"
|
|
252
|
+
f" ON CONFLICT(entry_name) DO UPDATE SET"
|
|
253
|
+
f" meta = excluded.meta, data = excluded.data, modified_at = CURRENT_TIMESTAMP",
|
|
254
|
+
(entry_name, meta_blob, data_blob),
|
|
255
|
+
)
|
|
256
|
+
self._conn.commit()
|
|
257
|
+
|
|
258
|
+
def close(self) -> None:
|
|
259
|
+
self._conn.close()
|
|
260
|
+
_release_lock(self._lock_fd)
|
|
261
|
+
self._lock_fd = None
|
|
262
|
+
|
|
263
|
+
@property
|
|
264
|
+
def db_path(self) -> Path:
|
|
265
|
+
return self.cache_dir / "cache.db"
|
|
266
|
+
|
|
267
|
+
@property
|
|
268
|
+
def app_version(self) -> Optional[str]:
|
|
269
|
+
row = self._conn.execute("SELECT value FROM _meta WHERE key = 'app_version'").fetchone()
|
|
270
|
+
return row[0] if row else None
|
|
271
|
+
|
|
272
|
+
def get_section_stats(self, section: CacheSection) -> "SectionStats":
|
|
273
|
+
row = self._conn.execute(
|
|
274
|
+
f"SELECT COUNT(*),"
|
|
275
|
+
f" COALESCE(SUM(LENGTH(meta) + LENGTH(data)), 0),"
|
|
276
|
+
f" MIN(created_at),"
|
|
277
|
+
f" MAX(modified_at)"
|
|
278
|
+
f" FROM {section.value}",
|
|
279
|
+
).fetchone()
|
|
280
|
+
assert row is not None
|
|
281
|
+
return SectionStats(
|
|
282
|
+
section=section,
|
|
283
|
+
entry_count=row[0],
|
|
284
|
+
total_blob_bytes=row[1],
|
|
285
|
+
oldest_created=row[2],
|
|
286
|
+
newest_modified=row[3],
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
def list_entries(self, section: CacheSection) -> List["EntryInfo"]:
|
|
290
|
+
rows = self._conn.execute(
|
|
291
|
+
f"SELECT entry_name, created_at, modified_at,"
|
|
292
|
+
f" LENGTH(meta), LENGTH(data)"
|
|
293
|
+
f" FROM {section.value}"
|
|
294
|
+
f" ORDER BY entry_name",
|
|
295
|
+
).fetchall()
|
|
296
|
+
return [
|
|
297
|
+
EntryInfo(
|
|
298
|
+
entry_name=r[0],
|
|
299
|
+
created_at=r[1],
|
|
300
|
+
modified_at=r[2],
|
|
301
|
+
meta_bytes=r[3] or 0,
|
|
302
|
+
data_bytes=r[4] or 0,
|
|
303
|
+
)
|
|
304
|
+
for r in rows
|
|
305
|
+
]
|
|
306
|
+
|
|
307
|
+
def clear_section(self, section: CacheSection) -> int:
|
|
308
|
+
cursor = self._conn.execute(f"DELETE FROM {section.value}")
|
|
309
|
+
self._conn.commit()
|
|
310
|
+
return cursor.rowcount
|
|
311
|
+
|
|
312
|
+
def clear_all(self) -> int:
|
|
313
|
+
total = 0
|
|
314
|
+
for table in _TABLE_NAMES:
|
|
315
|
+
cursor = self._conn.execute(f"DELETE FROM {table}")
|
|
316
|
+
total += cursor.rowcount
|
|
317
|
+
self._conn.commit()
|
|
318
|
+
return total
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
@dataclass
|
|
322
|
+
class SectionStats:
|
|
323
|
+
section: CacheSection
|
|
324
|
+
entry_count: int
|
|
325
|
+
total_blob_bytes: int
|
|
326
|
+
oldest_created: Optional[str]
|
|
327
|
+
newest_modified: Optional[str]
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
@dataclass
|
|
331
|
+
class EntryInfo:
|
|
332
|
+
entry_name: str
|
|
333
|
+
created_at: Optional[str]
|
|
334
|
+
modified_at: Optional[str]
|
|
335
|
+
meta_bytes: int
|
|
336
|
+
data_bytes: int
|