Skip to content

Commit 948fbd1

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 40b2105 commit 948fbd1

File tree

4 files changed

+93
-73
lines changed

4 files changed

+93
-73
lines changed

fortls/debug.py

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

1313

1414
class DebugError(Exception):
@@ -460,14 +460,14 @@ def read_config(root: str | None):
460460
print("\nTesting parser")
461461
separator()
462462

463-
ensure_file_accessible(args.debug_filepath)
464463
pp_suffixes, pp_defs, include_dirs = read_config(args.debug_rootpath)
465-
466-
print(f' File = "{args.debug_filepath}"')
467464
file_obj = FortranFile(args.debug_filepath, pp_suffixes)
468-
err_str, _ = file_obj.load_from_disk()
469-
if err_str:
470-
raise DebugError(f"Reading file failed: {err_str}")
465+
try:
466+
file_obj.load_from_disk()
467+
except ParserError as exc:
468+
msg = f"Reading file {args.debug_filepath} failed: {str(exc)}"
469+
raise DebugError(msg) from exc
470+
print(f' File = "{args.debug_filepath}"')
471471
print(f" Detected format: {'fixed' if file_obj.fixed else 'free'}")
472472
print("\n" + "=" * 80 + "\nParser Output\n" + "=" * 80 + "\n")
473473
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
@@ -871,41 +871,44 @@ def copy(self) -> FortranFile:
871871
copy_obj.set_contents(self.contents_split)
872872
return copy_obj
873873

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

910913
def apply_change(self, change: dict) -> bool:
911914
"""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
@@ -31,3 +30,9 @@ def test_submodule():
3130
except Exception as e:
3231
print(e)
3332
assert False
33+
34+
35+
def test_load_from_disk_exception():
36+
file = FortranFile("/path/to/nonexistent/file.f90")
37+
with pytest.raises(FileReadDecodeError):
38+
file.load_from_disk()

0 commit comments

Comments
 (0)