22
33import ast
44import asyncio
5+ import itertools
56import os
7+ import sys
68import weakref
9+ import zlib
710from abc import ABC , abstractmethod
811from collections import OrderedDict
912from concurrent .futures import ProcessPoolExecutor
2326 final ,
2427)
2528
29+ from ....__version__ import __version__
2630from ....utils .async_cache import AsyncSimpleLRUCache
2731from ....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
2834from ....utils .logging import LoggingDescriptor
2935from ....utils .path import path_is_relative_to
3036from ....utils .uri import Uri
3642from ..utils .ast_utils import HasError , HasErrors , Token
3743from ..utils .async_ast import walk
3844from ..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
4046from .entities import CommandLineVariableDefinition , VariableDefinition
4147
4248if TYPE_CHECKING :
4349 from ..protocol import RobotLanguageServerProtocol
4450 from .namespace import Namespace
4551
46-
4752from .library_doc import (
4853 ROBOT_LIBRARY_PACKAGE ,
4954 ArgumentSpec ,
5358 KeywordDoc ,
5459 KeywordStore ,
5560 LibraryDoc ,
61+ LibraryType ,
62+ ModuleSpec ,
5663 VariablesDoc ,
5764 complete_library_import ,
5865 complete_resource_import ,
6168 find_library ,
6269 find_variables ,
6370 get_library_doc ,
71+ get_module_spec ,
6472 get_variables_doc ,
6573 is_embedded_keyword ,
6674 is_library_by_path ,
@@ -451,13 +459,54 @@ async def get_libdoc(self) -> VariablesDoc:
451459 return self ._lib_doc
452460
453461
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+
454486class ImportsManager :
455487 _logger = LoggingDescriptor ()
456488
457489 def __init__ (self , parent_protocol : RobotLanguageServerProtocol , folder : Uri , config : RobotConfig ) -> None :
458490 super ().__init__ ()
459491 self .parent_protocol = parent_protocol
492+
460493 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+
461510 self .config : RobotConfig = config
462511 self ._libaries_lock = Lock ()
463512 self ._libaries : OrderedDict [_LibrariesEntryKey , _LibrariesEntry ] = OrderedDict ()
@@ -693,6 +742,58 @@ async def remove(k: _VariablesEntryKey, e: _VariablesEntry) -> None:
693742 except RuntimeError :
694743 pass
695744
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+
696797 async def find_library (self , name : str , base_dir : str , variables : Optional [Dict [str , Any ]] = None ) -> str :
697798 return await self ._library_files_cache .get (self ._find_library , name , base_dir , variables )
698799
@@ -776,14 +877,30 @@ async def get_libdoc_for_library_import(
776877 sentinel : Any = None ,
777878 variables : Optional [Dict [str , Any ]] = None ,
778879 ) -> LibraryDoc :
779- source = await self .find_library (
880+ meta , source = await self .get_library_meta (
780881 name ,
781882 base_dir ,
782883 variables ,
783884 )
784885
785886 async def _get_libdoc () -> LibraryDoc :
786887 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 )
787904
788905 with ProcessPoolExecutor (max_workers = 1 ) as executor :
789906 result = await asyncio .wait_for (
@@ -804,6 +921,18 @@ async def _get_libdoc() -> LibraryDoc:
804921 self ._logger .warning (
805922 lambda : f"stdout captured at loading library { name } { repr (args )} :\n { result .stdout } "
806923 )
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 )
807936
808937 return result
809938
@@ -921,9 +1050,9 @@ def _create_handler(self, kw: Any) -> Any:
9211050 keywords = [
9221051 KeywordDoc (
9231052 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 ),
9251054 doc = kw [0 ].doc ,
926- tags = tuple (kw [0 ].tags ),
1055+ tags = list (kw [0 ].tags ),
9271056 source = kw [0 ].source ,
9281057 name_token = get_keyword_name_token_from_line (kw [0 ].lineno ),
9291058 line_no = kw [0 ].lineno ,
@@ -940,7 +1069,7 @@ def _create_handler(self, kw: Any) -> Any:
9401069 if isinstance (kw [1 ], UserErrorHandler )
9411070 else None ,
9421071 arguments = ArgumentSpec .from_robot_argument_spec (kw [1 ].arguments ),
943- parent = libdoc ,
1072+ parent = libdoc . digest ,
9441073 )
9451074 for kw in [(KeywordDocBuilder (resource = True ).build_keyword (lw ), lw ) for lw in lib .handlers ]
9461075 ],
0 commit comments