diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 8f3b4fd4..00000000 --- a/.flake8 +++ /dev/null @@ -1,25 +0,0 @@ -[flake8] -extend-select = - # bugbear - B - # bugbear opinions - B9 - # implicit str concat - ISC -extend-ignore = - # slice notation whitespace, invalid - E203 - # line length, handled by bugbear B950 - E501 - # bare except, handled by bugbear B001 - E722 - # zip with strict=, requires python >= 3.10 - B905 - # string formatting opinion, B028 renamed to B907 - B028 - B907 -# up to 88 allowed by bugbear B950 -max-line-length = 80 -per-file-ignores = - # __init__ exports names - src/flask/__init__.py: F401 diff --git a/.gitignore b/.gitignore index e6713351..83aa92e5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,25 +1,10 @@ -.DS_Store -.env -.flaskenv -*.pyc -*.pyo -env/ -venv/ -.venv/ -env* -dist/ -build/ -*.egg -*.egg-info/ -.tox/ -.cache/ -.pytest_cache/ .idea/ -docs/_build/ -.vscode - -# Coverage reports -htmlcov/ +.vscode/ +__pycache__/ +.tox/ .coverage .coverage.* -*,cover +htmlcov/ +docs/_build/ +dist/ +venv/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 00293c57..c262399a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,33 +1,16 @@ ci: - autoupdate_branch: "2.3.x" autoupdate_schedule: monthly repos: - - repo: https://github.com/asottile/pyupgrade - rev: v3.10.1 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.1.3 hooks: - - id: pyupgrade - args: ["--py38-plus"] - - repo: https://github.com/asottile/reorder-python-imports - rev: v3.10.0 - hooks: - - id: reorder-python-imports - name: Reorder Python imports (src, tests) - files: "^(?!examples/)" - args: ["--application-directories", "src"] - - repo: https://github.com/psf/black - rev: 23.7.0 - hooks: - - id: black - - repo: https://github.com/PyCQA/flake8 - rev: 6.1.0 - hooks: - - id: flake8 - additional_dependencies: - - flake8-bugbear - - flake8-implicit-str-concat + - id: ruff + - id: ruff-format - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 hooks: + - id: check-merge-conflict + - id: debug-statements - id: fix-byte-order-marker - id: trailing-whitespace - id: end-of-file-fixer diff --git a/examples/celery/pyproject.toml b/examples/celery/pyproject.toml index cd394678..25887ca2 100644 --- a/examples/celery/pyproject.toml +++ b/examples/celery/pyproject.toml @@ -12,3 +12,6 @@ build-backend = "flit_core.buildapi" [tool.flit.module] name = "task_app" + +[tool.ruff] +src = ["src"] diff --git a/examples/javascript/js_example/views.py b/examples/javascript/js_example/views.py index 0d4b6561..9f0d26c5 100644 --- a/examples/javascript/js_example/views.py +++ b/examples/javascript/js_example/views.py @@ -2,7 +2,7 @@ from flask import jsonify from flask import render_template from flask import request -from js_example import app +from . import app @app.route("/", defaults={"js": "fetch"}) diff --git a/examples/javascript/pyproject.toml b/examples/javascript/pyproject.toml index 6c4d7def..0ec631d2 100644 --- a/examples/javascript/pyproject.toml +++ b/examples/javascript/pyproject.toml @@ -27,3 +27,6 @@ filterwarnings = ["error"] [tool.coverage.run] branch = true source = ["js_example", "tests"] + +[tool.ruff] +src = ["src"] diff --git a/examples/tutorial/flaskr/__init__.py b/examples/tutorial/flaskr/__init__.py index bb9cce5a..e35934d6 100644 --- a/examples/tutorial/flaskr/__init__.py +++ b/examples/tutorial/flaskr/__init__.py @@ -31,12 +31,13 @@ def create_app(test_config=None): return "Hello, World!" # register the database commands - from flaskr import db + from . import db db.init_app(app) # apply the blueprints to the app - from flaskr import auth, blog + from . import auth + from . import blog app.register_blueprint(auth.bp) app.register_blueprint(blog.bp) diff --git a/examples/tutorial/flaskr/auth.py b/examples/tutorial/flaskr/auth.py index b423e6ae..34c03a20 100644 --- a/examples/tutorial/flaskr/auth.py +++ b/examples/tutorial/flaskr/auth.py @@ -11,7 +11,7 @@ from flask import url_for from werkzeug.security import check_password_hash from werkzeug.security import generate_password_hash -from flaskr.db import get_db +from .db import get_db bp = Blueprint("auth", __name__, url_prefix="/auth") diff --git a/examples/tutorial/flaskr/blog.py b/examples/tutorial/flaskr/blog.py index 3704626b..be0d92c4 100644 --- a/examples/tutorial/flaskr/blog.py +++ b/examples/tutorial/flaskr/blog.py @@ -7,8 +7,8 @@ from flask import request from flask import url_for from werkzeug.exceptions import abort -from flaskr.auth import login_required -from flaskr.db import get_db +from .auth import login_required +from .db import get_db bp = Blueprint("blog", __name__) diff --git a/examples/tutorial/pyproject.toml b/examples/tutorial/pyproject.toml index 2c806b15..73a674ce 100644 --- a/examples/tutorial/pyproject.toml +++ b/examples/tutorial/pyproject.toml @@ -34,3 +34,6 @@ filterwarnings = ["error"] [tool.coverage.run] branch = true source = ["flaskr", "tests"] + +[tool.ruff] +src = ["src"] diff --git a/pyproject.toml b/pyproject.toml index e4042cb6..652de0da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -106,3 +106,24 @@ module = [ "importlib_metadata", ] ignore_missing_imports = true + +[tool.ruff] +src = ["src"] +fix = true +show-fixes = true +show-source = true + +[tool.ruff.lint] +select = [ + "B", # flake8-bugbear + "E", # pycodestyle error + "F", # pyflakes + "I", # isort + "UP", # pyupgrade + "W", # pycodestyle warning +] +ignore-init-module-imports = true + +[tool.ruff.lint.isort] +force-single-line = true +order-by-type = false diff --git a/src/flask/app.py b/src/flask/app.py index d710cb96..7a1cf4d4 100644 --- a/src/flask/app.py +++ b/src/flask/app.py @@ -1183,7 +1183,8 @@ class Flask(App): # class to the correct type try: rv = self.response_class.force_type( - rv, request.environ # type: ignore[arg-type] + rv, # type: ignore[arg-type] + request.environ, ) except TypeError as e: raise TypeError( @@ -1272,7 +1273,8 @@ class Flask(App): return response def do_teardown_request( - self, exc: BaseException | None = _sentinel # type: ignore + self, + exc: BaseException | None = _sentinel, # type: ignore[assignment] ) -> None: """Called after the request is dispatched and the response is returned, right before the request context is popped. @@ -1305,7 +1307,8 @@ class Flask(App): request_tearing_down.send(self, _async_wrapper=self.ensure_sync, exc=exc) def do_teardown_appcontext( - self, exc: BaseException | None = _sentinel # type: ignore + self, + exc: BaseException | None = _sentinel, # type: ignore[assignment] ) -> None: """Called right before the application context is popped. diff --git a/src/flask/helpers.py b/src/flask/helpers.py index 13a5aa21..8601c2fd 100644 --- a/src/flask/helpers.py +++ b/src/flask/helpers.py @@ -21,6 +21,7 @@ from .signals import message_flashed if t.TYPE_CHECKING: # pragma: no cover from werkzeug.wrappers import Response as BaseResponse + from .wrappers import Response @@ -48,9 +49,7 @@ def get_load_dotenv(default: bool = True) -> bool: def stream_with_context( - generator_or_function: ( - t.Iterator[t.AnyStr] | t.Callable[..., t.Iterator[t.AnyStr]] - ) + generator_or_function: t.Iterator[t.AnyStr] | t.Callable[..., t.Iterator[t.AnyStr]] ) -> t.Iterator[t.AnyStr]: """Request contexts disappear when the response is started on the server. This is done for efficiency reasons and to make it less likely to encounter diff --git a/src/flask/json/provider.py b/src/flask/json/provider.py index 3c22bc8f..46d64d6a 100644 --- a/src/flask/json/provider.py +++ b/src/flask/json/provider.py @@ -134,9 +134,7 @@ class DefaultJSONProvider(JSONProvider): method) will call the ``__html__`` method to get a string. """ - default: t.Callable[[t.Any], t.Any] = staticmethod( - _default - ) # type: ignore[assignment] + default: t.Callable[[t.Any], t.Any] = staticmethod(_default) # type: ignore[assignment] """Apply this function to any object that :meth:`json.dumps` does not know how to serialize. It should return a valid JSON type or raise a ``TypeError``. diff --git a/src/flask/sansio/app.py b/src/flask/sansio/app.py index 0f7d2cbf..fdc4714e 100644 --- a/src/flask/sansio/app.py +++ b/src/flask/sansio/app.py @@ -35,9 +35,10 @@ from .scaffold import setupmethod if t.TYPE_CHECKING: # pragma: no cover from werkzeug.wrappers import Response as BaseResponse - from .blueprints import Blueprint + from ..testing import FlaskClient from ..testing import FlaskCliRunner + from .blueprints import Blueprint T_shell_context_processor = t.TypeVar( "T_shell_context_processor", bound=ft.ShellContextProcessorCallable @@ -905,7 +906,9 @@ class App(Scaffold): Moved from ``flask.redirect``, which calls this method. """ return _wz_redirect( - location, code=code, Response=self.response_class # type: ignore[arg-type] + location, + code=code, + Response=self.response_class, # type: ignore[arg-type] ) def inject_url_defaults(self, endpoint: str, values: dict) -> None: diff --git a/src/flask/sessions.py b/src/flask/sessions.py index e5650d68..34f71d8c 100644 --- a/src/flask/sessions.py +++ b/src/flask/sessions.py @@ -14,7 +14,8 @@ from .json.tag import TaggedJSONSerializer if t.TYPE_CHECKING: # pragma: no cover from .app import Flask - from .wrappers import Request, Response + from .wrappers import Request + from .wrappers import Response class SessionMixin(MutableMapping): diff --git a/src/flask/views.py b/src/flask/views.py index c7a2b621..bfc18af3 100644 --- a/src/flask/views.py +++ b/src/flask/views.py @@ -6,7 +6,6 @@ from . import typing as ft from .globals import current_app from .globals import request - http_method_funcs = frozenset( ["get", "post", "head", "options", "delete", "put", "trace", "patch"] ) diff --git a/tests/test_appctx.py b/tests/test_appctx.py index aa3a8b4e..ca9e079e 100644 --- a/tests/test_appctx.py +++ b/tests/test_appctx.py @@ -196,17 +196,14 @@ def test_clean_pop(app): @app.teardown_request def teardown_req(error=None): - 1 / 0 + raise ZeroDivisionError @app.teardown_appcontext def teardown_app(error=None): called.append("TEARDOWN") - try: - with app.test_request_context(): - called.append(flask.current_app.name) - except ZeroDivisionError: - pass + with app.app_context(): + called.append(flask.current_app.name) assert called == ["flask_test", "TEARDOWN"] assert not flask.current_app diff --git a/tests/test_apps/subdomaintestmodule/__init__.py b/tests/test_apps/subdomaintestmodule/__init__.py index 9b83c0d7..b4ce4b16 100644 --- a/tests/test_apps/subdomaintestmodule/__init__.py +++ b/tests/test_apps/subdomaintestmodule/__init__.py @@ -1,4 +1,3 @@ from flask import Module - mod = Module(__name__, "foo", subdomain="foo") diff --git a/tests/test_basic.py b/tests/test_basic.py index d8bca980..214cfee0 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -19,7 +19,6 @@ from werkzeug.routing import RequestRedirect import flask - require_cpython_gc = pytest.mark.skipif( python_implementation() != "CPython", reason="Requires CPython GC behavior", @@ -190,7 +189,8 @@ def test_url_mapping(app, client): def test_werkzeug_routing(app, client): - from werkzeug.routing import Submount, Rule + from werkzeug.routing import Rule + from werkzeug.routing import Submount app.url_map.add( Submount("/foo", [Rule("/bar", endpoint="bar"), Rule("/", endpoint="index")]) @@ -210,7 +210,8 @@ def test_werkzeug_routing(app, client): def test_endpoint_decorator(app, client): - from werkzeug.routing import Submount, Rule + from werkzeug.routing import Rule + from werkzeug.routing import Submount app.url_map.add( Submount("/foo", [Rule("/bar", endpoint="bar"), Rule("/", endpoint="index")]) @@ -431,9 +432,9 @@ def test_session_special_types(app, client): client.get("/") s = flask.session assert s["t"] == (1, 2, 3) - assert type(s["b"]) is bytes + assert type(s["b"]) is bytes # noqa: E721 assert s["b"] == b"\xff" - assert type(s["m"]) is Markup + assert type(s["m"]) is Markup # noqa: E721 assert s["m"] == Markup("") assert s["u"] == the_uuid assert s["d"] == now @@ -784,7 +785,7 @@ def test_teardown_request_handler_error(app, client): @app.route("/") def fails(): - 1 // 0 + raise ZeroDivisionError rv = client.get("/") assert rv.status_code == 500 @@ -851,7 +852,7 @@ def test_error_handling(app, client): @app.route("/error") def error(): - 1 // 0 + raise ZeroDivisionError @app.route("/forbidden") def error2(): @@ -877,7 +878,7 @@ def test_error_handling_processing(app, client): @app.route("/") def broken_func(): - 1 // 0 + raise ZeroDivisionError @app.after_request def after_request(resp): @@ -1047,12 +1048,13 @@ def test_error_handler_after_processor_error(app, client): @app.before_request def before_request(): if _trigger == "before": - 1 // 0 + raise ZeroDivisionError @app.after_request def after_request(response): if _trigger == "after": - 1 // 0 + raise ZeroDivisionError + return response @app.route("/") @@ -1507,7 +1509,7 @@ def test_exception_propagation(app, client, key): @app.route("/") def index(): - 1 // 0 + raise ZeroDivisionError if key is not None: app.config[key] = True diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py index 76cee660..69bc71ad 100644 --- a/tests/test_blueprints.py +++ b/tests/test_blueprints.py @@ -634,10 +634,11 @@ def test_add_template_test_with_name_and_template(app, client): def test_context_processing(app, client): answer_bp = flask.Blueprint("answer_bp", __name__) - template_string = lambda: flask.render_template_string( # noqa: E731 - "{% if notanswer %}{{ notanswer }} is not the answer. {% endif %}" - "{% if answer %}{{ answer }} is the answer.{% endif %}" - ) + def template_string(): + return flask.render_template_string( + "{% if notanswer %}{{ notanswer }} is not the answer. {% endif %}" + "{% if answer %}{{ answer }} is the answer.{% endif %}" + ) # App global context processor @answer_bp.app_context_processor diff --git a/tests/test_signals.py b/tests/test_signals.py index 6174fe83..32ab333e 100644 --- a/tests/test_signals.py +++ b/tests/test_signals.py @@ -98,7 +98,7 @@ def test_request_exception_signal(): @app.route("/") def index(): - 1 // 0 + raise ZeroDivisionError def record(sender, exception): recorded.append(exception) @@ -169,7 +169,7 @@ def test_appcontext_tearing_down_signal(app, client): @app.route("/") def index(): - 1 // 0 + raise ZeroDivisionError flask.appcontext_tearing_down.connect(record_teardown, app) try: diff --git a/tests/test_testing.py b/tests/test_testing.py index 6a90cc45..de052152 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -219,7 +219,7 @@ def test_test_client_context_binding(app, client): @app.route("/other") def other(): - 1 // 0 + raise ZeroDivisionError with client: resp = client.get("/") @@ -227,18 +227,15 @@ def test_test_client_context_binding(app, client): assert resp.data == b"Hello World!" assert resp.status_code == 200 + with client: resp = client.get("/other") assert not hasattr(flask.g, "value") assert b"Internal Server Error" in resp.data assert resp.status_code == 500 flask.g.value = 23 - try: - flask.g.value - except (AttributeError, RuntimeError): - pass - else: - raise AssertionError("some kind of exception expected") + with pytest.raises(RuntimeError): + flask.g.value # noqa: B018 def test_reuse_client(client): diff --git a/tests/test_views.py b/tests/test_views.py index 8d870def..eab5eda2 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -41,10 +41,10 @@ def test_method_based_view(app): def test_view_patching(app): class Index(flask.views.MethodView): def get(self): - 1 // 0 + raise ZeroDivisionError def post(self): - 1 // 0 + raise ZeroDivisionError class Other(Index): def get(self): diff --git a/tests/typing/typing_app_decorators.py b/tests/typing/typing_app_decorators.py index 6b2188aa..c8d01113 100644 --- a/tests/typing/typing_app_decorators.py +++ b/tests/typing/typing_app_decorators.py @@ -1,7 +1,5 @@ from __future__ import annotations -import typing as t - from flask import Flask from flask import Response @@ -29,10 +27,10 @@ async def before_async() -> None: @app.teardown_appcontext -def teardown_sync(exc: t.Optional[BaseException]) -> None: +def teardown_sync(exc: BaseException | None) -> None: ... @app.teardown_appcontext -async def teardown_async(exc: t.Optional[BaseException]) -> None: +async def teardown_async(exc: BaseException | None) -> None: ... diff --git a/tests/typing/typing_route.py b/tests/typing/typing_route.py index bbd044ae..8bc271b2 100644 --- a/tests/typing/typing_route.py +++ b/tests/typing/typing_route.py @@ -29,12 +29,12 @@ def hello_json() -> Response: @app.route("/json/dict") -def hello_json_dict() -> t.Dict[str, t.Any]: +def hello_json_dict() -> dict[str, t.Any]: return {"response": "Hello, World!"} @app.route("/json/dict") -def hello_json_list() -> t.List[t.Any]: +def hello_json_list() -> list[t.Any]: return [{"message": "Hello"}, {"message": "World"}]