Skip to content

Commit e865100

Browse files
committed
refactor: adds custom exceptions
This is a first attempt at the problem. Slowly I will have to remove the levels of nested handling of certain errors and allow them to bubble up.
1 parent a5fc5ed commit e865100

File tree

4 files changed

+94
-71
lines changed

4 files changed

+94
-71
lines changed

fortls/debug.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from .helper_functions import only_dirs, resolve_globs
1111
from .jsonrpc import JSONRPC2Connection, ReadWriter, path_from_uri
1212
from .langserver import LangServer
13-
from .parsers.internal.parser import FortranFile, preprocess_file
13+
from .parsers.internal.parser import FortranFile, ParserError, preprocess_file
1414

1515

1616
class DebugError(Exception):
@@ -421,13 +421,16 @@ def debug_parser(args):
421421
separator()
422422

423423
ensure_file_accessible(args.debug_filepath)
424-
pp_suffixes, pp_defs, include_dirs = read_config(args.debug_rootpath, args.config)
424+
pp_suffixes, pp_defs, include_dirs = read_config(args.debug_rootpath)
425425

426426
print(f' File = "{args.debug_filepath}"')
427427
file_obj = FortranFile(args.debug_filepath, pp_suffixes)
428-
err_str, _ = file_obj.load_from_disk()
429-
if err_str:
430-
raise DebugError(f"Reading file failed: {err_str}")
428+
try:
429+
file_obj.load_from_disk()
430+
except ParserError as exc:
431+
msg = f"Reading file {args.debug_filepath} failed: {str(exc)}"
432+
raise DebugError(msg) from exc
433+
print(f' File = "{args.debug_filepath}"')
431434
print(f" Detected format: {'fixed' if file_obj.fixed else 'free'}")
432435
print("\n" + "=" * 80 + "\nParser Output\n" + "=" * 80 + "\n")
433436
file_ast = file_obj.parse(debug=True, pp_defs=pp_defs, include_dirs=include_dirs)

