├── .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 |
--------------------------------------------------------------------------------