Skip to content
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
__pycache__/
*.pyc
1 change: 1 addition & 0 deletions deps/tests/bump/main/output/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
requirements.txt
2 changes: 1 addition & 1 deletion deps/tests/bump/main/output/requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
click==8.2.1
click==8.3.1
41 changes: 41 additions & 0 deletions exceptions/bare_raise.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import ast
import sys


class BareRaiseChecker(ast.NodeVisitor):
def __init__(self, filename):
self.filename = filename
self.except_depth = 0
self.issues = []

def visit_ExceptHandler(self, node):
self.except_depth += 1
self.generic_visit(node)
self.except_depth -= 1

def visit_Raise(self, node):
if node.exc is None and self.except_depth == 0:
self.issues.append((node.lineno, node.col_offset))
self.generic_visit(node)


def main(filenames):
exit_status = 0
for filename in filenames:
with open(filename, "rb") as f:
source = f.read()
try:
tree = ast.parse(source, filename=filename)
except SyntaxError:
continue
checker = BareRaiseChecker(filename)
checker.visit(tree)
for lineno, col in checker.issues:
print(f"{filename}:{lineno}:{col}: bare 'raise' outside except block")
exit_status = 99

sys.exit(exit_status)


if __name__ == "__main__":
main(sys.argv[1:])
5 changes: 5 additions & 0 deletions exceptions/ick.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[[rule]]
name = "bare_raise"
impl = "python"
scope = "file"
inputs = ["*.py"]
28 changes: 28 additions & 0 deletions exceptions/tests/bare_raise/main/input/demo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
def bad():
raise # flagged: bare raise outside except


def nested_bad():
if True:
raise # flagged: still outside except


def good_reraise():
try:
pass
except Exception:
raise # ok: re-raises inside except


def good_nested_except():
try:
pass
except TypeError:
try:
pass
except ValueError:
raise # ok: inside nested except


