diff --git a/docs/options.rst b/docs/options.rst index c5327418..51c6bc9b 100644 --- a/docs/options.rst +++ b/docs/options.rst @@ -49,6 +49,7 @@ All the ``fortls`` settings with their default arguments can be found below "incremental_sync": false, "sort_keywords": false, "disable_autoupdate": false, + "allow_conda_autoupdate": false, "debug_log": false, "source_dirs": ["./**"], diff --git a/fortls/fortls.schema.json b/fortls/fortls.schema.json index f6a8c09a..e776b402 100644 --- a/fortls/fortls.schema.json +++ b/fortls/fortls.schema.json @@ -35,7 +35,13 @@ }, "disable_autoupdate": { "title": "Disable Autoupdate", - "description": "fortls automatically checks PyPi for newer version and installs them.Use this option to disable the autoupdate feature.", + "description": "fortls automatically checks PyPi for newer version and installs them. Use this option to disable the autoupdate feature.", + "default": false, + "type": "boolean" + }, + "allow_conda_autoupdate": { + "title": "Allow Conda Autoupdate", + "description": "By default, fortls does not permit autoupdating in conda environments. Use this option to enable fortls to automatically check conda-forge for newer version and install them.", "default": false, "type": "boolean" }, diff --git a/fortls/interface.py b/fortls/interface.py index 05cc8c18..dc874fad 100644 --- a/fortls/interface.py +++ b/fortls/interface.py @@ -88,7 +88,16 @@ def cli(name: str = "fortls") -> argparse.ArgumentParser: action="store_true", help=( "fortls automatically checks PyPi for newer version and installs them." - "Use this option to disable the autoupdate feature." + " Use this option to disable the autoupdate feature." + ), + ) + parser.add_argument( + "--allow_conda_autoupdate", + action="store_true", + help=( + "By default, fortls does not permit autoupdating in conda environments." + " Use this option to enable fortls to automatically check conda-forge" + " for newer version and install them." ), ) # XXX: Deprecated, argument not attached to anything. Remove diff --git a/fortls/langserver.py b/fortls/langserver.py index 422061d9..36a20cfd 100644 --- a/fortls/langserver.py +++ b/fortls/langserver.py @@ -202,7 +202,7 @@ def serve_initialize(self, request: dict): self._config_logger(request) self._load_intrinsics() self._add_source_dirs() - if self._update_version_pypi(): + if self._update_version(): self.post_message( "Please restart the server for the new version to activate", Severity.info, @@ -1590,6 +1590,9 @@ def _load_config_file_general(self, config_dict: dict) -> None: self.disable_autoupdate = config_dict.get( "disable_autoupdate", self.disable_autoupdate ) + self.allow_conda_autoupdate = config_dict.get( + "allow_conda_autoupdate", self.allow_conda_autoupdate + ) # Autocomplete options ------------------------------------------------- self.autocomplete_no_prefix = config_dict.get( @@ -1760,8 +1763,8 @@ def _create_ref_link(self, obj) -> dict: schar = echar = 0 return uri_json(path_to_uri(obj_file.path), sline, schar, sline, echar) - def _update_version_pypi(self, test: bool = False): - """Fetch updates from PyPi for fortls + def _update_version(self, test: bool = False): + """Fetch updates from PyPi or conda-forge for fortls Parameters ---------- @@ -1778,7 +1781,8 @@ def _update_version_pypi(self, test: bool = False): request = urllib.request.Request("https://pypi.org/pypi/fortls/json") with urllib.request.urlopen(request) as resp: info = json.loads(resp.read().decode("utf-8")) - remote_v = version.parse(info["info"]["version"]) + remote_v_str = info["info"]["version"] + remote_v = version.parse(remote_v_str) # Do not update from remote if it is a prerelease if remote_v.is_prerelease: return False @@ -1788,36 +1792,81 @@ def _update_version_pypi(self, test: bool = False): "A newer version of fortls is available for download", Severity.info, ) - # Anaconda environments should handle their updates through conda if os.path.exists(os.path.join(sys.prefix, "conda-meta")): - return False - self.post_message( - f"Downloading from PyPi fortls {info['info']['version']}", - Severity.info, - ) - # Run pip - result = subprocess.run( - [ - sys.executable, - "-m", - "pip", - "install", - "fortls", - "--upgrade", - "--user", - ], - capture_output=True, - ) - if result.stdout: - log.info(result.stdout.decode("utf-8")) - if result.stderr: - log.error(result.stderr.decode("utf-8")) - return True + return self._update_version_conda(remote_v_str) + return self._update_version_pypi(remote_v_str) # No internet connection exceptions except (URLError, KeyError): self.post_message("Failed to update the fortls", Severity.warn) return False + def _update_version_pypi(self, version: str): + """Fetch updates from PyPi for fortls + + Parameters + ---------- + version : str + version to install, only used for update notification + """ + self.post_message( + f"Downloading from PyPi fortls {version}", + Severity.info, + ) + # Run pip + result = subprocess.run( + [ + sys.executable, + "-m", + "pip", + "install", + "fortls", + "--upgrade", + "--user", + ], + capture_output=True, + ) + if result.stdout: + log.info(result.stdout.decode("utf-8")) + if result.stderr: + log.error(result.stderr.decode("utf-8")) + return result.returncode == 0 + + def _update_version_conda(self, version: str): + """Fetch updates from conda-forge for fortls + + Parameters + ---------- + version : str + version to install, only used for update notification + """ + + if not self.allow_conda_autoupdate: + return False + + self.post_message( + f"Downloading from conda-forge fortls {version}", + Severity.info, + ) + # Run conda + result = subprocess.run( + [ + "conda", + "update", + "-c", + "conda-forge", + "--yes", + "--quiet", + "fortls", + # "conda-forge::fortls", + ], + capture_output=True, + ) + if result.stdout: + log.info(result.stdout.decode("utf-8")) + if result.stderr: + log.error(result.stderr.decode("utf-8")) + return result.returncode == 0 + class JSONRPC2Error(Exception): def __init__(self, code, message, data=None): diff --git a/test/test_interface.py b/test/test_interface.py index 0e550e2d..c2422e7c 100644 --- a/test/test_interface.py +++ b/test/test_interface.py @@ -12,7 +12,7 @@ def test_command_line_general_options(): args = parser.parse_args( "-c config_file.json -n 2 --notify_init --incremental_sync --sort_keywords" - " --disable_autoupdate --debug_log".split() + " --disable_autoupdate --allow_conda_autoupdate --debug_log".split() ) assert args.config == "config_file.json" assert args.nthreads == 2 @@ -20,6 +20,7 @@ def test_command_line_general_options(): assert args.incremental_sync assert args.sort_keywords assert args.disable_autoupdate + assert args.allow_conda_autoupdate assert args.debug_log @@ -83,14 +84,14 @@ def test_command_line_code_actions_options(): assert args.enable_code_actions -def unittest_server_init(): +def unittest_server_init(conn=None): from fortls.langserver import LangServer root = (Path(__file__).parent / "test_source").resolve() parser = cli("fortls") args = parser.parse_args("-c f90_config.json".split()) - server = LangServer(None, vars(args)) + server = LangServer(conn, vars(args)) server.root_path = root server._load_config_file() @@ -104,6 +105,7 @@ def test_config_file_general_options(): assert server.incremental_sync assert server.sort_keywords assert server.disable_autoupdate + assert server.allow_conda_autoupdate def test_config_file_dir_parsing_options(): @@ -164,28 +166,23 @@ def test_config_file_codeactions_options(): assert server.enable_code_actions -def test_version_update_pypi(): +def test_version_update(): from packaging import version from fortls.jsonrpc import JSONRPC2Connection, ReadWriter - from fortls.langserver import LangServer - - parser = cli("fortls") - args = parser.parse_args("-c f90_config.json".split()) - args = vars(args) - args["disable_autoupdate"] = False stdin, stdout = sys.stdin.buffer, sys.stdout.buffer - s = LangServer(conn=JSONRPC2Connection(ReadWriter(stdin, stdout)), settings=args) - s.root_path = (Path(__file__).parent / "test_source").resolve() - did_update = s._update_version_pypi(test=True) + s, root = unittest_server_init(JSONRPC2Connection(ReadWriter(stdin, stdout))) + s.disable_autoupdate = False + + did_update = s._update_version(test=True) assert did_update s.disable_autoupdate = True - did_update = s._update_version_pypi() + did_update = s._update_version() assert not did_update s.disable_autoupdate = False s._version = version.parse("999.0.0") - did_update = s._update_version_pypi() + did_update = s._update_version() assert not did_update diff --git a/test/test_source/f90_config.json b/test/test_source/f90_config.json index 2d4779b7..6a41d91a 100644 --- a/test/test_source/f90_config.json +++ b/test/test_source/f90_config.json @@ -4,6 +4,7 @@ "incremental_sync": true, "sort_keywords": true, "disable_autoupdate": true, + "allow_conda_autoupdate": true, "source_dirs": ["subdir", "pp/**"], "incl_suffixes": [".FF", ".fpc", ".h", "f20"],