robotcode-robot 2.4.0__tar.gz → 2.5.0__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 (41) hide show
  1. {robotcode_robot-2.4.0 → robotcode_robot-2.5.0}/.gitignore +5 -1
  2. {robotcode_robot-2.4.0 → robotcode_robot-2.5.0}/PKG-INFO +1 -1
  3. robotcode_robot-2.5.0/src/robotcode/robot/__version__.py +1 -0
  4. robotcode_robot-2.5.0/src/robotcode/robot/diagnostics/data_cache.py +336 -0
  5. {robotcode_robot-2.4.0 → robotcode_robot-2.5.0}/src/robotcode/robot/diagnostics/document_cache_helper.py +232 -98
  6. {robotcode_robot-2.4.0 → robotcode_robot-2.5.0}/src/robotcode/robot/diagnostics/entities.py +78 -66
  7. robotcode_robot-2.5.0/src/robotcode/robot/diagnostics/import_resolver.py +656 -0
  8. {robotcode_robot-2.4.0 → robotcode_robot-2.5.0}/src/robotcode/robot/diagnostics/imports_manager.py +459 -312
  9. {robotcode_robot-2.4.0 → robotcode_robot-2.5.0}/src/robotcode/robot/diagnostics/keyword_finder.py +37 -33
  10. {robotcode_robot-2.4.0 → robotcode_robot-2.5.0}/src/robotcode/robot/diagnostics/library_doc.py +522 -275
  11. {robotcode_robot-2.4.0 → robotcode_robot-2.5.0}/src/robotcode/robot/diagnostics/model_helper.py +11 -24
  12. robotcode_robot-2.5.0/src/robotcode/robot/diagnostics/namespace.py +806 -0
  13. {robotcode_robot-2.4.0 → robotcode_robot-2.5.0}/src/robotcode/robot/diagnostics/namespace_analyzer.py +231 -140
  14. robotcode_robot-2.5.0/src/robotcode/robot/diagnostics/project_index.py +191 -0
  15. robotcode_robot-2.5.0/src/robotcode/robot/diagnostics/scope_tree.py +227 -0
  16. robotcode_robot-2.5.0/src/robotcode/robot/diagnostics/variable_scope.py +90 -0
  17. {robotcode_robot-2.4.0 → robotcode_robot-2.5.0}/src/robotcode/robot/diagnostics/workspace_config.py +1 -0
  18. {robotcode_robot-2.4.0 → robotcode_robot-2.5.0}/src/robotcode/robot/utils/__init__.py +3 -0
  19. {robotcode_robot-2.4.0 → robotcode_robot-2.5.0}/src/robotcode/robot/utils/ast.py +9 -16
  20. robotcode_robot-2.5.0/src/robotcode/robot/utils/robot_patching.py +46 -0
  21. robotcode_robot-2.5.0/src/robotcode/robot/utils/robot_path.py +82 -0
  22. {robotcode_robot-2.4.0 → robotcode_robot-2.5.0}/src/robotcode/robot/utils/variables.py +6 -6
  23. robotcode_robot-2.4.0/src/robotcode/robot/__version__.py +0 -1
  24. robotcode_robot-2.4.0/src/robotcode/robot/diagnostics/data_cache.py +0 -90
  25. robotcode_robot-2.4.0/src/robotcode/robot/diagnostics/namespace.py +0 -1863
  26. robotcode_robot-2.4.0/src/robotcode/robot/utils/robot_path.py +0 -69
  27. {robotcode_robot-2.4.0 → robotcode_robot-2.5.0}/README.md +0 -0
  28. {robotcode_robot-2.4.0 → robotcode_robot-2.5.0}/pyproject.toml +0 -0
  29. {robotcode_robot-2.4.0 → robotcode_robot-2.5.0}/src/robotcode/robot/__init__.py +0 -0
  30. {robotcode_robot-2.4.0 → robotcode_robot-2.5.0}/src/robotcode/robot/config/__init__.py +0 -0
  31. {robotcode_robot-2.4.0 → robotcode_robot-2.5.0}/src/robotcode/robot/config/loader.py +0 -0
  32. {robotcode_robot-2.4.0 → robotcode_robot-2.5.0}/src/robotcode/robot/config/model.py +0 -0
  33. {robotcode_robot-2.4.0 → robotcode_robot-2.5.0}/src/robotcode/robot/config/utils.py +0 -0
  34. {robotcode_robot-2.4.0 → robotcode_robot-2.5.0}/src/robotcode/robot/diagnostics/__init__.py +0 -0
  35. {robotcode_robot-2.4.0 → robotcode_robot-2.5.0}/src/robotcode/robot/diagnostics/diagnostics_modifier.py +0 -0
  36. {robotcode_robot-2.4.0 → robotcode_robot-2.5.0}/src/robotcode/robot/diagnostics/errors.py +0 -0
  37. {robotcode_robot-2.4.0 → robotcode_robot-2.5.0}/src/robotcode/robot/py.typed +0 -0
  38. {robotcode_robot-2.4.0 → robotcode_robot-2.5.0}/src/robotcode/robot/utils/markdownformatter.py +0 -0
  39. {robotcode_robot-2.4.0 → robotcode_robot-2.5.0}/src/robotcode/robot/utils/match.py +0 -0
  40. {robotcode_robot-2.4.0 → robotcode_robot-2.5.0}/src/robotcode/robot/utils/stubs.py +0 -0
  41. {robotcode_robot-2.4.0 → robotcode_robot-2.5.0}/src/robotcode/robot/utils/visitor.py +0 -0
@@ -334,4 +334,8 @@ bundled/libs
334
334
  results/
335
335
 
336
336
  # kilocode
337
- .kilocode/
337
+ .kilocode/
338
+
339
+ # .agents
340
+ .agents/
341
+ skills-lock.json
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: robotcode-robot
3
- Version: 2.4.0
3
+ Version: 2.5.0
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
@@ -0,0 +1 @@
1
+ __version__ = "2.5.0"
@@ -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