Skip to content
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

The plugin fails when flake8 is invoked with stdin #52

Closed
SebastiaanZ opened this issue Dec 9, 2019 · 1 comment · Fixed by #53
Closed

The plugin fails when flake8 is invoked with stdin #52

SebastiaanZ opened this issue Dec 9, 2019 · 1 comment · Fixed by #53
Assignees
Labels
bug Something isn't working
Milestone

Comments

@SebastiaanZ
Copy link

When flake8 is invoked in stdin mode, the flake8-annotations plugin fails with the error message [Errno 2] No such file or directory: 'stdin'. This is because in stdin mode, the filename argument flake8 passes to the checker class does not refer to an actual file, but contains the display name flake8 uses in the output to indicate where the code is coming from. (By default, this display name is 'stdin', hence why it's mentioned in the error message above; this display name is configurable with the CL argument --stdin-display-name.)

Since flake8-annotations does not use the input passed by flake8 but rather parses the file directly from disk itself, it tries to use filename to open a file and that fails.

To Reproduce
An easy way to reproduce the error is by piping something to flake8 when the plugin is enabled. I've added the -v verbosity option :

$ printf 'print("Hello, world!")' | flake8 -v -
(...) snipped to reduce output
flake8.checker            MainProcess    285 CRITICAL Plugin TYP raised an unexpected exception
Traceback (most recent call last):
  File "/home/sebastiaan/.local/share/virtualenvs/aoc-2019-1zqaskJS/lib/python3.8/site-packages/flake8/checker.py", line 435, in run_check
    return plugin["plugin"](**arguments)
  File "/home/sebastiaan/.local/share/virtualenvs/aoc-2019-1zqaskJS/lib/python3.8/site-packages/flake8_annotations/checker.py", line 27, in __init__
    self.tree, self.lines = self.load_file(Path(filename))
  File "/home/sebastiaan/.local/share/virtualenvs/aoc-2019-1zqaskJS/lib/python3.8/site-packages/flake8_annotations/checker.py", line 73, in load_file
    with src_filepath.open("r", encoding="utf-8") as f:
  File "/usr/local/lib/python3.8/pathlib.py", line 1200, in open
    return io.open(self, mode, buffering, encoding, errors, newline,
  File "/usr/local/lib/python3.8/pathlib.py", line 1054, in _opener
    return self._accessor.open(self, flags, mode)
FileNotFoundError: [Errno 2] No such file or directory: 'stdin'
"flake8-annotations" failed during execution due to "[Errno 2] No such file or directory: 'stdin'"
Run flake8 with greater verbosity to see more details
(...) snipped to reduce output

Version Information

[master=] -> $ pipenv run flake8 --version
3.7.9 (flake8-annotations: 1.1.1, flake8-tidy-imports: 3.1.0, import-order: 0.18.1, mccabe: 0.6.1, pycodestyle: 2.5.0, pyflakes: 2.1.1) CPython 3.8.0 on Linux
[master=] -> $ pipenv run python -V
Python 3.8.0

I have also tested this with Python 3.7.4 and the same bug occurs.

Possible solution
As far as I'm aware, there's no guaranteed way to find out whether or not flake8 was called to lint input from stdin or from a file on disk using the arguments that are (potentially) passed to the checker class. The reason is that flake8/pycodestyle takes care of processing the input (file or stdin) and then passes that parsed input to the plugin checker class. This means that there is normally no need for a plugin to parse the file contents again.

Unfortunately, since the ast tree provided by flake8 is untyped, we do need to parse the file again and the least expensive way of doing that is by reading the file again. (We could also join together the lines list for that, but that's more expensive.) However, there's another option: If you add a parse_options method to the plugin's checker class, flake8 will pass the command line arguments to it. This means we can check for the - indicating stdin mode:

    # Quick mock-up; I've set a default value of `False` for `read_from_stdin`
    @classmethod
    def parse_options(cls, option_manager: OptionManager, options: Values, args: List[str]) -> None:
        """Parse the command line options provided to `flake8`."""
        if "-" in args:
            cls.read_from_stdin = True

Obviously, we still need to actually get the value from stdin that we're supposed to lint. Since stdin has already been exhausted by pycodestyle, we can do that by adding the lines parameter to the checker's __init__:

    def __init__(self, tree: ast.Module, filename: str, lines: List[str]):
        # Unfortunately no way that I can find around requesting the ast-parsed tree from flake8
        # Removing tree unregisters the plugin, and per the documentation the alternative is
        # requesting per-line information
        self.lines = lines  # <- this also means we don't have to manually split it later
        self.tree = self.load_file(filename)

and use self.lines in read_file if we're in stdin mode:

    def load_file(self, path: str) -> ast.Module:
        """Parse the provided Python file and return an (typed AST, source) tuple."""
        if self.read_from_stdin:
            # Reconstruct source as a single string from `self.lines`
            src = "".join(self.lines)
        else:
            # Read source as single string from file on disk
            src_filepath = Path(path)
            with src_filepath.open("r", encoding="utf-8") as f:
                src = f.read()

        if PY_GTE_38:
            # Built-in ast requires a flag to parse type comments
            tree = ast.parse(src, type_comments=True)
        else:
            # typed-ast will implicitly parse type comments
            tree = ast.parse(src)

        return tree

I've confirmed that this works (and, with some small adjustments for the changed method signatures, the tests also pass), but there may be a better way.

@SebastiaanZ SebastiaanZ added the bug Something isn't working label Dec 9, 2019
@sco1 sco1 self-assigned this Dec 9, 2019
@sco1 sco1 added this to the v1.1.2 milestone Dec 9, 2019
sco1 added a commit that referenced this issue Dec 10, 2019
Previously, when the checker instance is created it requested the filename from flake8 in order to load the source and parse to obtain a typed AST, as the flake8 provided AST is not guaranteed to contain type information. While sufficient for parsing files, this falls apart when attempting to parse code piped to stdin & causes our plugin to error out.

To mitigate these issues, the source is requested from flake8 as lines (list of strings), which is joined & passed into the typed AST generation rather than farming to a file IO task. This also allows us to drop the request for the AST from flake8 that we don't use, as requesting the lines will ensure that the plugin is registered.

The related test helpers have been updated to accommodate the change in instantiation.

Closes #52
@sco1
Copy link
Owner

sco1 commented Dec 10, 2019

After experimenting between the various proposed alternatives, it ends up being a pretty significant simplification to just request the list of lines from flake8 and join them when we need the source to obtain a typed AST for the rest of the checker. This obviates any need for this plugin to separately consider stdin vs. file inputs since flake8 is already doing this for us.

This fix will be included in v1.1.2, to be merged by #53

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants