├── .python-version ├── .sublime-dependency ├── LICENSE ├── README.md └── st4_py38 └── lsp_utils ├── __init__.py ├── _client_handler ├── __init__.py ├── abstract_plugin.py ├── api_decorator.py └── interface.py ├── _util ├── __init__.py └── weak_method.py ├── api_wrapper_interface.py ├── constants.py ├── generic_client_handler.py ├── helpers.py ├── node_runtime.py ├── npm_client_handler.py ├── pip_client_handler.py ├── server_npm_resource.py ├── server_pip_resource.py ├── server_resource_interface.py └── third_party ├── semantic_version ├── LICENSE ├── README.rst ├── __init__.py └── base.py └── update-info.log /.python-version: -------------------------------------------------------------------------------- 1 | 3.8 2 | -------------------------------------------------------------------------------- /.sublime-dependency: -------------------------------------------------------------------------------- 1 | 10 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 SublimeLSP 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LSP utilities for Package Control 2 | 3 | Module with LSP-related utilities for Sublime Text. 4 | 5 | 📘 [Documentation](https://sublimelsp.github.io/lsp_utils/) 6 | 7 | ## How to use 8 | 9 | 1. Create a `dependencies.json` file in your package root with the following contents: 10 | 11 | ```js 12 | { 13 | "*": { 14 | "*": [ 15 | "lsp_utils", 16 | "sublime_lib" 17 | ] 18 | } 19 | } 20 | ``` 21 | 22 | 2. Run the **Package Control: Satisfy Dependencies** command via the _Command Palette_. 23 | 24 | See also [Documentation on Dependencies](https://packagecontrol.io/docs/dependencies) 25 | -------------------------------------------------------------------------------- /st4_py38/lsp_utils/__init__.py: -------------------------------------------------------------------------------- 1 | from ._client_handler import ClientHandler 2 | from ._client_handler import notification_handler 3 | from ._client_handler import request_handler 4 | from .api_wrapper_interface import ApiWrapperInterface 5 | from .constants import SETTINGS_FILENAME 6 | from .generic_client_handler import GenericClientHandler 7 | from .node_runtime import NodeRuntime 8 | from .npm_client_handler import NpmClientHandler 9 | from .server_npm_resource import ServerNpmResource 10 | from .server_pip_resource import ServerPipResource 11 | from .server_resource_interface import ServerResourceInterface 12 | from .server_resource_interface import ServerStatus 13 | 14 | __all__ = [ 15 | 'ApiWrapperInterface', 16 | 'ClientHandler', 17 | 'SETTINGS_FILENAME', 18 | 'GenericClientHandler', 19 | 'NodeRuntime', 20 | 'NpmClientHandler', 21 | 'ServerResourceInterface', 22 | 'ServerStatus', 23 | 'ServerNpmResource', 24 | 'ServerPipResource', 25 | 'notification_handler', 26 | 'request_handler', 27 | ] 28 | -------------------------------------------------------------------------------- /st4_py38/lsp_utils/_client_handler/__init__.py: -------------------------------------------------------------------------------- 1 | from .abstract_plugin import ClientHandler 2 | from .api_decorator import notification_handler 3 | from .api_decorator import request_handler 4 | 5 | __all__ = [ 6 | 'ClientHandler', 7 | 'notification_handler', 8 | 'request_handler', 9 | ] 10 | -------------------------------------------------------------------------------- /st4_py38/lsp_utils/_client_handler/abstract_plugin.py: -------------------------------------------------------------------------------- 1 | from .._util import weak_method 2 | from ..api_wrapper_interface import ApiWrapperInterface 3 | from ..server_resource_interface import ServerStatus 4 | from .api_decorator import register_decorated_handlers 5 | from .interface import ClientHandlerInterface 6 | from functools import partial 7 | from LSP.plugin import AbstractPlugin 8 | from LSP.plugin import ClientConfig 9 | from LSP.plugin import Notification 10 | from LSP.plugin import register_plugin 11 | from LSP.plugin import Request 12 | from LSP.plugin import Response 13 | from LSP.plugin import Session 14 | from LSP.plugin import unregister_plugin 15 | from LSP.plugin import WorkspaceFolder 16 | from LSP.plugin.core.rpc import method2attr 17 | from LSP.plugin.core.typing import Any, Callable, Dict, List, Optional, Tuple, TypedDict 18 | from os import path 19 | from weakref import ref 20 | import sublime 21 | 22 | __all__ = ['ClientHandler'] 23 | 24 | LanguagesDict = TypedDict('LanguagesDict', { 25 | 'document_selector': Optional[str], 26 | 'languageId': Optional[str], 27 | 'scopes': Optional[List[str]], 28 | 'syntaxes': Optional[List[str]], 29 | }, total=False) 30 | ApiNotificationHandler = Callable[[Any], None] 31 | ApiRequestHandler = Callable[[Any, Callable[[Any], None]], None] 32 | 33 | 34 | class ApiWrapper(ApiWrapperInterface): 35 | def __init__(self, plugin: 'ref[AbstractPlugin]'): 36 | self.__plugin = plugin 37 | 38 | def __session(self) -> Optional[Session]: 39 | plugin = self.__plugin() 40 | return plugin.weaksession() if plugin else None 41 | 42 | # --- ApiWrapperInterface ----------------------------------------------------------------------------------------- 43 | 44 | def on_notification(self, method: str, handler: ApiNotificationHandler) -> None: 45 | def handle_notification(weak_handler: ApiNotificationHandler, params: Any) -> None: 46 | weak_handler(params) 47 | 48 | plugin = self.__plugin() 49 | if plugin: 50 | setattr(plugin, method2attr(method), partial(handle_notification, weak_method(handler))) 51 | 52 | def on_request(self, method: str, handler: ApiRequestHandler) -> None: 53 | def send_response(request_id: Any, result: Any) -> None: 54 | session = self.__session() 55 | if session: 56 | session.send_response(Response(request_id, result)) 57 | 58 | def on_response(weak_handler: ApiRequestHandler, params: Any, request_id: Any) -> None: 59 | weak_handler(params, lambda result: send_response(request_id, result)) 60 | 61 | plugin = self.__plugin() 62 | if plugin: 63 | setattr(plugin, method2attr(method), partial(on_response, weak_method(handler))) 64 | 65 | def send_notification(self, method: str, params: Any) -> None: 66 | session = self.__session() 67 | if session: 68 | session.send_notification(Notification(method, params)) 69 | 70 | def send_request(self, method: str, params: Any, handler: Callable[[Any, bool], None]) -> None: 71 | session = self.__session() 72 | if session: 73 | session.send_request( 74 | Request(method, params), lambda result: handler(result, False), lambda result: handler(result, True)) 75 | else: 76 | handler(None, True) 77 | 78 | 79 | class ClientHandler(AbstractPlugin, ClientHandlerInterface): 80 | """ 81 | The base class for creating an LSP plugin. 82 | """ 83 | 84 | # --- AbstractPlugin handlers ------------------------------------------------------------------------------------- 85 | 86 | @classmethod 87 | def name(cls) -> str: 88 | return cls.get_displayed_name() 89 | 90 | @classmethod 91 | def configuration(cls) -> Tuple[sublime.Settings, str]: 92 | return cls.read_settings() 93 | 94 | @classmethod 95 | def additional_variables(cls) -> Dict[str, str]: 96 | return cls.get_additional_variables() 97 | 98 | @classmethod 99 | def needs_update_or_installation(cls) -> bool: 100 | if cls.manages_server(): 101 | server = cls.get_server() 102 | return bool(server and server.needs_installation()) 103 | return False 104 | 105 | @classmethod 106 | def install_or_update(cls) -> None: 107 | server = cls.get_server() 108 | if server: 109 | server.install_or_update() 110 | 111 | @classmethod 112 | def can_start(cls, window: sublime.Window, initiating_view: sublime.View, 113 | workspace_folders: List[WorkspaceFolder], configuration: ClientConfig) -> Optional[str]: 114 | if cls.manages_server(): 115 | server = cls.get_server() 116 | if not server or server.get_status() == ServerStatus.ERROR: 117 | return "{}: Error installing server dependencies.".format(cls.package_name) 118 | if server.get_status() != ServerStatus.READY: 119 | return "{}: Server installation in progress...".format(cls.package_name) 120 | message = cls.is_allowed_to_start(window, initiating_view, workspace_folders, configuration) 121 | if message: 122 | return message 123 | # Lazily update command after server has initialized if not set manually by the user. 124 | if not configuration.command: 125 | configuration.command = cls.get_command() 126 | return None 127 | 128 | @classmethod 129 | def on_pre_start(cls, window: sublime.Window, initiating_view: sublime.View, 130 | workspace_folders: List[WorkspaceFolder], configuration: ClientConfig) -> Optional[str]: 131 | extra_paths = cls.get_additional_paths() 132 | if extra_paths: 133 | original_path_raw = configuration.env.get('PATH') or '' 134 | if isinstance(original_path_raw, str): 135 | original_paths = original_path_raw.split(path.pathsep) 136 | else: 137 | original_paths = original_path_raw 138 | # To fix https://github.com/TerminalFi/LSP-copilot/issues/163 , 139 | # We don't want to add the same path multiple times whenever a new server session is created. 140 | # Note that additional paths should be prepended to the original paths. 141 | wanted_paths = [path for path in extra_paths if path not in original_paths] 142 | wanted_paths.extend(original_paths) 143 | configuration.env['PATH'] = path.pathsep.join(wanted_paths) 144 | return None 145 | 146 | # --- ClientHandlerInterface -------------------------------------------------------------------------------------- 147 | 148 | @classmethod 149 | def setup(cls) -> None: 150 | register_plugin(cls) 151 | 152 | @classmethod 153 | def cleanup(cls) -> None: 154 | unregister_plugin(cls) 155 | 156 | # --- Internals --------------------------------------------------------------------------------------------------- 157 | 158 | def __init__(self, *args: Any, **kwargs: Any) -> None: 159 | super().__init__(*args, **kwargs) 160 | api = ApiWrapper(ref(self)) # type: ignore 161 | register_decorated_handlers(self, api) 162 | self.on_ready(api) 163 | -------------------------------------------------------------------------------- /st4_py38/lsp_utils/_client_handler/api_decorator.py: -------------------------------------------------------------------------------- 1 | from ..api_wrapper_interface import ApiWrapperInterface 2 | from .interface import ClientHandlerInterface 3 | from LSP.plugin.core.typing import Any, Callable, List, Optional, TypeVar, Union 4 | import inspect 5 | 6 | __all__ = [ 7 | "notification_handler", 8 | "request_handler", 9 | "register_decorated_handlers", 10 | ] 11 | 12 | T = TypeVar('T') 13 | # the first argument is always "self" 14 | NotificationHandler = Callable[[Any, Any], None] 15 | RequestHandler = Callable[[Any, Any, Callable[[Any], None]], None] 16 | MessageMethods = Union[str, List[str]] 17 | 18 | _HANDLER_MARKS = { 19 | "notification": "__handle_notification_message_methods", 20 | "request": "__handle_request_message_methods", 21 | } 22 | 23 | 24 | def notification_handler(notification_methods: MessageMethods) -> Callable[[NotificationHandler], NotificationHandler]: 25 | """ 26 | Marks the decorated function as a "notification" message handler. 27 | 28 | On server sending the notification, the decorated function will be called with the `params` argument which contains 29 | the payload. 30 | """ 31 | 32 | return _create_handler("notification", notification_methods) 33 | 34 | 35 | def request_handler(request_methods: MessageMethods) -> Callable[[RequestHandler], RequestHandler]: 36 | """ 37 | Marks the decorated function as a "request" message handler. 38 | 39 | On server sending the request, the decorated function will be called with two arguments (`params` and `respond`). 40 | The first argument (`params`) is the payload of the request and the second argument (`respond`) is the function that 41 | must be used to respond to the request. The `respond` function takes any data that should be sent back to the 42 | server. 43 | """ 44 | 45 | return _create_handler("request", request_methods) 46 | 47 | 48 | def _create_handler(client_event: str, message_methods: MessageMethods) -> Callable[[T], T]: 49 | """ Marks the decorated function as a message handler. """ 50 | 51 | message_methods = [message_methods] if isinstance(message_methods, str) else message_methods 52 | 53 | def decorator(func: T) -> T: 54 | setattr(func, _HANDLER_MARKS[client_event], message_methods) 55 | return func 56 | 57 | return decorator 58 | 59 | 60 | def register_decorated_handlers(client_handler: ClientHandlerInterface, api: ApiWrapperInterface) -> None: 61 | """ 62 | Register decorator-style custom message handlers. 63 | 64 | This method works as following: 65 | 66 | 1. Scan through all methods of `client_handler`. 67 | 2. If a method is decorated, it will have a "handler mark" attribute which is set by the decorator. 68 | 3. Register the method with wanted message methods, which are stored in the "handler mark" attribute. 69 | 70 | :param client_handler: The instance of the client handler. 71 | :param api: The API instance for interacting with the server. 72 | """ 73 | for _, func in inspect.getmembers(client_handler, predicate=inspect.isroutine): 74 | for client_event, handler_mark in _HANDLER_MARKS.items(): 75 | message_methods = getattr(func, handler_mark, None) # type: Optional[List[str]] 76 | if message_methods is None: 77 | continue 78 | 79 | event_registrator = getattr(api, "on_" + client_event, None) 80 | if callable(event_registrator): 81 | for message_method in message_methods: 82 | event_registrator(message_method, func) 83 | 84 | # it makes no sense that a handler handles both "notification" and "request" 85 | # so we do early break once we've registered a handler 86 | break 87 | -------------------------------------------------------------------------------- /st4_py38/lsp_utils/_client_handler/interface.py: -------------------------------------------------------------------------------- 1 | from ..api_wrapper_interface import ApiWrapperInterface 2 | from ..server_resource_interface import ServerResourceInterface 3 | from abc import ABCMeta 4 | from abc import abstractmethod 5 | from LSP.plugin import ClientConfig 6 | from LSP.plugin import DottedDict 7 | from LSP.plugin import WorkspaceFolder 8 | from LSP.plugin.core.typing import Dict, List, Optional, Tuple 9 | import sublime 10 | 11 | __all__ = ['ClientHandlerInterface'] 12 | 13 | 14 | class ClientHandlerInterface(metaclass=ABCMeta): 15 | package_name = '' 16 | 17 | @classmethod 18 | @abstractmethod 19 | def setup(cls) -> None: 20 | ... 21 | 22 | @classmethod 23 | @abstractmethod 24 | def cleanup(cls) -> None: 25 | ... 26 | 27 | @classmethod 28 | @abstractmethod 29 | def get_displayed_name(cls) -> str: 30 | ... 31 | 32 | @classmethod 33 | @abstractmethod 34 | def package_storage(cls) -> str: 35 | ... 36 | 37 | @classmethod 38 | @abstractmethod 39 | def get_additional_variables(cls) -> Dict[str, str]: 40 | ... 41 | 42 | @classmethod 43 | @abstractmethod 44 | def get_additional_paths(cls) -> List[str]: 45 | ... 46 | 47 | @classmethod 48 | @abstractmethod 49 | def manages_server(cls) -> bool: 50 | ... 51 | 52 | @classmethod 53 | @abstractmethod 54 | def get_command(cls) -> List[str]: 55 | ... 56 | 57 | @classmethod 58 | @abstractmethod 59 | def binary_path(cls) -> str: 60 | ... 61 | 62 | @classmethod 63 | @abstractmethod 64 | def get_server(cls) -> Optional[ServerResourceInterface]: 65 | ... 66 | 67 | @classmethod 68 | @abstractmethod 69 | def get_binary_arguments(cls) -> List[str]: 70 | ... 71 | 72 | @classmethod 73 | @abstractmethod 74 | def read_settings(cls) -> Tuple[sublime.Settings, str]: 75 | ... 76 | 77 | @classmethod 78 | @abstractmethod 79 | def on_settings_read(cls, settings: sublime.Settings) -> bool: 80 | ... 81 | 82 | @classmethod 83 | @abstractmethod 84 | def is_allowed_to_start( 85 | cls, 86 | window: sublime.Window, 87 | initiating_view: sublime.View, 88 | workspace_folders: List[WorkspaceFolder], 89 | configuration: ClientConfig, 90 | ) -> Optional[str]: 91 | ... 92 | 93 | @abstractmethod 94 | def on_ready(self, api: ApiWrapperInterface) -> None: 95 | ... 96 | 97 | @abstractmethod 98 | def on_settings_changed(self, settings: DottedDict) -> None: 99 | ... 100 | -------------------------------------------------------------------------------- /st4_py38/lsp_utils/_util/__init__.py: -------------------------------------------------------------------------------- 1 | from .weak_method import weak_method 2 | 3 | __all__ = [ 4 | 'weak_method', 5 | ] 6 | -------------------------------------------------------------------------------- /st4_py38/lsp_utils/_util/weak_method.py: -------------------------------------------------------------------------------- 1 | from LSP.plugin.core.typing import Any, Callable 2 | from types import MethodType 3 | import weakref 4 | 5 | 6 | __all__ = ['weak_method'] 7 | 8 | 9 | # An implementation of weak method borrowed from sublime_lib [1] 10 | # 11 | # We need it to be able to weak reference bound methods as `weakref.WeakMethod` is not available in 12 | # 3.3 runtime. 13 | # 14 | # The reason this is necessary is explained in the documentation of `weakref.WeakMethod`: 15 | # > A custom ref subclass which simulates a weak reference to a bound method (i.e., a method defined 16 | # > on a class and looked up on an instance). Since a bound method is ephemeral, a standard weak 17 | # > reference cannot keep hold of it. 18 | # 19 | # [1] https://github.com/SublimeText/sublime_lib/blob/master/st3/sublime_lib/_util/weak_method.py 20 | 21 | def weak_method(method: Callable[..., Any]) -> Callable[..., Any]: 22 | assert isinstance(method, MethodType) 23 | self_ref = weakref.ref(method.__self__) 24 | function_ref = weakref.ref(method.__func__) 25 | 26 | def wrapped(*args: Any, **kwargs: Any) -> Any: 27 | self = self_ref() 28 | fn = function_ref() 29 | if self is None or fn is None: 30 | print('[lsp_utils] Error: weak_method not called due to a deleted reference', [self, fn]) 31 | return 32 | return fn(self, *args, **kwargs) # type: ignore 33 | 34 | return wrapped 35 | -------------------------------------------------------------------------------- /st4_py38/lsp_utils/api_wrapper_interface.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod 2 | from LSP.plugin.core.typing import Any, Callable 3 | 4 | __all__ = ['ApiWrapperInterface'] 5 | 6 | 7 | NotificationHandler = Callable[[Any], None] 8 | RequestHandler = Callable[[Any, Callable[[Any], None]], None] 9 | 10 | 11 | class ApiWrapperInterface(metaclass=ABCMeta): 12 | """ 13 | An interface for sending and receiving requests and notifications from and to the server. An implementation of it 14 | is available through the :func:`GenericClientHandler.on_ready()` override. 15 | """ 16 | 17 | @abstractmethod 18 | def on_notification(self, method: str, handler: NotificationHandler) -> None: 19 | """ 20 | Registers a handler for given notification name. The handler will be called with optional params. 21 | """ 22 | ... 23 | 24 | @abstractmethod 25 | def on_request(self, method: str, handler: RequestHandler) -> None: 26 | """ 27 | Registers a handler for given request name. The handler will be called with two arguments - first the params 28 | sent with the request and second the function that must be used to respond to the request. The response 29 | function takes params to respond with. 30 | """ 31 | ... 32 | 33 | @abstractmethod 34 | def send_notification(self, method: str, params: Any) -> None: 35 | """ 36 | Sends a notification to the server. 37 | """ 38 | ... 39 | 40 | @abstractmethod 41 | def send_request(self, method: str, params: Any, handler: Callable[[Any, bool], None]) -> None: 42 | """ 43 | Sends a request to the server. The handler will be called with the result received from the server and 44 | a boolean value `False` if request has succeeded and `True` if it returned an error. 45 | """ 46 | ... 47 | -------------------------------------------------------------------------------- /st4_py38/lsp_utils/constants.py: -------------------------------------------------------------------------------- 1 | SETTINGS_FILENAME = 'lsp_utils.sublime-settings' 2 | -------------------------------------------------------------------------------- /st4_py38/lsp_utils/generic_client_handler.py: -------------------------------------------------------------------------------- 1 | from ._client_handler import ClientHandler 2 | from .api_wrapper_interface import ApiWrapperInterface 3 | from .helpers import rmtree_ex 4 | from .server_resource_interface import ServerResourceInterface 5 | from abc import ABCMeta 6 | from LSP.plugin import ClientConfig 7 | from LSP.plugin import DottedDict 8 | from LSP.plugin import WorkspaceFolder 9 | from LSP.plugin.core.typing import Any, Dict, List, Optional, Tuple 10 | import os 11 | import sublime 12 | 13 | __all__ = ['GenericClientHandler'] 14 | 15 | 16 | class GenericClientHandler(ClientHandler, metaclass=ABCMeta): 17 | """ 18 | An generic implementation of an LSP plugin handler. 19 | """ 20 | 21 | package_name = '' 22 | """ 23 | The name of the released package. Also used for the name of the LSP client and for reading package settings. 24 | 25 | This name must be set and must match the basename of the corresponding `*.sublime-settings` file. 26 | It's also used as a directory name for package storage when implementing a server resource interface. 27 | Recommended to use `__package__` value fo this one. If you need to override handler name in the UI, 28 | override :meth:`get_displayed_name()` also. 29 | 30 | :required: Yes 31 | """ 32 | 33 | # --- ClientHandler handlers -------------------------------------------------------------------------------------- 34 | 35 | @classmethod 36 | def setup(cls) -> None: 37 | if not cls.package_name: 38 | raise Exception('ERROR: [lsp_utils] package_name is required to instantiate an instance of {}'.format(cls)) 39 | super().setup() 40 | 41 | @classmethod 42 | def cleanup(cls) -> None: 43 | 44 | def run_async() -> None: 45 | if os.path.isdir(cls.package_storage()): 46 | rmtree_ex(cls.package_storage()) 47 | 48 | try: 49 | from package_control import events # type: ignore 50 | if events.remove(cls.package_name): 51 | sublime.set_timeout_async(run_async, 1000) 52 | except ImportError: 53 | pass # Package Control is not required. 54 | 55 | super().cleanup() 56 | 57 | @classmethod 58 | def get_displayed_name(cls) -> str: 59 | """ 60 | Returns the name that will be shown in the ST UI (for example in the status field). 61 | 62 | Defaults to the value of :attr:`package_name`. 63 | """ 64 | return cls.package_name 65 | 66 | @classmethod 67 | def storage_path(cls) -> str: 68 | """ 69 | The storage path. Use this as your base directory to install server files. Its path is '$DATA/Package Storage'. 70 | """ 71 | return super().storage_path() 72 | 73 | @classmethod 74 | def package_storage(cls) -> str: 75 | """ 76 | The storage path for this package. Its path is '$DATA/Package Storage/[Package_Name]'. 77 | """ 78 | return os.path.join(cls.storage_path(), cls.package_name) 79 | 80 | @classmethod 81 | def get_command(cls) -> List[str]: 82 | """ 83 | Returns a list of arguments to use to start the server. The default implementation returns combined result of 84 | :meth:`binary_path()` and :meth:`get_binary_arguments()`. 85 | """ 86 | return [cls.binary_path()] + cls.get_binary_arguments() 87 | 88 | @classmethod 89 | def binary_path(cls) -> str: 90 | """ 91 | The filesystem path to the server executable. 92 | 93 | The default implementation returns `binary_path` property of the server instance (returned from 94 | :meth:`get_server()`), if available. 95 | """ 96 | if cls.manages_server(): 97 | server = cls.get_server() 98 | if server: 99 | return server.binary_path 100 | return '' 101 | 102 | @classmethod 103 | def get_binary_arguments(cls) -> List[str]: 104 | """ 105 | Returns a list of extra arguments to append to the `command` when starting the server. 106 | 107 | See :meth:`get_command()`. 108 | """ 109 | return [] 110 | 111 | @classmethod 112 | def read_settings(cls) -> Tuple[sublime.Settings, str]: 113 | filename = "{}.sublime-settings".format(cls.package_name) 114 | loaded_settings = sublime.load_settings(filename) 115 | changed = cls.on_settings_read(loaded_settings) 116 | if changed: 117 | sublime.save_settings(filename) 118 | filepath = "Packages/{}/{}".format(cls.package_name, filename) 119 | return (loaded_settings, filepath) 120 | 121 | @classmethod 122 | def get_additional_variables(cls) -> Dict[str, str]: 123 | """ 124 | Override to add more variables here to be expanded when reading settings. 125 | 126 | Default implementation adds a `${server_path}` variable that holds filesystem path to the server 127 | binary (only when :meth:`manages_server` is `True`). 128 | 129 | Remember to call the super class and merge the results if overriding. 130 | """ 131 | return { 132 | 'pathsep': os.pathsep, 133 | 'server_path': cls.binary_path(), 134 | } 135 | 136 | @classmethod 137 | def get_additional_paths(cls) -> List[str]: 138 | """ 139 | Override to prepend additional paths to the default PATH environment variable. 140 | 141 | Remember to call the super class and merge the results if overriding. 142 | """ 143 | return [] 144 | 145 | @classmethod 146 | def manages_server(cls) -> bool: 147 | """ 148 | Whether this handler manages a server. If the response is `True` then the :meth:`get_server()` should also be 149 | implemented. 150 | """ 151 | return False 152 | 153 | @classmethod 154 | def get_server(cls) -> Optional[ServerResourceInterface]: 155 | """ 156 | :returns: The instance of the server managed by this plugin. Only used when :meth:`manages_server()` 157 | returns `True`. 158 | """ 159 | return None 160 | 161 | @classmethod 162 | def on_settings_read(cls, settings: sublime.Settings) -> bool: 163 | """ 164 | Called when package settings were read. Receives a `sublime.Settings` object. 165 | 166 | It's recommended to use :meth:`on_settings_changed()` instead if you don't need to persistent your changes to 167 | the disk. 168 | 169 | :returns: `True` to save modifications back into the settings file. 170 | """ 171 | return False 172 | 173 | @classmethod 174 | def is_allowed_to_start( 175 | cls, 176 | window: sublime.Window, 177 | initiating_view: sublime.View, 178 | workspace_folders: List[WorkspaceFolder], 179 | configuration: ClientConfig, 180 | ) -> Optional[str]: 181 | """ 182 | Determines if the session is allowed to start. 183 | 184 | :returns: A string describing the reason why we should not start a language server session, or `None` if we 185 | should go ahead and start a session. 186 | """ 187 | return None 188 | 189 | def __init__(self, *args: Any, **kwargs: Any) -> None: 190 | # Seems unnecessary to override but it's to hide the original argument from the documentation. 191 | super().__init__(*args, **kwargs) 192 | 193 | def on_ready(self, api: ApiWrapperInterface) -> None: 194 | """ 195 | Called when the instance is ready. 196 | 197 | :param api: The API instance for interacting with the server. 198 | """ 199 | pass 200 | 201 | def on_settings_changed(self, settings: DottedDict) -> None: 202 | """ 203 | Override this method to alter the settings that are returned to the server for the 204 | workspace/didChangeConfiguration notification and the workspace/configuration requests. 205 | 206 | :param settings: The settings that the server should receive. 207 | """ 208 | pass 209 | -------------------------------------------------------------------------------- /st4_py38/lsp_utils/helpers.py: -------------------------------------------------------------------------------- 1 | from LSP.plugin.core.typing import Any, Callable, Dict, List, Optional, Tuple 2 | import os 3 | import shutil 4 | import sublime 5 | import subprocess 6 | import threading 7 | 8 | StringCallback = Callable[[str], None] 9 | SemanticVersion = Tuple[int, int, int] 10 | 11 | is_windows = sublime.platform() == 'windows' 12 | 13 | 14 | def run_command_sync( 15 | args: List[str], 16 | cwd: Optional[str] = None, 17 | extra_env: Optional[Dict[str, str]] = None, 18 | extra_paths: List[str] = [], 19 | shell: bool = is_windows, 20 | ) -> Tuple[str, Optional[str]]: 21 | """ 22 | Runs the given command synchronously. 23 | 24 | :returns: A two-element tuple with the returned value and an optional error. If running the command has failed, the 25 | first tuple element will be empty string and the second will contain the potential `stderr` output. If the 26 | command has succeeded then the second tuple element will be `None`. 27 | """ 28 | try: 29 | env = None 30 | if extra_env or extra_paths: 31 | env = os.environ.copy() 32 | if extra_env: 33 | env.update(extra_env) 34 | if extra_paths: 35 | env['PATH'] = os.path.pathsep.join(extra_paths) + os.path.pathsep + env['PATH'] 36 | startupinfo = None 37 | if is_windows: 38 | startupinfo = subprocess.STARTUPINFO() # type: ignore 39 | startupinfo.dwFlags |= subprocess.SW_HIDE | subprocess.STARTF_USESHOWWINDOW # type: ignore 40 | output = subprocess.check_output( 41 | args, cwd=cwd, shell=shell, stderr=subprocess.STDOUT, env=env, startupinfo=startupinfo) 42 | return (decode_bytes(output).strip(), None) 43 | except subprocess.CalledProcessError as error: 44 | return ('', decode_bytes(error.output).strip()) 45 | 46 | 47 | def run_command_async(args: List[str], on_success: StringCallback, on_error: StringCallback, **kwargs: Any) -> None: 48 | """ 49 | Runs the given command asynchronously. 50 | 51 | On success calls the provided `on_success` callback with the value the the command has returned. 52 | On error calls the provided `on_error` callback with the potential `stderr` output. 53 | """ 54 | 55 | def execute(on_success: StringCallback, on_error: StringCallback, args: List[str]) -> None: 56 | result, error = run_command_sync(args, **kwargs) 57 | on_error(error) if error is not None else on_success(result) 58 | 59 | thread = threading.Thread(target=execute, args=(on_success, on_error, args)) 60 | thread.start() 61 | 62 | 63 | def decode_bytes(data: bytes) -> str: 64 | """ 65 | Decodes provided bytes using `utf-8` decoding, ignoring potential decoding errors. 66 | """ 67 | return data.decode('utf-8', 'ignore') 68 | 69 | 70 | def rmtree_ex(path: str, ignore_errors: bool = False) -> None: 71 | # On Windows, "shutil.rmtree" will raise file not found errors when deleting a long path (>255 chars). 72 | # See https://stackoverflow.com/a/14076169/4643765 73 | # See https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation 74 | path = R'\\?\{}'.format(path) if sublime.platform() == 'windows' else path 75 | shutil.rmtree(path, ignore_errors) 76 | 77 | 78 | def version_to_string(version: SemanticVersion) -> str: 79 | """ 80 | Returns a string representation of a version tuple. 81 | """ 82 | return '.'.join([str(c) for c in version]) 83 | 84 | 85 | def log_and_show_message(message: str, additional_logs: Optional[str] = None, show_in_status: bool = True) -> None: 86 | """ 87 | Logs the message in the console and optionally sets it as a status message on the window. 88 | 89 | :param message: The message to log or show in the status. 90 | :param additional_logs: The extra value to log on a separate line. 91 | :param show_in_status: Whether to briefly show the message in the status bar of the current window. 92 | """ 93 | print(message, '\n', additional_logs) if additional_logs else print(message) 94 | if show_in_status: 95 | sublime.active_window().status_message(message) 96 | -------------------------------------------------------------------------------- /st4_py38/lsp_utils/node_runtime.py: -------------------------------------------------------------------------------- 1 | from .constants import SETTINGS_FILENAME 2 | from .helpers import rmtree_ex 3 | from .helpers import run_command_sync 4 | from .helpers import SemanticVersion 5 | from .helpers import version_to_string 6 | from .third_party.semantic_version import NpmSpec, Version 7 | from contextlib import contextmanager 8 | from LSP.plugin.core.logging import debug 9 | from LSP.plugin.core.typing import cast, Any, Dict, Generator, List, Optional, Tuple, Union 10 | from os import path 11 | from os import remove 12 | from sublime_lib import ActivityIndicator 13 | import os 14 | import shutil 15 | import sublime 16 | import subprocess 17 | import sys 18 | import tarfile 19 | import urllib.request 20 | import zipfile 21 | 22 | __all__ = ['NodeRuntime'] 23 | 24 | IS_WINDOWS_7_OR_LOWER = sys.platform == 'win32' and sys.getwindowsversion()[:2] <= (6, 1) # type: ignore 25 | 26 | NODE_RUNTIME_VERSION = '20.18.0' 27 | NODE_DIST_URL = 'https://nodejs.org/dist/v{version}/{filename}' 28 | 29 | ELECTRON_RUNTIME_VERSION = '33.0.0' # includes Node.js v20.18.0 30 | ELECTRON_NODE_VERSION = '20.18.0' 31 | ELECTRON_DIST_URL = 'https://github.com/electron/electron/releases/download/v{version}/{filename}' 32 | YARN_URL = 'https://github.com/yarnpkg/yarn/releases/download/v1.22.22/yarn-1.22.22.js' 33 | 34 | NO_NODE_FOUND_MESSAGE = 'Could not start {package_name} due to not being able to resolve suitable Node.js \ 35 | runtime on the PATH. Press the "Download Node.js" button to get required Node.js version \ 36 | (note that it will be used only by LSP and will not affect your system otherwise).' 37 | 38 | 39 | class NodeRuntime: 40 | _node_runtime_resolved = False 41 | _node_runtime = None # Optional[NodeRuntime] 42 | """ 43 | Cached instance of resolved Node.js runtime. This is only done once per-session to avoid unnecessary IO. 44 | """ 45 | 46 | @classmethod 47 | def get( 48 | cls, package_name: str, storage_path: str, required_node_version: Union[str, SemanticVersion] 49 | ) -> Optional['NodeRuntime']: 50 | if isinstance(required_node_version, tuple): 51 | required_semantic_version = NpmSpec('>={}'.format(version_to_string(required_node_version))) 52 | else: 53 | required_semantic_version = NpmSpec(required_node_version) 54 | if cls._node_runtime_resolved: 55 | if cls._node_runtime: 56 | cls._node_runtime.check_satisfies_version(required_semantic_version) 57 | return cls._node_runtime 58 | cls._node_runtime_resolved = True 59 | cls._node_runtime = cls._resolve_node_runtime(package_name, storage_path, required_semantic_version) 60 | debug('Resolved Node.js Runtime for package {}: {}'.format(package_name, cls._node_runtime)) 61 | return cls._node_runtime 62 | 63 | @classmethod 64 | def _resolve_node_runtime( 65 | cls, package_name: str, storage_path: str, required_node_version: NpmSpec 66 | ) -> 'NodeRuntime': 67 | resolved_runtime = None # type: Optional[NodeRuntime] 68 | default_runtimes = ['system', 'local'] 69 | settings = sublime.load_settings(SETTINGS_FILENAME) 70 | selected_runtimes = cast(List[str], settings.get('nodejs_runtime') or default_runtimes) 71 | log_lines = ['--- lsp_utils Node.js resolving start ---'] 72 | for runtime_type in selected_runtimes: 73 | if runtime_type == 'system': 74 | log_lines.append('Resolving Node.js Runtime in env PATH for package {}...'.format(package_name)) 75 | path_runtime = NodeRuntimePATH() 76 | try: 77 | path_runtime.check_binary_present() 78 | except Exception as ex: 79 | log_lines.append(' * Failed: {}'.format(ex)) 80 | continue 81 | try: 82 | path_runtime.check_satisfies_version(required_node_version) 83 | resolved_runtime = path_runtime 84 | break 85 | except Exception as ex: 86 | log_lines.append(' * {}'.format(ex)) 87 | elif runtime_type == 'local': 88 | log_lines.append('Resolving Node.js Runtime from lsp_utils for package {}...'.format(package_name)) 89 | use_electron = cast(bool, settings.get('local_use_electron') or False) 90 | runtime_dir = path.join(storage_path, 'lsp_utils', 'node-runtime') 91 | local_runtime = ElectronRuntimeLocal(runtime_dir) if use_electron else NodeRuntimeLocal(runtime_dir) 92 | try: 93 | local_runtime.check_binary_present() 94 | except Exception as ex: 95 | log_lines.append(' * Binaries check failed: {}'.format(ex)) 96 | if selected_runtimes[0] != 'local': 97 | if not sublime.ok_cancel_dialog( 98 | NO_NODE_FOUND_MESSAGE.format(package_name=package_name), 'Download Node.js'): 99 | log_lines.append(' * Download skipped') 100 | continue 101 | # Remove outdated runtimes. 102 | if path.isdir(runtime_dir): 103 | for directory in next(os.walk(runtime_dir))[1]: 104 | old_dir = path.join(runtime_dir, directory) 105 | print('[lsp_utils] Deleting outdated Node.js runtime directory "{}"'.format(old_dir)) 106 | try: 107 | rmtree_ex(old_dir) 108 | except Exception as ex: 109 | log_lines.append(' * Failed deleting: {}'.format(ex)) 110 | try: 111 | local_runtime.install_node() 112 | except Exception as ex: 113 | log_lines.append(' * Failed downloading: {}'.format(ex)) 114 | continue 115 | try: 116 | local_runtime.check_binary_present() 117 | except Exception as ex: 118 | log_lines.append(' * Failed: {}'.format(ex)) 119 | continue 120 | try: 121 | local_runtime.check_satisfies_version(required_node_version) 122 | resolved_runtime = local_runtime 123 | break 124 | except Exception as ex: 125 | log_lines.append(' * {}'.format(ex)) 126 | if not resolved_runtime: 127 | log_lines.append('--- lsp_utils Node.js resolving end ---') 128 | print('\n'.join(log_lines)) 129 | raise Exception('Failed resolving Node.js Runtime. Please check in the console for more details.') 130 | return resolved_runtime 131 | 132 | def __init__(self) -> None: 133 | self._node = None # type: Optional[str] 134 | self._npm = None # type: Optional[str] 135 | self._version = None # type: Optional[Version] 136 | self._additional_paths = [] # type: List[str] 137 | 138 | def __repr__(self) -> str: 139 | return '{}(node: {}, npm: {}, version: {})'.format( 140 | self.__class__.__name__, self._node, self._npm, self._version if self._version else None) 141 | 142 | def install_node(self) -> None: 143 | raise Exception('Not supported!') 144 | 145 | def node_bin(self) -> Optional[str]: 146 | return self._node 147 | 148 | def npm_bin(self) -> Optional[str]: 149 | return self._npm 150 | 151 | def node_env(self) -> Dict[str, str]: 152 | if IS_WINDOWS_7_OR_LOWER: 153 | return {'NODE_SKIP_PLATFORM_CHECK': '1'} 154 | return {} 155 | 156 | def check_binary_present(self) -> None: 157 | if self._node is None: 158 | raise Exception('"node" binary not found') 159 | if self._npm is None: 160 | raise Exception('"npm" binary not found') 161 | 162 | def check_satisfies_version(self, required_node_version: NpmSpec) -> None: 163 | node_version = self.resolve_version() 164 | if node_version not in required_node_version: 165 | raise Exception( 166 | 'Node.js version requirement failed. Expected {}, got {}.'.format(required_node_version, node_version)) 167 | 168 | def resolve_version(self) -> Version: 169 | if self._version: 170 | return self._version 171 | if not self._node: 172 | raise Exception('Node.js not initialized') 173 | # In this case we have fully resolved binary path already so shouldn't need `shell` on Windows. 174 | version, error = run_command_sync([self._node, '--version'], extra_env=self.node_env(), shell=False) 175 | if error is None: 176 | self._version = Version(version.replace('v', '')) 177 | else: 178 | raise Exception('Failed resolving Node.js version. Error:\n{}'.format(error)) 179 | return self._version 180 | 181 | def run_node( 182 | self, 183 | args: List[str], 184 | stdin: int = subprocess.PIPE, 185 | stdout: int = subprocess.PIPE, 186 | stderr: int = subprocess.PIPE, 187 | env: Dict[str, Any] = {} 188 | ) -> Optional['subprocess.Popen[bytes]']: 189 | node_bin = self.node_bin() 190 | if node_bin is None: 191 | return None 192 | os_env = os.environ.copy() 193 | os_env.update(self.node_env()) 194 | os_env.update(env) 195 | startupinfo = None 196 | if sys.platform == 'win32': 197 | startupinfo = subprocess.STARTUPINFO() 198 | startupinfo.dwFlags |= subprocess.SW_HIDE | subprocess.STARTF_USESHOWWINDOW 199 | return subprocess.Popen( 200 | [node_bin] + args, stdin=stdin, stdout=stdout, stderr=stderr, env=os_env, startupinfo=startupinfo) 201 | 202 | def run_install(self, cwd: str) -> None: 203 | if not path.isdir(cwd): 204 | raise Exception('Specified working directory "{}" does not exist'.format(cwd)) 205 | if not self._node: 206 | raise Exception('Node.js not installed. Use NodeInstaller to install it first.') 207 | args = [ 208 | 'ci', 209 | '--omit=dev', 210 | '--scripts-prepend-node-path=true', 211 | '--verbose', 212 | ] 213 | stdout, error = run_command_sync( 214 | self.npm_command() + args, cwd=cwd, extra_env=self.node_env(), extra_paths=self._additional_paths, 215 | shell=False 216 | ) 217 | print('[lsp_utils] START output of command: "{}"'.format(' '.join(args))) 218 | print(stdout) 219 | print('[lsp_utils] Command output END') 220 | if error is not None: 221 | raise Exception('Failed to run npm command "{}":\n{}'.format(' '.join(args), error)) 222 | 223 | def npm_command(self) -> List[str]: 224 | if self._npm is None: 225 | raise Exception('Npm command not initialized') 226 | return [self._npm] 227 | 228 | 229 | class NodeRuntimePATH(NodeRuntime): 230 | def __init__(self) -> None: 231 | super().__init__() 232 | self._node = shutil.which('node') 233 | self._npm = shutil.which('npm') 234 | 235 | 236 | class NodeRuntimeLocal(NodeRuntime): 237 | def __init__(self, base_dir: str, node_version: str = NODE_RUNTIME_VERSION): 238 | super().__init__() 239 | self._base_dir = path.abspath(path.join(base_dir, node_version)) 240 | self._node_version = node_version 241 | self._node_dir = path.join(self._base_dir, 'node') 242 | self._install_in_progress_marker_file = path.join(self._base_dir, '.installing') 243 | self._resolve_paths() 244 | 245 | # --- NodeRuntime overrides ---------------------------------------------------------------------------------------- 246 | 247 | def npm_command(self) -> List[str]: 248 | if not self._node or not self._npm: 249 | raise Exception('Node.js or Npm command not initialized') 250 | return [self._node, self._npm] 251 | 252 | def install_node(self) -> None: 253 | os.makedirs(os.path.dirname(self._install_in_progress_marker_file), exist_ok=True) 254 | open(self._install_in_progress_marker_file, 'a').close() 255 | with ActivityIndicator(sublime.active_window(), '[LSP] Setting up local Node.js'): 256 | install_node = NodeInstaller(self._base_dir, self._node_version) 257 | install_node.run() 258 | self._resolve_paths() 259 | remove(self._install_in_progress_marker_file) 260 | self._resolve_paths() 261 | 262 | # --- private methods ---------------------------------------------------------------------------------------------- 263 | 264 | def _resolve_paths(self) -> None: 265 | if path.isfile(self._install_in_progress_marker_file): 266 | # Will trigger re-installation. 267 | return 268 | self._node = self._resolve_binary() 269 | self._node_lib = self._resolve_lib() 270 | self._npm = path.join(self._node_lib, 'npm', 'bin', 'npm-cli.js') 271 | self._additional_paths = [path.dirname(self._node)] if self._node else [] 272 | 273 | def _resolve_binary(self) -> Optional[str]: 274 | exe_path = path.join(self._node_dir, 'node.exe') 275 | binary_path = path.join(self._node_dir, 'bin', 'node') 276 | if path.isfile(exe_path): 277 | return exe_path 278 | if path.isfile(binary_path): 279 | return binary_path 280 | return None 281 | 282 | def _resolve_lib(self) -> str: 283 | lib_path = path.join(self._node_dir, 'lib', 'node_modules') 284 | if not path.isdir(lib_path): 285 | lib_path = path.join(self._node_dir, 'node_modules') 286 | return lib_path 287 | 288 | 289 | class NodeInstaller: 290 | '''Command to install a local copy of Node.js''' 291 | 292 | def __init__(self, base_dir: str, node_version: str = NODE_RUNTIME_VERSION) -> None: 293 | """ 294 | :param base_dir: The base directory for storing given Node.js runtime version 295 | :param node_version: The Node.js version to install 296 | """ 297 | self._base_dir = base_dir 298 | self._node_version = node_version 299 | self._cache_dir = path.join(self._base_dir, 'cache') 300 | 301 | def run(self) -> None: 302 | archive, url = self._node_archive() 303 | print('[lsp_utils] Downloading Node.js {} from {}'.format(self._node_version, url)) 304 | if not self._archive_exists(archive): 305 | self._download_node(url, archive) 306 | self._install_node(archive) 307 | 308 | def _node_archive(self) -> Tuple[str, str]: 309 | platform = sublime.platform() 310 | arch = sublime.arch() 311 | if platform == 'windows' and arch == 'x64': 312 | node_os = 'win' 313 | archive = 'zip' 314 | elif platform == 'linux': 315 | node_os = 'linux' 316 | archive = 'tar.gz' 317 | elif platform == 'osx': 318 | node_os = 'darwin' 319 | archive = 'tar.gz' 320 | else: 321 | raise Exception('{} {} is not supported'.format(arch, platform)) 322 | filename = 'node-v{}-{}-{}.{}'.format(self._node_version, node_os, arch, archive) 323 | dist_url = NODE_DIST_URL.format(version=self._node_version, filename=filename) 324 | return filename, dist_url 325 | 326 | def _archive_exists(self, filename: str) -> bool: 327 | archive = path.join(self._cache_dir, filename) 328 | return path.isfile(archive) 329 | 330 | def _download_node(self, url: str, filename: str) -> None: 331 | if not path.isdir(self._cache_dir): 332 | os.makedirs(self._cache_dir) 333 | archive = path.join(self._cache_dir, filename) 334 | with urllib.request.urlopen(url) as response: 335 | with open(archive, 'wb') as f: 336 | shutil.copyfileobj(response, f) 337 | 338 | def _install_node(self, filename: str) -> None: 339 | archive = path.join(self._cache_dir, filename) 340 | opener = zipfile.ZipFile if filename.endswith('.zip') else tarfile.open # type: Any 341 | try: 342 | with opener(archive) as f: 343 | names = f.namelist() if hasattr(f, 'namelist') else f.getnames() 344 | install_dir, _ = next(x for x in names if '/' in x).split('/', 1) 345 | bad_members = [x for x in names if x.startswith('/') or x.startswith('..')] 346 | if bad_members: 347 | raise Exception('{} appears to be malicious, bad filenames: {}'.format(filename, bad_members)) 348 | f.extractall(self._base_dir) 349 | with chdir(self._base_dir): 350 | os.rename(install_dir, 'node') 351 | except Exception as ex: 352 | raise ex 353 | finally: 354 | remove(archive) 355 | 356 | 357 | class ElectronRuntimeLocal(NodeRuntime): 358 | def __init__(self, base_dir: str): 359 | super().__init__() 360 | self._base_dir = path.abspath(path.join(base_dir, ELECTRON_NODE_VERSION)) 361 | self._yarn = path.join(self._base_dir, 'yarn.js') 362 | self._install_in_progress_marker_file = path.join(self._base_dir, '.installing') 363 | if not path.isfile(self._install_in_progress_marker_file): 364 | self._resolve_paths() 365 | 366 | # --- NodeRuntime overrides ---------------------------------------------------------------------------------------- 367 | 368 | def node_env(self) -> Dict[str, str]: 369 | extra_env = super().node_env() 370 | extra_env.update({'ELECTRON_RUN_AS_NODE': 'true'}) 371 | return extra_env 372 | 373 | def install_node(self) -> None: 374 | os.makedirs(os.path.dirname(self._install_in_progress_marker_file), exist_ok=True) 375 | open(self._install_in_progress_marker_file, 'a').close() 376 | with ActivityIndicator(sublime.active_window(), '[LSP] Setting up local Node.js'): 377 | install_node = ElectronInstaller(self._base_dir) 378 | install_node.run() 379 | self._resolve_paths() 380 | remove(self._install_in_progress_marker_file) 381 | 382 | def run_install(self, cwd: str) -> None: 383 | self._run_yarn(['import'], cwd) 384 | args = [ 385 | 'install', 386 | '--production', 387 | '--frozen-lockfile', 388 | '--scripts-prepend-node-path=true', 389 | '--cache-folder={}'.format(path.join(self._base_dir, 'cache', 'yarn')), 390 | # '--verbose', 391 | ] 392 | self._run_yarn(args, cwd) 393 | 394 | # --- private methods ---------------------------------------------------------------------------------------------- 395 | 396 | def _resolve_paths(self) -> None: 397 | self._node = self._resolve_binary() 398 | self._npm = path.join(self._base_dir, 'yarn.js') 399 | 400 | def _resolve_binary(self) -> Optional[str]: 401 | binary_path = None 402 | platform = sublime.platform() 403 | if platform == 'osx': 404 | binary_path = path.join(self._base_dir, 'Electron.app', 'Contents', 'MacOS', 'Electron') 405 | elif platform == 'windows': 406 | binary_path = path.join(self._base_dir, 'electron.exe') 407 | else: 408 | binary_path = path.join(self._base_dir, 'electron') 409 | return binary_path if binary_path and path.isfile(binary_path) else None 410 | 411 | def _run_yarn(self, args: List[str], cwd: str) -> None: 412 | if not path.isdir(cwd): 413 | raise Exception('Specified working directory "{}" does not exist'.format(cwd)) 414 | if not self._node: 415 | raise Exception('Node.js not installed. Use NodeInstaller to install it first.') 416 | stdout, error = run_command_sync( 417 | [self._node, self._yarn] + args, cwd=cwd, extra_env=self.node_env(), shell=False 418 | ) 419 | print('[lsp_utils] START output of command: "{}"'.format(' '.join(args))) 420 | print(stdout) 421 | print('[lsp_utils] Command output END') 422 | if error is not None: 423 | raise Exception('Failed to run yarn command "{}":\n{}'.format(' '.join(args), error)) 424 | 425 | 426 | class ElectronInstaller: 427 | '''Command to install a local copy of Node.js''' 428 | 429 | def __init__(self, base_dir: str) -> None: 430 | """ 431 | :param base_dir: The base directory for storing given Node.js runtime version 432 | """ 433 | self._base_dir = base_dir 434 | self._cache_dir = path.join(self._base_dir, 'cache') 435 | 436 | def run(self) -> None: 437 | archive, url = self._node_archive() 438 | print( 439 | '[lsp_utils] Downloading Electron {} (Node.js runtime {}) from {}'.format( 440 | ELECTRON_RUNTIME_VERSION, ELECTRON_NODE_VERSION, url 441 | ) 442 | ) 443 | if not self._archive_exists(archive): 444 | self._download(url, archive) 445 | self._install(archive) 446 | self._download_yarn() 447 | 448 | def _node_archive(self) -> Tuple[str, str]: 449 | platform = sublime.platform() 450 | arch = sublime.arch() 451 | if platform == 'windows': 452 | platform_code = 'win32' 453 | elif platform == 'linux': 454 | platform_code = 'linux' 455 | elif platform == 'osx': 456 | platform_code = 'darwin' 457 | else: 458 | raise Exception('{} {} is not supported'.format(arch, platform)) 459 | filename = 'electron-v{}-{}-{}.zip'.format(ELECTRON_RUNTIME_VERSION, platform_code, arch) 460 | dist_url = ELECTRON_DIST_URL.format(version=ELECTRON_RUNTIME_VERSION, filename=filename) 461 | return filename, dist_url 462 | 463 | def _archive_exists(self, filename: str) -> bool: 464 | archive = path.join(self._cache_dir, filename) 465 | return path.isfile(archive) 466 | 467 | def _download(self, url: str, filename: str) -> None: 468 | if not path.isdir(self._cache_dir): 469 | os.makedirs(self._cache_dir) 470 | archive = path.join(self._cache_dir, filename) 471 | with urllib.request.urlopen(url) as response: 472 | with open(archive, 'wb') as f: 473 | shutil.copyfileobj(response, f) 474 | 475 | def _install(self, filename: str) -> None: 476 | archive = path.join(self._cache_dir, filename) 477 | try: 478 | if sublime.platform() == 'windows': 479 | with zipfile.ZipFile(archive) as f: 480 | names = f.namelist() 481 | _, _ = next(x for x in names if '/' in x).split('/', 1) 482 | bad_members = [x for x in names if x.startswith('/') or x.startswith('..')] 483 | if bad_members: 484 | raise Exception('{} appears to be malicious, bad filenames: {}'.format(filename, bad_members)) 485 | f.extractall(self._base_dir) 486 | else: 487 | # ZipFile doesn't handle symlinks and permissions correctly on Linux and Mac. Use unzip instead. 488 | _, error = run_command_sync(['unzip', archive, '-d', self._base_dir], cwd=self._cache_dir) 489 | if error: 490 | raise Exception('Error unzipping electron archive: {}'.format(error)) 491 | except Exception as ex: 492 | raise ex 493 | finally: 494 | remove(archive) 495 | 496 | def _download_yarn(self) -> None: 497 | archive = path.join(self._base_dir, 'yarn.js') 498 | with urllib.request.urlopen(YARN_URL) as response: 499 | with open(archive, 'wb') as f: 500 | shutil.copyfileobj(response, f) 501 | 502 | 503 | @contextmanager 504 | def chdir(new_dir: str) -> Generator[None, None, None]: 505 | '''Context Manager for changing the working directory''' 506 | cur_dir = os.getcwd() 507 | os.chdir(new_dir) 508 | try: 509 | yield 510 | finally: 511 | os.chdir(cur_dir) 512 | -------------------------------------------------------------------------------- /st4_py38/lsp_utils/npm_client_handler.py: -------------------------------------------------------------------------------- 1 | from .generic_client_handler import GenericClientHandler 2 | from .server_npm_resource import ServerNpmResource 3 | from .server_resource_interface import ServerResourceInterface 4 | from LSP.plugin import ClientConfig 5 | from LSP.plugin import WorkspaceFolder 6 | from LSP.plugin.core.typing import Dict, List, Optional, Tuple 7 | from os import path 8 | import sublime 9 | 10 | __all__ = ['NpmClientHandler'] 11 | 12 | 13 | class NpmClientHandler(GenericClientHandler): 14 | """ 15 | An implementation of :class:`GenericClientHandler` for handling NPM-based LSP plugins. 16 | 17 | Automatically manages an NPM-based server by installing and updating it in the package storage directory. 18 | """ 19 | __server = None # type: Optional[ServerNpmResource] 20 | 21 | server_directory = '' 22 | """ 23 | The path to the server source directory, relative to the root directory of this package. 24 | 25 | :required: Yes 26 | """ 27 | 28 | server_binary_path = '' 29 | """ 30 | The path to the server "binary", relative to plugin's storage directory. 31 | 32 | :required: Yes 33 | """ 34 | 35 | skip_npm_install = False 36 | """ 37 | Whether to skip the step that runs "npm install" in case the server doesn't need any dependencies. 38 | 39 | :required: No 40 | """ 41 | 42 | # --- NpmClientHandler handlers ----------------------------------------------------------------------------------- 43 | 44 | @classmethod 45 | def minimum_node_version(cls) -> Tuple[int, int, int]: 46 | """ 47 | .. deprecated:: 2.1.0 48 | Use :meth:`required_node_version` instead. 49 | 50 | The minimum Node version required for this plugin. 51 | 52 | :returns: The semantic version tuple with the minimum required version. Defaults to :code:`(8, 0, 0)`. 53 | """ 54 | return (8, 0, 0) 55 | 56 | @classmethod 57 | def required_node_version(cls) -> str: 58 | """ 59 | The NPM semantic version (typically a range) specifying which version of Node is required for this plugin. 60 | 61 | Examples: 62 | - `16.1.1` - only allows a single version 63 | - `16.x` - allows any build for major version 16 64 | - `>=16` - allows version 16 and above 65 | - `16 - 18` allows any version between version 16 and 18 (inclusive). It's important to have spaces around 66 | the dash in this case. 67 | 68 | Also see more examples and a testing playground at https://semver.npmjs.com/ . 69 | 70 | :returns: Required NPM semantic version. Defaults to :code:`0.0.0` which means "no restrictions". 71 | """ 72 | return '0.0.0' 73 | 74 | @classmethod 75 | def get_additional_variables(cls) -> Dict[str, str]: 76 | """ 77 | Overrides :meth:`GenericClientHandler.get_additional_variables`, providing additional variable for use in the 78 | settings. 79 | 80 | The additional variables are: 81 | 82 | - `${node_bin}`: - holds the binary path of currently used Node.js runtime. This can resolve to just `node` 83 | when using Node.js runtime from the PATH or to a full filesystem path if using the local Node.js runtime. 84 | - `${server_directory_path}` - holds filesystem path to the server directory (only 85 | when :meth:`GenericClientHandler.manages_server()` is `True`). 86 | 87 | Remember to call the super class and merge the results if overriding. 88 | """ 89 | variables = super().get_additional_variables() 90 | variables.update({ 91 | 'node_bin': cls._node_bin(), 92 | 'server_directory_path': cls._server_directory_path(), 93 | }) 94 | return variables 95 | 96 | @classmethod 97 | def get_additional_paths(cls) -> List[str]: 98 | node_bin = cls._node_bin() 99 | if node_bin: 100 | node_path = path.dirname(node_bin) 101 | if node_path: 102 | return [node_path] 103 | return [] 104 | 105 | # --- GenericClientHandler handlers ------------------------------------------------------------------------------- 106 | 107 | @classmethod 108 | def get_command(cls) -> List[str]: 109 | return [cls._node_bin(), cls.binary_path()] + cls.get_binary_arguments() 110 | 111 | @classmethod 112 | def get_binary_arguments(cls) -> List[str]: 113 | return ['--stdio'] 114 | 115 | @classmethod 116 | def manages_server(cls) -> bool: 117 | return True 118 | 119 | @classmethod 120 | def get_server(cls) -> Optional[ServerResourceInterface]: 121 | if not cls.__server: 122 | cls.__server = ServerNpmResource.create({ 123 | 'package_name': cls.package_name, 124 | 'server_directory': cls.server_directory, 125 | 'server_binary_path': cls.server_binary_path, 126 | 'package_storage': cls.package_storage(), 127 | 'minimum_node_version': cls.minimum_node_version(), 128 | 'required_node_version': cls.required_node_version(), 129 | 'storage_path': cls.storage_path(), 130 | 'skip_npm_install': cls.skip_npm_install, 131 | }) 132 | return cls.__server 133 | 134 | @classmethod 135 | def cleanup(cls) -> None: 136 | cls.__server = None 137 | super().cleanup() 138 | 139 | @classmethod 140 | def can_start(cls, window: sublime.Window, initiating_view: sublime.View, 141 | workspace_folders: List[WorkspaceFolder], configuration: ClientConfig) -> Optional[str]: 142 | reason = super().can_start(window, initiating_view, workspace_folders, configuration) 143 | if reason: 144 | return reason 145 | node_env = cls._node_env() 146 | if node_env: 147 | configuration.env.update(node_env) 148 | return None 149 | 150 | # --- Internal ---------------------------------------------------------------------------------------------------- 151 | 152 | @classmethod 153 | def _server_directory_path(cls) -> str: 154 | if cls.__server: 155 | return cls.__server.server_directory_path 156 | return '' 157 | 158 | @classmethod 159 | def _node_bin(cls) -> str: 160 | if cls.__server: 161 | return cls.__server.node_bin 162 | return '' 163 | 164 | @classmethod 165 | def _node_env(cls) -> Optional[Dict[str, str]]: 166 | if cls.__server: 167 | return cls.__server.node_env 168 | return None 169 | -------------------------------------------------------------------------------- /st4_py38/lsp_utils/pip_client_handler.py: -------------------------------------------------------------------------------- 1 | from .generic_client_handler import GenericClientHandler 2 | from .server_pip_resource import ServerPipResource 3 | from .server_resource_interface import ServerResourceInterface 4 | from LSP.plugin.core.typing import List, Optional 5 | from os import path 6 | import shutil 7 | import sublime 8 | 9 | __all__ = ['PipClientHandler'] 10 | 11 | 12 | class PipClientHandler(GenericClientHandler): 13 | """ 14 | An implementation of :class:`GenericClientHandler` for handling pip-based LSP plugins. 15 | 16 | Automatically manages a pip-based server by installing and updating dependencies based on provided 17 | `requirements.txt` file. 18 | """ 19 | __server = None # type: Optional[ServerPipResource] 20 | 21 | requirements_txt_path = '' 22 | """ 23 | The path to the `requirements.txt` file containing a list of dependencies required by the server. 24 | 25 | If the package `LSP-foo` has a `requirements.txt` file at the root then the path will be just `requirements.txt`. 26 | 27 | The file format is `dependency_name==dependency_version` or just a direct path to the dependency (for example to 28 | a github repo). For example: 29 | 30 | .. code:: 31 | 32 | pyls==0.1.2 33 | colorama==1.2.2 34 | git+https://github.com/tomv564/pyls-mypy.git 35 | 36 | :required: Yes 37 | """ 38 | 39 | server_filename = '' 40 | """ 41 | The file name of the binary used to start the server. 42 | 43 | :required: Yes 44 | """ 45 | 46 | @classmethod 47 | def get_python_binary(cls) -> str: 48 | """ 49 | Returns a binary name or a full path to the Python interpreter used to create environment for the server. 50 | 51 | When only the binary name is specified then it will be expected that it can be found on the PATH. 52 | """ 53 | if sublime.platform() == 'windows': 54 | if shutil.which('py'): 55 | return 'py' 56 | return 'python' 57 | return 'python3' 58 | 59 | # --- GenericClientHandler handlers ------------------------------------------------------------------------------- 60 | 61 | @classmethod 62 | def manages_server(cls) -> bool: 63 | return True 64 | 65 | @classmethod 66 | def get_server(cls) -> Optional[ServerResourceInterface]: 67 | if not cls.__server: 68 | python_binary = cls.get_python_binary() 69 | if not shutil.which(python_binary): 70 | raise Exception('Python binary "{}" not found!'.format(python_binary)) 71 | cls.__server = ServerPipResource( 72 | cls.storage_path(), cls.package_name, cls.requirements_txt_path, cls.server_filename, python_binary) 73 | return cls.__server 74 | 75 | @classmethod 76 | def get_additional_paths(cls) -> List[str]: 77 | server = cls.get_server() 78 | if server: 79 | return [path.dirname(server.binary_path)] 80 | return [] 81 | -------------------------------------------------------------------------------- /st4_py38/lsp_utils/server_npm_resource.py: -------------------------------------------------------------------------------- 1 | from .helpers import rmtree_ex 2 | from .helpers import SemanticVersion 3 | from .node_runtime import NodeRuntime 4 | from .server_resource_interface import ServerResourceInterface 5 | from .server_resource_interface import ServerStatus 6 | from hashlib import md5 7 | from LSP.plugin.core.typing import Dict, Optional, TypedDict, Union 8 | from os import makedirs 9 | from os import path 10 | from os import remove 11 | from os import walk 12 | from sublime_lib import ResourcePath 13 | 14 | __all__ = ['ServerNpmResource'] 15 | 16 | ServerNpmResourceCreateOptions = TypedDict('ServerNpmResourceCreateOptions', { 17 | 'package_name': str, 18 | 'server_directory': str, 19 | 'server_binary_path': str, 20 | 'package_storage': str, 21 | 'storage_path': str, 22 | 'minimum_node_version': SemanticVersion, 23 | 'required_node_version': str, 24 | 'skip_npm_install': bool, 25 | }) 26 | 27 | 28 | class ServerNpmResource(ServerResourceInterface): 29 | """ 30 | An implementation of :class:`lsp_utils.ServerResourceInterface` implementing server management for 31 | node-based severs. Handles installation and updates of the server in package storage. 32 | """ 33 | 34 | @classmethod 35 | def create(cls, options: ServerNpmResourceCreateOptions) -> 'ServerNpmResource': 36 | package_name = options['package_name'] 37 | server_directory = options['server_directory'] 38 | server_binary_path = options['server_binary_path'] 39 | package_storage = options['package_storage'] 40 | storage_path = options['storage_path'] 41 | minimum_node_version = options['minimum_node_version'] 42 | required_node_version = options['required_node_version'] # type: Union[str, SemanticVersion] 43 | skip_npm_install = options['skip_npm_install'] 44 | # Fallback to "minimum_node_version" if "required_node_version" is 0.0.0 (not overridden). 45 | if '0.0.0' == required_node_version: 46 | required_node_version = minimum_node_version 47 | node_runtime = NodeRuntime.get(package_name, storage_path, required_node_version) 48 | if not node_runtime: 49 | raise Exception('Failed resolving Node.js Runtime. Please see Sublime Text console for more information.') 50 | return ServerNpmResource( 51 | package_name, server_directory, server_binary_path, package_storage, node_runtime, skip_npm_install) 52 | 53 | def __init__(self, package_name: str, server_directory: str, server_binary_path: str, 54 | package_storage: str, node_runtime: NodeRuntime, skip_npm_install: bool) -> None: 55 | if not package_name or not server_directory or not server_binary_path or not node_runtime: 56 | raise Exception('ServerNpmResource could not initialize due to wrong input') 57 | self._status = ServerStatus.UNINITIALIZED 58 | self._package_name = package_name 59 | self._package_storage = package_storage 60 | self._server_src = 'Packages/{}/{}/'.format(self._package_name, server_directory) 61 | node_version = str(node_runtime.resolve_version()) 62 | self._node_version = node_version 63 | self._server_dest = path.join(package_storage, node_version, server_directory) 64 | self._binary_path = path.join(package_storage, node_version, server_binary_path) 65 | self._installation_marker_file = path.join(package_storage, node_version, '.installing') 66 | self._node_runtime = node_runtime 67 | self._skip_npm_install = skip_npm_install 68 | 69 | @property 70 | def server_directory_path(self) -> str: 71 | return self._server_dest 72 | 73 | @property 74 | def node_bin(self) -> str: 75 | node_bin = self._node_runtime.node_bin() 76 | if node_bin is None: 77 | raise Exception('Failed to resolve path to the Node.js runtime') 78 | return node_bin 79 | 80 | @property 81 | def node_env(self) -> Optional[Dict[str, str]]: 82 | return self._node_runtime.node_env() 83 | 84 | # --- ServerResourceInterface ------------------------------------------------------------------------------------- 85 | 86 | @property 87 | def binary_path(self) -> str: 88 | return self._binary_path 89 | 90 | def get_status(self) -> int: 91 | return self._status 92 | 93 | def needs_installation(self) -> bool: 94 | installed = False 95 | if self._skip_npm_install or path.isdir(path.join(self._server_dest, 'node_modules')): 96 | # Server already installed. Check if version has changed or last installation did not complete. 97 | src_package_json = ResourcePath(self._server_src, 'package.json') 98 | if not src_package_json.exists(): 99 | raise Exception('Missing required "package.json" in {}'.format(self._server_src)) 100 | src_hash = md5(src_package_json.read_bytes()).hexdigest() 101 | try: 102 | with open(path.join(self._server_dest, 'package.json'), 'rb') as file: 103 | dst_hash = md5(file.read()).hexdigest() 104 | if src_hash == dst_hash and not path.isfile(self._installation_marker_file): 105 | installed = True 106 | except FileNotFoundError: 107 | # Needs to be re-installed. 108 | pass 109 | if installed: 110 | self._status = ServerStatus.READY 111 | return False 112 | return True 113 | 114 | def install_or_update(self) -> None: 115 | try: 116 | self._cleanup_package_storage() 117 | makedirs(path.dirname(self._installation_marker_file), exist_ok=True) 118 | open(self._installation_marker_file, 'a').close() 119 | if path.isdir(self._server_dest): 120 | rmtree_ex(self._server_dest) 121 | ResourcePath(self._server_src).copytree(self._server_dest, exist_ok=True) 122 | if not self._skip_npm_install: 123 | self._node_runtime.run_install(cwd=self._server_dest) 124 | remove(self._installation_marker_file) 125 | except Exception as error: 126 | self._status = ServerStatus.ERROR 127 | raise Exception('Error installing the server:\n{}'.format(error)) 128 | self._status = ServerStatus.READY 129 | 130 | def _cleanup_package_storage(self) -> None: 131 | if not path.isdir(self._package_storage): 132 | return 133 | """Clean up subdirectories of package storage that belong to other node versions.""" 134 | subdirectories = next(walk(self._package_storage))[1] 135 | for directory in subdirectories: 136 | if directory == self._node_version: 137 | continue 138 | node_storage_path = path.join(self._package_storage, directory) 139 | print('[lsp_utils] Deleting outdated storage directory "{}"'.format(node_storage_path)) 140 | rmtree_ex(node_storage_path) 141 | -------------------------------------------------------------------------------- /st4_py38/lsp_utils/server_pip_resource.py: -------------------------------------------------------------------------------- 1 | from .helpers import rmtree_ex 2 | from .helpers import run_command_sync 3 | from .server_resource_interface import ServerResourceInterface 4 | from .server_resource_interface import ServerStatus 5 | from hashlib import md5 6 | from LSP.plugin.core.typing import Any, Optional 7 | from os import path 8 | from sublime_lib import ResourcePath 9 | import os 10 | import sublime 11 | 12 | __all__ = ['ServerPipResource'] 13 | 14 | 15 | class ServerPipResource(ServerResourceInterface): 16 | """ 17 | An implementation of :class:`lsp_utils.ServerResourceInterface` implementing server management for 18 | pip-based servers. Handles installation and updates of the server in the package storage. 19 | 20 | :param storage_path: The path to the package storage (pass :meth:`lsp_utils.GenericClientHandler.storage_path()`) 21 | :param package_name: The package name (used as a directory name for storage) 22 | :param requirements_path: The path to the `requirements.txt` file, relative to the package directory. 23 | If the package `LSP-foo` has a `requirements.txt` file at the root then the path will be `requirements.txt`. 24 | :param server_binary_filename: The name of the file used to start the server. 25 | """ 26 | 27 | @classmethod 28 | def file_extension(cls) -> str: 29 | return '.exe' if sublime.platform() == 'windows' else '' 30 | 31 | @classmethod 32 | def run(cls, *args: Any, cwd: Optional[str] = None) -> str: 33 | output, error = run_command_sync(list(args), cwd=cwd) 34 | if error: 35 | raise Exception(error) 36 | return output 37 | 38 | def __init__(self, storage_path: str, package_name: str, requirements_path: str, 39 | server_binary_filename: str, python_binary: str) -> None: 40 | self._storage_path = storage_path 41 | self._package_name = package_name 42 | self._requirements_path_relative = requirements_path 43 | self._requirements_path = 'Packages/{}/{}'.format(self._package_name, requirements_path) 44 | self._server_binary_filename = server_binary_filename 45 | self._python_binary = python_binary 46 | self._status = ServerStatus.UNINITIALIZED 47 | 48 | def basedir(self) -> str: 49 | return path.join(self._storage_path, self._package_name) 50 | 51 | def bindir(self) -> str: 52 | bin_dir = 'Scripts' if sublime.platform() == 'windows' else 'bin' 53 | return path.join(self.basedir(), bin_dir) 54 | 55 | def server_binary(self) -> str: 56 | return path.join(self.bindir(), self._server_binary_filename + self.file_extension()) 57 | 58 | def pip_binary(self) -> str: 59 | return path.join(self.bindir(), 'pip' + self.file_extension()) 60 | 61 | def python_version(self) -> str: 62 | return path.join(self.basedir(), 'python_version') 63 | 64 | # --- ServerResourceInterface handlers ---------------------------------------------------------------------------- 65 | 66 | @property 67 | def binary_path(self) -> str: 68 | return self.server_binary() 69 | 70 | def get_status(self) -> int: 71 | return self._status 72 | 73 | def needs_installation(self) -> bool: 74 | if not path.exists(self.server_binary()) or not path.exists(self.pip_binary()): 75 | return True 76 | if not path.exists(self.python_version()): 77 | return True 78 | with open(self.python_version(), 'r') as f: 79 | if f.readline().strip() != self.run(self._python_binary, '--version').strip(): 80 | return True 81 | src_requirements_resource = ResourcePath(self._requirements_path) 82 | if not src_requirements_resource.exists(): 83 | raise Exception('Missing required "requirements.txt" in {}'.format(self._requirements_path)) 84 | src_requirements_hash = md5(src_requirements_resource.read_bytes()).hexdigest() 85 | try: 86 | with open(path.join(self.basedir(), self._requirements_path_relative), 'rb') as file: 87 | dst_requirements_hash = md5(file.read()).hexdigest() 88 | if src_requirements_hash != dst_requirements_hash: 89 | return True 90 | except FileNotFoundError: 91 | # Needs to be re-installed. 92 | return True 93 | self._status = ServerStatus.READY 94 | return False 95 | 96 | def install_or_update(self) -> None: 97 | rmtree_ex(self.basedir(), ignore_errors=True) 98 | try: 99 | os.makedirs(self.basedir(), exist_ok=True) 100 | self.run(self._python_binary, '-m', 'venv', self._package_name, cwd=self._storage_path) 101 | dest_requirements_txt_path = path.join(self._storage_path, self._package_name, 'requirements.txt') 102 | ResourcePath(self._requirements_path).copy(dest_requirements_txt_path) 103 | self.run(self.pip_binary(), 'install', '-r', dest_requirements_txt_path, '--disable-pip-version-check') 104 | with open(self.python_version(), 'w') as f: 105 | f.write(self.run(self._python_binary, '--version')) 106 | except Exception as error: 107 | self._status = ServerStatus.ERROR 108 | raise Exception('Error installing the server:\n{}'.format(error)) 109 | self._status = ServerStatus.READY 110 | -------------------------------------------------------------------------------- /st4_py38/lsp_utils/server_resource_interface.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta 2 | from abc import abstractmethod 3 | from abc import abstractproperty 4 | 5 | __all__ = ['ServerStatus', 'ServerResourceInterface'] 6 | 7 | 8 | class ServerStatus(): 9 | """ 10 | A :class:`ServerStatus` enum for use as a return value from :func:`ServerResourceInterface.get_status()`. 11 | """ 12 | 13 | UNINITIALIZED = 1 14 | """Initial status of the server.""" 15 | ERROR = 2 16 | """Initiallation or update has failed.""" 17 | READY = 3 18 | """Server is ready to provide resources.""" 19 | 20 | 21 | class ServerResourceInterface(metaclass=ABCMeta): 22 | """ 23 | An interface for implementating server resource handlers. Use this interface in plugins that manage their own 24 | server binary (:func:`GenericClientHandler.manages_server` returns `True`). 25 | 26 | After implementing this interface, return an instance of implemented class from 27 | :meth:`GenericClientHandler.get_server()`. 28 | """ 29 | 30 | @abstractmethod 31 | def needs_installation(self) -> bool: 32 | """ 33 | This is the place to check whether the binary needs an update, or whether it needs to be installed before 34 | starting the language server. 35 | 36 | :returns: `True` if the server needs to be installed or updated. This will result in calling 37 | :meth:`install_or_update()`. 38 | """ 39 | ... 40 | 41 | @abstractmethod 42 | def install_or_update(self) -> None: 43 | """ 44 | Do the actual update/installation of the server binary. Don't start extra threads to do the work as everything 45 | is handled automatically. 46 | """ 47 | ... 48 | 49 | @abstractmethod 50 | def get_status(self) -> int: 51 | """ 52 | Determines the current status of the server. The state changes as the server is being installed, updated or 53 | runs into an error doing those. Initialize with :attr:`ServerStatus.UNINITIALIZED` and change to either 54 | Set to :attr:`ServerStatus.ERROR` or :attr:`ServerStatus.READY` depending on if the server was installed 55 | correctly or is already installed. 56 | 57 | :returns: A number corresponding to the :class:`ServerStatus` class members. 58 | """ 59 | ... 60 | 61 | @abstractproperty 62 | def binary_path(self) -> str: 63 | """ 64 | Returns a filesystem path to the server binary. 65 | """ 66 | ... 67 | -------------------------------------------------------------------------------- /st4_py38/lsp_utils/third_party/semantic_version/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) The python-semanticversion project 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /st4_py38/lsp_utils/third_party/semantic_version/README.rst: -------------------------------------------------------------------------------- 1 | Introduction 2 | ============ 3 | 4 | This small python library provides a few tools to handle `SemVer`_ in Python. 5 | It follows strictly the 2.0.0 version of the SemVer scheme. 6 | 7 | .. image:: https://github.com/rbarrois/python-semanticversion/actions/workflows/test.yml/badge.svg 8 | :target: https://github.com/rbarrois/python-semanticversion/actions/workflows/test.yml 9 | 10 | .. image:: https://img.shields.io/pypi/v/semantic_version.svg 11 | :target: https://python-semanticversion.readthedocs.io/en/latest/changelog.html 12 | :alt: Latest Version 13 | 14 | .. image:: https://img.shields.io/pypi/pyversions/semantic_version.svg 15 | :target: https://pypi.python.org/pypi/semantic_version/ 16 | :alt: Supported Python versions 17 | 18 | .. image:: https://img.shields.io/pypi/wheel/semantic_version.svg 19 | :target: https://pypi.python.org/pypi/semantic_version/ 20 | :alt: Wheel status 21 | 22 | .. image:: https://img.shields.io/pypi/l/semantic_version.svg 23 | :target: https://pypi.python.org/pypi/semantic_version/ 24 | :alt: License 25 | 26 | Links 27 | ----- 28 | 29 | - Package on `PyPI`_: https://pypi.org/project/semantic-version/ 30 | - Doc on `ReadTheDocs `_: https://python-semanticversion.readthedocs.io/ 31 | - Source on `GitHub `_: http://github.com/rbarrois/python-semanticversion/ 32 | - Build on Github Actions: https://github.com/rbarrois/python-semanticversion/actions 33 | - Semantic Version specification: `SemVer`_ 34 | 35 | 36 | Getting started 37 | =============== 38 | 39 | Install the package from `PyPI`_, using pip: 40 | 41 | .. code-block:: sh 42 | 43 | pip install semantic-version 44 | 45 | Or from GitHub: 46 | 47 | .. code-block:: sh 48 | 49 | $ git clone git://github.com/rbarrois/python-semanticversion.git 50 | 51 | 52 | Import it in your code: 53 | 54 | 55 | .. code-block:: python 56 | 57 | import semantic_version 58 | 59 | 60 | This module provides classes to handle semantic versions: 61 | 62 | - ``Version`` represents a version number (``0.1.1-alpha+build.2012-05-15``) 63 | - ``BaseSpec``-derived classes represent requirement specifications (``>=0.1.1,<0.3.0``): 64 | 65 | - ``SimpleSpec`` describes a natural description syntax 66 | - ``NpmSpec`` is used for NPM-style range descriptions. 67 | 68 | Versions 69 | -------- 70 | 71 | Defining a ``Version`` is quite simple: 72 | 73 | 74 | .. code-block:: pycon 75 | 76 | >>> import semantic_version 77 | >>> v = semantic_version.Version('0.1.1') 78 | >>> v.major 79 | 0 80 | >>> v.minor 81 | 1 82 | >>> v.patch 83 | 1 84 | >>> v.prerelease 85 | [] 86 | >>> v.build 87 | [] 88 | >>> list(v) 89 | [0, 1, 1, [], []] 90 | 91 | If the provided version string is invalid, a ``ValueError`` will be raised: 92 | 93 | .. code-block:: pycon 94 | 95 | >>> semantic_version.Version('0.1') 96 | Traceback (most recent call last): 97 | File "", line 1, in 98 | File "/Users/rbarrois/dev/semantic_version/src/semantic_version/base.py", line 64, in __init__ 99 | major, minor, patch, prerelease, build = self.parse(version_string, partial) 100 | File "/Users/rbarrois/dev/semantic_version/src/semantic_version/base.py", line 86, in parse 101 | raise ValueError('Invalid version string: %r' % version_string) 102 | ValueError: Invalid version string: '0.1' 103 | 104 | 105 | One may also create a ``Version`` with named components: 106 | 107 | .. code-block:: pycon 108 | 109 | >>> semantic_version.Version(major=0, minor=1, patch=2) 110 | Version('0.1.2') 111 | 112 | In that case, ``major``, ``minor`` and ``patch`` are mandatory, and must be integers. 113 | ``prerelease`` and ``build``, if provided, must be tuples of strings: 114 | 115 | .. code-block:: pycon 116 | 117 | >>> semantic_version.Version(major=0, minor=1, patch=2, prerelease=('alpha', '2')) 118 | Version('0.1.2-alpha.2') 119 | 120 | 121 | Some user-supplied input might not match the semantic version scheme. 122 | For such cases, the ``Version.coerce`` method will try to convert any 123 | version-like string into a valid semver version: 124 | 125 | .. code-block:: pycon 126 | 127 | >>> Version.coerce('0') 128 | Version('0.0.0') 129 | >>> Version.coerce('0.1.2.3.4') 130 | Version('0.1.2+3.4') 131 | >>> Version.coerce('0.1.2a3') 132 | Version('0.1.2-a3') 133 | 134 | Working with versions 135 | """"""""""""""""""""" 136 | 137 | Obviously, versions can be compared: 138 | 139 | 140 | .. code-block:: pycon 141 | 142 | >>> semantic_version.Version('0.1.1') < semantic_version.Version('0.1.2') 143 | True 144 | >>> semantic_version.Version('0.1.1') > semantic_version.Version('0.1.1-alpha') 145 | True 146 | >>> semantic_version.Version('0.1.1') <= semantic_version.Version('0.1.1-alpha') 147 | False 148 | 149 | You can also get a new version that represents a bump in one of the version levels: 150 | 151 | .. code-block:: pycon 152 | 153 | >>> v = semantic_version.Version('0.1.1+build') 154 | >>> new_v = v.next_major() 155 | >>> str(new_v) 156 | '1.0.0' 157 | >>> v = semantic_version.Version('1.1.1+build') 158 | >>> new_v = v.next_minor() 159 | >>> str(new_v) 160 | '1.2.0' 161 | >>> v = semantic_version.Version('1.1.1+build') 162 | >>> new_v = v.next_patch() 163 | >>> str(new_v) 164 | '1.1.2' 165 | 166 | 167 | 168 | Requirement specification 169 | ------------------------- 170 | 171 | python-semanticversion provides a couple of ways to describe a range of accepted 172 | versions: 173 | 174 | - The ``SimpleSpec`` class provides a simple, easily understood scheme -- 175 | somewhat inspired from PyPI range notations; 176 | - The ``NpmSpec`` class supports the whole NPM range specification scheme: 177 | 178 | .. code-block:: pycon 179 | 180 | >>> Version('0.1.2') in NpmSpec('0.1.0-alpha.2 .. 0.2.4') 181 | True 182 | >>> Version('0.1.2') in NpmSpec('>=0.1.1 <0.1.3 || 2.x') 183 | True 184 | >>> Version('2.3.4') in NpmSpec('>=0.1.1 <0.1.3 || 2.x') 185 | True 186 | 187 | The ``SimpleSpec`` scheme 188 | """"""""""""""""""""""""" 189 | 190 | Basic usage is simply a comparator and a base version: 191 | 192 | .. code-block:: pycon 193 | 194 | >>> s = SimpleSpec('>=0.1.1') # At least 0.1.1 195 | >>> s.match(Version('0.1.1')) 196 | True 197 | >>> s.match(Version('0.1.1-alpha1')) # pre-release doesn't satisfy version spec 198 | False 199 | >>> s.match(Version('0.1.0')) 200 | False 201 | 202 | Combining specifications can be expressed as follows: 203 | 204 | .. code-block:: pycon 205 | 206 | >>> SimpleSpec('>=0.1.1,<0.3.0') 207 | 208 | Simpler test syntax is also available using the ``in`` keyword: 209 | 210 | .. code-block:: pycon 211 | 212 | >>> s = SimpleSpec('==0.1.1') 213 | >>> Version('0.1.1+git7ccc72') in s # build variants are equivalent to full versions 214 | True 215 | >>> Version('0.1.1-alpha1') in s # pre-release variants don't match the full version. 216 | False 217 | >>> Version('0.1.2') in s 218 | False 219 | 220 | 221 | Refer to the full documentation at 222 | https://python-semanticversion.readthedocs.io/en/latest/ for more details on the 223 | ``SimpleSpec`` scheme. 224 | 225 | 226 | 227 | Using a specification 228 | """"""""""""""""""""" 229 | 230 | The ``SimpleSpec.filter`` method filters an iterable of ``Version``: 231 | 232 | .. code-block:: pycon 233 | 234 | >>> s = SimpleSpec('>=0.1.0,<0.4.0') 235 | >>> versions = (Version('0.%d.0' % i) for i in range(6)) 236 | >>> for v in s.filter(versions): 237 | ... print v 238 | 0.1.0 239 | 0.2.0 240 | 0.3.0 241 | 242 | It is also possible to select the 'best' version from such iterables: 243 | 244 | 245 | .. code-block:: pycon 246 | 247 | >>> s = SimpleSpec('>=0.1.0,<0.4.0') 248 | >>> versions = (Version('0.%d.0' % i) for i in range(6)) 249 | >>> s.select(versions) 250 | Version('0.3.0') 251 | 252 | 253 | 254 | Contributing 255 | ============ 256 | 257 | In order to contribute to the source code: 258 | 259 | - Open an issue on `GitHub`_: https://github.com/rbarrois/python-semanticversion/issues 260 | - Fork the `repository `_ 261 | and submit a pull request on `GitHub`_ 262 | - Or send me a patch (mailto:raphael.barrois+semver@polytechnique.org) 263 | 264 | When submitting patches or pull requests, you should respect the following rules: 265 | 266 | - Coding conventions are based on :pep:`8` 267 | - The whole test suite must pass after adding the changes 268 | - The test coverage for a new feature must be 100% 269 | - New features and methods should be documented in the ``reference`` section 270 | and included in the ``changelog`` 271 | - Include your name in the ``contributors`` section 272 | 273 | .. note:: All files should contain the following header:: 274 | 275 | # -*- encoding: utf-8 -*- 276 | # Copyright (c) The python-semanticversion project 277 | 278 | .. _SemVer: http://semver.org/ 279 | .. _PyPI: http://pypi.python.org/ 280 | -------------------------------------------------------------------------------- /st4_py38/lsp_utils/third_party/semantic_version/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (c) The python-semanticversion project 3 | # This code is distributed under the two-clause BSD License. 4 | 5 | 6 | from .base import compare, match, validate, SimpleSpec, NpmSpec, Spec, SpecItem, Version 7 | 8 | 9 | __author__ = "Raphaël Barrois " 10 | -------------------------------------------------------------------------------- /st4_py38/lsp_utils/third_party/semantic_version/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (c) The python-semanticversion project 3 | # This code is distributed under the two-clause BSD License. 4 | 5 | import functools 6 | import re 7 | import warnings 8 | 9 | 10 | def _has_leading_zero(value): 11 | return (value 12 | and value[0] == '0' 13 | and value.isdigit() 14 | and value != '0') 15 | 16 | 17 | class MaxIdentifier(object): 18 | __slots__ = [] 19 | 20 | def __repr__(self): 21 | return 'MaxIdentifier()' 22 | 23 | def __eq__(self, other): 24 | return isinstance(other, self.__class__) 25 | 26 | 27 | @functools.total_ordering 28 | class NumericIdentifier(object): 29 | __slots__ = ['value'] 30 | 31 | def __init__(self, value): 32 | self.value = int(value) 33 | 34 | def __repr__(self): 35 | return 'NumericIdentifier(%r)' % self.value 36 | 37 | def __eq__(self, other): 38 | if isinstance(other, NumericIdentifier): 39 | return self.value == other.value 40 | return NotImplemented 41 | 42 | def __lt__(self, other): 43 | if isinstance(other, MaxIdentifier): 44 | return True 45 | elif isinstance(other, AlphaIdentifier): 46 | return True 47 | elif isinstance(other, NumericIdentifier): 48 | return self.value < other.value 49 | else: 50 | return NotImplemented 51 | 52 | 53 | @functools.total_ordering 54 | class AlphaIdentifier(object): 55 | __slots__ = ['value'] 56 | 57 | def __init__(self, value): 58 | self.value = value.encode('ascii') 59 | 60 | def __repr__(self): 61 | return 'AlphaIdentifier(%r)' % self.value 62 | 63 | def __eq__(self, other): 64 | if isinstance(other, AlphaIdentifier): 65 | return self.value == other.value 66 | return NotImplemented 67 | 68 | def __lt__(self, other): 69 | if isinstance(other, MaxIdentifier): 70 | return True 71 | elif isinstance(other, NumericIdentifier): 72 | return False 73 | elif isinstance(other, AlphaIdentifier): 74 | return self.value < other.value 75 | else: 76 | return NotImplemented 77 | 78 | 79 | class Version(object): 80 | 81 | version_re = re.compile(r'^(\d+)\.(\d+)\.(\d+)(?:-([0-9a-zA-Z.-]+))?(?:\+([0-9a-zA-Z.-]+))?$') 82 | partial_version_re = re.compile(r'^(\d+)(?:\.(\d+)(?:\.(\d+))?)?(?:-([0-9a-zA-Z.-]*))?(?:\+([0-9a-zA-Z.-]*))?$') 83 | 84 | def __init__( 85 | self, 86 | version_string=None, 87 | major=None, 88 | minor=None, 89 | patch=None, 90 | prerelease=None, 91 | build=None, 92 | partial=False): 93 | if partial: 94 | warnings.warn( 95 | "Partial versions will be removed in 3.0; use SimpleSpec('1.x.x') instead.", 96 | DeprecationWarning, 97 | stacklevel=2, 98 | ) 99 | has_text = version_string is not None 100 | has_parts = not (major is minor is patch is prerelease is build is None) 101 | if not has_text ^ has_parts: 102 | raise ValueError("Call either Version('1.2.3') or Version(major=1, ...).") 103 | 104 | if has_text: 105 | major, minor, patch, prerelease, build = self.parse(version_string, partial) 106 | else: 107 | # Convenience: allow to omit prerelease/build. 108 | prerelease = tuple(prerelease or ()) 109 | if not partial: 110 | build = tuple(build or ()) 111 | self._validate_kwargs(major, minor, patch, prerelease, build, partial) 112 | 113 | self.major = major 114 | self.minor = minor 115 | self.patch = patch 116 | self.prerelease = prerelease 117 | self.build = build 118 | 119 | self.partial = partial 120 | 121 | # Cached precedence keys 122 | # _cmp_precedence_key is used for semver-precedence comparison 123 | self._cmp_precedence_key = self._build_precedence_key(with_build=False) 124 | # _sort_precedence_key is used for self.precedence_key, esp. for sorted(...) 125 | self._sort_precedence_key = self._build_precedence_key(with_build=True) 126 | 127 | @classmethod 128 | def _coerce(cls, value, allow_none=False): 129 | if value is None and allow_none: 130 | return value 131 | return int(value) 132 | 133 | def next_major(self): 134 | if self.prerelease and self.minor == self.patch == 0: 135 | return Version( 136 | major=self.major, 137 | minor=0, 138 | patch=0, 139 | partial=self.partial, 140 | ) 141 | else: 142 | return Version( 143 | major=self.major + 1, 144 | minor=0, 145 | patch=0, 146 | partial=self.partial, 147 | ) 148 | 149 | def next_minor(self): 150 | if self.prerelease and self.patch == 0: 151 | return Version( 152 | major=self.major, 153 | minor=self.minor, 154 | patch=0, 155 | partial=self.partial, 156 | ) 157 | else: 158 | return Version( 159 | major=self.major, 160 | minor=self.minor + 1, 161 | patch=0, 162 | partial=self.partial, 163 | ) 164 | 165 | def next_patch(self): 166 | if self.prerelease: 167 | return Version( 168 | major=self.major, 169 | minor=self.minor, 170 | patch=self.patch, 171 | partial=self.partial, 172 | ) 173 | else: 174 | return Version( 175 | major=self.major, 176 | minor=self.minor, 177 | patch=self.patch + 1, 178 | partial=self.partial, 179 | ) 180 | 181 | def truncate(self, level='patch'): 182 | """Return a new Version object, truncated up to the selected level.""" 183 | if level == 'build': 184 | return self 185 | elif level == 'prerelease': 186 | return Version( 187 | major=self.major, 188 | minor=self.minor, 189 | patch=self.patch, 190 | prerelease=self.prerelease, 191 | partial=self.partial, 192 | ) 193 | elif level == 'patch': 194 | return Version( 195 | major=self.major, 196 | minor=self.minor, 197 | patch=self.patch, 198 | partial=self.partial, 199 | ) 200 | elif level == 'minor': 201 | return Version( 202 | major=self.major, 203 | minor=self.minor, 204 | patch=None if self.partial else 0, 205 | partial=self.partial, 206 | ) 207 | elif level == 'major': 208 | return Version( 209 | major=self.major, 210 | minor=None if self.partial else 0, 211 | patch=None if self.partial else 0, 212 | partial=self.partial, 213 | ) 214 | else: 215 | raise ValueError("Invalid truncation level `%s`." % level) 216 | 217 | @classmethod 218 | def coerce(cls, version_string, partial=False): 219 | """Coerce an arbitrary version string into a semver-compatible one. 220 | 221 | The rule is: 222 | - If not enough components, fill minor/patch with zeroes; unless 223 | partial=True 224 | - If more than 3 dot-separated components, extra components are "build" 225 | data. If some "build" data already appeared, append it to the 226 | extra components 227 | 228 | Examples: 229 | >>> Version.coerce('0.1') 230 | Version(0, 1, 0) 231 | >>> Version.coerce('0.1.2.3') 232 | Version(0, 1, 2, (), ('3',)) 233 | >>> Version.coerce('0.1.2.3+4') 234 | Version(0, 1, 2, (), ('3', '4')) 235 | >>> Version.coerce('0.1+2-3+4_5') 236 | Version(0, 1, 0, (), ('2-3', '4-5')) 237 | """ 238 | base_re = re.compile(r'^\d+(?:\.\d+(?:\.\d+)?)?') 239 | 240 | match = base_re.match(version_string) 241 | if not match: 242 | raise ValueError( 243 | "Version string lacks a numerical component: %r" 244 | % version_string 245 | ) 246 | 247 | version = version_string[:match.end()] 248 | if not partial: 249 | # We need a not-partial version. 250 | while version.count('.') < 2: 251 | version += '.0' 252 | 253 | # Strip leading zeros in components 254 | # Version is of the form nn, nn.pp or nn.pp.qq 255 | version = '.'.join( 256 | # If the part was '0', we end up with an empty string. 257 | part.lstrip('0') or '0' 258 | for part in version.split('.') 259 | ) 260 | 261 | if match.end() == len(version_string): 262 | return Version(version, partial=partial) 263 | 264 | rest = version_string[match.end():] 265 | 266 | # Cleanup the 'rest' 267 | rest = re.sub(r'[^a-zA-Z0-9+.-]', '-', rest) 268 | 269 | if rest[0] == '+': 270 | # A 'build' component 271 | prerelease = '' 272 | build = rest[1:] 273 | elif rest[0] == '.': 274 | # An extra version component, probably 'build' 275 | prerelease = '' 276 | build = rest[1:] 277 | elif rest[0] == '-': 278 | rest = rest[1:] 279 | if '+' in rest: 280 | prerelease, build = rest.split('+', 1) 281 | else: 282 | prerelease, build = rest, '' 283 | elif '+' in rest: 284 | prerelease, build = rest.split('+', 1) 285 | else: 286 | prerelease, build = rest, '' 287 | 288 | build = build.replace('+', '.') 289 | 290 | if prerelease: 291 | version = '%s-%s' % (version, prerelease) 292 | if build: 293 | version = '%s+%s' % (version, build) 294 | 295 | return cls(version, partial=partial) 296 | 297 | @classmethod 298 | def parse(cls, version_string, partial=False, coerce=False): 299 | """Parse a version string into a tuple of components: 300 | (major, minor, patch, prerelease, build). 301 | 302 | Args: 303 | version_string (str), the version string to parse 304 | partial (bool), whether to accept incomplete input 305 | coerce (bool), whether to try to map the passed in string into a 306 | valid Version. 307 | """ 308 | if not version_string: 309 | raise ValueError('Invalid empty version string: %r' % version_string) 310 | 311 | if partial: 312 | version_re = cls.partial_version_re 313 | else: 314 | version_re = cls.version_re 315 | 316 | match = version_re.match(version_string) 317 | if not match: 318 | raise ValueError('Invalid version string: %r' % version_string) 319 | 320 | major, minor, patch, prerelease, build = match.groups() 321 | 322 | if _has_leading_zero(major): 323 | raise ValueError("Invalid leading zero in major: %r" % version_string) 324 | if _has_leading_zero(minor): 325 | raise ValueError("Invalid leading zero in minor: %r" % version_string) 326 | if _has_leading_zero(patch): 327 | raise ValueError("Invalid leading zero in patch: %r" % version_string) 328 | 329 | major = int(major) 330 | minor = cls._coerce(minor, partial) 331 | patch = cls._coerce(patch, partial) 332 | 333 | if prerelease is None: 334 | if partial and (build is None): 335 | # No build info, strip here 336 | return (major, minor, patch, None, None) 337 | else: 338 | prerelease = () 339 | elif prerelease == '': 340 | prerelease = () 341 | else: 342 | prerelease = tuple(prerelease.split('.')) 343 | cls._validate_identifiers(prerelease, allow_leading_zeroes=False) 344 | 345 | if build is None: 346 | if partial: 347 | build = None 348 | else: 349 | build = () 350 | elif build == '': 351 | build = () 352 | else: 353 | build = tuple(build.split('.')) 354 | cls._validate_identifiers(build, allow_leading_zeroes=True) 355 | 356 | return (major, minor, patch, prerelease, build) 357 | 358 | @classmethod 359 | def _validate_identifiers(cls, identifiers, allow_leading_zeroes=False): 360 | for item in identifiers: 361 | if not item: 362 | raise ValueError( 363 | "Invalid empty identifier %r in %r" 364 | % (item, '.'.join(identifiers)) 365 | ) 366 | 367 | if item[0] == '0' and item.isdigit() and item != '0' and not allow_leading_zeroes: 368 | raise ValueError("Invalid leading zero in identifier %r" % item) 369 | 370 | @classmethod 371 | def _validate_kwargs(cls, major, minor, patch, prerelease, build, partial): 372 | if ( 373 | major != int(major) 374 | or minor != cls._coerce(minor, partial) 375 | or patch != cls._coerce(patch, partial) 376 | or prerelease is None and not partial 377 | or build is None and not partial 378 | ): 379 | raise ValueError( 380 | "Invalid kwargs to Version(major=%r, minor=%r, patch=%r, " 381 | "prerelease=%r, build=%r, partial=%r" % ( 382 | major, minor, patch, prerelease, build, partial 383 | )) 384 | if prerelease is not None: 385 | cls._validate_identifiers(prerelease, allow_leading_zeroes=False) 386 | if build is not None: 387 | cls._validate_identifiers(build, allow_leading_zeroes=True) 388 | 389 | def __iter__(self): 390 | return iter((self.major, self.minor, self.patch, self.prerelease, self.build)) 391 | 392 | def __str__(self): 393 | version = '%d' % self.major 394 | if self.minor is not None: 395 | version = '%s.%d' % (version, self.minor) 396 | if self.patch is not None: 397 | version = '%s.%d' % (version, self.patch) 398 | 399 | if self.prerelease or (self.partial and self.prerelease == () and self.build is None): 400 | version = '%s-%s' % (version, '.'.join(self.prerelease)) 401 | if self.build or (self.partial and self.build == ()): 402 | version = '%s+%s' % (version, '.'.join(self.build)) 403 | return version 404 | 405 | def __repr__(self): 406 | return '%s(%r%s)' % ( 407 | self.__class__.__name__, 408 | str(self), 409 | ', partial=True' if self.partial else '', 410 | ) 411 | 412 | def __hash__(self): 413 | # We don't include 'partial', since this is strictly equivalent to having 414 | # at least a field being `None`. 415 | return hash((self.major, self.minor, self.patch, self.prerelease, self.build)) 416 | 417 | def _build_precedence_key(self, with_build=False): 418 | """Build a precedence key. 419 | 420 | The "build" component should only be used when sorting an iterable 421 | of versions. 422 | """ 423 | if self.prerelease: 424 | prerelease_key = tuple( 425 | NumericIdentifier(part) if part.isdigit() else AlphaIdentifier(part) 426 | for part in self.prerelease 427 | ) 428 | else: 429 | prerelease_key = ( 430 | MaxIdentifier(), 431 | ) 432 | 433 | if not with_build: 434 | return ( 435 | self.major, 436 | self.minor, 437 | self.patch, 438 | prerelease_key, 439 | ) 440 | 441 | build_key = tuple( 442 | NumericIdentifier(part) if part.isdigit() else AlphaIdentifier(part) 443 | for part in self.build or () 444 | ) 445 | 446 | return ( 447 | self.major, 448 | self.minor, 449 | self.patch, 450 | prerelease_key, 451 | build_key, 452 | ) 453 | 454 | @property 455 | def precedence_key(self): 456 | return self._sort_precedence_key 457 | 458 | def __cmp__(self, other): 459 | if not isinstance(other, self.__class__): 460 | return NotImplemented 461 | if self < other: 462 | return -1 463 | elif self > other: 464 | return 1 465 | elif self == other: 466 | return 0 467 | else: 468 | return NotImplemented 469 | 470 | def __eq__(self, other): 471 | if not isinstance(other, self.__class__): 472 | return NotImplemented 473 | return ( 474 | self.major == other.major 475 | and self.minor == other.minor 476 | and self.patch == other.patch 477 | and (self.prerelease or ()) == (other.prerelease or ()) 478 | and (self.build or ()) == (other.build or ()) 479 | ) 480 | 481 | def __ne__(self, other): 482 | if not isinstance(other, self.__class__): 483 | return NotImplemented 484 | return tuple(self) != tuple(other) 485 | 486 | def __lt__(self, other): 487 | if not isinstance(other, self.__class__): 488 | return NotImplemented 489 | return self._cmp_precedence_key < other._cmp_precedence_key 490 | 491 | def __le__(self, other): 492 | if not isinstance(other, self.__class__): 493 | return NotImplemented 494 | return self._cmp_precedence_key <= other._cmp_precedence_key 495 | 496 | def __gt__(self, other): 497 | if not isinstance(other, self.__class__): 498 | return NotImplemented 499 | return self._cmp_precedence_key > other._cmp_precedence_key 500 | 501 | def __ge__(self, other): 502 | if not isinstance(other, self.__class__): 503 | return NotImplemented 504 | return self._cmp_precedence_key >= other._cmp_precedence_key 505 | 506 | 507 | class SpecItem(object): 508 | """A requirement specification.""" 509 | 510 | KIND_ANY = '*' 511 | KIND_LT = '<' 512 | KIND_LTE = '<=' 513 | KIND_EQUAL = '==' 514 | KIND_SHORTEQ = '=' 515 | KIND_EMPTY = '' 516 | KIND_GTE = '>=' 517 | KIND_GT = '>' 518 | KIND_NEQ = '!=' 519 | KIND_CARET = '^' 520 | KIND_TILDE = '~' 521 | KIND_COMPATIBLE = '~=' 522 | 523 | # Map a kind alias to its full version 524 | KIND_ALIASES = { 525 | KIND_SHORTEQ: KIND_EQUAL, 526 | KIND_EMPTY: KIND_EQUAL, 527 | } 528 | 529 | re_spec = re.compile(r'^(<|<=||=|==|>=|>|!=|\^|~|~=)(\d.*)$') 530 | 531 | def __init__(self, requirement_string, _warn=True): 532 | if _warn: 533 | warnings.warn( 534 | "The `SpecItem` class will be removed in 3.0.", 535 | DeprecationWarning, 536 | stacklevel=2, 537 | ) 538 | kind, spec = self.parse(requirement_string) 539 | self.kind = kind 540 | self.spec = spec 541 | self._clause = Spec(requirement_string).clause 542 | 543 | @classmethod 544 | def parse(cls, requirement_string): 545 | if not requirement_string: 546 | raise ValueError("Invalid empty requirement specification: %r" % requirement_string) 547 | 548 | # Special case: the 'any' version spec. 549 | if requirement_string == '*': 550 | return (cls.KIND_ANY, '') 551 | 552 | match = cls.re_spec.match(requirement_string) 553 | if not match: 554 | raise ValueError("Invalid requirement specification: %r" % requirement_string) 555 | 556 | kind, version = match.groups() 557 | if kind in cls.KIND_ALIASES: 558 | kind = cls.KIND_ALIASES[kind] 559 | 560 | spec = Version(version, partial=True) 561 | if spec.build is not None and kind not in (cls.KIND_EQUAL, cls.KIND_NEQ): 562 | raise ValueError( 563 | "Invalid requirement specification %r: build numbers have no ordering." 564 | % requirement_string 565 | ) 566 | return (kind, spec) 567 | 568 | @classmethod 569 | def from_matcher(cls, matcher): 570 | if matcher == Always(): 571 | return cls('*', _warn=False) 572 | elif matcher == Never(): 573 | return cls('<0.0.0-', _warn=False) 574 | elif isinstance(matcher, Range): 575 | return cls('%s%s' % (matcher.operator, matcher.target), _warn=False) 576 | 577 | def match(self, version): 578 | return self._clause.match(version) 579 | 580 | def __str__(self): 581 | return '%s%s' % (self.kind, self.spec) 582 | 583 | def __repr__(self): 584 | return '' % (self.kind, self.spec) 585 | 586 | def __eq__(self, other): 587 | if not isinstance(other, SpecItem): 588 | return NotImplemented 589 | return self.kind == other.kind and self.spec == other.spec 590 | 591 | def __hash__(self): 592 | return hash((self.kind, self.spec)) 593 | 594 | 595 | def compare(v1, v2): 596 | return Version(v1).__cmp__(Version(v2)) 597 | 598 | 599 | def match(spec, version): 600 | return Spec(spec).match(Version(version)) 601 | 602 | 603 | def validate(version_string): 604 | """Validates a version string againt the SemVer specification.""" 605 | try: 606 | Version.parse(version_string) 607 | return True 608 | except ValueError: 609 | return False 610 | 611 | 612 | DEFAULT_SYNTAX = 'simple' 613 | 614 | 615 | class BaseSpec(object): 616 | """A specification of compatible versions. 617 | 618 | Usage: 619 | >>> Spec('>=1.0.0', syntax='npm') 620 | 621 | A version matches a specification if it matches any 622 | of the clauses of that specification. 623 | 624 | Internally, a Spec is AnyOf( 625 | AllOf(Matcher, Matcher, Matcher), 626 | AllOf(...), 627 | ) 628 | """ 629 | SYNTAXES = {} 630 | 631 | @classmethod 632 | def register_syntax(cls, subclass): 633 | syntax = subclass.SYNTAX 634 | if syntax is None: 635 | raise ValueError("A Spec needs its SYNTAX field to be set.") 636 | elif syntax in cls.SYNTAXES: 637 | raise ValueError( 638 | "Duplicate syntax for %s: %r, %r" 639 | % (syntax, cls.SYNTAXES[syntax], subclass) 640 | ) 641 | cls.SYNTAXES[syntax] = subclass 642 | return subclass 643 | 644 | def __init__(self, expression): 645 | super(BaseSpec, self).__init__() 646 | self.expression = expression 647 | self.clause = self._parse_to_clause(expression) 648 | 649 | @classmethod 650 | def parse(cls, expression, syntax=DEFAULT_SYNTAX): 651 | """Convert a syntax-specific expression into a BaseSpec instance.""" 652 | return cls.SYNTAXES[syntax](expression) 653 | 654 | @classmethod 655 | def _parse_to_clause(cls, expression): 656 | """Converts an expression to a clause.""" 657 | raise NotImplementedError() 658 | 659 | def filter(self, versions): 660 | """Filter an iterable of versions satisfying the Spec.""" 661 | for version in versions: 662 | if self.match(version): 663 | yield version 664 | 665 | def match(self, version): 666 | """Check whether a Version satisfies the Spec.""" 667 | return self.clause.match(version) 668 | 669 | def select(self, versions): 670 | """Select the best compatible version among an iterable of options.""" 671 | options = list(self.filter(versions)) 672 | if options: 673 | return max(options) 674 | return None 675 | 676 | def __contains__(self, version): 677 | """Whether `version in self`.""" 678 | if isinstance(version, Version): 679 | return self.match(version) 680 | return False 681 | 682 | def __eq__(self, other): 683 | if not isinstance(other, self.__class__): 684 | return NotImplemented 685 | 686 | return self.clause == other.clause 687 | 688 | def __hash__(self): 689 | return hash(self.clause) 690 | 691 | def __str__(self): 692 | return self.expression 693 | 694 | def __repr__(self): 695 | return '<%s: %r>' % (self.__class__.__name__, self.expression) 696 | 697 | 698 | class Clause(object): 699 | __slots__ = [] 700 | 701 | def match(self, version): 702 | raise NotImplementedError() 703 | 704 | def __and__(self, other): 705 | raise NotImplementedError() 706 | 707 | def __or__(self, other): 708 | raise NotImplementedError() 709 | 710 | def __eq__(self, other): 711 | raise NotImplementedError() 712 | 713 | def prettyprint(self, indent='\t'): 714 | """Pretty-print the clause. 715 | """ 716 | return '\n'.join(self._pretty()).replace('\t', indent) 717 | 718 | def _pretty(self): 719 | """Actual pretty-printing logic. 720 | 721 | Yields: 722 | A list of string. Indentation is performed with \t. 723 | """ 724 | yield repr(self) 725 | 726 | def __ne__(self, other): 727 | return not self == other 728 | 729 | def simplify(self): 730 | return self 731 | 732 | 733 | class AnyOf(Clause): 734 | __slots__ = ['clauses'] 735 | 736 | def __init__(self, *clauses): 737 | super(AnyOf, self).__init__() 738 | self.clauses = frozenset(clauses) 739 | 740 | def match(self, version): 741 | return any(c.match(version) for c in self.clauses) 742 | 743 | def simplify(self): 744 | subclauses = set() 745 | for clause in self.clauses: 746 | simplified = clause.simplify() 747 | if isinstance(simplified, AnyOf): 748 | subclauses |= simplified.clauses 749 | elif simplified == Never(): 750 | continue 751 | else: 752 | subclauses.add(simplified) 753 | if len(subclauses) == 1: 754 | return subclauses.pop() 755 | return AnyOf(*subclauses) 756 | 757 | def __hash__(self): 758 | return hash((AnyOf, self.clauses)) 759 | 760 | def __iter__(self): 761 | return iter(self.clauses) 762 | 763 | def __eq__(self, other): 764 | return isinstance(other, self.__class__) and self.clauses == other.clauses 765 | 766 | def __and__(self, other): 767 | if isinstance(other, AllOf): 768 | return other & self 769 | elif isinstance(other, Matcher) or isinstance(other, AnyOf): 770 | return AllOf(self, other) 771 | else: 772 | return NotImplemented 773 | 774 | def __or__(self, other): 775 | if isinstance(other, AnyOf): 776 | clauses = list(self.clauses | other.clauses) 777 | elif isinstance(other, Matcher) or isinstance(other, AllOf): 778 | clauses = list(self.clauses | set([other])) 779 | else: 780 | return NotImplemented 781 | return AnyOf(*clauses) 782 | 783 | def __repr__(self): 784 | return 'AnyOf(%s)' % ', '.join(sorted(repr(c) for c in self.clauses)) 785 | 786 | def _pretty(self): 787 | yield 'AnyOF(' 788 | for clause in self.clauses: 789 | lines = list(clause._pretty()) 790 | for line in lines[:-1]: 791 | yield '\t' + line 792 | yield '\t' + lines[-1] + ',' 793 | yield ')' 794 | 795 | 796 | class AllOf(Clause): 797 | __slots__ = ['clauses'] 798 | 799 | def __init__(self, *clauses): 800 | super(AllOf, self).__init__() 801 | self.clauses = frozenset(clauses) 802 | 803 | def match(self, version): 804 | return all(clause.match(version) for clause in self.clauses) 805 | 806 | def simplify(self): 807 | subclauses = set() 808 | for clause in self.clauses: 809 | simplified = clause.simplify() 810 | if isinstance(simplified, AllOf): 811 | subclauses |= simplified.clauses 812 | elif simplified == Always(): 813 | continue 814 | else: 815 | subclauses.add(simplified) 816 | if len(subclauses) == 1: 817 | return subclauses.pop() 818 | return AllOf(*subclauses) 819 | 820 | def __hash__(self): 821 | return hash((AllOf, self.clauses)) 822 | 823 | def __iter__(self): 824 | return iter(self.clauses) 825 | 826 | def __eq__(self, other): 827 | return isinstance(other, self.__class__) and self.clauses == other.clauses 828 | 829 | def __and__(self, other): 830 | if isinstance(other, Matcher) or isinstance(other, AnyOf): 831 | clauses = list(self.clauses | set([other])) 832 | elif isinstance(other, AllOf): 833 | clauses = list(self.clauses | other.clauses) 834 | else: 835 | return NotImplemented 836 | return AllOf(*clauses) 837 | 838 | def __or__(self, other): 839 | if isinstance(other, AnyOf): 840 | return other | self 841 | elif isinstance(other, Matcher): 842 | return AnyOf(self, AllOf(other)) 843 | elif isinstance(other, AllOf): 844 | return AnyOf(self, other) 845 | else: 846 | return NotImplemented 847 | 848 | def __repr__(self): 849 | return 'AllOf(%s)' % ', '.join(sorted(repr(c) for c in self.clauses)) 850 | 851 | def _pretty(self): 852 | yield 'AllOF(' 853 | for clause in self.clauses: 854 | lines = list(clause._pretty()) 855 | for line in lines[:-1]: 856 | yield '\t' + line 857 | yield '\t' + lines[-1] + ',' 858 | yield ')' 859 | 860 | 861 | class Matcher(Clause): 862 | __slots__ = [] 863 | 864 | def __and__(self, other): 865 | if isinstance(other, AllOf): 866 | return other & self 867 | elif isinstance(other, Matcher) or isinstance(other, AnyOf): 868 | return AllOf(self, other) 869 | else: 870 | return NotImplemented 871 | 872 | def __or__(self, other): 873 | if isinstance(other, AnyOf): 874 | return other | self 875 | elif isinstance(other, Matcher) or isinstance(other, AllOf): 876 | return AnyOf(self, other) 877 | else: 878 | return NotImplemented 879 | 880 | 881 | class Never(Matcher): 882 | __slots__ = [] 883 | 884 | def match(self, version): 885 | return False 886 | 887 | def __hash__(self): 888 | return hash((Never,)) 889 | 890 | def __eq__(self, other): 891 | return isinstance(other, self.__class__) 892 | 893 | def __and__(self, other): 894 | return self 895 | 896 | def __or__(self, other): 897 | return other 898 | 899 | def __repr__(self): 900 | return 'Never()' 901 | 902 | 903 | class Always(Matcher): 904 | __slots__ = [] 905 | 906 | def match(self, version): 907 | return True 908 | 909 | def __hash__(self): 910 | return hash((Always,)) 911 | 912 | def __eq__(self, other): 913 | return isinstance(other, self.__class__) 914 | 915 | def __and__(self, other): 916 | return other 917 | 918 | def __or__(self, other): 919 | return self 920 | 921 | def __repr__(self): 922 | return 'Always()' 923 | 924 | 925 | class Range(Matcher): 926 | OP_EQ = '==' 927 | OP_GT = '>' 928 | OP_GTE = '>=' 929 | OP_LT = '<' 930 | OP_LTE = '<=' 931 | OP_NEQ = '!=' 932 | 933 | # <1.2.3 matches 1.2.3-a1 934 | PRERELEASE_ALWAYS = 'always' 935 | # <1.2.3 does not match 1.2.3-a1 936 | PRERELEASE_NATURAL = 'natural' 937 | # 1.2.3-a1 is only considered if target == 1.2.3-xxx 938 | PRERELEASE_SAMEPATCH = 'same-patch' 939 | 940 | # 1.2.3 matches 1.2.3+* 941 | BUILD_IMPLICIT = 'implicit' 942 | # 1.2.3 matches only 1.2.3, not 1.2.3+4 943 | BUILD_STRICT = 'strict' 944 | 945 | __slots__ = ['operator', 'target', 'prerelease_policy', 'build_policy'] 946 | 947 | def __init__(self, operator, target, prerelease_policy=PRERELEASE_NATURAL, build_policy=BUILD_IMPLICIT): 948 | super(Range, self).__init__() 949 | if target.build and operator not in (self.OP_EQ, self.OP_NEQ): 950 | raise ValueError( 951 | "Invalid range %s%s: build numbers have no ordering." 952 | % (operator, target)) 953 | self.operator = operator 954 | self.target = target 955 | self.prerelease_policy = prerelease_policy 956 | self.build_policy = self.BUILD_STRICT if target.build else build_policy 957 | 958 | def match(self, version): 959 | if self.build_policy != self.BUILD_STRICT: 960 | version = version.truncate('prerelease') 961 | 962 | if version.prerelease: 963 | same_patch = self.target.truncate() == version.truncate() 964 | 965 | if self.prerelease_policy == self.PRERELEASE_SAMEPATCH and not same_patch: 966 | return False 967 | 968 | if self.operator == self.OP_EQ: 969 | if self.build_policy == self.BUILD_STRICT: 970 | return ( 971 | self.target.truncate('prerelease') == version.truncate('prerelease') 972 | and version.build == self.target.build 973 | ) 974 | return version == self.target 975 | elif self.operator == self.OP_GT: 976 | return version > self.target 977 | elif self.operator == self.OP_GTE: 978 | return version >= self.target 979 | elif self.operator == self.OP_LT: 980 | if ( 981 | version.prerelease 982 | and self.prerelease_policy == self.PRERELEASE_NATURAL 983 | and version.truncate() == self.target.truncate() 984 | and not self.target.prerelease 985 | ): 986 | return False 987 | return version < self.target 988 | elif self.operator == self.OP_LTE: 989 | return version <= self.target 990 | else: 991 | assert self.operator == self.OP_NEQ 992 | if self.build_policy == self.BUILD_STRICT: 993 | return not ( 994 | self.target.truncate('prerelease') == version.truncate('prerelease') 995 | and version.build == self.target.build 996 | ) 997 | 998 | if ( 999 | version.prerelease 1000 | and self.prerelease_policy == self.PRERELEASE_NATURAL 1001 | and version.truncate() == self.target.truncate() 1002 | and not self.target.prerelease 1003 | ): 1004 | return False 1005 | return version != self.target 1006 | 1007 | def __hash__(self): 1008 | return hash((Range, self.operator, self.target, self.prerelease_policy)) 1009 | 1010 | def __eq__(self, other): 1011 | return ( 1012 | isinstance(other, self.__class__) 1013 | and self.operator == other.operator 1014 | and self.target == other.target 1015 | and self.prerelease_policy == other.prerelease_policy 1016 | ) 1017 | 1018 | def __str__(self): 1019 | return '%s%s' % (self.operator, self.target) 1020 | 1021 | def __repr__(self): 1022 | policy_part = ( 1023 | '' if self.prerelease_policy == self.PRERELEASE_NATURAL 1024 | else ', prerelease_policy=%r' % self.prerelease_policy 1025 | ) + ( 1026 | '' if self.build_policy == self.BUILD_IMPLICIT 1027 | else ', build_policy=%r' % self.build_policy 1028 | ) 1029 | return 'Range(%r, %r%s)' % ( 1030 | self.operator, 1031 | self.target, 1032 | policy_part, 1033 | ) 1034 | 1035 | 1036 | @BaseSpec.register_syntax 1037 | class SimpleSpec(BaseSpec): 1038 | 1039 | SYNTAX = 'simple' 1040 | 1041 | @classmethod 1042 | def _parse_to_clause(cls, expression): 1043 | return cls.Parser.parse(expression) 1044 | 1045 | class Parser: 1046 | NUMBER = r'\*|0|[1-9][0-9]*' 1047 | NAIVE_SPEC = re.compile(r"""^ 1048 | (?P<|<=||=|==|>=|>|!=|\^|~|~=) 1049 | (?P{nb})(?:\.(?P{nb})(?:\.(?P{nb}))?)? 1050 | (?:-(?P[a-z0-9A-Z.-]*))? 1051 | (?:\+(?P[a-z0-9A-Z.-]*))? 1052 | $ 1053 | """.format(nb=NUMBER), 1054 | re.VERBOSE, 1055 | ) 1056 | 1057 | @classmethod 1058 | def parse(cls, expression): 1059 | blocks = expression.split(',') 1060 | clause = Always() 1061 | for block in blocks: 1062 | if not cls.NAIVE_SPEC.match(block): 1063 | raise ValueError("Invalid simple block %r" % block) 1064 | clause &= cls.parse_block(block) 1065 | 1066 | return clause 1067 | 1068 | PREFIX_CARET = '^' 1069 | PREFIX_TILDE = '~' 1070 | PREFIX_COMPATIBLE = '~=' 1071 | PREFIX_EQ = '==' 1072 | PREFIX_NEQ = '!=' 1073 | PREFIX_GT = '>' 1074 | PREFIX_GTE = '>=' 1075 | PREFIX_LT = '<' 1076 | PREFIX_LTE = '<=' 1077 | 1078 | PREFIX_ALIASES = { 1079 | '=': PREFIX_EQ, 1080 | '': PREFIX_EQ, 1081 | } 1082 | 1083 | EMPTY_VALUES = ['*', 'x', 'X', None] 1084 | 1085 | @classmethod 1086 | def parse_block(cls, expr): 1087 | if not cls.NAIVE_SPEC.match(expr): 1088 | raise ValueError("Invalid simple spec component: %r" % expr) 1089 | prefix, major_t, minor_t, patch_t, prerel, build = cls.NAIVE_SPEC.match(expr).groups() 1090 | prefix = cls.PREFIX_ALIASES.get(prefix, prefix) 1091 | 1092 | major = None if major_t in cls.EMPTY_VALUES else int(major_t) 1093 | minor = None if minor_t in cls.EMPTY_VALUES else int(minor_t) 1094 | patch = None if patch_t in cls.EMPTY_VALUES else int(patch_t) 1095 | 1096 | if major is None: # '*' 1097 | target = Version(major=0, minor=0, patch=0) 1098 | if prefix not in (cls.PREFIX_EQ, cls.PREFIX_GTE): 1099 | raise ValueError("Invalid simple spec: %r" % expr) 1100 | elif minor is None: 1101 | target = Version(major=major, minor=0, patch=0) 1102 | elif patch is None: 1103 | target = Version(major=major, minor=minor, patch=0) 1104 | else: 1105 | target = Version( 1106 | major=major, 1107 | minor=minor, 1108 | patch=patch, 1109 | prerelease=prerel.split('.') if prerel else (), 1110 | build=build.split('.') if build else (), 1111 | ) 1112 | 1113 | if (major is None or minor is None or patch is None) and (prerel or build): 1114 | raise ValueError("Invalid simple spec: %r" % expr) 1115 | 1116 | if build is not None and prefix not in (cls.PREFIX_EQ, cls.PREFIX_NEQ): 1117 | raise ValueError("Invalid simple spec: %r" % expr) 1118 | 1119 | if prefix == cls.PREFIX_CARET: 1120 | # Accept anything with the same most-significant digit 1121 | if target.major: 1122 | high = target.next_major() 1123 | elif target.minor: 1124 | high = target.next_minor() 1125 | else: 1126 | high = target.next_patch() 1127 | return Range(Range.OP_GTE, target) & Range(Range.OP_LT, high) 1128 | 1129 | elif prefix == cls.PREFIX_TILDE: 1130 | assert major is not None 1131 | # Accept any higher patch in the same minor 1132 | # Might go higher if the initial version was a partial 1133 | if minor is None: 1134 | high = target.next_major() 1135 | else: 1136 | high = target.next_minor() 1137 | return Range(Range.OP_GTE, target) & Range(Range.OP_LT, high) 1138 | 1139 | elif prefix == cls.PREFIX_COMPATIBLE: 1140 | assert major is not None 1141 | # ~1 is 1.0.0..2.0.0; ~=2.2 is 2.2.0..3.0.0; ~=1.4.5 is 1.4.5..1.5.0 1142 | if minor is None or patch is None: 1143 | # We got a partial version 1144 | high = target.next_major() 1145 | else: 1146 | high = target.next_minor() 1147 | return Range(Range.OP_GTE, target) & Range(Range.OP_LT, high) 1148 | 1149 | elif prefix == cls.PREFIX_EQ: 1150 | if major is None: 1151 | return Range(Range.OP_GTE, target) 1152 | elif minor is None: 1153 | return Range(Range.OP_GTE, target) & Range(Range.OP_LT, target.next_major()) 1154 | elif patch is None: 1155 | return Range(Range.OP_GTE, target) & Range(Range.OP_LT, target.next_minor()) 1156 | elif build == '': 1157 | return Range(Range.OP_EQ, target, build_policy=Range.BUILD_STRICT) 1158 | else: 1159 | return Range(Range.OP_EQ, target) 1160 | 1161 | elif prefix == cls.PREFIX_NEQ: 1162 | assert major is not None 1163 | if minor is None: 1164 | # !=1.x => <1.0.0 || >=2.0.0 1165 | return Range(Range.OP_LT, target) | Range(Range.OP_GTE, target.next_major()) 1166 | elif patch is None: 1167 | # !=1.2.x => <1.2.0 || >=1.3.0 1168 | return Range(Range.OP_LT, target) | Range(Range.OP_GTE, target.next_minor()) 1169 | elif prerel == '': 1170 | # !=1.2.3- 1171 | return Range(Range.OP_NEQ, target, prerelease_policy=Range.PRERELEASE_ALWAYS) 1172 | elif build == '': 1173 | # !=1.2.3+ or !=1.2.3-a2+ 1174 | return Range(Range.OP_NEQ, target, build_policy=Range.BUILD_STRICT) 1175 | else: 1176 | return Range(Range.OP_NEQ, target) 1177 | 1178 | elif prefix == cls.PREFIX_GT: 1179 | assert major is not None 1180 | if minor is None: 1181 | # >1.x => >=2.0 1182 | return Range(Range.OP_GTE, target.next_major()) 1183 | elif patch is None: 1184 | return Range(Range.OP_GTE, target.next_minor()) 1185 | else: 1186 | return Range(Range.OP_GT, target) 1187 | 1188 | elif prefix == cls.PREFIX_GTE: 1189 | return Range(Range.OP_GTE, target) 1190 | 1191 | elif prefix == cls.PREFIX_LT: 1192 | assert major is not None 1193 | if prerel == '': 1194 | # <1.2.3- 1195 | return Range(Range.OP_LT, target, prerelease_policy=Range.PRERELEASE_ALWAYS) 1196 | return Range(Range.OP_LT, target) 1197 | 1198 | else: 1199 | assert prefix == cls.PREFIX_LTE 1200 | assert major is not None 1201 | if minor is None: 1202 | # <=1.x => <2.0 1203 | return Range(Range.OP_LT, target.next_major()) 1204 | elif patch is None: 1205 | return Range(Range.OP_LT, target.next_minor()) 1206 | else: 1207 | return Range(Range.OP_LTE, target) 1208 | 1209 | 1210 | class LegacySpec(SimpleSpec): 1211 | def __init__(self, *expressions): 1212 | warnings.warn( 1213 | "The Spec() class will be removed in 3.1; use SimpleSpec() instead.", 1214 | PendingDeprecationWarning, 1215 | stacklevel=2, 1216 | ) 1217 | 1218 | if len(expressions) > 1: 1219 | warnings.warn( 1220 | "Passing 2+ arguments to SimpleSpec will be removed in 3.0; concatenate them with ',' instead.", 1221 | DeprecationWarning, 1222 | stacklevel=2, 1223 | ) 1224 | expression = ','.join(expressions) 1225 | super(LegacySpec, self).__init__(expression) 1226 | 1227 | @property 1228 | def specs(self): 1229 | return list(self) 1230 | 1231 | def __iter__(self): 1232 | warnings.warn( 1233 | "Iterating over the components of a SimpleSpec object will be removed in 3.0.", 1234 | DeprecationWarning, 1235 | stacklevel=2, 1236 | ) 1237 | try: 1238 | clauses = list(self.clause) 1239 | except TypeError: # Not an iterable 1240 | clauses = [self.clause] 1241 | for clause in clauses: 1242 | yield SpecItem.from_matcher(clause) 1243 | 1244 | 1245 | Spec = LegacySpec 1246 | 1247 | 1248 | @BaseSpec.register_syntax 1249 | class NpmSpec(BaseSpec): 1250 | SYNTAX = 'npm' 1251 | 1252 | @classmethod 1253 | def _parse_to_clause(cls, expression): 1254 | return cls.Parser.parse(expression) 1255 | 1256 | class Parser: 1257 | JOINER = '||' 1258 | HYPHEN = ' - ' 1259 | 1260 | NUMBER = r'x|X|\*|0|[1-9][0-9]*' 1261 | PART = r'[a-zA-Z0-9.-]*' 1262 | NPM_SPEC_BLOCK = re.compile(r""" 1263 | ^(?:v)? # Strip optional initial v 1264 | (?P<|<=|>=|>|=|\^|~|) # Operator, can be empty 1265 | (?P{nb})(?:\.(?P{nb})(?:\.(?P{nb}))?)? 1266 | (?:-(?P{part}))? # Optional re-release 1267 | (?:\+(?P{part}))? # Optional build 1268 | $""".format(nb=NUMBER, part=PART), 1269 | re.VERBOSE, 1270 | ) 1271 | 1272 | @classmethod 1273 | def range(cls, operator, target): 1274 | return Range(operator, target, prerelease_policy=Range.PRERELEASE_SAMEPATCH) 1275 | 1276 | @classmethod 1277 | def parse(cls, expression): 1278 | result = Never() 1279 | groups = expression.split(cls.JOINER) 1280 | for group in groups: 1281 | group = group.strip() 1282 | if not group: 1283 | group = '>=0.0.0' 1284 | 1285 | subclauses = [] 1286 | if cls.HYPHEN in group: 1287 | low, high = group.split(cls.HYPHEN, 2) 1288 | subclauses = cls.parse_simple('>=' + low) + cls.parse_simple('<=' + high) 1289 | 1290 | else: 1291 | blocks = group.split(' ') 1292 | for block in blocks: 1293 | if not cls.NPM_SPEC_BLOCK.match(block): 1294 | raise ValueError("Invalid NPM block in %r: %r" % (expression, block)) 1295 | 1296 | subclauses.extend(cls.parse_simple(block)) 1297 | 1298 | prerelease_clauses = [] 1299 | non_prerel_clauses = [] 1300 | for clause in subclauses: 1301 | if clause.target.prerelease: 1302 | if clause.operator in (Range.OP_GT, Range.OP_GTE): 1303 | prerelease_clauses.append(Range( 1304 | operator=Range.OP_LT, 1305 | target=Version( 1306 | major=clause.target.major, 1307 | minor=clause.target.minor, 1308 | patch=clause.target.patch + 1, 1309 | ), 1310 | prerelease_policy=Range.PRERELEASE_ALWAYS, 1311 | )) 1312 | elif clause.operator in (Range.OP_LT, Range.OP_LTE): 1313 | prerelease_clauses.append(Range( 1314 | operator=Range.OP_GTE, 1315 | target=Version( 1316 | major=clause.target.major, 1317 | minor=clause.target.minor, 1318 | patch=0, 1319 | prerelease=(), 1320 | ), 1321 | prerelease_policy=Range.PRERELEASE_ALWAYS, 1322 | )) 1323 | prerelease_clauses.append(clause) 1324 | non_prerel_clauses.append(cls.range( 1325 | operator=clause.operator, 1326 | target=clause.target.truncate(), 1327 | )) 1328 | else: 1329 | non_prerel_clauses.append(clause) 1330 | if prerelease_clauses: 1331 | result |= AllOf(*prerelease_clauses) 1332 | result |= AllOf(*non_prerel_clauses) 1333 | 1334 | return result 1335 | 1336 | PREFIX_CARET = '^' 1337 | PREFIX_TILDE = '~' 1338 | PREFIX_EQ = '=' 1339 | PREFIX_GT = '>' 1340 | PREFIX_GTE = '>=' 1341 | PREFIX_LT = '<' 1342 | PREFIX_LTE = '<=' 1343 | 1344 | PREFIX_ALIASES = { 1345 | '': PREFIX_EQ, 1346 | } 1347 | 1348 | PREFIX_TO_OPERATOR = { 1349 | PREFIX_EQ: Range.OP_EQ, 1350 | PREFIX_LT: Range.OP_LT, 1351 | PREFIX_LTE: Range.OP_LTE, 1352 | PREFIX_GTE: Range.OP_GTE, 1353 | PREFIX_GT: Range.OP_GT, 1354 | } 1355 | 1356 | EMPTY_VALUES = ['*', 'x', 'X', None] 1357 | 1358 | @classmethod 1359 | def parse_simple(cls, simple): 1360 | match = cls.NPM_SPEC_BLOCK.match(simple) 1361 | 1362 | prefix, major_t, minor_t, patch_t, prerel, build = match.groups() 1363 | 1364 | prefix = cls.PREFIX_ALIASES.get(prefix, prefix) 1365 | major = None if major_t in cls.EMPTY_VALUES else int(major_t) 1366 | minor = None if minor_t in cls.EMPTY_VALUES else int(minor_t) 1367 | patch = None if patch_t in cls.EMPTY_VALUES else int(patch_t) 1368 | 1369 | if build is not None and prefix not in [cls.PREFIX_EQ]: 1370 | # Ignore the 'build' part when not comparing to a specific part. 1371 | build = None 1372 | 1373 | if major is None: # '*', 'x', 'X' 1374 | target = Version(major=0, minor=0, patch=0) 1375 | if prefix not in [cls.PREFIX_EQ, cls.PREFIX_GTE]: 1376 | raise ValueError("Invalid expression %r" % simple) 1377 | prefix = cls.PREFIX_GTE 1378 | elif minor is None: 1379 | target = Version(major=major, minor=0, patch=0) 1380 | elif patch is None: 1381 | target = Version(major=major, minor=minor, patch=0) 1382 | else: 1383 | target = Version( 1384 | major=major, 1385 | minor=minor, 1386 | patch=patch, 1387 | prerelease=prerel.split('.') if prerel else (), 1388 | build=build.split('.') if build else (), 1389 | ) 1390 | 1391 | if (major is None or minor is None or patch is None) and (prerel or build): 1392 | raise ValueError("Invalid NPM spec: %r" % simple) 1393 | 1394 | if prefix == cls.PREFIX_CARET: 1395 | if target.major: # ^1.2.4 => >=1.2.4 <2.0.0 ; ^1.x => >=1.0.0 <2.0.0 1396 | high = target.truncate().next_major() 1397 | elif target.minor: # ^0.1.2 => >=0.1.2 <0.2.0 1398 | high = target.truncate().next_minor() 1399 | elif minor is None: # ^0.x => >=0.0.0 <1.0.0 1400 | high = target.truncate().next_major() 1401 | elif patch is None: # ^0.2.x => >=0.2.0 <0.3.0 1402 | high = target.truncate().next_minor() 1403 | else: # ^0.0.1 => >=0.0.1 <0.0.2 1404 | high = target.truncate().next_patch() 1405 | return [cls.range(Range.OP_GTE, target), cls.range(Range.OP_LT, high)] 1406 | 1407 | elif prefix == cls.PREFIX_TILDE: 1408 | assert major is not None 1409 | if minor is None: # ~1.x => >=1.0.0 <2.0.0 1410 | high = target.next_major() 1411 | else: # ~1.2.x => >=1.2.0 <1.3.0; ~1.2.3 => >=1.2.3 <1.3.0 1412 | high = target.next_minor() 1413 | return [cls.range(Range.OP_GTE, target), cls.range(Range.OP_LT, high)] 1414 | 1415 | elif prefix == cls.PREFIX_EQ: 1416 | if major is None: 1417 | return [cls.range(Range.OP_GTE, target)] 1418 | elif minor is None: 1419 | return [cls.range(Range.OP_GTE, target), cls.range(Range.OP_LT, target.next_major())] 1420 | elif patch is None: 1421 | return [cls.range(Range.OP_GTE, target), cls.range(Range.OP_LT, target.next_minor())] 1422 | else: 1423 | return [cls.range(Range.OP_EQ, target)] 1424 | 1425 | elif prefix == cls.PREFIX_GT: 1426 | assert major is not None 1427 | if minor is None: # >1.x 1428 | return [cls.range(Range.OP_GTE, target.next_major())] 1429 | elif patch is None: # >1.2.x => >=1.3.0 1430 | return [cls.range(Range.OP_GTE, target.next_minor())] 1431 | else: 1432 | return [cls.range(Range.OP_GT, target)] 1433 | 1434 | elif prefix == cls.PREFIX_GTE: 1435 | return [cls.range(Range.OP_GTE, target)] 1436 | 1437 | elif prefix == cls.PREFIX_LT: 1438 | assert major is not None 1439 | return [cls.range(Range.OP_LT, target)] 1440 | 1441 | else: 1442 | assert prefix == cls.PREFIX_LTE 1443 | assert major is not None 1444 | if minor is None: # <=1.x => <2.0.0 1445 | return [cls.range(Range.OP_LT, target.next_major())] 1446 | elif patch is None: # <=1.2.x => <1.3.0 1447 | return [cls.range(Range.OP_LT, target.next_minor())] 1448 | else: 1449 | return [cls.range(Range.OP_LTE, target)] 1450 | -------------------------------------------------------------------------------- /st4_py38/lsp_utils/third_party/update-info.log: -------------------------------------------------------------------------------- 1 | ref: 2.10.0 2 | 272a363824b1e09ae4e494ad00092f8782248821 3 | --------------------------------------------------------------------------------