robotcode-core 2.6.1__tar.gz → 2.7.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.
- {robotcode_core-2.6.1 → robotcode_core-2.7.0}/PKG-INFO +1 -1
- robotcode_core-2.7.0/src/robotcode/core/__version__.py +1 -0
- {robotcode_core-2.6.1 → robotcode_core-2.7.0}/src/robotcode/core/documents_manager.py +45 -2
- {robotcode_core-2.6.1 → robotcode_core-2.7.0}/src/robotcode/core/text_document.py +25 -1
- {robotcode_core-2.6.1 → robotcode_core-2.7.0}/src/robotcode/core/utils/path.py +57 -1
- robotcode_core-2.6.1/src/robotcode/core/__version__.py +0 -1
- {robotcode_core-2.6.1 → robotcode_core-2.7.0}/.gitignore +0 -0
- {robotcode_core-2.6.1 → robotcode_core-2.7.0}/README.md +0 -0
- {robotcode_core-2.6.1 → robotcode_core-2.7.0}/pyproject.toml +0 -0
- {robotcode_core-2.6.1 → robotcode_core-2.7.0}/src/robotcode/core/__init__.py +0 -0
- {robotcode_core-2.6.1 → robotcode_core-2.7.0}/src/robotcode/core/async_tools.py +0 -0
- {robotcode_core-2.6.1 → robotcode_core-2.7.0}/src/robotcode/core/concurrent.py +0 -0
- {robotcode_core-2.6.1 → robotcode_core-2.7.0}/src/robotcode/core/event.py +0 -0
- {robotcode_core-2.6.1 → robotcode_core-2.7.0}/src/robotcode/core/filewatcher.py +0 -0
- {robotcode_core-2.6.1 → robotcode_core-2.7.0}/src/robotcode/core/ignore_spec.py +0 -0
- {robotcode_core-2.6.1 → robotcode_core-2.7.0}/src/robotcode/core/language.py +0 -0
- {robotcode_core-2.6.1 → robotcode_core-2.7.0}/src/robotcode/core/lsp/__init__.py +0 -0
- {robotcode_core-2.6.1 → robotcode_core-2.7.0}/src/robotcode/core/lsp/types.py +0 -0
- {robotcode_core-2.6.1 → robotcode_core-2.7.0}/src/robotcode/core/py.typed +0 -0
- {robotcode_core-2.6.1 → robotcode_core-2.7.0}/src/robotcode/core/types.py +0 -0
- {robotcode_core-2.6.1 → robotcode_core-2.7.0}/src/robotcode/core/uri.py +0 -0
- {robotcode_core-2.6.1 → robotcode_core-2.7.0}/src/robotcode/core/utils/__init__.py +0 -0
- {robotcode_core-2.6.1 → robotcode_core-2.7.0}/src/robotcode/core/utils/caching.py +0 -0
- {robotcode_core-2.6.1 → robotcode_core-2.7.0}/src/robotcode/core/utils/cli.py +0 -0
- {robotcode_core-2.6.1 → robotcode_core-2.7.0}/src/robotcode/core/utils/dataclasses.py +0 -0
- {robotcode_core-2.6.1 → robotcode_core-2.7.0}/src/robotcode/core/utils/debugpy.py +0 -0
- {robotcode_core-2.6.1 → robotcode_core-2.7.0}/src/robotcode/core/utils/glob_path.py +0 -0
- {robotcode_core-2.6.1 → robotcode_core-2.7.0}/src/robotcode/core/utils/inspect.py +0 -0
- {robotcode_core-2.6.1 → robotcode_core-2.7.0}/src/robotcode/core/utils/logging.py +0 -0
- {robotcode_core-2.6.1 → robotcode_core-2.7.0}/src/robotcode/core/utils/net.py +0 -0
- {robotcode_core-2.6.1 → robotcode_core-2.7.0}/src/robotcode/core/utils/process.py +0 -0
- {robotcode_core-2.6.1 → robotcode_core-2.7.0}/src/robotcode/core/utils/safe_eval.py +0 -0
- {robotcode_core-2.6.1 → robotcode_core-2.7.0}/src/robotcode/core/utils/version.py +0 -0
- {robotcode_core-2.6.1 → robotcode_core-2.7.0}/src/robotcode/core/workspace.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.7.0"
|
|
@@ -9,6 +9,7 @@ from typing import (
|
|
|
9
9
|
Iterator,
|
|
10
10
|
List,
|
|
11
11
|
Optional,
|
|
12
|
+
Tuple,
|
|
12
13
|
Union,
|
|
13
14
|
)
|
|
14
15
|
|
|
@@ -17,8 +18,11 @@ from .event import event
|
|
|
17
18
|
from .language import LanguageDefinition, language_id_filter
|
|
18
19
|
from .lsp.types import DocumentUri
|
|
19
20
|
from .text_document import TextDocument
|
|
20
|
-
from .uri import Uri
|
|
21
|
+
from .uri import InvalidUriError, Uri
|
|
21
22
|
from .utils.logging import LoggingDescriptor
|
|
23
|
+
from .utils.path import DiskInfo, disk_info_from_stat
|
|
24
|
+
|
|
25
|
+
_STAT_VERIFY_MAX_RETRIES: Final = 3
|
|
22
26
|
|
|
23
27
|
|
|
24
28
|
class CantReadDocumentError(Exception):
|
|
@@ -58,6 +62,39 @@ class DocumentsManager:
|
|
|
58
62
|
|
|
59
63
|
raise FileNotFoundError(str(uri))
|
|
60
64
|
|
|
65
|
+
def read_document_text_with_disk_info(
|
|
66
|
+
self, uri: Uri, language_id: Union[str, Callable[[Any], bool], None]
|
|
67
|
+
) -> Tuple[str, Optional[DiskInfo]]:
|
|
68
|
+
"""Read document text and capture a stat snapshot bound to that exact content.
|
|
69
|
+
|
|
70
|
+
Uses a stat → read → stat-verify protocol: when the file changes while it
|
|
71
|
+
is being read, the read is retried. Returns ``None`` as info when no
|
|
72
|
+
consistent snapshot could be captured (non-file URI, stat error, or the
|
|
73
|
+
file never settled) — such text must not be persisted to or validated
|
|
74
|
+
against a disk cache.
|
|
75
|
+
"""
|
|
76
|
+
try:
|
|
77
|
+
path = uri.to_path()
|
|
78
|
+
st_before = os.stat(path)
|
|
79
|
+
except (InvalidUriError, OSError):
|
|
80
|
+
return self.read_document_text(uri, language_id), None
|
|
81
|
+
|
|
82
|
+
text = self.read_document_text(uri, language_id)
|
|
83
|
+
|
|
84
|
+
for _ in range(_STAT_VERIFY_MAX_RETRIES):
|
|
85
|
+
try:
|
|
86
|
+
st_after = os.stat(path)
|
|
87
|
+
except OSError:
|
|
88
|
+
return text, None
|
|
89
|
+
|
|
90
|
+
if (st_before.st_mtime_ns, st_before.st_size) == (st_after.st_mtime_ns, st_after.st_size):
|
|
91
|
+
return text, disk_info_from_stat(st_after)
|
|
92
|
+
|
|
93
|
+
st_before = st_after
|
|
94
|
+
text = self.read_document_text(uri, language_id)
|
|
95
|
+
|
|
96
|
+
return text, None
|
|
97
|
+
|
|
61
98
|
def detect_language_id(self, path_or_uri: Union[str, "os.PathLike[Any]", Uri]) -> str:
|
|
62
99
|
path = path_or_uri.to_path() if isinstance(path_or_uri, Uri) else Path(path_or_uri)
|
|
63
100
|
|
|
@@ -84,11 +121,13 @@ class DocumentsManager:
|
|
|
84
121
|
return result
|
|
85
122
|
|
|
86
123
|
try:
|
|
124
|
+
text, disk_info = self.read_document_text_with_disk_info(uri, language_id)
|
|
87
125
|
return self._append_document(
|
|
88
126
|
document_uri=DocumentUri(uri),
|
|
89
127
|
language_id=language_id or self.detect_language_id(path),
|
|
90
|
-
text=
|
|
128
|
+
text=text,
|
|
91
129
|
version=version,
|
|
130
|
+
disk_info=disk_info,
|
|
92
131
|
)
|
|
93
132
|
except (SystemExit, KeyboardInterrupt):
|
|
94
133
|
raise
|
|
@@ -147,12 +186,14 @@ class DocumentsManager:
|
|
|
147
186
|
text: str,
|
|
148
187
|
language_id: Optional[str] = None,
|
|
149
188
|
version: Optional[int] = None,
|
|
189
|
+
disk_info: Optional[DiskInfo] = None,
|
|
150
190
|
) -> TextDocument:
|
|
151
191
|
result = TextDocument(
|
|
152
192
|
document_uri=document_uri,
|
|
153
193
|
language_id=language_id,
|
|
154
194
|
text=text,
|
|
155
195
|
version=version,
|
|
196
|
+
disk_info=disk_info,
|
|
156
197
|
)
|
|
157
198
|
|
|
158
199
|
result.cache_invalidate.add(self._on_document_cache_invalidate)
|
|
@@ -166,6 +207,7 @@ class DocumentsManager:
|
|
|
166
207
|
language_id: str,
|
|
167
208
|
text: str,
|
|
168
209
|
version: Optional[int] = None,
|
|
210
|
+
disk_info: Optional[DiskInfo] = None,
|
|
169
211
|
) -> TextDocument:
|
|
170
212
|
with self._lock:
|
|
171
213
|
document = self._create_document(
|
|
@@ -173,6 +215,7 @@ class DocumentsManager:
|
|
|
173
215
|
language_id=language_id,
|
|
174
216
|
text=text,
|
|
175
217
|
version=version,
|
|
218
|
+
disk_info=disk_info,
|
|
176
219
|
)
|
|
177
220
|
|
|
178
221
|
self._documents[document_uri] = document
|
|
@@ -24,6 +24,7 @@ from robotcode.core.event import event
|
|
|
24
24
|
from robotcode.core.lsp.types import DocumentUri, Position, Range
|
|
25
25
|
from robotcode.core.uri import Uri
|
|
26
26
|
from robotcode.core.utils.logging import LoggingDescriptor
|
|
27
|
+
from robotcode.core.utils.path import DiskInfo
|
|
27
28
|
|
|
28
29
|
|
|
29
30
|
def is_multibyte_char(char: str) -> bool:
|
|
@@ -108,6 +109,7 @@ class TextDocument:
|
|
|
108
109
|
text: str,
|
|
109
110
|
language_id: Optional[str] = None,
|
|
110
111
|
version: Optional[int] = None,
|
|
112
|
+
disk_info: Optional[DiskInfo] = None,
|
|
111
113
|
) -> None:
|
|
112
114
|
super().__init__()
|
|
113
115
|
|
|
@@ -119,6 +121,7 @@ class TextDocument:
|
|
|
119
121
|
self._text = text
|
|
120
122
|
self._orig_text = text
|
|
121
123
|
self._orig_version = version
|
|
124
|
+
self._disk_info = disk_info
|
|
122
125
|
self._lines: Optional[List[str]] = None
|
|
123
126
|
self._cache: Dict[weakref.ref[Any], CacheEntry] = collections.defaultdict(CacheEntry)
|
|
124
127
|
self._data_lock = RLock(name=f"Document.data_lock '{document_uri}'", default_timeout=120)
|
|
@@ -143,6 +146,16 @@ class TextDocument:
|
|
|
143
146
|
with self._lock:
|
|
144
147
|
return self._text
|
|
145
148
|
|
|
149
|
+
@property
|
|
150
|
+
def disk_info(self) -> Optional[DiskInfo]:
|
|
151
|
+
"""Stat snapshot of the disk content this document's text was read from.
|
|
152
|
+
|
|
153
|
+
``None`` means the text is not known to match any disk state (e.g. it
|
|
154
|
+
came from the editor or the file changed while it was being read).
|
|
155
|
+
"""
|
|
156
|
+
with self._lock:
|
|
157
|
+
return self._disk_info
|
|
158
|
+
|
|
146
159
|
def save(self, version: Optional[int], text: Optional[str]) -> None:
|
|
147
160
|
self.apply_full_change(version, text, save=True)
|
|
148
161
|
|
|
@@ -158,7 +171,14 @@ class TextDocument:
|
|
|
158
171
|
self._lines = None
|
|
159
172
|
|
|
160
173
|
@_logger.call
|
|
161
|
-
def apply_full_change(
|
|
174
|
+
def apply_full_change(
|
|
175
|
+
self,
|
|
176
|
+
version: Optional[int],
|
|
177
|
+
text: Optional[str],
|
|
178
|
+
*,
|
|
179
|
+
save: bool = False,
|
|
180
|
+
disk_info: Optional[DiskInfo] = None,
|
|
181
|
+
) -> None:
|
|
162
182
|
with self._cache_invalidating():
|
|
163
183
|
if version is not None:
|
|
164
184
|
self._version = version
|
|
@@ -167,11 +187,14 @@ class TextDocument:
|
|
|
167
187
|
self._lines = None
|
|
168
188
|
if save:
|
|
169
189
|
self._orig_text = self._text
|
|
190
|
+
if text is not None or save:
|
|
191
|
+
self._disk_info = disk_info
|
|
170
192
|
|
|
171
193
|
@_logger.call
|
|
172
194
|
def apply_incremental_change(self, version: Optional[int], range: Range, text: str) -> None:
|
|
173
195
|
with self._cache_invalidating():
|
|
174
196
|
try:
|
|
197
|
+
self._disk_info = None
|
|
175
198
|
if version is not None:
|
|
176
199
|
self._version = version
|
|
177
200
|
|
|
@@ -371,6 +394,7 @@ class TextDocument:
|
|
|
371
394
|
|
|
372
395
|
def _clear(self) -> None:
|
|
373
396
|
self._lines = None
|
|
397
|
+
self._disk_info = None
|
|
374
398
|
self._invalidate_data()
|
|
375
399
|
|
|
376
400
|
@_logger.call
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import re
|
|
3
3
|
import sys
|
|
4
|
+
import time
|
|
5
|
+
from dataclasses import dataclass, field
|
|
4
6
|
from pathlib import Path
|
|
5
|
-
from typing import Any, Optional, Tuple, Union
|
|
7
|
+
from typing import Any, Final, Optional, Tuple, Union
|
|
6
8
|
|
|
7
9
|
|
|
8
10
|
def path_is_relative_to(
|
|
@@ -88,3 +90,57 @@ def same_file_id(
|
|
|
88
90
|
if id1 is None or id2 is None:
|
|
89
91
|
return False
|
|
90
92
|
return id1 == id2
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
RACY_MTIME_EPSILON_NS: Final = 2_000_000_000
|
|
96
|
+
"""Snapshot age below which a file state is not trusted for persistent caching.
|
|
97
|
+
|
|
98
|
+
Analogous to git's "racily clean" handling: if a file's mtime is within this
|
|
99
|
+
epsilon of the current time when its content is read, a later write could land
|
|
100
|
+
in the same filesystem timestamp tick and leave mtime and size unchanged, so
|
|
101
|
+
the snapshot must not be persisted. 2 s covers the coarsest common filesystem
|
|
102
|
+
timestamp granularity (FAT).
|
|
103
|
+
|
|
104
|
+
Known limitation: the age is computed against the local clock, so on a network
|
|
105
|
+
mount whose server clock trails the client by more than the epsilon a
|
|
106
|
+
just-written file already counts as trusted — the stat-only design cannot
|
|
107
|
+
detect that; a content hash would be required to close it.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass(frozen=True)
|
|
112
|
+
class DiskInfo:
|
|
113
|
+
"""Stat snapshot taken at the moment file content was read from disk.
|
|
114
|
+
|
|
115
|
+
Equality compares only ``mtime_ns`` and ``size`` — ``trusted`` describes the
|
|
116
|
+
capture moment (see ``RACY_MTIME_EPSILON_NS``), not the file state, and must
|
|
117
|
+
never influence comparisons of stored metadata against fresh probes.
|
|
118
|
+
|
|
119
|
+
Deliberately carries no ``st_ino``: file IDs are 0 or unstable on
|
|
120
|
+
FAT/exFAT/ReFS and change across SMB/FUSE remounts, which would invalidate
|
|
121
|
+
caches spuriously; the racy-mtime guard already covers the same-tick
|
|
122
|
+
rewrite case an inode check would catch.
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
mtime_ns: int
|
|
126
|
+
size: int
|
|
127
|
+
trusted: bool = field(default=True, compare=False)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def disk_info_from_stat(st: os.stat_result, now_ns: Optional[int] = None) -> DiskInfo:
|
|
131
|
+
if now_ns is None:
|
|
132
|
+
now_ns = time.time_ns()
|
|
133
|
+
return DiskInfo(
|
|
134
|
+
mtime_ns=st.st_mtime_ns,
|
|
135
|
+
size=st.st_size,
|
|
136
|
+
# a negative age (mtime in the future, e.g. clock skew) is also untrusted
|
|
137
|
+
trusted=now_ns - st.st_mtime_ns >= RACY_MTIME_EPSILON_NS,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def probe_disk_info(path: Union[str, "os.PathLike[str]"]) -> Optional[DiskInfo]:
|
|
142
|
+
"""Return the current :class:`DiskInfo` for *path*, or ``None`` on error."""
|
|
143
|
+
try:
|
|
144
|
+
return disk_info_from_stat(os.stat(path))
|
|
145
|
+
except OSError:
|
|
146
|
+
return None
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "2.6.1"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|