-
Notifications
You must be signed in to change notification settings - Fork 53
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
cEP-0025.md: Integrate pyflakes AST #144
Conversation
pseudocode for plugin implementation yet to be added. |
cEP-0025.md
Outdated
result.deadScopes) | ||
} | ||
|
||
yield HiddenResult(self, content) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
create a HiddenResult subclass, and use OO. a dict is ugly, and brittle. backwards compatibility and deprecation with a dict is limited, so a dict will mean we have problems when the pyflakes internals change.
Also please see all the issues related to HiddenResults. Fixing a few of them in your project will make everything much neater.
I believe that the flake8 wrapper (another part of the project) should be included in a different cEP. |
cEP-0025.md
Outdated
|
||
## Introduction | ||
|
||
The current approach of plugin development for flake8 does not utilises |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This jumps straight to flake8
, without helping the reader understand how flake8 is related to this coala project.
You might want to start with something like flake8 is the most commonly used linter framework in the python world. (see https://pythonwheels.com/ for a list of most downloaded pypi packages, and there are other pypi stats pages which might be more appropriate to use.) flake8 has two builtin enabled linters, pyflakes and pycodestyle.
Additional linters are supported using the flake8 plugin system, and may be Python AST based or built using pycodestyle's internal logical-line-based checker API. There has not been an option to use the pyflakes internal AST based checker API to create custom linters.
(might be worth mentioning https://github.com/flintwork/flint for history sake..)
Then you can explain why using the pyflakes internal AST based checker API to create custom linters is a fantastic idea .. ;-)
To cover the pycodestyle's internal logical-line-based checker API, the best example is https://github.com/openstack-dev/hacking , which has lots of little rules.
It would also be useful to investigate the https://github.com/PyCQA/bandit plugin system (recently added to PyCQA), as that is AST based iirc.
It wouldnt hurt to also mention pylint, which has plugin support and the best Python ast IMO, astroid. And it would definitely be a good idea to document why we are using pyflakes instead of astroid.
One of the reasons is along the lines of what you've already said - pyflakes provides a lot of re-usable analysis, making plugins easy, while astroid is more detailed and powerful, it is much more complicated. And there is a much larger ecosystem of flake8 plugins.
A project objective not relevant for this cEP is that we may be able to get flake8 to add a new 'pyflakes ast' plugin type, which has the same API as our own, or a similar enough API that the wrapper is very thin.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually the last sentence isnt quite correct. The original project has a goal of "invoke existing flake8 plugins" (which would be the pycodestyle API and raw python ast plugin types which flake8 supports) which is beyond the scope of this pyflakes cEP
However the project also mentions "This allows the coala and flake8 communities to cooperate on plugin development using a common plugin framework.", so it is within the scope of this project to ensure that the new pyflakes ast plugins created are also usable by flake8. We might supply a patch to flake8 to support the new pyflakes ast plugin type, or this repo/package might provide a flake8-plugin which invokes all of the new pyflakes-ast plugins in this repo.
coala already has PyFlakesBear
and PycodestyleBear
, so we dont need a Flake8 bear for its default plugins. But there are lovely flake8 existing plugins which we would like to have as separate bears.
See coala/coala-bears#1388 for the start of a rabbit hole about why we dont want to depend on flake8 directly. And this problem would also occur if we depended on any flake8 plugins as they will give flake8 as a dependency, and then we are stuck with flake8 dictating our versions. And this problem would occur if this package of flake8-pyflakes-ast-plugins added a dependency on flake8. How to solve this dependency problem is an implementation detail which can be omitted from this cEP.
cEP-0025.md
Outdated
|
||
1. There will be a separate repository named as coala-pyflakes which will | ||
be installable via `pip install coala-pyflakes`. | ||
2. The repository will contain a new package `pyflakes-plugins` which |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this package contains primarily bears, it needs to include either bears or coala in the name.
But it might be worth creating a non-coala API for these linter plugins - an API which is similar to other frameworks like flake8, so they could also run the same linters.
Then the package contains a bunch of generic pyflakes plugins, a coala meta bear (the pyflakes ast provider), and a coala bear meta wrapper (which creates a wrapper bear for each of the plugins).
Or we could have two packages, one for the generic pyflakes plugins, and a different one for the coala glue.
cEP-0025.md
Outdated
class PyflakesResult(HiddenResult): | ||
|
||
@enforce_signature | ||
def __init__(self, origin, deadScopes): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
enforce signature depends on having type annotations here.
cEP-0025.md
Outdated
self.generator_scope = self.get_scope(GeneratorScope, deadScopes) | ||
self.doctest_scope = self.get_scope(DoctestScope, deadScopes) | ||
|
||
def get_scope(self, scope_level, scopes): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
get_scope -> get_scopes ; always use plural to indicate a list is returned.
and then update the instance attributes.
cEP-0025.md
Outdated
if isinstance(node, FutureImportation): | ||
yield Result.from_values( | ||
origin=self, | ||
message=('Future import %s found' % node_name), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this example assumes creates an error for any future imports.
pls investigate the new py37 future import, to see if it is desirable.
If yes, the logic needs to be improved.
If not, then this is a NoFutureImportBear
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was planning to make this bear work 2 ways:
- To make sure that a particular import is added to every module. For eg: If I want that print_function should be imported into every module, I would make it compulsory and use this to find instances where I have failed to add them.
- If I don't want them, or don't want some (create a blacklist). The NoFutureImportBear you are discussing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As an example NoFutureImportBear
is better.
It is a simple bear that coala can use, as there are no py3 future imports which are supported on py34. Just add a comment which explains why this bear is simple.
An objective of this cEP and project is to show that it will allow simple rules based on the pyflakes ast.
Making it a complicated bear detracts from that goal.
cEP-0025.md
Outdated
Pyflakes provides with a basic API that does the traversing. | ||
So, if a developer uses enhanced AST he just needs to work on | ||
the implementation of the new logic that his/her plugin provides | ||
and not about the fidelity of the basic node handlers. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
and important problem not yet discussed is that pyflakes internals show the state with erroneous objects as-is in the scopes.
That could lead to plugins doing the 'wrong' thing. Usually the wrong thing has been done in the code, and needs to be fixed in the source, and then coala restarted, so
- maybe no pyflakes plugins are run unless pyflakes found no errors.
- erroneous objects are detected (does pyflakes keep enough metadata to do this?), and then either removed (creating more problems?) or flagged (meaning plugin authors need to check a flag..ugh).
Need pointers for |
cEP-0025.md
Outdated
checker API (checkout [Hacking](https://github.com/openstack-dev/hacking)). | ||
|
||
There has not been an option to use the pyflakes internal AST based checker | ||
API to create custom linters. Thus, the full potential of enhanced AST isn't |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
enhanced AST
here vs pyflakes internal AST
on the previous line.
The reader may not get the leap from pyflakes internal AST
to enhanced AST
being the same thing.
cEP-0025.md
Outdated
Here is a brief overview of the architecture: | ||
|
||
1. There will be a separate repository named as coala-pyflakes which will | ||
be installable via `pip install coala-pyflakes`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems like we have three packages in one repo.
- pyflakes-generic-plugins is easy -- it contains the generic plugins, and other generic stuff
- pyflakes-plugin-bears is pyflakes enhanced AST coala Bears.
If pyflakes-plugin-bears
contains metabear PyFlakesASTBear
...
What is in coala-pyflakes
?
cEP-0025.md
Outdated
function, module, generator ,and doctest level. Thus, providing | ||
the developer multiple views of a file allowing him to retrieve | ||
results as per his demand. | ||
3. Apart from scope information, the metabear even returns a list of |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
even -> also
cEP-0025.md
Outdated
|
||
Result.__init__(self, origin, message='') | ||
|
||
self.class_scope = self.get_scopes(ClassScope, deadScopes) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
all of these, except module_scope, should be plural: foo_scopes
.
cEP-0025.md
Outdated
self.doctest_scope = self.get_scopes(DoctestScope, deadScopes) | ||
self.pyflakes_messages = pyflakes_messages | ||
|
||
def get_scopes(self, scope_level, scopes): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
scope_level -> scope_type.
many of these scopes can be nested in various ways, such as functions in a doctest.
We'll probably want to add some helper which makes it easier for plugins to skip anything in a doctest, as they are usually more complicated, and need different logic.
And this is why we probably need to support a visitor pattern, as plugins think about scopes from outer to the inner, and want to exclude large branches that dont interest them. But the visitor pattern can be thought about later after we've built some plugins and find the pain points.
Actually this is something that is worth mentioning in more detail above. I am pretty sure Python ast doesnt provide doctests, and pretty sure that pycodestyle also doesnt have any reasonable way to create plugins for doctest. The pyflakes ability to provide doctest is a serious feature for projects like coala who use them extensively. Currently errors in them are caught by pyflakes, but the style of them is mostly ignored.
Maybe this is another good example for the cEP, as you could easily show a plugin which is probably very difficult in any other framework.
cEP-0025.md
Outdated
yield PyFlakesResult(self, result.deadScopes, result.messages) | ||
``` | ||
|
||
## Demonstrating use of PyFlakesASTBear by creating a plugin |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
plugin -> simple bear.
cEP-0025.md
Outdated
|
||
## Implementation of PyFlakesASTBear | ||
|
||
1. PyFlakesASTBear will be LocalBear returning result of the type |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use backticks everywhere that a class name or literal string is mentioned
cEP-0025.md
Outdated
for the dependent bear. The result structure is described by | ||
PyFlakesResult class. | ||
2. PyFlakesResult consists of a snapshot of the module at class, | ||
function, module, generator ,and doctest level. Thus, providing |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
misplaced ,
cEP-0025.md
Outdated
self.class_scopes = self.get_scopes(ClassScope, deadScopes) | ||
self.function_scopes = self.get_scopes(FunctionScope, deadScopes) | ||
self.generator_scopes = self.get_scopes(GeneratorScope, deadScopes) | ||
self.doctest_scopes = self.get_scopes(DoctestScope, deadScopes) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suspect there may only be one doctest scope.
can you check this. if so, make it singular instead of plural, and list it under the module_scope variable
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No. There exists multiple doctest_scope
. 1 scope per doctest. That's how it works.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Eww. ok
cEP-0025.md
Outdated
|
||
Result.__init__(self, origin, message='') | ||
|
||
self.module_scope = self.get_scopes(ModuleScope, deadScopes) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this needs a [0]
at the end to get only the first result.
cEP-0025.md
Outdated
corrected = self.remove_import(file, lineno, node.name) | ||
yield Result.from_values( | ||
origin=self, | ||
message=('Future import %s found' % node.name), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
round brackets not needed.
cEP-0025.md
Outdated
# Get the maximum column offset of FutureImportation for a line | ||
max_column_for_a_line = dict() | ||
for result in dependency_results.get(PyFlakesASTBear.name, []): | ||
for node in result.get_nodes(result.module_scope[0], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yup; [0]
needed in the Result subclass, not here.
cEP-0025.md
Outdated
for node in result.get_nodes(result.module_scope[0], | ||
FutureImportation): | ||
lineno = result.get_node_location(node)[0] | ||
col_offset = result.get_node_location(node)[2] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this [2]
here isnt self-explanatory.
maybe get_node_location should return something which is easier to use.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could even opt to remove the wrapper function.
node.source.lineno
or node.source.col__offset
will be good.
cEP-0025.md
Outdated
) | ||
yield Result.from_values( | ||
origin=self, | ||
message=('Future import/imports found'), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Simplify to be 'Future import(s) found'
cEP-0025.md
Outdated
## A generic implementation for detecting `__future__` imports | ||
|
||
This implementation doesn't requires coala to be installed and is | ||
designed so that it can be used in other tools like flake8. This |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
clarify that coala will also be able to use these plugins, but these are also usable by flake8.
Also this example should be above the 'fixes' bear.
The fixes bear is the finale -- it shows why we would use coala bears -- there are features of coala which bears can use that generic plugins can not use.
cEP-0025.md
Outdated
|
||
def get_node_location(self, node): | ||
return {'lineno': node.source.lineno, | ||
'depth': node.source.depth - 1, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
depth
isnt used in your examples.
and this method is just a thin wrapper around node.source
... maybe it should return node.source
, which allows access to lineno
and col_offset
, and if pyflakes internals ever change then get_node_locationcould return its own
NamedTuple` which provides those attributes.
But maybe we dont need get_node_location
. If the pyflakes internals ever removed or changed node.source
, we could wrap all nodes with a backwards compatible class which provides a node
and node.source
which has the attributes which plugins expect.
cEP-0025.md
Outdated
for result in dependency_results.get(PyFlakesASTBear.name, []): | ||
for node in result.get_nodes(result.module_scope, | ||
FutureImportation): | ||
lineno = result.get_node_location(node)['lineno'] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
or this could be just lineno = node.source.lineno
. I am not seeing the benefit of get_node_location
.
cEP-0025.md
Outdated
message='Future import %s found' % node.name, | ||
file=filename, | ||
diffs={filename: corrected}, | ||
line=result.get_node_location(node)['lineno']) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this could be line=lineno
message = '{code}: Future import {name} found' | ||
.format(name=node.name, | ||
code=CODE) | ||
yield (node.source.lineno, node.source.col_offset, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see you are using node.source.foo
here, and it looks nice.
cEP-0025.md
Outdated
for result in dependency_results.get(PyFlakesASTBear.name, []): | ||
for node in result.get_nodes(result.module_scope, | ||
FutureImportation): | ||
lineno = result.get_node_location(node)['lineno'] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
get_node_location()
in this method also looks to be more complexity than is needed.
cEP-0025.md
Outdated
col_offset = result.get_node_location(node)['col_offset'] | ||
if lineno in max_column_for_a_line: | ||
max_column_for_a_line[lineno] = max( | ||
max_column_for_a_line[lineno], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
very strange indentation here.
cEP-0025.md
Outdated
line = file[lineno - 1] | ||
# If a line contains \ in the end | ||
if line.rstrip()[-1] == '\\': | ||
next_line = file[lineno] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
and what happens if the next_line
also contains \
.. :P (now you may understand why I hate \
so passionately.
Some ways to handle this neatly is to create a concept of a 'logical line', which provides information about the entire line, and also have metadata about each distinct statement which gives (start_line,start_col),(end_line,end_col)
.
Given the complexity of this, I suggest you add a code comment that the algorithm hasnt been adjusted to support \
. That will make the logic a lot simpler and then you can solve the \
problem later, when you have real code and unit tests to verify all silly corner cases have been checked.
ack 95bc075 |
@gitmate-bot ff |
Hey! I'm GitMate.io! This pull request is being fastforwarded automatically. Please DO NOT push while fastforward is in progress or your changes would be lost permanently |
Automated fastforward with GitMate.io was successful! 🎉 |
Closes #143