Skip to content

Commit cf4eaad

Browse files
Add support for flake8 per-file-ignores (#28)
* Add support for flake8 per-file-ignores * Make config parsing helper functions class methods Co-authored-by: Brandon T. Willard <[email protected]> Co-authored-by: Edgar Andrés Margffoy Tuay <[email protected]>
1 parent 8ffb159 commit cf4eaad

File tree

4 files changed

+130
-38
lines changed

4 files changed

+130
-38
lines changed

pylsp/config/flake8_conf.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
('ignore', 'plugins.flake8.ignore', list),
2929
('max-line-length', 'plugins.flake8.maxLineLength', int),
3030
('select', 'plugins.flake8.select', list),
31+
('per-file-ignores', 'plugins.flake8.perFileIgnores', list),
3132
]
3233

3334

@@ -48,3 +49,9 @@ def project_config(self, document_path):
4849
files = find_parents(self.root_path, document_path, PROJECT_CONFIGS)
4950
config = self.read_config_from_files(files)
5051
return self.parse_config(config, CONFIG_KEY, OPTIONS)
52+
53+
@classmethod
54+
def _parse_list_opt(cls, string):
55+
if string.startswith("\n"):
56+
return [s.strip().rstrip(",") for s in string.split("\n") if s.strip()]
57+
return [s.strip() for s in string.split(",") if s.strip()]

pylsp/config/source.py

Lines changed: 36 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -27,62 +27,62 @@ def project_config(self, document_path):
2727
"""Return project-level (i.e. workspace directory) configuration."""
2828
raise NotImplementedError()
2929

30-
@staticmethod
31-
def read_config_from_files(files):
30+
@classmethod
31+
def read_config_from_files(cls, files):
3232
config = configparser.RawConfigParser()
3333
for filename in files:
3434
if os.path.exists(filename) and not os.path.isdir(filename):
3535
config.read(filename)
3636

3737
return config
3838

39-
@staticmethod
40-
def parse_config(config, key, options):
39+
@classmethod
40+
def parse_config(cls, config, key, options):
4141
"""Parse the config with the given options."""
4242
conf = {}
4343
for source, destination, opt_type in options:
44-
opt_value = _get_opt(config, key, source, opt_type)
44+
opt_value = cls._get_opt(config, key, source, opt_type)
4545
if opt_value is not None:
46-
_set_opt(conf, destination, opt_value)
46+
cls._set_opt(conf, destination, opt_value)
4747
return conf
4848

49+
@classmethod
50+
def _get_opt(cls, config, key, option, opt_type):
51+
"""Get an option from a configparser with the given type."""
52+
for opt_key in [option, option.replace('-', '_')]:
53+
if not config.has_option(key, opt_key):
54+
continue
4955

50-
def _get_opt(config, key, option, opt_type):
51-
"""Get an option from a configparser with the given type."""
52-
for opt_key in [option, option.replace('-', '_')]:
53-
if not config.has_option(key, opt_key):
54-
continue
56+
if opt_type == bool:
57+
return config.getboolean(key, opt_key)
5558

56-
if opt_type == bool:
57-
return config.getboolean(key, opt_key)
59+
if opt_type == int:
60+
return config.getint(key, opt_key)
5861

59-
if opt_type == int:
60-
return config.getint(key, opt_key)
62+
if opt_type == str:
63+
return config.get(key, opt_key)
6164

62-
if opt_type == str:
63-
return config.get(key, opt_key)
65+
if opt_type == list:
66+
return cls._parse_list_opt(config.get(key, opt_key))
6467

65-
if opt_type == list:
66-
return _parse_list_opt(config.get(key, opt_key))
68+
raise ValueError("Unknown option type: %s" % opt_type)
6769

68-
raise ValueError("Unknown option type: %s" % opt_type)
70+
@classmethod
71+
def _parse_list_opt(cls, string):
72+
return [s.strip() for s in string.split(",") if s.strip()]
6973

74+
@classmethod
75+
def _set_opt(cls, config_dict, path, value):
76+
"""Set the value in the dictionary at the given path if the value is not None."""
77+
if value is None:
78+
return
7079

71-
def _parse_list_opt(string):
72-
return [s.strip() for s in string.split(",") if s.strip()]
80+
if '.' not in path:
81+
config_dict[path] = value
82+
return
7383

84+
key, rest = path.split(".", 1)
85+
if key not in config_dict:
86+
config_dict[key] = {}
7487

75-
def _set_opt(config_dict, path, value):
76-
"""Set the value in the dictionary at the given path if the value is not None."""
77-
if value is None:
78-
return
79-
80-
if '.' not in path:
81-
config_dict[path] = value
82-
return
83-
84-
key, rest = path.split(".", 1)
85-
if key not in config_dict:
86-
config_dict[key] = {}
87-
88-
_set_opt(config_dict[key], rest, value)
88+
cls._set_opt(config_dict[key], rest, value)

