Merge lp:~adiroiban/pocket-lint/plugin-system into lp:pocket-lint

Proposed by Adi Roiban
Status: Needs review
Proposed branch: lp:~adiroiban/pocket-lint/plugin-system
Merge into: lp:pocket-lint
Diff against target: 308 lines (+155/-58)
2 files modified
pocketlint/contrib/pep257_plugin.py (+97/-0)
pocketlint/formatcheck.py (+58/-58)
To merge this branch: bzr merge lp:~adiroiban/pocket-lint/plugin-system
Reviewer Review Type Date Requested Status
Curtis Hovey Pending
Review via email: mp+215554@code.launchpad.net

Description of the change

Description
-----------

I want to have google-js-linter, pep257 , some custom regex checkers, css/LESS/SASS checker, HTML5 etc... then there is jshint/jslint ... maybe someone want pylint :)

I think that is hard to ship all this checkers with pocket lint.

One option is to let pocket-link define a framework for checkers and then users can add custom checkers as plugins.

Each plugin has its own configuration and can also use global configuration (once we have a well defined configuration object)

Changes
-------

As a drive-by fix I have updated PocektLintOption to not have the dirty hack for filling values from command line options.

PocketLintOption now have the addPlugin method... there is no method to remove plugins :)

I have defined a base Pluging which can register itself for a list of languages. We might want a checker for all files... ex line lenght like checker or tab checker or traling spaces checker

check is the entry paint and a helper for calling the plugin for multiple files.

check_all is the main method which should be defined by plugins.

I am also thinking at check_line to remove the need for plugins to split the file if they only want to do line checks.

I added `enabled` attribute, but I am not sure if it make sense... From an 3rd party user, if plugin is not enable, just don't add it. I am thinking that maybe we want to use the plugin system for own checker... like jshint.

Please let me know what do you think.\

Does it make sense? Do you find it useful? Is the API ok?

Thanks!

To post a comment you must log in.

Unmerged revisions

453. By Adi Roiban <email address hidden>

