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

Now able to run as a standalone module or shell program, with limited functionality #146

Merged
merged 1 commit into from
Mar 4, 2023
Merged
Show file tree
Hide file tree
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
1 change: 0 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ repos:
- flake8-bugbear
- flake8-comprehensions
- flake8-2020
- flake8-bandit
- flake8-builtins
- flake8-bugbear
- flake8-comprehensions
Expand Down
101 changes: 82 additions & 19 deletions flake8_trio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,14 @@
from __future__ import annotations

import ast
import functools
import keyword
import os
import re
import subprocess
import sys
import tokenize
from argparse import ArgumentTypeError, Namespace
from argparse import ArgumentParser, ArgumentTypeError, Namespace
from typing import TYPE_CHECKING

import libcst as cst
Expand All @@ -37,6 +40,24 @@
__version__ = "23.2.5"


# taken from https://github.com/Zac-HD/shed
@functools.lru_cache
def _get_git_repo_root(cwd: str | None = None) -> str:
return subprocess.run(
["git", "rev-parse", "--show-toplevel"],
check=True,
timeout=10,
capture_output=True,
text=True,
cwd=cwd,
).stdout.strip()


@functools.cache
def _should_format(fname: str) -> bool:
return fname.endswith((".py",))


# Enable support in libcst for new grammar
# See e.g. https://github.com/Instagram/LibCST/issues/862
# wrapping the call and restoring old values in case there's other libcst parsers
Expand All @@ -53,6 +74,44 @@ def cst_parse_module_native(source: str) -> cst.Module:
return mod


def main():
parser = ArgumentParser(prog="flake8_trio")
parser.add_argument(
nargs="*",
metavar="file",
dest="files",
help="Files(s) to format, instead of autodetection.",
)
Plugin.add_options(parser)
args = parser.parse_args()
Plugin.parse_options(args)
if args.files:
# TODO: go through subdirectories if directory/ies specified
all_filenames = args.files
Comment on lines +89 to +90
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤷‍♂️, I'm pretty happy to say that you have to pass exact files if you're passing files.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I figured, since that's how shed does it, but I would like to support as much of flake8's functionality as possible to ease any transition from running [name of this project] as a flake8 plugin to running it standalone. Ofc flake8 is kinda crazy so I won't bother supporting everything (e.g. --extend-ignore, --ignore, --select, --extend-select I think kinda just makes everything more complicated), but I think supporting directories shouldn't be very complicated and doesn't have much downsides.

else:
# Get all tracked files from `git ls-files`
try:
root = os.path.relpath(_get_git_repo_root())
all_filenames = subprocess.run(
["git", "ls-files"],
check=True,
timeout=10,
stdout=subprocess.PIPE,
text=True,
cwd=root,
).stdout.splitlines()
except (subprocess.SubprocessError, FileNotFoundError):
print("Doesn't seem to be a git repo; pass filenames to format.")
sys.exit(1)
all_filenames = [
os.path.join(root, f) for f in all_filenames if _should_format(f)
]
for file in all_filenames:
plugin = Plugin.from_filename(file)
for error in sorted(plugin.run()):
print(f"{file}:{error}")
Comment on lines +109 to +112
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A small ProcessPoolExecutor invocation here can make it n_cores times faster for large repos... although we'd still want to sort by file and lineno. Let's do that later.



class Plugin:
name = __name__
version = __version__
Expand Down Expand Up @@ -86,16 +145,23 @@ def run(self) -> Iterable[Error]:
yield from Flake8TrioRunner_cst(self.options).run(self._module)

@staticmethod
def add_options(option_manager: OptionManager):
# Disable TRIO9xx calls
option_manager.extend_default_ignore(default_disabled_error_codes)
def add_options(option_manager: OptionManager | ArgumentParser):
if isinstance(option_manager, ArgumentParser):
# if run as standalone
add_argument = option_manager.add_argument
else: # if run as a flake8 plugin
# Disable TRIO9xx calls
jakkdl marked this conversation as resolved.
Show resolved Hide resolved
option_manager.extend_default_ignore(default_disabled_error_codes)
# add parameter to parse from flake8 config
add_argument = functools.partial(
option_manager.add_option, parse_from_config=True
)