def good_raise_with_arg():
raise ValueError("something") # ok: not bare
2 changes: 2 additions & 0 deletions exceptions/tests/bare_raise/main/output/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
demo.py:2:4: bare 'raise' outside except block
demo.py:7:8: bare 'raise' outside except block
Empty file added fstrings/__init__.py
Empty file.
72 changes: 72 additions & 0 deletions fstrings/fstring_repr.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""Ick rule: replace {repr(...)} in f-strings with {...!r}."""
import sys
from pathlib import Path

import libcst as cst
from fixit import LintRule, Invalid, Valid


class FstringReprRule(LintRule):
MESSAGE = "Use {x!r} instead of {repr(x)} in f-strings"

VALID = [
Valid("f'{x!r}'"),
Valid("f'{x}'"),
Valid("f'{repr}'"),
Valid("f'{repr(x, y)}'"),
]
INVALID = [
Invalid(
"f'{repr(x)}'",
expected_replacement="f'{x!r}'",
),
Invalid(
"f'value is {repr(obj)} here'",
expected_replacement="f'value is {obj!r} here'",
),
Invalid(
"f'{repr(x):10}'",
expected_replacement="f'{x!r:10}'",
),
]

def visit_FormattedStringExpression(self, node: cst.FormattedStringExpression) -> None:
expr = node.expression
if not (
isinstance(expr, cst.Call)
and isinstance(expr.func, cst.Name)
and expr.func.value == 'repr'
and len(expr.args) == 1
and not isinstance(expr.args[0].value, cst.StarredElement)
and expr.args[0].keyword is None
and expr.args[0].star == ''
):
return
if node.conversion is not None:
return
inner = expr.args[0].value
new_node = node.with_changes(expression=inner, conversion='r')
self.report(node, replacement=new_node)


def main():
from fixit.api import fixit_bytes, generate_config
from fixit.ftypes import Options, QualifiedRule

options = Options(rules=[QualifiedRule('fstrings.fstring_repr', 'FstringReprRule')])
for path_str in sys.argv[1:]:
path = Path(path_str)
src = path.read_bytes()
config = generate_config(path, options=options)
gen = fixit_bytes(path, src, config=config, autofix=True)
try:
while True:
next(gen)
except StopIteration as e:
if e.value is not None:
path.write_bytes(e.value)
sys.exit(0)


if __name__ == '__main__':
main()
6 changes: 6 additions & 0 deletions fstrings/ick.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[[rule]]
name = "fstring_repr"
impl = "python"
scope = "file"
inputs = ["*.py"]
deps = ["fixit", "libcst"]
4 changes: 4 additions & 0 deletions fstrings/test_fstring_repr.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from fixit.testing import add_lint_rule_tests_to_module
from python.fstrings.fstring_repr import FstringReprRule

add_lint_rule_tests_to_module(globals(), [FstringReprRule])
4 changes: 4 additions & 0 deletions fstrings/tests/fstring_repr/basic/input/example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
x = 'hello'
print(f'value is {repr(x)}')
print(f'{repr(x):10}')
print(f'{x!r}') # already correct, no change
4 changes: 4 additions & 0 deletions fstrings/tests/fstring_repr/basic/output/example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
x = 'hello'
print(f'value is {x!r}')
print(f'{x!r:10}')
print(f'{x!r}') # already correct, no change
19 changes: 11 additions & 8 deletions gha/tests/upgrade/main/output/.github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ jobs:

steps:
- name: Checkout
uses: actions/checkout@v4
uses: "actions/checkout@v6"
- name: Set Up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
uses: "actions/setup-python@v6"
with:
python-version: ${{ matrix.python-version }}
-
uses: "astral-sh/setup-uv@v6"
uses: "astral-sh/setup-uv@v7"
- name: Install
run: |
uv pip install -e .[test]
Expand All @@ -47,18 +47,20 @@ jobs:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
-
uses: "actions/checkout@v6"
-
uses: "actions/setup-python@v6"
with:
python-version: "3.12"
-
uses: "astral-sh/setup-uv@v6"
uses: "astral-sh/setup-uv@v7"
- name: Install
run: uv pip install build
- name: Build
run: python -m build
- name: Upload
uses: actions/upload-artifact@v4
uses: "actions/upload-artifact@v7"
with:
name: sdist
path: dist
Expand All @@ -70,7 +72,8 @@ jobs:
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v4
-
uses: "actions/download-artifact@v8"
with:
name: sdist
path: dist
Expand Down
10 changes: 5 additions & 5 deletions gha/upgrade.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Generated by upgrade_upgrade.py

"actions/checkout" = "v4"
"actions/setup-python" = "v5"
"actions/upload-artifact" = "v4"
"actions/download-artifact" = "v4"
"astral-sh/setup-uv" = "v6"
"actions/checkout" = "v6"
"actions/setup-python" = "v6"
"actions/upload-artifact" = "v7"
"actions/download-artifact" = "v8"
"astral-sh/setup-uv" = "v7"
40 changes: 40 additions & 0 deletions gitignores.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""Ick rule: ensure .gitignore at repo root contains __pycache__ and *.pyc entries."""
import re
import sys
from pathlib import Path

REQUIRED_EXACT = ['__pycache__/']
REQUIRED_PATTERNS = [re.compile(r'^(\*\*/)?(\*\.py\[?c[do\]]*)$')]


def _line_matches_any_pattern(line: str) -> bool:
return any(p.match(line) for p in REQUIRED_PATTERNS)


def check(root: Path) -> int:
gitignore = root / '.gitignore'
lines = gitignore.read_text().splitlines(True) if gitignore.exists() else []
stripped = [l.rstrip('\n') for l in lines]

missing = []
for line in REQUIRED_EXACT:
if line not in stripped:
missing.append(line + "\n")
if not any(_line_matches_any_pattern(l) for l in stripped):
missing.append('*.pyc\n')

if missing:
with gitignore.open('a') as f:
if lines and not lines[-1].endswith('\n'):
f.write('\n')
f.writelines(missing)

return 0


def main():
sys.exit(check(Path('.')))


if __name__ == '__main__':
main()
5 changes: 5 additions & 0 deletions ick.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
[[ruleset]]
path = "."
prefix = ""

[[rule]]
name = "gitignores"
scope = "repo"
impl = "python"
Loading
Loading