2
2
3
3
import ast
4
4
import asyncio
5
+ import itertools
5
6
import os
7
+ import sys
6
8
import weakref
9
+ import zlib
7
10
from abc import ABC , abstractmethod
8
11
from collections import OrderedDict
9
12
from concurrent .futures import ProcessPoolExecutor
23
26
final ,
24
27
)
25
28
29
+ from ....__version__ import __version__
26
30
from ....utils .async_cache import AsyncSimpleLRUCache
27
31
from ....utils .async_tools import Lock , async_tasking_event , create_sub_task , threaded
32
+ from ....utils .dataclasses import as_json , from_json
33
+ from ....utils .glob_path import iter_files
28
34
from ....utils .logging import LoggingDescriptor
29
35
from ....utils .path import path_is_relative_to
30
36
from ....utils .uri import Uri
36
42
from ..utils .ast_utils import HasError , HasErrors , Token
37
43
from ..utils .async_ast import walk
38
44
from ..utils .robot_path import find_file_ex
39
- from ..utils .version import get_robot_version
45
+ from ..utils .version import get_robot_version , get_robot_version_str
40
46
from .entities import CommandLineVariableDefinition , VariableDefinition
41
47
42
48
if TYPE_CHECKING :
43
49
from ..protocol import RobotLanguageServerProtocol
44
50
from .namespace import Namespace
45
51
46
-
47
52
from .library_doc import (
48
53
ROBOT_LIBRARY_PACKAGE ,
49
54
ArgumentSpec ,
53
58
KeywordDoc ,
54
59
KeywordStore ,
55
60
LibraryDoc ,
61
+ LibraryType ,
62
+ ModuleSpec ,
56
63
VariablesDoc ,
57
64
complete_library_import ,
58
65
complete_resource_import ,
61
68
find_library ,
62
69
find_variables ,
63
70
get_library_doc ,
71
+ get_module_spec ,
64
72
get_variables_doc ,
65
73
is_embedded_keyword ,
66
74
is_library_by_path ,
@@ -451,13 +459,54 @@ async def get_libdoc(self) -> VariablesDoc:
451
459
return self ._lib_doc
452
460
453
461
462
+ @dataclass
463
+ class LibraryMetaData :
464
+ meta_version : str
465
+ name : Optional [str ]
466
+ origin : Optional [str ]
467
+ submodule_search_locations : Optional [List [str ]]
468
+ by_path : bool
469
+
470
+ mtimes : Optional [Dict [str , int ]] = None
471
+
472
+ @property
473
+ def filepath_base (self ) -> Path :
474
+ if self .by_path :
475
+ if self .origin is not None :
476
+ p = Path (self .origin )
477
+
478
+ return Path (f"{ zlib .adler32 (str (p .parent ).encode ('utf-8' )):08x} _{ p .stem } " )
479
+ else :
480
+ if self .name is not None :
481
+ return Path (self .name .replace ("." , "/" ))
482
+
483
+ raise ValueError ("Cannot determine filepath base." )
484
+
485
+
454
486
class ImportsManager :
455
487
_logger = LoggingDescriptor ()
456
488
457
489
def __init__ (self , parent_protocol : RobotLanguageServerProtocol , folder : Uri , config : RobotConfig ) -> None :
458
490
super ().__init__ ()
459
491
self .parent_protocol = parent_protocol
492
+
460
493
self .folder = folder
494
+ get_robot_version ()
495
+
496
+ cache_base_path = self .folder .to_path ()
497
+ if isinstance (self .parent_protocol .initialization_options , dict ):
498
+ if "storageUri" in self .parent_protocol .initialization_options :
499
+ cache_base_path = Uri (self .parent_protocol .initialization_options ["storageUri" ]).to_path ()
500
+ self ._logger .trace (lambda : f"use { cache_base_path } as base for caching" )
501
+
502
+ self .lib_doc_cache_path = (
503
+ cache_base_path
504
+ / ".robotcode_cache"
505
+ / f"{ sys .version_info .major } .{ sys .version_info .minor } .{ sys .version_info .micro } "
506
+ / get_robot_version_str ()
507
+ / "libdoc"
508
+ )
509
+
461
510
self .config : RobotConfig = config
462
511
self ._libaries_lock = Lock ()
463
512
self ._libaries : OrderedDict [_LibrariesEntryKey , _LibrariesEntry ] = OrderedDict ()
@@ -693,6 +742,58 @@ async def remove(k: _VariablesEntryKey, e: _VariablesEntry) -> None:
693
742
except RuntimeError :
694
743
pass
695
744
745
+ async def get_library_meta (
746
+ self ,
747
+ name : str ,
748
+ base_dir : str = "." ,
749
+ variables : Optional [Dict [str , Optional [Any ]]] = None ,
750
+ ) -> Tuple [Optional [LibraryMetaData ], str ]:
751
+ try :
752
+ import_name = await self .find_library (
753
+ name ,
754
+ base_dir = base_dir ,
755
+ variables = variables ,
756
+ )
757
+
758
+ result : Optional [LibraryMetaData ] = None
759
+ module_spec : Optional [ModuleSpec ] = None
760
+ if is_library_by_path (import_name ):
761
+ if (p := Path (import_name )).exists ():
762
+ result = LibraryMetaData (__version__ , p .stem , import_name , None , True )
763
+ else :
764
+ module_spec = get_module_spec (import_name )
765
+ if module_spec is not None and module_spec .origin is not None :
766
+ result = LibraryMetaData (
767
+ __version__ ,
768
+ module_spec .name ,
769
+ module_spec .origin ,
770
+ module_spec .submodule_search_locations ,
771
+ False ,
772
+ )
773
+ if result is not None :
774
+ if result .origin is not None :
775
+ result .mtimes = {result .origin : Path (result .origin ).resolve ().stat ().st_mtime_ns }
776
+
777
+ if result .submodule_search_locations :
778
+ if result .mtimes is None :
779
+ result .mtimes = {}
780
+ result .mtimes .update (
781
+ {
782
+ str (f ): f .resolve ().stat ().st_mtime_ns
783
+ for f in itertools .chain (
784
+ * (iter_files (loc , "**/*.py" ) for loc in result .submodule_search_locations )
785
+ )
786
+ }
787
+ )
788
+
789
+ return result , import_name
790
+ except (SystemExit , KeyboardInterrupt ):
791
+ raise
792
+ except BaseException :
793
+ pass
794
+
795
+ return None , import_name
796
+
696
797
async def find_library (self , name : str , base_dir : str , variables : Optional [Dict [str , Any ]] = None ) -> str :
697
798
return await self ._library_files_cache .get (self ._find_library , name , base_dir , variables )
698
799
@@ -776,14 +877,30 @@ async def get_libdoc_for_library_import(
776
877
sentinel : Any = None ,
777
878
variables : Optional [Dict [str , Any ]] = None ,
778
879
) -> LibraryDoc :
779
- source = await self .find_library (
880
+ meta , source = await self .get_library_meta (
780
881
name ,
781
882
base_dir ,
782
883
variables ,
783
884
)
784
885
785
886
async def _get_libdoc () -> LibraryDoc :
786
887
self ._logger .debug (lambda : f"Load Library { source } { repr (args )} " )
888
+ if meta is not None :
889
+ meta_file = Path (self .lib_doc_cache_path , meta .filepath_base .with_suffix (".meta.json" ))
890
+ if meta_file .exists ():
891
+ try :
892
+ saved_meta = from_json (meta_file .read_text ("utf-8" ), LibraryMetaData )
893
+ if saved_meta == meta :
894
+ return from_json (
895
+ Path (self .lib_doc_cache_path , meta .filepath_base .with_suffix (".spec.json" )).read_text (
896
+ "utf-8"
897
+ ),
898
+ LibraryDoc ,
899
+ )
900
+ except (SystemExit , KeyboardInterrupt ):
901
+ raise
902
+ except BaseException as e :
903
+ self ._logger .exception (e )
787
904
788
905
with ProcessPoolExecutor (max_workers = 1 ) as executor :
789
906
result = await asyncio .wait_for (
@@ -804,6 +921,18 @@ async def _get_libdoc() -> LibraryDoc:
804
921
self ._logger .warning (
805
922
lambda : f"stdout captured at loading library { name } { repr (args )} :\n { result .stdout } "
806
923
)
924
+ try :
925
+ if meta is not None and result .library_type in [LibraryType .CLASS , LibraryType .MODULE ]:
926
+ meta_file = Path (self .lib_doc_cache_path , meta .filepath_base .with_suffix (".meta.json" ))
927
+ meta_file .parent .mkdir (parents = True , exist_ok = True )
928
+ meta_file .write_text (as_json (meta ), "utf-8" )
929
+
930
+ spec_file = Path (self .lib_doc_cache_path , meta .filepath_base .with_suffix (".spec.json" ))
931
+ spec_file .write_text (as_json (result ), "utf-8" )
932
+ except (SystemExit , KeyboardInterrupt ):
933
+ raise
934
+ except BaseException as e :
935
+ self ._logger .exception (e )
807
936
808
937
return result
809
938
@@ -921,9 +1050,9 @@ def _create_handler(self, kw: Any) -> Any:
921
1050
keywords = [
922
1051
KeywordDoc (
923
1052
name = kw [0 ].name ,
924
- args = tuple (KeywordArgumentDoc .from_robot (a ) for a in kw [0 ].args ),
1053
+ args = list (KeywordArgumentDoc .from_robot (a ) for a in kw [0 ].args ),
925
1054
doc = kw [0 ].doc ,
926
- tags = tuple (kw [0 ].tags ),
1055
+ tags = list (kw [0 ].tags ),
927
1056
source = kw [0 ].source ,
928
1057
name_token = get_keyword_name_token_from_line (kw [0 ].lineno ),
929
1058
line_no = kw [0 ].lineno ,
@@ -940,7 +1069,7 @@ def _create_handler(self, kw: Any) -> Any:
940
1069
if isinstance (kw [1 ], UserErrorHandler )
941
1070
else None ,
942
1071
arguments = ArgumentSpec .from_robot_argument_spec (kw [1 ].arguments ),
943
- parent = libdoc ,
1072
+ parent = libdoc . digest ,
944
1073
)
945
1074
for kw in [(KeywordDocBuilder (resource = True ).build_keyword (lw ), lw ) for lw in lib .handlers ]
946
1075
],
0 commit comments