Skip to content

Commit 608d94c

Browse files
authored
Merge pull request #132 from gnikit/gnikit/issue131
Gnikit/issue131
2 parents c3e8694 + 5d1f363 commit 608d94c

File tree

5 files changed

+119
-55
lines changed

5 files changed

+119
-55
lines changed

.github/workflows/main.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525
run: pip install .[dev]
2626

2727
- name: Unittests
28-
run: pytest -v
28+
run: pytest
2929

3030
- name: Lint
3131
run: black --diff --check --verbose .
@@ -45,7 +45,8 @@ jobs:
4545
- name: Coverage report
4646
run: |
4747
pip install .[dev]
48-
pytest --cov=fortls --cov-report=xml
48+
pytest
49+
shell: bash
4950

5051
- name: Upload coverage to Codecov
5152
uses: codecov/codecov-action@v3

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@
44

55
## 2.6.0
66

7+
### Added
8+
9+
- Added doctests in the pytest test suite
10+
([#131](https://github.com/gnikit/fortls/issues/131))
11+
712
### Changed
813

914
- Redesigned the `fortls` website to be more aesthetically pleasing and user-friendly

fortls/helper_functions.py

Lines changed: 93 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import os
44
from pathlib import Path
55

6-
from fortls.constants import KEYWORD_ID_DICT, KEYWORD_LIST, FRegex, log, sort_keywords
6+
from fortls.constants import KEYWORD_ID_DICT, KEYWORD_LIST, FRegex, sort_keywords
77
from fortls.ftypes import Range
88

99

@@ -52,6 +52,22 @@ def detect_fixed_format(file_lines: list[str]) -> bool:
5252
-------
5353
bool
5454
True if file_lines are of Fixed Fortran style
55+
56+
Examples
57+
--------
58+
59+
>>> detect_fixed_format([' free format'])
60+
False
61+
62+
>>> detect_fixed_format([' INTEGER, PARAMETER :: N = 10'])
63+
False
64+
65+
>>> detect_fixed_format(['C Fixed format'])
66+
True
67+
68+
Lines wih ampersands are not fixed format
69+
>>> detect_fixed_format(['trailing line & ! comment'])
70+
False
5571
"""
5672
for line in file_lines:
5773
if FRegex.FREE_FORMAT_TEST.match(line):
@@ -62,7 +78,7 @@ def detect_fixed_format(file_lines: list[str]) -> bool:
6278
# Trailing ampersand indicates free or intersection format
6379
if not FRegex.FIXED_COMMENT.match(line):
6480
line_end = line.split("!")[0].strip()
65-
if len(line_end) > 0 and line_end[-1] == "&":
81+
if len(line_end) > 0 and line_end.endswith("&"):
6682
return False
6783
return True
6884

@@ -136,21 +152,20 @@ def separate_def_list(test_str: str) -> list[str] | None:
136152
137153
Examples
138154
--------
139-
>>> separate_def_list("var1, var2, var3")
140-
["var1", "var2", "var3"]
141-
155+
>>> separate_def_list('var1, var2, var3')
156+
['var1', 'var2', 'var3']
142157
143-
>>> separate_def_list("var, init_var(3) = [1,2,3], array(3,3)")
144-
["var", "init_var", "array"]
158+
>>> separate_def_list('var, init_var(3) = [1,2,3], array(3,3)')
159+
['var', 'init_var(3) = [1,2,3]', 'array(3,3)']
145160
"""
146161
stripped_str = strip_strings(test_str)
147162
paren_count = 0
148-
def_list = []
163+
def_list: list[str] = []
149164
curr_str = ""
150165
for char in stripped_str:
151-
if (char == "(") or (char == "["):
166+
if char in ("(", "["):
152167
paren_count += 1
153-
elif (char == ")") or (char == "]"):
168+
elif char in (")", "]"):
154169
paren_count -= 1
155170
elif (char == ",") and (paren_count == 0):
156171
curr_str = curr_str.strip()
@@ -208,17 +223,17 @@ def find_paren_match(string: str) -> int:
208223
209224
Examples
210225
--------
211-
>>> find_paren_match("a, b)")
226+
>>> find_paren_match('a, b)')
212227
4
213228
214229
Multiple parenthesis that are closed
215230
216-
>>> find_paren_match("a, (b, c), d)")
231+
>>> find_paren_match('a, (b, c), d)')
217232
12
218233
219234
If the outermost parenthesis is not closed function returns -1
220235
221-
>>> find_paren_match("a, (b, (c, d)")
236+
>>> find_paren_match('a, (b, (c, d)')
222237
-1
223238
"""
224239
paren_count = 1
@@ -233,7 +248,9 @@ def find_paren_match(string: str) -> int:
233248
return ind
234249

235250

236-
def get_line_prefix(pre_lines: list, curr_line: str, col: int, qs: bool = True) -> str:
251+
def get_line_prefix(
252+
pre_lines: list[str], curr_line: str, col: int, qs: bool = True
253+
) -> str:
237254
"""Get code line prefix from current line and preceding continuation lines
238255
239256
Parameters
@@ -252,6 +269,11 @@ def get_line_prefix(pre_lines: list, curr_line: str, col: int, qs: bool = True)
252269
-------
253270
str
254271
part of the line including any relevant line continuations before ``col``
272+
273+
Examples
274+
--------
275+
>>> get_line_prefix([''], '#pragma once', 0) is None
276+
True
255277
"""
256278
if (curr_line is None) or (col > len(curr_line)) or (curr_line.startswith("#")):
257279
return None
@@ -293,47 +315,70 @@ def resolve_globs(glob_path: str, root_path: str = None) -> list[str]:
293315
list[str]
294316
Expanded glob patterns with absolute paths.
295317
Absolute paths are used to resolve any potential ambiguity
318+
319+
Examples
320+
--------
321+
322+
Relative to a root path
323+
>>> import os, pathlib
324+
>>> resolve_globs('test', os.getcwd()) == [str(pathlib.Path(os.getcwd()) / 'test')]
325+
True
326+
327+
Absolute path resolution
328+
>>> resolve_globs('test') == [str(pathlib.Path(os.getcwd()) / 'test')]
329+
True
296330
"""
297331
# Resolve absolute paths i.e. not in our root_path
298332
if os.path.isabs(glob_path) or not root_path:
299333
p = Path(glob_path).resolve()
300-
root = p.root
334+
root = p.anchor # drive letter + root path
301335
rel = str(p.relative_to(root)) # contains glob pattern
302336
return [str(p.resolve()) for p in Path(root).glob(rel)]
303337
else:
304338
return [str(p.resolve()) for p in Path(root_path).resolve().glob(glob_path)]
305339

306340

307-
def only_dirs(paths: list[str], err_msg: list = []) -> list[str]:
341+
def only_dirs(paths: list[str]) -> list[str]:
308342
"""From a list of strings returns only paths that are directories
309343
310344
Parameters
311345
----------
312346
paths : list[str]
313347
A list containing the files and directories
314-
err_msg : list, optional
315-
A list to append error messages if any, else use log channel, by default []
316348
317349
Returns
318350
-------
319351
list[str]
320352
A list containing only valid directories
353+
354+
Raises
355+
------
356+
FileNotFoundError
357+
A list containing all the non existing directories
358+
359+
Examples
360+
--------
361+
362+
>>> only_dirs(['./test/', './test/test_source/', './test/test_source/test.f90'])
363+
['./test/', './test/test_source/']
364+
365+
>>> only_dirs(['/fake/dir/a', '/fake/dir/b', '/fake/dir/c'])
366+
Traceback (most recent call last):
367+
FileNotFoundError: /fake/dir/a
368+
/fake/dir/b
369+
/fake/dir/c
321370
"""
322371
dirs: list[str] = []
372+
errs: list[str] = []
323373
for p in paths:
324374
if os.path.isdir(p):
325375
dirs.append(p)
326376
elif os.path.isfile(p):
327377
continue
328378
else:
329-
msg: str = (
330-
f"Directory '{p}' specified in Configuration settings file does not"
331-
" exist"
332-
)
333-
if err_msg:
334-
err_msg.append([2, msg])
335-
else:
336-
log.warning(msg)
379+
errs.append(p)
380+
if errs:
381+
raise FileNotFoundError("\n".join(errs))
337382
return dirs
338383

339384

@@ -388,12 +433,12 @@ def get_paren_substring(string: str) -> str | None:
388433
389434
Examples
390435
--------
391-
>>> get_paren_substring("some line(a, b, (c, d))")
392-
"a, b, (c, d)"
436+
>>> get_paren_substring('some line(a, b, (c, d))')
437+
'a, b, (c, d)'
393438
394439
If the line has incomplete parenthesis however, ``None`` is returned
395-
>>> get_paren_substring("some line(a, b")
396-
None
440+
>>> get_paren_substring('some line(a, b') is None
441+
True
397442
"""
398443
i1 = string.find("(")
399444
i2 = string.rfind(")")
@@ -419,15 +464,18 @@ def get_paren_level(line: str) -> tuple[str, list[Range]]:
419464
420465
Examples
421466
--------
422-
>>> get_paren_level("CALL sub1(arg1,arg2")
467+
>>> get_paren_level('CALL sub1(arg1,arg2')
423468
('arg1,arg2', [Range(start=10, end=19)])
424469
425470
If the range is interrupted by parenthesis, another Range variable is used
426471
to mark the ``start`` and ``end`` of the argument
427472
428-
>>> get_paren_level("CALL sub1(arg1(i),arg2")
473+
>>> get_paren_level('CALL sub1(arg1(i),arg2')
429474
('arg1,arg2', [Range(start=10, end=14), Range(start=17, end=22)])
430475
476+
>>> get_paren_level('')
477+
('', [Range(start=0, end=0)])
478+
431479
"""
432480
if line == "":
433481
return "", [Range(0, 0)]
@@ -442,18 +490,18 @@ def get_paren_level(line: str) -> tuple[str, list[Range]]:
442490
if char == string_char:
443491
in_string = False
444492
continue
445-
if (char == "(") or (char == "["):
493+
if char in ("(", "["):
446494
level -= 1
447495
if level == 0:
448496
i1 = i
449497
elif level < 0:
450498
sections.append(Range(i + 1, i1))
451499
break
452-
elif (char == ")") or (char == "]"):
500+
elif char in (")", "]"):
453501
level += 1
454502
if level == 1:
455503
sections.append(Range(i + 1, i1))
456-
elif (char == "'") or (char == '"'):
504+
elif char in ("'", '"'):
457505
in_string = True
458506
string_char = char
459507
if level == 0:
@@ -480,16 +528,19 @@ def get_var_stack(line: str) -> list[str]:
480528
481529
Examples
482530
--------
483-
>>> get_var_stack("myvar%foo%bar")
484-
["myvar", "foo", "bar"]
531+
>>> get_var_stack('myvar%foo%bar')
532+
['myvar', 'foo', 'bar']
533+
534+
>>> get_var_stack('myarray(i)%foo%bar')
535+
['myarray', 'foo', 'bar']
485536
486-
>>> get_var_stack("myarray(i)%foo%bar")
487-
["myarray", "foo", "bar"]
537+
In this case it will operate at the end of the string i.e. ``'this%foo'``
488538
489-
In this case it will operate at the end of the string i.e. ``"this%foo"``
539+
>>> get_var_stack('CALL self%method(this%foo')
540+
['this', 'foo']
490541
491-
>>> get_var_stack("CALL self%method(this%foo")
492-
["this", "foo"]
542+
>>> get_var_stack('')
543+
['']
493544
"""
494545
if len(line) == 0:
495546
return [""]

fortls/langserver.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1540,14 +1540,13 @@ def _load_config_file_dirs(self, config_dict: dict) -> None:
15401540
# with glob resolution
15411541
source_dirs = config_dict.get("source_dirs", [])
15421542
for path in source_dirs:
1543-
self.source_dirs.update(
1544-
set(
1545-
only_dirs(
1546-
resolve_globs(path, self.root_path),
1547-
self.post_messages,
1548-
)
1549-
)
1550-
)
1543+
try:
1544+
dirs = only_dirs(resolve_globs(path, self.root_path))
1545+
self.source_dirs.update(set(dirs))
1546+
except FileNotFoundError as e:
1547+
err = f"Directories input in Configuration file do not exit:\n{e}"
1548+
self.post_messages([Severity.warn, err])
1549+
15511550
# Keep all directories present in source_dirs but not excl_paths
15521551
self.source_dirs = {i for i in self.source_dirs if i not in self.excl_paths}
15531552
self.incl_suffixes = config_dict.get("incl_suffixes", [])
@@ -1613,9 +1612,12 @@ def _load_config_file_preproc(self, config_dict: dict) -> None:
16131612
self.pp_defs = {key: "" for key in self.pp_defs}
16141613

16151614
for path in config_dict.get("include_dirs", set()):
1616-
self.include_dirs.update(
1617-
only_dirs(resolve_globs(path, self.root_path), self.post_messages)
1618-
)
1615+
try:
1616+
dirs = only_dirs(resolve_globs(path, self.root_path))
1617+
self.include_dirs.update(set(dirs))
1618+
except FileNotFoundError as e:
1619+
err = f"Directories input in Configuration file do not exit:\n{e}"
1620+
self.post_messages([Severity.warn, err])
16191621

16201622
def _add_source_dirs(self) -> None:
16211623
"""Will recursively add all subdirectories that contain Fortran

pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,8 @@ write_to = "fortls/_version.py"
1212

1313
[tool.isort]
1414
profile = "black"
15+
16+
[tool.pytest.ini_options]
17+
minversion = "7.0"
18+
addopts = "-v --cov=fortls --cov-report=html --cov-report=xml --cov-context=test --doctest-modules"
19+
testpaths = ["fortls", "test"]

0 commit comments

Comments
 (0)