diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fcba426d..71652d32 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,4 +56,6 @@ jobs: env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }} - run: python -m build && twine upload --skip-existing dist/* + run: | + python tests/test_version_and_changelog.py --ensure-tag + python -m build && twine upload --skip-existing dist/* diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 326ab7ee..f9f23764 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -44,6 +44,7 @@ repos: # Required for pyright strict mode - anyio - flake8 + - GitPython - hypothesis - hypothesmith - pytest diff --git a/CHANGELOG.md b/CHANGELOG.md index 94fedfe6..38f8cc21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ ## Future - TRIO91X now supports comprehensions +- TRIO100 and TRIO91X now supports autofixing +- Renamed `--enable-visitor-codes-regex` to `--enable` +- Added `--disable`, `--autofix` and `--error-on-autofix` ## 23.2.5 - Fix false alarms for `@pytest.fixture`-decorated functions in TRIO101, TRIO910 and TRIO911 diff --git a/README.md b/README.md index 3dfbf56d..71a2dae5 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,23 @@ pip install flake8-trio ## Configuration [You can configure `flake8` with command-line options](https://flake8.pycqa.org/en/latest/user/configuration.html), -but we prefer using a config file. The file needs to start with a section marker `[flake8]` and the following options are then parsed using flake8's config parser, and can be used just like any other flake8 options. +but we prefer using a config file. The file needs to start with a section marker `[flake8]` and the following options are then parsed using flake8's config parser, and can be used just like any other flake8 options. +Note that it's not currently possible to use a configuration file when running flake8_trio standalone. + +### `--enable` +Comma-separated list of error codes to enable, similar to flake8 --select but is additionally more performant as it will disable non-enabled visitors from running instead of just silencing their errors. + +### `--disable` +Comma-separated list of error codes to disable, similar to flake8 --ignore but is additionally more performant as it will disable non-enabled visitors from running instead of just silencing their errors. + +### `--autofix` +Comma-separated list of error-codes to enable autofixing for if implemented. Requires running as a standalone program. + +### `--error-on-autofix` +Whether to also print an error message for autofixed errors. + +### `--anyio` +Change the default library to be anyio instead of trio. If trio is imported it will assume both are available and print suggestions with [anyio|trio]. ### `no-checkpoint-warning-decorators` Specify a list of decorators to disable checkpointing checks for, turning off TRIO910 and TRIO911 warnings for functions decorated with any decorator matching any in the list. Matching is done with [fnmatch](https://docs.python.org/3/library/fnmatch.html). Defaults to disabling for `asynccontextmanager`. @@ -120,3 +136,21 @@ async def my_function(): arbitrary_other_function(my_blocking_call=None) ``` + + +### Using flake8_trio with pre-commit +If you use [pre-commit](https://pre-commit.com/), you can use it with flake8_trio by +adding the following to your `.pre-commit-config.yaml`: + +```yaml +minimum_pre_commit_version: '2.9.0' +repos: +- repo: https://github.com/Zac-HD/flake8-trio + rev: 23.2.5 + hooks: + - id: flake8-trio + # args: [--enable=TRIO, --disable=TRIO9, --autofix=TRIO] +``` + +This is often considerably faster for large projects, because `pre-commit` +can avoid running `flake8_trio` on unchanged files. diff --git a/tests/autofix_files/trio100.py.diff b/tests/autofix_files/trio100.py.diff index 1b9ed2c1..5cfd173c 100644 --- a/tests/autofix_files/trio100.py.diff +++ b/tests/autofix_files/trio100.py.diff @@ -1,6 +1,6 @@ --- +++ -@@ -3,24 +3,24 @@ +@@ x,24 x,24 @@ import trio @@ -38,7 +38,7 @@ with trio.move_on_after(10): await trio.sleep(1) -@@ -37,8 +37,8 @@ +@@ x,8 x,8 @@ with open("filename") as _: ... @@ -49,7 +49,7 @@ send_channel, receive_channel = trio.open_memory_channel(0) async with trio.fail_after(10): -@@ -49,22 +49,22 @@ +@@ x,22 x,22 @@ async for _ in receive_channel: ... diff --git a/tests/autofix_files/trio100_simple_autofix.py.diff b/tests/autofix_files/trio100_simple_autofix.py.diff index ac5e82b7..5d711a03 100644 --- a/tests/autofix_files/trio100_simple_autofix.py.diff +++ b/tests/autofix_files/trio100_simple_autofix.py.diff @@ -1,6 +1,6 @@ --- +++ -@@ -3,50 +3,51 @@ +@@ x,50 x,51 @@ # a # b diff --git a/tests/autofix_files/trio910.py.diff b/tests/autofix_files/trio910.py.diff index e79b955e..d563f817 100644 --- a/tests/autofix_files/trio910.py.diff +++ b/tests/autofix_files/trio910.py.diff @@ -1,6 +1,6 @@ --- +++ -@@ -45,12 +45,14 @@ +@@ x,12 x,14 @@ async def foo1(): # error: 0, "exit", Statement("function definition", lineno) bar() @@ -15,7 +15,7 @@ async def foo_if_2(): -@@ -81,6 +83,7 @@ +@@ x,6 x,7 @@ async def foo_ifexp_2(): # error: 0, "exit", Statement("function definition", lineno) print(_ if False and await foo() else await foo()) @@ -23,7 +23,7 @@ # nested function definition -@@ -89,6 +92,7 @@ +@@ x,6 x,7 @@ async def foo_func_2(): # error: 4, "exit", Statement("function definition", lineno) bar() @@ -31,7 +31,7 @@ # we don't get a newline after the nested function definition before the checkpoint -@@ -97,17 +101,21 @@ +@@ x,17 x,21 @@ async def foo_func_3(): # error: 0, "exit", Statement("function definition", lineno) async def foo_func_4(): await foo() @@ -53,7 +53,7 @@ # fmt: on -@@ -144,11 +152,13 @@ +@@ x,11 x,13 @@ async def foo_condition_2(): # error: 0, "exit", Statement("function definition", lineno) if False and await foo(): ... @@ -67,7 +67,7 @@ async def foo_condition_4(): # safe -@@ -170,6 +180,7 @@ +@@ x,6 x,7 @@ async def foo_while_1(): # error: 0, "exit", Statement("function definition", lineno) while _: await foo() @@ -75,7 +75,7 @@ async def foo_while_2(): # now safe -@@ -188,12 +199,14 @@ +@@ x,12 x,14 @@ async def foo_while_4(): # error: 0, "exit", Statement("function definition", lineno) while False: await foo() @@ -90,7 +90,7 @@ async def foo_for_2(): # now safe -@@ -216,6 +229,7 @@ +@@ x,6 x,7 @@ break else: await foo() @@ -98,7 +98,7 @@ async def foo_while_break_3(): # error: 0, "exit", Statement("function definition", lineno) -@@ -224,6 +238,7 @@ +@@ x,6 x,7 @@ break else: ... @@ -106,7 +106,7 @@ async def foo_while_break_4(): # error: 0, "exit", Statement("function definition", lineno) -@@ -231,6 +246,7 @@ +@@ x,6 x,7 @@ break else: ... @@ -114,7 +114,7 @@ async def foo_while_continue_1(): # safe -@@ -254,6 +270,7 @@ +@@ x,6 x,7 @@ continue else: ... @@ -122,7 +122,7 @@ async def foo_while_continue_4(): # error: 0, "exit", Statement("function definition", lineno) -@@ -261,6 +278,7 @@ +@@ x,6 x,7 @@ continue else: ... @@ -130,7 +130,7 @@ async def foo_async_for_1(): -@@ -299,6 +317,7 @@ +@@ x,6 x,7 @@ raise else: await foo() @@ -138,7 +138,7 @@ async def foo_try_2(): # safe -@@ -349,6 +368,7 @@ +@@ x,6 x,7 @@ pass else: pass @@ -146,7 +146,7 @@ async def foo_try_7(): # safe -@@ -390,6 +410,7 @@ +@@ x,6 x,7 @@ await trio.sleep(0) except: ... @@ -154,7 +154,7 @@ # safe -@@ -417,16 +438,19 @@ +@@ x,16 x,19 @@ except: ... finally: @@ -174,7 +174,7 @@ return # error: 8, "return", Statement("function definition", lineno-2) await foo() -@@ -435,6 +459,7 @@ +@@ x,6 x,7 @@ if _: await foo() return # safe @@ -182,7 +182,7 @@ # loop over non-empty static collection -@@ -462,12 +487,14 @@ +@@ x,12 x,14 @@ async def foo_range_4(): # error: 0, "exit", Statement("function definition", lineno) for i in range(10, 5): await foo() @@ -197,7 +197,7 @@ # https://github.com/Zac-HD/flake8-trio/issues/47 -@@ -551,6 +578,7 @@ +@@ x,6 x,7 @@ # should error async def foo_comprehension_2(): # error: 0, "exit", Statement("function definition", lineno) [await foo() for x in range(10) if bar()] diff --git a/tests/autofix_files/trio911.py.diff b/tests/autofix_files/trio911.py.diff index d9c63d51..91e9bb7f 100644 --- a/tests/autofix_files/trio911.py.diff +++ b/tests/autofix_files/trio911.py.diff @@ -1,6 +1,6 @@ --- +++ -@@ -24,7 +24,9 @@ +@@ x,7 x,9 @@ async def foo_yield_2(): @@ -10,7 +10,7 @@ yield # error: 4, "yield", Statement("yield", lineno-1) await foo() -@@ -32,22 +34,29 @@ +@@ x,22 x,29 @@ async def foo_yield_3(): # error: 0, "exit", Statement("yield", lineno+2) await foo() yield @@ -40,7 +40,7 @@ return # error: 4, "return", Statement("yield", lineno-1) -@@ -68,6 +77,7 @@ +@@ x,6 x,7 @@ async def foo_async_with_3(): async with trio.fail_after(5): yield @@ -48,7 +48,7 @@ yield # error: 8, "yield", Statement("yield", lineno-1) -@@ -77,6 +87,7 @@ +@@ x,6 x,7 @@ yield # safe else: yield # safe @@ -56,7 +56,7 @@ # await anext(iter) is not called on break -@@ -85,6 +96,7 @@ +@@ x,6 x,7 @@ yield if ...: break @@ -64,7 +64,7 @@ async def foo_async_for_3(): # safe -@@ -102,13 +114,16 @@ +@@ x,13 x,16 @@ async def foo_for(): # error: 0, "exit", Statement("yield", lineno+3) await foo() for i in "": @@ -81,7 +81,7 @@ # while -@@ -121,13 +136,16 @@ +@@ x,13 x,16 @@ else: await foo() # will always run yield # safe @@ -98,7 +98,7 @@ # no checkpoint after yield if else is entered -@@ -136,39 +154,52 @@ +@@ x,39 x,52 @@ await foo() yield else: @@ -151,7 +151,7 @@ await foo() -@@ -178,16 +209,19 @@ +@@ x,16 x,19 @@ async def foo_while_continue_1(): # error: 0, "exit", Statement("yield", lineno+3) await foo() while foo(): @@ -171,7 +171,7 @@ yield # error: 8, "yield", Statement("yield", lineno) if foo(): continue -@@ -197,6 +231,7 @@ +@@ x,6 x,7 @@ while foo(): yield # safe await foo() @@ -179,7 +179,7 @@ # --- while + break --- -@@ -207,7 +242,9 @@ +@@ x,7 x,9 @@ break else: await foo() @@ -189,7 +189,7 @@ # no checkpoint on break -@@ -218,6 +255,7 @@ +@@ x,6 x,7 @@ if ...: break await foo() @@ -197,7 +197,7 @@ # guaranteed if else and break -@@ -229,6 +267,7 @@ +@@ x,6 x,7 @@ else: await foo() # runs if 0-iter yield # safe @@ -205,7 +205,7 @@ # break at non-guaranteed checkpoint -@@ -239,7 +278,9 @@ +@@ x,7 x,9 @@ await foo() # might not run else: await foo() # might not run @@ -215,7 +215,7 @@ # check break is reset on nested -@@ -255,7 +296,9 @@ +@@ x,7 x,9 @@ await foo() yield # safe await foo() @@ -225,7 +225,7 @@ # check multiple breaks -@@ -270,7 +313,9 @@ +@@ x,7 x,9 @@ await foo() if ...: break @@ -235,7 +235,7 @@ async def foo_while_break_7(): # error: 0, "exit", Statement("function definition", lineno)# error: 0, "exit", Statement("yield", lineno+5) -@@ -280,6 +325,7 @@ +@@ x,6 x,7 @@ break yield break @@ -243,7 +243,7 @@ async def foo_while_endless_1(): -@@ -292,6 +338,7 @@ +@@ x,6 x,7 @@ while foo(): await foo() yield @@ -251,7 +251,7 @@ async def foo_while_endless_3(): -@@ -313,9 +360,11 @@ +@@ x,9 x,11 @@ # try async def foo_try_1(): # error: 0, "exit", Statement("function definition", lineno) # error: 0, "exit", Statement("yield", lineno+2) try: @@ -263,7 +263,7 @@ # no checkpoint after yield in ValueError -@@ -323,12 +372,14 @@ +@@ x,12 x,14 @@ try: await foo() except ValueError: @@ -278,7 +278,7 @@ async def foo_try_3(): # error: 0, "exit", Statement("yield", lineno+6) -@@ -337,13 +388,16 @@ +@@ x,13 x,16 @@ except: await foo() else: @@ -295,7 +295,7 @@ yield # error: 8, "yield", Statement("function definition", lineno-4) finally: await foo() -@@ -353,6 +407,7 @@ +@@ x,6 x,7 @@ try: await foo() finally: @@ -303,7 +303,7 @@ # try might crash before checkpoint yield # error: 8, "yield", Statement("function definition", lineno-5) await foo() -@@ -363,7 +418,9 @@ +@@ x,7 x,9 @@ await foo() except ValueError: pass @@ -313,7 +313,7 @@ async def foo_try_7(): # error: 0, "exit", Statement("yield", lineno+17) -@@ -376,6 +433,7 @@ +@@ x,6 x,7 @@ yield await foo() except SyntaxError: @@ -321,7 +321,7 @@ yield # error: 8, "yield", Statement("yield", lineno-7) await foo() finally: -@@ -384,6 +442,7 @@ +@@ x,6 x,7 @@ # by any of the excepts, jumping straight to the finally. # Then the error will be propagated upwards yield # safe @@ -329,7 +329,7 @@ ## safe only if (try or else) and all except bodies either await or raise -@@ -399,6 +458,7 @@ +@@ x,6 x,7 @@ raise else: await foo() @@ -337,7 +337,7 @@ # no checkpoint after yield in else -@@ -409,6 +469,7 @@ +@@ x,6 x,7 @@ await foo() else: yield @@ -345,7 +345,7 @@ # bare except means we'll jump to finally after full execution of either try or the except -@@ -439,6 +500,7 @@ +@@ x,6 x,7 @@ except ValueError: await foo() finally: @@ -353,7 +353,7 @@ yield # error: 8, "yield", Statement("function definition", lineno-6) await foo() -@@ -447,6 +509,7 @@ +@@ x,6 x,7 @@ try: await foo() finally: @@ -361,7 +361,7 @@ # try might crash before checkpoint yield # error: 8, "yield", Statement("function definition", lineno-5) await foo() -@@ -455,9 +518,11 @@ +@@ x,9 x,11 @@ # if async def foo_if_1(): if ...: @@ -373,7 +373,7 @@ yield # error: 8, "yield", Statement("function definition", lineno-5) await foo() -@@ -468,7 +533,9 @@ +@@ x,7 x,9 @@ ... else: yield @@ -383,7 +383,7 @@ async def foo_if_3(): # error: 0, "exit", Statement("yield", lineno+6) -@@ -477,7 +544,9 @@ +@@ x,7 x,9 @@ yield else: ... @@ -393,7 +393,7 @@ async def foo_if_4(): # error: 0, "exit", Statement("yield", lineno+7) -@@ -487,7 +556,9 @@ +@@ x,7 x,9 @@ await foo() else: ... @@ -403,7 +403,7 @@ async def foo_if_5(): # error: 0, "exit", Statement("yield", lineno+8) -@@ -498,7 +569,9 @@ +@@ x,7 x,9 @@ else: yield ... @@ -413,7 +413,7 @@ async def foo_if_6(): # error: 0, "exit", Statement("yield", lineno+8) -@@ -509,7 +582,9 @@ +@@ x,7 x,9 @@ yield await foo() ... @@ -423,7 +423,7 @@ async def foo_if_7(): # error: 0, "exit", Statement("function definition", lineno) -@@ -517,6 +592,7 @@ +@@ x,6 x,7 @@ await foo() yield await foo() @@ -431,7 +431,7 @@ async def foo_if_8(): # error: 0, "exit", Statement("function definition", lineno) -@@ -526,21 +602,25 @@ +@@ x,21 x,25 @@ await foo() yield await foo() @@ -457,7 +457,7 @@ # normal function -@@ -585,7 +665,9 @@ +@@ x,7 x,9 @@ await foo() async def foo_func_2(): # error: 4, "exit", Statement("yield", lineno+1) @@ -467,7 +467,7 @@ # autofix doesn't insert newline after nested function def and before checkpoint -@@ -597,6 +679,7 @@ +@@ x,6 x,7 @@ async def foo_func_4(): await foo() @@ -475,7 +475,7 @@ async def foo_func_5(): # error: 0, "exit", Statement("yield", lineno+2) -@@ -609,12 +692,14 @@ +@@ x,12 x,14 @@ async def foo_func_7(): await foo() ... @@ -490,7 +490,7 @@ # loop over non-empty static collection -@@ -641,6 +726,7 @@ +@@ x,6 x,7 @@ if ...: continue await foo() @@ -498,7 +498,7 @@ yield # error: 4, "yield", Stmt("yield", line-7) # continue/else -@@ -649,6 +735,7 @@ +@@ x,6 x,7 @@ continue await foo() else: @@ -506,7 +506,7 @@ yield # error: 8, "yield", Stmt("yield", line-8) await foo() yield -@@ -693,6 +780,7 @@ +@@ x,6 x,7 @@ for _ in (): await foo() @@ -514,7 +514,7 @@ yield # error: 4, "yield", Stmt("yield", line-4) for _ in {1: 2, 3: 4}: -@@ -701,14 +789,17 @@ +@@ x,14 x,17 @@ for _ in " ".strip(): await foo() @@ -532,7 +532,7 @@ yield # error: 4, "yield", Stmt("yield", line-4) for _ in (*(1, 2),): -@@ -717,10 +808,12 @@ +@@ x,10 x,12 @@ for _ in {**{}}: await foo() @@ -545,7 +545,7 @@ yield # error: 4, "yield", Stmt("yield", line-4) for _ in {**{1: 2}}: -@@ -746,31 +839,38 @@ +@@ x,31 x,38 @@ for _ in {}: await foo() @@ -584,7 +584,7 @@ yield # error: 4, "yield", Stmt("yield", line-4) # while -@@ -784,6 +884,7 @@ +@@ x,6 x,7 @@ if ...: break await foo() @@ -592,7 +592,7 @@ yield # error: 4, "yield", Stmt("yield", line-6) while True: -@@ -796,6 +897,7 @@ +@@ x,6 x,7 @@ while False: await foo() # type: ignore[unreachable] @@ -600,7 +600,7 @@ yield # error: 4, "yield", Stmt("yield", line-4) while "hello": -@@ -805,11 +907,13 @@ +@@ x,11 x,13 @@ # false positive on containers while [1, 2]: await foo() @@ -614,7 +614,7 @@ yield # error: 4, "yield", Stmt("yield", line-5) # range with constant arguments also handled, see more extensive tests in 910 -@@ -827,10 +931,12 @@ +@@ x,10 x,12 @@ for i in range(1 + 1): # not handled await foo() @@ -627,7 +627,7 @@ yield # error: 4, "yield", Stmt("yield", line-4) for i in range(+3): -@@ -839,6 +945,7 @@ +@@ x,6 x,7 @@ for i in range(-3.5): # type: ignore await foo() @@ -635,7 +635,7 @@ yield # error: 4, "yield", Stmt("yield", line-4) # duplicated from 910 to have all range tests in one place -@@ -862,20 +969,24 @@ +@@ x,20 x,24 @@ for i in range(10, 5): await foo() @@ -660,7 +660,7 @@ yield # error: 4, "yield", Stmt("yield", line-5) await foo() -@@ -899,6 +1010,7 @@ +@@ x,6 x,7 @@ # guaranteed iteration and await in value, but test is not guaranteed [await foo() for x in range(10) if bar()] @@ -668,7 +668,7 @@ yield # error: 4, "yield", Stmt("yield", line-4) # guaranteed iteration and await in value -@@ -907,6 +1019,7 @@ +@@ x,6 x,7 @@ # not guaranteed to iter [await foo() for x in bar()] @@ -676,7 +676,7 @@ yield # error: 4, "yield", Stmt("yield", line-4) # await statement in loop expression -@@ -918,10 +1031,12 @@ +@@ x,10 x,12 @@ yield # safe {await foo() for x in bar()} @@ -689,7 +689,7 @@ yield # error: 4, "yield", Stmt("yield", line-4) # other than `await` can be in both key&val -@@ -933,9 +1048,11 @@ +@@ x,9 x,11 @@ # generator expressions are never treated as safe (await foo() for x in range(10)) @@ -701,7 +701,7 @@ yield # error: 4, "yield", Stmt("yield", line-3) # async for always safe -@@ -948,27 +1065,33 @@ +@@ x,27 x,33 @@ # other than in generator expression (... async for x in bar()) @@ -735,7 +735,7 @@ yield # error: 4, "yield", Stmt("yield", line-2) # multiple ifs -@@ -976,6 +1099,7 @@ +@@ x,6 x,7 @@ yield [... for x in range(10) for y in bar() if await foo() if await foo()] diff --git a/tests/autofix_files/trio91x_autofix.py.diff b/tests/autofix_files/trio91x_autofix.py.diff index 0b273f61..34a1cf94 100644 --- a/tests/autofix_files/trio91x_autofix.py.diff +++ b/tests/autofix_files/trio91x_autofix.py.diff @@ -1,6 +1,6 @@ --- +++ -@@ -9,6 +9,7 @@ +@@ x,6 x,7 @@ # ARG --enable=TRIO910,TRIO911 from typing import Any @@ -8,7 +8,7 @@ def bar() -> Any: -@@ -21,30 +22,38 @@ +@@ x,30 x,38 @@ async def foo1(): # TRIO910: 0, "exit", Statement("function definition", lineno) bar() @@ -47,7 +47,7 @@ yield # TRIO911: 8, "yield", Statement("yield", lineno) -@@ -67,8 +76,10 @@ +@@ x,8 x,10 @@ async def foo_while4(): while True: if ...: @@ -58,7 +58,7 @@ yield # TRIO911: 12, "yield", Statement("yield", lineno) # TRIO911: 12, "yield", Statement("yield", lineno-2) # TRIO911: 12, "yield", Statement("function definition", lineno-5) # TRIO911: 12, "yield", Statement("yield", lineno-2) # this warns about the yield on lineno-2 twice, since it can arrive here from it in two different ways -@@ -76,15 +87,19 @@ +@@ x,15 x,19 @@ # check state management of nested loops async def foo_nested_while(): while True: diff --git a/tests/test_changelog_and_version.py b/tests/test_changelog_and_version.py old mode 100644 new mode 100755 index add42dce..f2382f2d --- a/tests/test_changelog_and_version.py +++ b/tests/test_changelog_and_version.py @@ -1,11 +1,13 @@ +#!/usr/bin/env python """Tests for flake8-trio package metadata.""" from __future__ import annotations import re +import sys import unittest from pathlib import Path -from typing import TYPE_CHECKING, NamedTuple +from typing import TYPE_CHECKING, NamedTuple, TypeVar import flake8_trio @@ -14,7 +16,11 @@ if TYPE_CHECKING: from collections.abc import Iterable -root_path = Path(__file__).parent.parent +ROOT_PATH = Path(__file__).parent.parent +CHANGELOG = ROOT_PATH / "CHANGELOG.md" +README = CHANGELOG.parent / "README.md" + +T = TypeVar("T", bound="Version") class Version(NamedTuple): @@ -23,13 +29,19 @@ class Version(NamedTuple): patch: int @classmethod - def from_string(cls, string: str): + def from_string(cls: type[T], string: str) -> T: return cls(*map(int, string.split("."))) + def __str__(self) -> str: + return ".".join(map(str, self)) + + +VERSION = Version.from_string(flake8_trio.__version__) + def get_releases() -> Iterable[Version]: valid_pattern = re.compile(r"^## (\d\d\.\d?\d\.\d?\d)$") - with open(root_path / "CHANGELOG.md", encoding="utf-8") as f: + with open(CHANGELOG, encoding="utf-8") as f: lines = f.readlines() for line in lines: version_match = valid_pattern.match(line) @@ -37,13 +49,13 @@ def get_releases() -> Iterable[Version]: yield Version.from_string(version_match.group(1)) -def test_last_release_against_changelog(): +def test_last_release_against_changelog() -> None: """Ensure we have the latest version covered in 'CHANGELOG.md'.""" - latest_release, *_ = get_releases() - assert latest_release == Version.from_string(flake8_trio.__version__) + latest_release = next(iter(get_releases())) + assert latest_release == VERSION -def test_version_increments_are_correct(): +def test_version_increments_are_correct() -> None: versions = list(get_releases()) for prev, current in zip(versions[1:], versions): assert prev < current # remember that `versions` is newest-first @@ -56,18 +68,57 @@ def test_version_increments_are_correct(): assert current == prev._replace(patch=prev.patch + 1), msg +def ensure_tagged() -> None: + from git.repo import Repo + + last_version = next(iter(get_releases())) + repo = Repo(ROOT_PATH) + if last_version not in repo.tags: + # create_tag is partially unknown in pyright, which kinda looks like + # https://github.com/gitpython-developers/GitPython/issues/1473 + # which should be resolved? + repo.create_tag(str(last_version)) # type: ignore + repo.remotes.origin.push(str(last_version)) + + +def update_version() -> None: + # If we've added a new version to the changelog, update __version__ to match + last_version = next(iter(get_releases())) + if VERSION != last_version: + INIT_FILE = ROOT_PATH / "flake8_trio" / "__init__.py" + subs = (f'__version__ = "{VERSION}"', f'__version__ = "{last_version}"') + INIT_FILE.write_text(INIT_FILE.read_text().replace(*subs)) + + # Similarly, update the pre-commit config example in the README + current = README.read_text() + wanted = re.sub( + pattern=r"^ rev: (\d+\.\d+\.\d+)$", + repl=f" rev: {last_version}", + string=current, + flags=re.MULTILINE, + ) + if current != wanted: + README.write_text(wanted) + + +if __name__ == "__main__": + update_version() + if "--ensure-tag" in sys.argv: + ensure_tagged() + + +# I wanted to move this test to a separate file, but that'd lead to merge conflicts, +# so will have to wait with that IGNORED_CODES_REGEX = r"TRIO107|TRIO108|TRIO\d\d\d_.*" class test_messages_documented(unittest.TestCase): def runTest(self): documented_errors: dict[str, set[str]] = {} - for filename in ( - "CHANGELOG.md", - "README.md", - ): - with open(root_path / filename, encoding="utf-8") as f: + for path in (CHANGELOG, README): + with open(path, encoding="utf-8") as f: lines = f.readlines() + filename = path.name documented_errors[filename] = set() for line in lines: for error_msg in re.findall(r"TRIO\d\d\d", line): diff --git a/tests/test_flake8_trio.py b/tests/test_flake8_trio.py index 4b0a3105..c9f54782 100644 --- a/tests/test_flake8_trio.py +++ b/tests/test_flake8_trio.py @@ -66,10 +66,13 @@ def check_version(test: str): } -# difflib generates lots of lines with one trailing space, which is an eyesore -# and trips up pre-commit, git diffs, etc. If there actually was diff trailing -# space in the content it's picked up elsewhere and by pre-commit. -def strip_difflib_space(s: str) -> str: +def format_difflib_line(s: str) -> str: + # replace line markers with x's, to not generate massive diffs when lines get moved + s = re.sub(r"(?<= )[+-]\d*(?=,)", "x", s) + + # difflib generates lots of lines with one trailing space, which is an eyesore + # and trips up pre-commit, git diffs, etc. If there actually was diff trailing + # space in the content it's picked up elsewhere and by pre-commit. if s[-2:] == " \n": return s[:-2] + "\n" return s @@ -78,7 +81,7 @@ def strip_difflib_space(s: str) -> str: def diff_strings(first: str, second: str, /) -> str: return "".join( map( - strip_difflib_space, + format_difflib_line, difflib.unified_diff( first.splitlines(keepends=True), second.splitlines(keepends=True), @@ -314,7 +317,7 @@ def _parse_eval_file(test: str, content: str) -> tuple[list[Error], list[str], s try: expected.append(Error(err_code, lineno, int(col), message, *args)) except AttributeError as e: - msg = f'Line {lineno}: Failed to format\n {message!r}\n"with\n{args}' + msg = f"Line {lineno}: Failed to format\n {message!r}\nwith\n{args}" raise ParseError(msg) from e enabled_codes_list = enabled_codes.split(",")