option_manager.add_option(
add_argument(
"--no-checkpoint-warning-decorators",
default="asynccontextmanager",
parse_from_config=True,
required=False,
comma_separated_list=True,
type=comma_separated_list,
help=(
"Comma-separated list of decorators to disable TRIO910 & TRIO911 "
"checkpoint warnings for. "
Expand All @@ -104,11 +170,10 @@ def add_options(option_manager: OptionManager):
"mydecorator,mypackage.mydecorators.*``"
),
)
option_manager.add_option(
add_argument(
"--startable-in-context-manager",
type=parse_trio114_identifiers,
default="",
parse_from_config=True,
required=False,
help=(
"Comma-separated list of method calls to additionally enable TRIO113 "
Expand All @@ -119,23 +184,21 @@ def add_options(option_manager: OptionManager):
"myfunction``"
),
)
option_manager.add_option(
add_argument(
"--trio200-blocking-calls",
type=parse_trio200_dict,
default={},
parse_from_config=True,
required=False,
help=(
"Comma-separated list of key->value pairs, where key is a [dotted] "
"function that if found inside an async function will raise TRIO200, "
"suggesting it be replaced with {value}"
),
)
option_manager.add_option(
add_argument(
"--enable-visitor-codes-regex",
type=re.compile,
default=".*",
parse_from_config=True,
required=False,
help=(
"Regex string of visitors to enable. Can be used to disable broken "
Expand All @@ -145,12 +208,11 @@ def add_options(option_manager: OptionManager):
"not report codes matching this regex."
),
)
option_manager.add_option(
add_argument(
"--anyio",
# action=store_true + parse_from_config does seem to work here, despite
# https://github.com/PyCQA/flake8/issues/1770
action="store_true",
parse_from_config=True,
required=False,
default=False,
help=(
Expand All @@ -165,11 +227,12 @@ def parse_options(options: Namespace):
Plugin.options = options


# flake8 ignores type parameters if using comma_separated_list
# so we need to reimplement that ourselves if we want to use "type"
# to check values
def comma_separated_list(raw_value: str) -> list[str]:
return [s.strip() for s in raw_value.split(",") if s.strip()]


def parse_trio114_identifiers(raw_value: str) -> list[str]:
values = [s.strip() for s in raw_value.split(",") if s.strip()]
values = comma_separated_list(raw_value)
for value in values:
if keyword.iskeyword(value) or not value.isidentifier():
raise ArgumentTypeError(f"{value!r} is not a valid method identifier")
Expand Down
4 changes: 4 additions & 0 deletions flake8_trio/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"""Entry file when executed with `python -m`."""
from . import main

main()
9 changes: 8 additions & 1 deletion flake8_trio/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,14 @@ def __init__(
self.message = message
self.args = args

def format_message(self):
return f"{self.code} " + self.message.format(*self.args)

# for yielding to flake8
def __iter__(self):
yield self.line
yield self.col
yield f"{self.code} " + self.message.format(*self.args)
yield self.format_message()
# We are no longer yielding `type(Plugin)` since that's quite tricky to do
# without circular imports, and afaik flake8 doesn't care anymore.
yield None
Expand All @@ -55,3 +58,7 @@ def __eq__(self, other: Any) -> bool:
def __repr__(self) -> str: # pragma: no cover
trailer = "".join(f", {x!r}" for x in self.args)
return f"<{self.code} error at {self.line}:{self.col}{trailer}>"

def __str__(self) -> str:
# flake8 adds 1 to the yielded column from `__iter__`, so we do the same here
return f"{self.line}:{self.col+1}: {self.format_message()}"
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[tool.pyright]
strict = ["*.py", "tests/*.py", "flake8_trio/**/*.py"]
exclude = ["**/node_modules", "**/__pycache__", "**/.*"]
reportUnusedCallResult=true
reportUnusedCallResult=false
reportUninitializedInstanceVariable=true
reportPropertyTypeMismatch=true
reportMissingSuperCall=true
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,6 @@ def local_file(name: str) -> Path:
long_description_content_type="text/markdown",
entry_points={
"flake8.extension": ["TRI = flake8_trio:Plugin"],
"console_scripts": ["flake8_trio=flake8_trio:main"],
},
)
Loading