Skip to content

Commit 7e8a4de

Browse files
authored
Merge pull request #3 from SVilgelm/file-header-path
Add file-header-path option
2 parents d3fe096 + c713bf1 commit 7e8a4de

File tree

5 files changed

+110
-28
lines changed

5 files changed

+110
-28
lines changed

.pylintrc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@ disable=duplicate-code, # pylint-file-header: Fails in unit tests
6262
missing-docstring, # pylint-file-header: Add DOCSTRINGS only where helpful
6363
relative-import, # pylint-file-header: ignore for unit tests
6464
redefined-outer-name, # pylint-file-header: conflicts with pytest fixtures
65-
too-few-public-methods # pylint-file-header: know what you are doing
65+
too-few-public-methods, # pylint-file-header: know what you are doing
66+
bad-option-value # pylint-file-header: conflict with super-with-arguments in py34
6667

6768
# Enable the message, report, category or checker with the given id(s). You can
6869
# either give multiple identifier separated by comma (,) or put this option

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,14 @@ Generate a `.pylintrc` file by executing `pylint --generate-rcfile`.
2020
Then add `pylintfileheader` to the plugins to load and set the `file-header` option to the [regular expression](https://docs.python.org/3/library/re.html#regular-expression-syntax) that the file header should match.
2121
When the `file-header` setting is omitted, pylint will pass.
2222

23+
### Options
24+
25+
* `file-header` is a regexp representing the file header that should be on top of a file.
26+
* `file-header-path` is the path to the file that contains the header. This is useful in case of long, multi-line headers, such as copyrights.
2327
* `file-header-ignore-empty-files` turns on the mode of ignoring the empty files, like `__init__.py`. The default value is `False`.
2428

29+
If both options `file-header` and `file-header-path` are set, then `file-header` will be used and the `file-header-path` is ignored.
30+
2531
## Example
2632

2733
### Setup

pylintfileheader/file_header_checker.py

Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,15 @@ class FileHeaderChecker(BaseChecker):
3636
'help': 'The file header that should be on top of a file.',
3737
}
3838
),
39+
(
40+
'file-header-path',
41+
{
42+
'default': None,
43+
'type': 'string',
44+
'metavar': '<file>',
45+
'help': 'The path to the file that contains the header.',
46+
}
47+
),
3948
(
4049
'file-header-ignore-empty-files',
4150
{
@@ -47,27 +56,40 @@ class FileHeaderChecker(BaseChecker):
4756
),
4857
)
4958

50-
def process_module(self, node):
51-
"""Process the astroid node stream."""
52-
if self.config.file_header:
53-
content = None
54-
with node.stream() as stream:
55-
# Explicit decoding required by python 3
56-
content = stream.read().decode('utf-8')
59+
def __init__(self, linter=None):
60+
# pylint: disable=super-with-arguments
61+
super(FileHeaderChecker, self).__init__(linter=linter)
62+
self.pattern = None
63+
self.header = None
5764

58-
if self.config.file_header_ignore_empty_files and not content:
59-
return
65+
def open(self):
66+
self.header = self.config.file_header
67+
if not self.header and self.config.file_header_path:
68+
with open(self.config.file_header_path, 'r') as header_file:
69+
self.header = header_file.read()
6070

71+
if self.header:
6172
if sys.version_info[0] < 3:
62-
pattern = re.compile(
63-
r'\A' + self.config.file_header, re.LOCALE | re.MULTILINE)
73+
opts = re.LOCALE | re.MULTILINE
6474
else:
65-
# The use of re.LOCALE is discouraged in python 3
66-
pattern = re.compile(
67-
r'\A' + self.config.file_header, re.MULTILINE)
75+
opts = re.MULTILINE
76+
self.pattern = re.compile(r'\A' + self.header, opts)
77+
78+
def process_module(self, node):
79+
"""Process the astroid node stream."""
80+
81+
if self.pattern is None:
82+
return
83+
84+
content = None
85+
with node.stream() as stream:
86+
# Explicit decoding required by python 3
87+
content = stream.read().decode('utf-8')
88+
89+
if self.config.file_header_ignore_empty_files and not content:
90+
return
6891

