Skip to content

Add support for flake8 per-file-ignores #28

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 3 commits into from
May 20, 2021
Merged
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
7 changes: 7 additions & 0 deletions pylsp/config/flake8_conf.py
Original file line number Diff line number Diff line change
@@ -28,6 +28,7 @@
('ignore', 'plugins.flake8.ignore', list),
('max-line-length', 'plugins.flake8.maxLineLength', int),
('select', 'plugins.flake8.select', list),
('per-file-ignores', 'plugins.flake8.perFileIgnores', list),
]


@@ -48,3 +49,9 @@ def project_config(self, document_path):
files = find_parents(self.root_path, document_path, PROJECT_CONFIGS)
config = self.read_config_from_files(files)
return self.parse_config(config, CONFIG_KEY, OPTIONS)

@classmethod
def _parse_list_opt(cls, string):
if string.startswith("\n"):
return [s.strip().rstrip(",") for s in string.split("\n") if s.strip()]
return [s.strip() for s in string.split(",") if s.strip()]
72 changes: 36 additions & 36 deletions pylsp/config/source.py
Original file line number Diff line number Diff line change
@@ -27,62 +27,62 @@ def project_config(self, document_path):
"""Return project-level (i.e. workspace directory) configuration."""
raise NotImplementedError()

@staticmethod
def read_config_from_files(files):
@classmethod
def read_config_from_files(cls, files):
config = configparser.RawConfigParser()
for filename in files:
if os.path.exists(filename) and not os.path.isdir(filename):
config.read(filename)

return config

@staticmethod
def parse_config(config, key, options):
@classmethod
def parse_config(cls, config, key, options):
"""Parse the config with the given options."""
conf = {}
for source, destination, opt_type in options:
opt_value = _get_opt(config, key, source, opt_type)
opt_value = cls._get_opt(config, key, source, opt_type)
if opt_value is not None:
_set_opt(conf, destination, opt_value)
cls._set_opt(conf, destination, opt_value)
return conf

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

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

if opt_type == bool:
return config.getboolean(key, opt_key)
if opt_type == int:
return config.getint(key, opt_key)

if opt_type == int:
return config.getint(key, opt_key)
if opt_type == str:
return config.get(key, opt_key)

if opt_type == str:
return config.get(key, opt_key)
if opt_type == list:
return cls._parse_list_opt(config.get(key, opt_key))

if opt_type == list:
return _parse_list_opt(config.get(key, opt_key))
raise ValueError("Unknown option type: %s" % opt_type)

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

@classmethod
def _set_opt(cls, config_dict, path, value):
"""Set the value in the dictionary at the given path if the value is not None."""
if value is None:
return

def _parse_list_opt(string):
return [s.strip() for s in string.split(",") if s.strip()]
if '.' not in path:
config_dict[path] = value
return

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

def _set_opt(config_dict, path, value):
"""Set the value in the dictionary at the given path if the value is not None."""
if value is None:
return

if '.' not in path:
config_dict[path] = value
return

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

_set_opt(config_dict[key], rest, value)
cls._set_opt(config_dict[key], rest, value)
15 changes: 13 additions & 2 deletions pylsp/plugins/flake8_lint.py
Original file line number Diff line number Diff line change
@@ -5,7 +5,9 @@
import logging
import os.path
import re
from subprocess import Popen, PIPE
from pathlib import PurePath
from subprocess import PIPE, Popen

from pylsp import hookimpl, lsp

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

ignores = settings.get("ignore", [])
per_file_ignores = settings.get("perFileIgnores")

if per_file_ignores:
for path in per_file_ignores:
file_pat, errors = path.split(":")
if PurePath(document.path).match(file_pat):
ignores.extend(errors.split(","))

opts = {
'config': settings.get('config'),
'exclude': settings.get('exclude'),
'filename': settings.get('filename'),
'hang-closing': settings.get('hangClosing'),
'ignore': settings.get('ignore'),
'ignore': ignores or None,
'max-line-length': settings.get('maxLineLength'),
'select': settings.get('select'),
}
74 changes: 74 additions & 0 deletions test/plugins/test_flake8_lint.py
Original file line number Diff line number Diff line change
@@ -85,3 +85,77 @@ def test_flake8_executable_param(workspace):

(call_args,) = popen_mock.call_args[0]
assert flake8_executable in call_args


def get_flake8_cfg_settings(workspace, config_str):
"""Write a ``setup.cfg``, load it in the workspace, and return the flake8 settings.

This function creates a ``setup.cfg``; you'll have to delete it yourself.
"""

with open(os.path.join(workspace.root_path, "setup.cfg"), "w+") as f:
f.write(config_str)

workspace.update_config({"pylsp": {"configurationSources": ["flake8"]}})

return workspace._config.plugin_settings("flake8")


def test_flake8_multiline(workspace):
config_str = r"""[flake8]
exclude =
blah/,
file_2.py
"""

doc_str = "print('hi')\nimport os\n"

doc_uri = uris.from_fs_path(os.path.join(workspace.root_path, "blah/__init__.py"))
workspace.put_document(doc_uri, doc_str)

flake8_settings = get_flake8_cfg_settings(workspace, config_str)

assert "exclude" in flake8_settings
assert len(flake8_settings["exclude"]) == 2

with patch('pylsp.plugins.flake8_lint.Popen') as popen_mock:
mock_instance = popen_mock.return_value
mock_instance.communicate.return_value = [bytes(), bytes()]

doc = workspace.get_document(doc_uri)
flake8_lint.pylsp_lint(workspace, doc)

call_args = popen_mock.call_args[0][0]
assert call_args == ["flake8", "-", "--exclude=blah/,file_2.py"]

os.unlink(os.path.join(workspace.root_path, "setup.cfg"))


def test_flake8_per_file_ignores(workspace):
config_str = r"""[flake8]
ignores = F403
per-file-ignores =
**/__init__.py:F401,E402
test_something.py:E402,
exclude =
file_1.py
file_2.py
"""

doc_str = "print('hi')\nimport os\n"

doc_uri = uris.from_fs_path(os.path.join(workspace.root_path, "blah/__init__.py"))
workspace.put_document(doc_uri, doc_str)

flake8_settings = get_flake8_cfg_settings(workspace, config_str)

assert "perFileIgnores" in flake8_settings
assert len(flake8_settings["perFileIgnores"]) == 2
assert "exclude" in flake8_settings
assert len(flake8_settings["exclude"]) == 2

doc = workspace.get_document(doc_uri)
res = flake8_lint.pylsp_lint(workspace, doc)
assert not res

os.unlink(os.path.join(workspace.root_path, "setup.cfg"))