fortls/langserver.py

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
load_intrinsics,
5252
set_lowercase_intrinsics,
5353
)
54-
from fortls.parsers.internal.parser import FortranFile, get_line_context
54+
from fortls.parsers.internal.parser import FortranFile, ParserError, get_line_context
5555
from fortls.parsers.internal.scope import Scope
5656
from fortls.parsers.internal.use import Use
5757
from fortls.parsers.internal.utilities import (
@@ -1313,9 +1313,10 @@ def serve_onChange(self, request: dict):
13131313
return
13141314
# Parse newly updated file
13151315
if reparse_req:
1316-
_, err_str = self.update_workspace_file(path, update_links=True)
1317-
if err_str is not None:
1318-
self.post_message(f"Change request failed for file '{path}': {err_str}")
1316+
try:
1317+
self.update_workspace_file(path, update_links=True)
1318+
except LSPError as e:
1319+
self.post_message(f"Change request failed for file '{path}': {str(e)}")
13191320
return
13201321
# Update include statements linking to this file
13211322
for _, tmp_file in self.workspace.items():
@@ -1350,11 +1351,12 @@ def serve_onSave(
13501351
for key in ast_old.global_dict:
13511352
self.obj_tree.pop(key, None)
13521353
return
1353-
did_change, err_str = self.update_workspace_file(
1354-
filepath, read_file=True, allow_empty=did_open
1355-
)
1356-
if err_str is not None:
1357-
self.post_message(f"Save request failed for file '{filepath}': {err_str}")
1354+
try:
1355+
did_change = self.update_workspace_file(
1356+
filepath, read_file=True, allow_empty=did_open
1357+
)
1358+
except LSPError as e:
1359+
self.post_message(f"Save request failed for file '{filepath}': {str(e)}")
13581360
return
13591361
if did_change:
13601362
# Update include statements linking to this file
@@ -1390,12 +1392,14 @@ def update_workspace_file(
13901392
return False, None
13911393
else:
13921394
return False, "File does not exist" # Error during load
1393-
err_string, file_changed = file_obj.load_from_disk()
1394-
if err_string:
1395-
log.error("%s : %s", err_string, filepath)
1396-
return False, err_string # Error during file read
1397-
if not file_changed:
1398-
return False, None
1395+
try:
1396+
file_changed = file_obj.load_from_disk()
1397+
if not file_changed:
1398+
return False, None
1399+
except ParserError as exc:
1400+
log.error("%s : %s", str(exc), filepath)
1401+
raise LSPError from exc
1402+
13991403
ast_new = file_obj.parse(
14001404
pp_defs=self.pp_defs, include_dirs=self.include_dirs
14011405
)
@@ -1452,9 +1456,11 @@ def file_init(
14521456
A Fortran file object or a string containing the error message
14531457
"""
14541458
file_obj = FortranFile(filepath, pp_suffixes)
1455-
err_str, _ = file_obj.load_from_disk()
1456-
if err_str:
1457-
return err_str
1459+
# TODO: allow to bubble up the error message
1460+
try:
1461+
file_obj.load_from_disk()
1462+
except ParserError as e:
1463+
return str(e)
14581464
try:
14591465
# On Windows multiprocess does not propagate global variables in a shell.
14601466
# Windows uses 'spawn' while Unix uses 'fork' which propagates globals.
@@ -1844,6 +1850,10 @@ def update_recursion_limit(limit: int) -> None:
18441850
sys.setrecursionlimit(limit)
18451851

18461852

1853+
class LSPError(Exception):
1854+
"""Base class for Language Server Protocol errors"""
1855+
1856+
18471857
class JSONRPC2Error(Exception):
18481858
def __init__(self, code, message, data=None):
18491859
self.code = code

fortls/parsers/internal/parser.py

Lines changed: 48 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -870,41 +870,44 @@ def copy(self) -> FortranFile:
870870
copy_obj.set_contents(self.contents_split)
871871
return copy_obj
872872

873-
def load_from_disk(self) -> tuple[str | None, bool | None]:
873+
def load_from_disk(self) -> bool:
874874
"""Read file from disk or update file contents only if they have changed
875875
A MD5 hash is used to determine that
876876
877877
Returns
878878
-------
879-
tuple[str|None, bool|None]
880-
``str`` : string containing IO error message else None
881-
``bool``: boolean indicating if the file has changed
879+
bool
880+
boolean indicating if the file has changed
881+
882+
Raises
883+
------
884+
FileReadDecodeError
885+
If the file could not be read or decoded
882886
"""
883887
contents: str
884888
try:
885889
with open(self.path, encoding="utf-8", errors="replace") as f:
886890
contents = re.sub(r"\t", r" ", f.read())
887-
except OSError:
888-
return "Could not read/decode file", None
889-
else:
890-
# Check if files are the same
891-
try:
892-
hash = hashlib.md5(
893-
contents.encode("utf-8"), usedforsecurity=False
894-
).hexdigest()
895-
# Python <=3.8 does not have the `usedforsecurity` option
896-
except TypeError:
897-
hash = hashlib.md5(contents.encode("utf-8")).hexdigest()
898-
899-
if hash == self.hash:
900-
return None, False
901-
902-
self.hash = hash
903-
self.contents_split = contents.splitlines()
904-
self.fixed = detect_fixed_format(self.contents_split)
905-
self.contents_pp = self.contents_split
906-
self.nLines = len(self.contents_split)
907-
return None, True
891+
except OSError as exc:
892+
raise FileReadDecodeError("Could not read/decode file") from exc
893+
# Check if files are the same
894+
try:
895+
hash = hashlib.md5(
896+
contents.encode("utf-8"), usedforsecurity=False
897+
).hexdigest()
898+
# Python <=3.8 does not have the `usedforsecurity` option
899+
except TypeError:
900+
hash = hashlib.md5(contents.encode("utf-8")).hexdigest()
901+
902+
if hash == self.hash:
903+
return False
904+
905+
self.hash = hash
906+
self.contents_split = contents.splitlines()
907+
self.fixed = detect_fixed_format(self.contents_split)
908+
self.contents_pp = self.contents_split
909+
self.nLines = len(self.contents_split)
910+
return True
908911

909912
def apply_change(self, change: dict) -> bool:
910913
"""Apply a change to the file."""
@@ -2261,24 +2264,18 @@ def append_multiline_macro(def_value: str | tuple, line: str):
22612264
if include_path is not None:
22622265
try:
22632266
include_file = FortranFile(include_path)
2264-
err_string, _ = include_file.load_from_disk()
2265-
if err_string is None:
2266-
log.debug("\n!!! Parsing include file '%s'", include_path)
2267-
_, _, _, defs_tmp = preprocess_file(
2268-
include_file.contents_split,
2269-
file_path=include_path,
2270-
pp_defs=defs_tmp,
2271-
include_dirs=include_dirs,
2272-
debug=debug,
2273-
)
2274-
log.debug("!!! Completed parsing include file\n")
2275-
2276-
else:
2277-
log.debug("!!! Failed to parse include file: %s", err_string)
2278-
2279-
except:
2280-
log.debug("!!! Failed to parse include file: exception")
2281-
2267+
include_file.load_from_disk()
2268+
log.debug("\n!!! Parsing include file '%s'", include_path)
2269+
_, _, _, defs_tmp = preprocess_file(
2270+
include_file.contents_split,
2271+
file_path=include_path,
2272+
pp_defs=defs_tmp,
2273+
include_dirs=include_dirs,
2274+
debug=debug,
2275+
)
2276+
log.debug("!!! Completed parsing include file")
2277+
except ParserError as e:
2278+
log.debug("!!! Failed to parse include file: %s", str(e))
22822279
else:
22832280
log.debug(
22842281
"%s !!! Could not locate include file (%d)", line.strip(), i + 1
@@ -2313,3 +2310,11 @@ def append_multiline_macro(def_value: str | tuple, line: str):
23132310
line = line_new
23142311
output_file.append(line)
23152312
return output_file, pp_skips, pp_defines, defs_tmp
2313+
2314+
2315+
class ParserError(Exception):
2316+
"""Parser base class exception"""
2317+
2318+
2319+
class FileReadDecodeError(ParserError):
2320+
"""File could not be read/decoded"""

test/test_parser.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1+
import pytest
12
from setup_tests import test_dir
23

3-
from fortls.parsers.internal.parser import FortranFile
4+
from fortls.parsers.internal.parser import FileReadDecodeError, FortranFile
45

56

67
def test_line_continuations():
78
file_path = test_dir / "parse" / "line_continuations.f90"
89
file = FortranFile(str(file_path))
9-
err_str, _ = file.load_from_disk()
10-
assert err_str is None
10+
file.load_from_disk()
1111
try:
1212
file.parse()
1313
assert True
@@ -19,8 +19,7 @@ def test_line_continuations():
1919
def test_submodule():
2020
file_path = test_dir / "parse" / "submodule.f90"
2121
file = FortranFile(str(file_path))
22-
err_str, _ = file.load_from_disk()
23-
assert err_str is None
22+
file.load_from_disk()
2423
try:
2524
ast = file.parse()
2625
assert True
@@ -48,3 +47,9 @@ def test_end_scopes_semicolon():
4847
ast = file.parse()
4948
assert err_str is None
5049
assert not ast.end_errors
50+
51+
52+
def test_load_from_disk_exception():
53+
file = FortranFile("/path/to/nonexistent/file.f90")
54+
with pytest.raises(FileReadDecodeError):
55+
file.load_from_disk()

0 commit comments

Comments
 (0)