Skip to content

Gnikit/issue131 #132

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
run: pip install .[dev]

- name: Unittests
run: pytest -v
run: pytest

- name: Lint
run: black --diff --check --verbose .
Expand All @@ -45,7 +45,8 @@ jobs:
- name: Coverage report
run: |
pip install .[dev]
pytest --cov=fortls --cov-report=xml
pytest
shell: bash

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@

## 2.6.0

### Added

- Added doctests in the pytest test suite
([#131](https://github.com/gnikit/fortls/issues/131))

### Changed

- Redesigned the `fortls` website to be more aesthetically pleasing and user-friendly
Expand Down
135 changes: 93 additions & 42 deletions fortls/helper_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import os
from pathlib import Path

from fortls.constants import KEYWORD_ID_DICT, KEYWORD_LIST, FRegex, log, sort_keywords
from fortls.constants import KEYWORD_ID_DICT, KEYWORD_LIST, FRegex, sort_keywords
from fortls.ftypes import Range


Expand Down Expand Up @@ -52,6 +52,22 @@ def detect_fixed_format(file_lines: list[str]) -> bool:
-------
bool
True if file_lines are of Fixed Fortran style

Examples
--------

>>> detect_fixed_format([' free format'])
False

>>> detect_fixed_format([' INTEGER, PARAMETER :: N = 10'])
False

>>> detect_fixed_format(['C Fixed format'])
True

Lines wih ampersands are not fixed format
>>> detect_fixed_format(['trailing line & ! comment'])
False
"""
for line in file_lines:
if FRegex.FREE_FORMAT_TEST.match(line):
Expand All @@ -62,7 +78,7 @@ def detect_fixed_format(file_lines: list[str]) -> bool:
# Trailing ampersand indicates free or intersection format
if not FRegex.FIXED_COMMENT.match(line):
line_end = line.split("!")[0].strip()
if len(line_end) > 0 and line_end[-1] == "&":
if len(line_end) > 0 and line_end.endswith("&"):
return False
return True

Expand Down Expand Up @@ -136,21 +152,20 @@ def separate_def_list(test_str: str) -> list[str] | None:

Examples
--------
>>> separate_def_list("var1, var2, var3")
["var1", "var2", "var3"]

>>> separate_def_list('var1, var2, var3')
['var1', 'var2', 'var3']

>>> separate_def_list("var, init_var(3) = [1,2,3], array(3,3)")
["var", "init_var", "array"]
>>> separate_def_list('var, init_var(3) = [1,2,3], array(3,3)')
['var', 'init_var(3) = [1,2,3]', 'array(3,3)']
"""
stripped_str = strip_strings(test_str)
paren_count = 0
def_list = []
def_list: list[str] = []
curr_str = ""
for char in stripped_str:
if (char == "(") or (char == "["):
if char in ("(", "["):
paren_count += 1
elif (char == ")") or (char == "]"):
elif char in (")", "]"):
paren_count -= 1
elif (char == ",") and (paren_count == 0):
curr_str = curr_str.strip()
Expand Down Expand Up @@ -208,17 +223,17 @@ def find_paren_match(string: str) -> int:

Examples
--------
>>> find_paren_match("a, b)")
>>> find_paren_match('a, b)')
4

Multiple parenthesis that are closed

>>> find_paren_match("a, (b, c), d)")
>>> find_paren_match('a, (b, c), d)')
12

If the outermost parenthesis is not closed function returns -1

>>> find_paren_match("a, (b, (c, d)")
>>> find_paren_match('a, (b, (c, d)')
-1
"""
paren_count = 1
Expand All @@ -233,7 +248,9 @@ def find_paren_match(string: str) -> int:
return ind


def get_line_prefix(pre_lines: list, curr_line: str, col: int, qs: bool = True) -> str:
def get_line_prefix(
pre_lines: list[str], curr_line: str, col: int, qs: bool = True
) -> str:
"""Get code line prefix from current line and preceding continuation lines

Parameters
Expand All @@ -252,6 +269,11 @@ def get_line_prefix(pre_lines: list, curr_line: str, col: int, qs: bool = True)
-------
str
part of the line including any relevant line continuations before ``col``

Examples
--------
>>> get_line_prefix([''], '#pragma once', 0) is None
True
"""
if (curr_line is None) or (col > len(curr_line)) or (curr_line.startswith("#")):
return None
Expand Down Expand Up @@ -293,47 +315,70 @@ def resolve_globs(glob_path: str, root_path: str = None) -> list[str]:
list[str]
Expanded glob patterns with absolute paths.
Absolute paths are used to resolve any potential ambiguity

Examples
--------

Relative to a root path
>>> import os, pathlib
>>> resolve_globs('test', os.getcwd()) == [str(pathlib.Path(os.getcwd()) / 'test')]
True

Absolute path resolution
>>> resolve_globs('test') == [str(pathlib.Path(os.getcwd()) / 'test')]
True
"""
# Resolve absolute paths i.e. not in our root_path
if os.path.isabs(glob_path) or not root_path:
p = Path(glob_path).resolve()
root = p.root
root = p.anchor # drive letter + root path
rel = str(p.relative_to(root)) # contains glob pattern
return [str(p.resolve()) for p in Path(root).glob(rel)]
else:
return [str(p.resolve()) for p in Path(root_path).resolve().glob(glob_path)]


def only_dirs(paths: list[str], err_msg: list = []) -> list[str]:
def only_dirs(paths: list[str]) -> list[str]:
"""From a list of strings returns only paths that are directories

Parameters
----------
paths : list[str]
A list containing the files and directories
err_msg : list, optional
A list to append error messages if any, else use log channel, by default []

Returns
-------
list[str]
A list containing only valid directories

Raises
------
FileNotFoundError
A list containing all the non existing directories

Examples
--------

>>> only_dirs(['./test/', './test/test_source/', './test/test_source/test.f90'])
['./test/', './test/test_source/']

>>> only_dirs(['/fake/dir/a', '/fake/dir/b', '/fake/dir/c'])
Traceback (most recent call last):
FileNotFoundError: /fake/dir/a
/fake/dir/b
/fake/dir/c
"""
dirs: list[str] = []
errs: list[str] = []
for p in paths:
if os.path.isdir(p):
dirs.append(p)
elif os.path.isfile(p):
continue
else:
msg: str = (
f"Directory '{p}' specified in Configuration settings file does not"
" exist"
)
if err_msg:
err_msg.append([2, msg])
else:
log.warning(msg)
errs.append(p)
if errs:
raise FileNotFoundError("\n".join(errs))
return dirs


Expand Down Expand Up @@ -388,12 +433,12 @@ def get_paren_substring(string: str) -> str | None:

Examples
--------
>>> get_paren_substring("some line(a, b, (c, d))")
"a, b, (c, d)"
>>> get_paren_substring('some line(a, b, (c, d))')
'a, b, (c, d)'

If the line has incomplete parenthesis however, ``None`` is returned
>>> get_paren_substring("some line(a, b")
None
>>> get_paren_substring('some line(a, b') is None
True
"""
i1 = string.find("(")
i2 = string.rfind(")")
Expand All @@ -419,15 +464,18 @@ def get_paren_level(line: str) -> tuple[str, list[Range]]:

Examples
--------
>>> get_paren_level("CALL sub1(arg1,arg2")
>>> get_paren_level('CALL sub1(arg1,arg2')
('arg1,arg2', [Range(start=10, end=19)])

If the range is interrupted by parenthesis, another Range variable is used
to mark the ``start`` and ``end`` of the argument

>>> get_paren_level("CALL sub1(arg1(i),arg2")
>>> get_paren_level('CALL sub1(arg1(i),arg2')
('arg1,arg2', [Range(start=10, end=14), Range(start=17, end=22)])

>>> get_paren_level('')
('', [Range(start=0, end=0)])

"""
if line == "":
return "", [Range(0, 0)]
Expand All @@ -442,18 +490,18 @@ def get_paren_level(line: str) -> tuple[str, list[Range]]:
if char == string_char:
in_string = False
continue
if (char == "(") or (char == "["):
if char in ("(", "["):
level -= 1
if level == 0:
i1 = i
elif level < 0:
sections.append(Range(i + 1, i1))
break
elif (char == ")") or (char == "]"):
elif char in (")", "]"):
level += 1
if level == 1:
sections.append(Range(i + 1, i1))
elif (char == "'") or (char == '"'):
elif char in ("'", '"'):
in_string = True
string_char = char
if level == 0:
Expand All @@ -480,16 +528,19 @@ def get_var_stack(line: str) -> list[str]:

Examples
--------
>>> get_var_stack("myvar%foo%bar")
["myvar", "foo", "bar"]
>>> get_var_stack('myvar%foo%bar')
['myvar', 'foo', 'bar']

>>> get_var_stack('myarray(i)%foo%bar')
['myarray', 'foo', 'bar']

>>> get_var_stack("myarray(i)%foo%bar")
["myarray", "foo", "bar"]
In this case it will operate at the end of the string i.e. ``'this%foo'``

In this case it will operate at the end of the string i.e. ``"this%foo"``
>>> get_var_stack('CALL self%method(this%foo')
['this', 'foo']

>>> get_var_stack("CALL self%method(this%foo")
["this", "foo"]
>>> get_var_stack('')
['']
"""
if len(line) == 0:
return [""]
Expand Down
24 changes: 13 additions & 11 deletions fortls/langserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -1540,14 +1540,13 @@ def _load_config_file_dirs(self, config_dict: dict) -> None:
# with glob resolution
source_dirs = config_dict.get("source_dirs", [])
for path in source_dirs:
self.source_dirs.update(
set(
only_dirs(
resolve_globs(path, self.root_path),
self.post_messages,
)
)
)
try:
dirs = only_dirs(resolve_globs(path, self.root_path))
self.source_dirs.update(set(dirs))
except FileNotFoundError as e:
err = f"Directories input in Configuration file do not exit:\n{e}"
self.post_messages([Severity.warn, err])

# Keep all directories present in source_dirs but not excl_paths
self.source_dirs = {i for i in self.source_dirs if i not in self.excl_paths}
self.incl_suffixes = config_dict.get("incl_suffixes", [])
Expand Down Expand Up @@ -1613,9 +1612,12 @@ def _load_config_file_preproc(self, config_dict: dict) -> None:
self.pp_defs = {key: "" for key in self.pp_defs}

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

def _add_source_dirs(self) -> None:
"""Will recursively add all subdirectories that contain Fortran
Expand Down
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,8 @@ write_to = "fortls/_version.py"

[tool.isort]
profile = "black"

[tool.pytest.ini_options]
minversion = "7.0"
addopts = "-v --cov=fortls --cov-report=html --cov-report=xml --cov-context=test --doctest-modules"
testpaths = ["fortls", "test"]