69-
matches = pattern.findall(content)
92+
matches = self.pattern.findall(content)
7093

71-
if len(matches) != 1:
72-
self.add_message('invalid-file-header', 1,
73-
args=self.config.file_header)
94+
if len(matches) != 1:
95+
self.add_message('invalid-file-header', 1, args=self.header)

pylintfileheadertest/file_header_checker_test.py

Lines changed: 60 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,13 @@
44
# ---------------------------------------------------------------------------------------------
55

66
# pylint: disable=invalid-name,unused-variable
7+
import re
8+
import sys
9+
710
from mock import MagicMock
811
import pylint.testutils
12+
import pytest
13+
914
from pylintfileheader.file_header_checker import FileHeaderChecker
1015

1116

@@ -43,15 +48,6 @@ def test_valid_header_not_at_top_message_added(self):
4348
args='# Valid\n# Header')):
4449
self.checker.process_module(node_mock)
4550

46-
def test_config_empty_no_message_added(self):
47-
"""When the `file-header` option is not set, no message should be added."""
48-
49-
self.checker.config.file_header = None
50-
node_mock = MagicMock()
51-
node_mock.stream.return_value.__enter__.return_value.read.return_value.decode.return_value = '# Invalid\n# Header'
52-
with self.assertNoMessages():
53-
self.checker.process_module(node_mock)
54-
5551
def test_ignore_empty_files(self):
5652
"""When the `file-header-ignore-empty-files` option is set to True."""
5753

@@ -71,3 +67,58 @@ def test_do_not_ignore_empty_files(self):
7167
line=1,
7268
args='# Valid\n# Header')):
7369
self.checker.process_module(node_mock)
70+
71+
72+
class TestFileHeaderCheckerNoConfig(pylint.testutils.CheckerTestCase):
73+
CHECKER_CLASS = FileHeaderChecker
74+
CONFIG = {}
75+
76+
def test_no_message_added(self):
77+
"""When the `file-header` option is not set, no message should be added."""
78+
79+
node_mock = MagicMock()
80+
node_mock.stream.return_value.__enter__.return_value.read.return_value.decode.return_value = '# Invalid\n# Header'
81+
with self.assertNoMessages():
82+
self.checker.process_module(node_mock)
83+
84+
85+
class TestFileHeaderCheckerPathMain(TestFileHeaderChecker):
86+
CHECKER_CLASS = FileHeaderChecker
87+
CONFIG = {'file_header_path': 'pylintfileheadertest/header.txt'}
88+
89+
90+
class TestFileHeaderCheckerPathExtra:
91+
CHECKER_CLASS = FileHeaderChecker
92+
93+
def get_checker(self, config):
94+
linter = pylint.testutils.UnittestLinter()
95+
checker = self.CHECKER_CLASS(linter)
96+
for key, value in config.items():
97+
setattr(checker.config, key, value)
98+
return checker
99+
100+
def test_incorrect_regex(self):
101+
with pytest.raises(re.error):
102+
self.get_checker({
103+
'file_header': '.+)',
104+
}).open()
105+
106+
def test_wrong_path(self):
107+
if sys.version_info[0] < 3:
108+
excp = IOError
109+
else:
110+
excp = FileNotFoundError
111+
112+
with pytest.raises(excp):
113+
self.get_checker({
114+
'file_header_path': 'foo-bar.txt',
115+
}).open()
116+
117+
def test_both_options(self):
118+
# no error expected since the file_header is used
119+
checker = self.get_checker({
120+
'file_header': '# Valid\n# Header',
121+
'file_header_path': 'foo-bar.txt',
122+
})
123+
checker.open()
124+
assert checker.header == '# Valid\n# Header'

pylintfileheadertest/header.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Valid
2+
# Header

0 commit comments

Comments
 (0)