pylsp/plugins/flake8_lint.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
import logging
66
import os.path
77
import re
8-
from subprocess import Popen, PIPE
8+
from pathlib import PurePath
9+
from subprocess import PIPE, Popen
10+
911
from pylsp import hookimpl, lsp
1012

1113
log = logging.getLogger(__name__)
@@ -24,12 +26,21 @@ def pylsp_lint(workspace, document):
2426
settings = config.plugin_settings('flake8', document_path=document.path)
2527
log.debug("Got flake8 settings: %s", settings)
2628

29+
ignores = settings.get("ignore", [])
30+
per_file_ignores = settings.get("perFileIgnores")
31+
32+
if per_file_ignores:
33+
for path in per_file_ignores:
34+
file_pat, errors = path.split(":")
35+
if PurePath(document.path).match(file_pat):
36+
ignores.extend(errors.split(","))
37+
2738
opts = {
2839
'config': settings.get('config'),
2940
'exclude': settings.get('exclude'),
3041
'filename': settings.get('filename'),
3142
'hang-closing': settings.get('hangClosing'),
32-
'ignore': settings.get('ignore'),
43+
'ignore': ignores or None,
3344
'max-line-length': settings.get('maxLineLength'),
3445
'select': settings.get('select'),
3546
}

test/plugins/test_flake8_lint.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,77 @@ def test_flake8_executable_param(workspace):
8585

8686
(call_args,) = popen_mock.call_args[0]
8787
assert flake8_executable in call_args
88+
89+
90+
def get_flake8_cfg_settings(workspace, config_str):
91+
"""Write a ``setup.cfg``, load it in the workspace, and return the flake8 settings.
92+
93+
This function creates a ``setup.cfg``; you'll have to delete it yourself.
94+
"""
95+
96+
with open(os.path.join(workspace.root_path, "setup.cfg"), "w+") as f:
97+
f.write(config_str)
98+
99+
workspace.update_config({"pylsp": {"configurationSources": ["flake8"]}})
100+
101+
return workspace._config.plugin_settings("flake8")
102+
103+
104+
def test_flake8_multiline(workspace):
105+
config_str = r"""[flake8]
106+
exclude =
107+
blah/,
108+
file_2.py
109+
"""
110+
111+
doc_str = "print('hi')\nimport os\n"
112+
113+
doc_uri = uris.from_fs_path(os.path.join(workspace.root_path, "blah/__init__.py"))
114+
workspace.put_document(doc_uri, doc_str)
115+
116+
flake8_settings = get_flake8_cfg_settings(workspace, config_str)
117+
118+
assert "exclude" in flake8_settings
119+
assert len(flake8_settings["exclude"]) == 2
120+
121+
with patch('pylsp.plugins.flake8_lint.Popen') as popen_mock:
122+
mock_instance = popen_mock.return_value
123+
mock_instance.communicate.return_value = [bytes(), bytes()]
124+
125+
doc = workspace.get_document(doc_uri)
126+
flake8_lint.pylsp_lint(workspace, doc)
127+
128+
call_args = popen_mock.call_args[0][0]
129+
assert call_args == ["flake8", "-", "--exclude=blah/,file_2.py"]
130+
131+
os.unlink(os.path.join(workspace.root_path, "setup.cfg"))
132+
133+
134+
def test_flake8_per_file_ignores(workspace):
135+
config_str = r"""[flake8]
136+
ignores = F403
137+
per-file-ignores =
138+
**/__init__.py:F401,E402
139+
test_something.py:E402,
140+
exclude =
141+
file_1.py
142+
file_2.py
143+
"""
144+
145+
doc_str = "print('hi')\nimport os\n"
146+
147+
doc_uri = uris.from_fs_path(os.path.join(workspace.root_path, "blah/__init__.py"))
148+
workspace.put_document(doc_uri, doc_str)
149+
150+
flake8_settings = get_flake8_cfg_settings(workspace, config_str)
151+
152+
assert "perFileIgnores" in flake8_settings
153+
assert len(flake8_settings["perFileIgnores"]) == 2
154+
assert "exclude" in flake8_settings
155+
assert len(flake8_settings["exclude"]) == 2
156+
157+
doc = workspace.get_document(doc_uri)
158+
res = flake8_lint.pylsp_lint(workspace, doc)
159+
assert not res
160+
161+
os.unlink(os.path.join(workspace.root_path, "setup.cfg"))

0 commit comments

Comments
 (0)