Initial code for plugin system.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'pocketlint/contrib/pep257_plugin.py'
2--- pocketlint/contrib/pep257_plugin.py 1970-01-01 00:00:00 +0000
3+++ pocketlint/contrib/pep257_plugin.py 2014-04-12 19:27:30 +0000
4@@ -0,0 +1,97 @@
5+import os
6+
7+try:
8+ import pep257
9+ # Shut up the linter.
10+ pep257
11+except ImportError:
12+ pep257 = None
13+from pocketlint.formatcheck import Language
14+
15+
16+class Plugin(object):
17+ """
18+ Base class for implementing pocket-lint plugins.
19+
20+ You will need to define `check_all` if your linter needs whole content
21+ or `check_line` if you will check each line.
22+
23+ Use `message` to add a message to reporter.
24+ """
25+ #: List of languages which can be checked using this plugin.
26+ languages = [Language.TEXT]
27+ #: Whether this plugin is enabled.
28+ enabled = True
29+ #: Path to current file that is checked.
30+ file_path = None
31+ #: Global options used by pocket-lint
32+ global_options = None
33+
34+ def check(self, language, file_path, text, reporter, options):
35+ """
36+ Check code using this plugin.
37+ """
38+ if not self.enabled:
39+ return
40+
41+ if language not in self.languages:
42+ return
43+
44+ self._reporter = reporter
45+ self.global_options = options
46+ self.file_path = file_path
47+
48+ self.check_all(text)
49+
50+ for line_no, line in enumerate(text.splitlines()):
51+ self.check_line(line, line_no + 1)
52+
53+ def check_all(self, text):
54+ """
55+ Check whole file content.
56+ """
57+ pass
58+
59+ def check_line(self, line, line_no):
60+ """
61+ Check single line.
62+ """
63+
64+ def message(self, line_no, message, icon=None):
65+ """
66+ Add a message to reporter.
67+ """
68+ self._reporter(
69+ line_no,
70+ message,
71+ icon=icon,
72+ base_dir=os.path.dirname(self.file_path),
73+ file_name=self.file_path,
74+ )
75+
76+
77+class PEP257Plugin(Plugin):
78+ """
79+ A plugin to check Python docstrings for PEP257.
80+ """
81+
82+ languages = [Language.PYTHON]
83+
84+ _default_options = {
85+ 'ignore': ['D203', 'D204', 'D401'],
86+ }
87+
88+ def __init__(self, options=None):
89+ if not options:
90+ options = self._default_options.copy()
91+ self._options = options
92+ self.enabled = pep257 != None
93+ self._checker = pep257.PEP257Checker()
94+
95+ def check_all(self, text):
96+ """Pass all content to pep257 module."""
97+ results = self._checker.check_source(text, self.file_path)
98+ for error in results:
99+ if error.code in self._options['ignore']:
100+ continue
101+ self.message(error.line, error.message, icon='error')
102
103=== modified file 'pocketlint/formatcheck.py'
104--- pocketlint/formatcheck.py 2014-03-09 21:04:28 +0000
105+++ pocketlint/formatcheck.py 2014-04-12 19:27:30 +0000
106@@ -83,14 +83,6 @@
107 except ImportError:
108 closure_linter = None
109
110-try:
111- import pep257
112- # Shut up the linter.
113- pep257
114-except ImportError:
115- pep257 = None
116-
117-
118 IS_PY3 = True if sys.version_info >= (3,) else False
119
120
121@@ -262,10 +254,14 @@
122
123
124 class PocketLintOptions(object):
125- """Default options used by pocketlint"""
126+ """Default options used by pocketlint."""
127
128- def __init__(self, command_options=None):
129+ def __init__(self):
130 self.max_line_length = 0
131+ self.verbose = False
132+ # Docstring options.
133+ self.do_format = False
134+ self.is_interactive = False
135
136 self.jslint = {
137 'enabled': True,
138@@ -287,19 +283,16 @@
139
140 self.regex_line = []
141
142- if command_options:
143- self._updateFromCommandLineOptions(command_options)
144-
145- def _updateFromCommandLineOptions(self, options):
146- """
147- Update with options received from command line.
148- """
149- # Update maximum line length.
150- self.max_line_length = options.max_line_length
151- self.pep8['max_line_length'] = options.max_line_length - 1
152- self.pep8['hang_closing'] = options.hang_closing
153- if hasattr(options, 'regex_line'):
154- self.regex_line = options.regex_line
155+ self._plugins = set()
156+
157+ @property
158+ def plugins(self):
159+ """Return a copy of current plugins."""
160+ return self._plugins.copy()
161+
162+ def addPlugin(self, plugin):
163+ """Add `plugin`."""
164+ self._plugins.add(plugin)
165
166
167 class BaseChecker(object):
168@@ -318,12 +311,9 @@
169 self.text = u(text)
170 self.set_reporter(reporter=reporter)
171
172- if options is None:
173- self.options = PocketLintOptions()
174- elif not isinstance(options, PocketLintOptions):
175- self.options = PocketLintOptions(command_options=options)
176- else:
177- self.options = options
178+ if not options:
179+ options = PocketLintOptions()
180+ self.options = options
181
182 def set_reporter(self, reporter=None):
183 """Set the reporter for messages."""
184@@ -397,6 +387,19 @@
185 self.file_path, self.text, self._reporter, self.options)
186 checker.check()
187
188+ self.check_plugins()
189+
190+ def check_plugins(self):
191+ """Checked code with registered plugins."""
192+ for plugin in self.options.plugins:
193+ plugin.check(
194+ self.language,
195+ self.file_path,
196+ self.text,
197+ self._reporter,
198+ self.options,
199+ )
200+
201
202 class AnyTextMixin:
203 """Common checks for many checkers."""
204@@ -512,9 +515,9 @@
205 class FastParser(object):
206 """A simple and pure-python parser that checks well-formedness.
207
208- This parser works in py 2 and 3. It handles entities and ignores
209- namespaces. This parser works with python ElementTree.
210- """
211+ This parser works in py 2 and 3. It handles entities and ignores
212+ namespaces. This parser works with python ElementTree.
213+ """
214
215 def __init__(self, html=0, target=None, encoding=None):
216 parser = expat.ParserCreate(encoding, None)
217@@ -702,7 +705,6 @@
218 self.check_text()
219 self.check_flakes()
220 self.check_pep8()
221- self.check_pep257()
222 self.check_windows_endlines()
223
224 def check_flakes(self):
225@@ -744,24 +746,6 @@
226 message = "%s: %s" % (message, location[3].strip())
227 self.message(location[1], message, icon='error')
228
229- def check_pep257(self):
230- """PEP 257 docstring style checker."""
231- if not pep257:
232- # PEP257 is not available.
233- return
234-
235- ignore_list = getattr(self.options, 'pep257_ignore', [])
236-
237- results = pep257.check_source(self.text, self.file_path)
238-
239- for error in results:
240- # PEP257 message contains the short error as first line from
241- # the long docstring explanation.
242- error_message = error.explanation.splitlines()[0]
243- if error_message in ignore_list:
244- continue
245- self.message(error.line, error_message, icon='error')
246-
247 def check_text(self):
248 """Call each line_method for each line in text."""
249 for line_no, line in enumerate(self.text.splitlines()):
250@@ -1160,6 +1144,7 @@
251
252 def check_text(self):
253 """Call each line_method for each line in text."""
254+
255 for line_no, line in enumerate(self.text.splitlines()):
256 line_no += 1
257 self.check_length(line_no, line)
258@@ -1168,8 +1153,8 @@
259 self.check_regex_line(line_no, line)
260
261
262-def get_option_parser():
263- """Return the option parser for this program."""
264+def parse_command_line(args):
265+ """Return a tuple with (options, source) for the command line request."""
266 usage = "usage: %prog [options] file1 file2"
267 parser = OptionParser(usage=usage)
268 parser.add_option(
269@@ -1197,7 +1182,18 @@
270 is_interactive=False,
271 max_line_length=DEFAULT_MAX_LENGTH,
272 )
273- return parser
274+
275+ (command_options, sources) = parser.parse_args(args=args)
276+
277+ # Create options based on parsed command line.
278+ options = PocketLintOptions()
279+ options.verbose = command_options.verbose
280+ options.do_format = command_options.do_format
281+ options.is_interactive = command_options.is_interactive
282+ options.max_line_length = command_options.max_line_length
283+ options.pep8['hang_closing'] = command_options.hang_closing
284+
285+ return (options, sources)
286
287
288 def check_sources(sources, options, reporter=None):
289@@ -1224,11 +1220,15 @@
290 """Run the command line operations."""
291 if argv is None:
292 argv = sys.argv
293- parser = get_option_parser()
294- (options, sources) = parser.parse_args(args=argv[1:])
295- # Handle standard args.
296+ (options, sources) = parse_command_line(args=argv[1:])
297+
298 if len(sources) == 0:
299- parser.error("Expected file paths.")
300+ sys.stderr.write("Expected file paths.\n")
301+ return 1
302+
303+ from pocketlint.contrib.pep257_plugin import PEP257Plugin
304+
305+ options.addPlugin(PEP257Plugin())
306 reporter = Reporter(Reporter.CONSOLE)
307 reporter.error_only = not options.verbose
308 return check_sources(sources, options, reporter)

Subscribers

People subscribed via source and target branches

to all changes: