diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 6ac59c8e2..000000000 --- a/.flake8 +++ /dev/null @@ -1,29 +0,0 @@ -[flake8] -extend-select = - # bugbear - B - # bugbear opinions - B9 - # implicit str concat - ISC -extend-ignore = - # slice notation whitespace, invalid - E203 - # import at top, too many circular import fixes - E402 - # 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 - **/__init__.py: F401 - # LocalProxy assigns lambdas - src/werkzeug/local.py: E731 diff --git a/.gitignore b/.gitignore index aecea1a7b..cd9550b9e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,26 +1,11 @@ -MANIFEST -build -dist -/src/Werkzeug.egg-info -*.pyc -*.pyo -.venv -.DS_Store -docs/_build -bench/a -bench/b -.tox +.idea/ +.vscode/ +__pycache__/ +.pytest_cache/ +.tox/ .coverage .coverage.* -coverage_out -htmlcov -.cache -.xprocess -.hypothesis -test_uwsgi_failed -.idea -.pytest_cache/ +htmlcov/ +docs/_build/ +dist/ venv/ -.vscode -.mypy_cache/ -.dmypy.json diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6425015cf..447fd5869 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,42 +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.5 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"] - - id: reorder-python-imports - name: Reorder Python imports (examples) - files: "^examples/" - args: ["--application-directories", "examples"] - - 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 - - repo: https://github.com/peterdemin/pip-compile-multi - rev: v2.6.3 - hooks: - - id: pip-compile-multi-verify + - id: ruff + - id: ruff-format - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.5.0 hooks: + - id: check-merge-conflict + - id: debug-statements - id: fix-byte-order-marker - id: trailing-whitespace - id: end-of-file-fixer - exclude: "^tests/.*.http$" diff --git a/pyproject.toml b/pyproject.toml index 3a1965554..3c3e766e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -102,3 +102,28 @@ module = [ "xprocess.*", ] ignore_missing_imports = true + +[tool.ruff] +extend-exclude = ["examples/"] +src = ["src"] +fix = false +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 = [ + "E402" # allow circular imports at end of file +] +ignore-init-module-imports = true + +[tool.ruff.lint.isort] +force-single-line = true +order-by-type = false diff --git a/src/werkzeug/datastructures/auth.py b/src/werkzeug/datastructures/auth.py index 2f2515020..04ebfb2e7 100644 --- a/src/werkzeug/datastructures/auth.py +++ b/src/werkzeug/datastructures/auth.py @@ -128,7 +128,7 @@ def to_header(self) -> str: if self.type == "basic": value = base64.b64encode( f"{self.username}:{self.password}".encode() - ).decode("utf8") + ).decode("ascii") return f"Basic {value}" if self.token is not None: @@ -269,7 +269,9 @@ def set_basic(self, realm: str = "authentication required") -> None: """ warnings.warn( "The 'set_basic' method is deprecated and will be removed in Werkzeug 3.0." - " Create and assign an instance instead." + " Create and assign an instance instead.", + DeprecationWarning, + stacklevel=2, ) self._type = "basic" dict.clear(self.parameters) # type: ignore[arg-type] @@ -296,7 +298,9 @@ def set_digest( """ warnings.warn( "The 'set_digest' method is deprecated and will be removed in Werkzeug 3.0." - " Create and assign an instance instead." + " Create and assign an instance instead.", + DeprecationWarning, + stacklevel=2, ) self._type = "digest" dict.clear(self.parameters) # type: ignore[arg-type] diff --git a/src/werkzeug/datastructures/mixins.pyi b/src/werkzeug/datastructures/mixins.pyi index 74ed4b81e..40453f703 100644 --- a/src/werkzeug/datastructures/mixins.pyi +++ b/src/werkzeug/datastructures/mixins.pyi @@ -21,7 +21,7 @@ class ImmutableListMixin(list[V]): _hash_cache: int | None def __hash__(self) -> int: ... # type: ignore def __delitem__(self, key: SupportsIndex | slice) -> NoReturn: ... - def __iadd__(self, other: t.Any) -> NoReturn: ... # type: ignore + def __iadd__(self, other: Any) -> NoReturn: ... # type: ignore def __imul__(self, other: SupportsIndex) -> NoReturn: ... def __setitem__(self, key: int | slice, value: V) -> NoReturn: ... # type: ignore def append(self, value: V) -> NoReturn: ... diff --git a/src/werkzeug/datastructures/structures.py b/src/werkzeug/datastructures/structures.py index 7ea7bee28..e863cd8a4 100644 --- a/src/werkzeug/datastructures/structures.py +++ b/src/werkzeug/datastructures/structures.py @@ -146,7 +146,7 @@ class MultiDict(TypeConversionDict): def __init__(self, mapping=None): if isinstance(mapping, MultiDict): - dict.__init__(self, ((k, l[:]) for k, l in mapping.lists())) + dict.__init__(self, ((k, vs[:]) for k, vs in mapping.lists())) elif isinstance(mapping, dict): tmp = {} for key, value in mapping.items(): diff --git a/src/werkzeug/debug/__init__.py b/src/werkzeug/debug/__init__.py index 3b04b534e..f8756d890 100644 --- a/src/werkzeug/debug/__init__.py +++ b/src/werkzeug/debug/__init__.py @@ -110,7 +110,7 @@ def _generate() -> str | bytes | None: guid, guid_type = winreg.QueryValueEx(rk, "MachineGuid") if guid_type == winreg.REG_SZ: - return guid.encode("utf-8") + return guid.encode() return guid except OSError: @@ -193,7 +193,7 @@ def get_pin_and_cookie_name( if not bit: continue if isinstance(bit, str): - bit = bit.encode("utf-8") + bit = bit.encode() h.update(bit) h.update(b"cookiesalt") diff --git a/src/werkzeug/debug/repr.py b/src/werkzeug/debug/repr.py index 3bf15a77a..1dcdd67be 100644 --- a/src/werkzeug/debug/repr.py +++ b/src/werkzeug/debug/repr.py @@ -80,9 +80,7 @@ def __call__(self, topic: t.Any | None = None) -> None: helper = _Helper() -def _add_subclass_info( - inner: str, obj: object, base: t.Type | tuple[t.Type, ...] -) -> str: +def _add_subclass_info(inner: str, obj: object, base: type | tuple[type, ...]) -> str: if isinstance(base, tuple): for cls in base: if type(obj) is cls: @@ -96,7 +94,7 @@ def _add_subclass_info( def _sequence_repr_maker( - left: str, right: str, base: t.Type, limit: int = 8 + left: str, right: str, base: type, limit: int = 8 ) -> t.Callable[[DebugReprGenerator, t.Iterable, bool], str]: def proxy(self: DebugReprGenerator, obj: t.Iterable, recursive: bool) -> str: if recursive: diff --git a/src/werkzeug/debug/tbtools.py b/src/werkzeug/debug/tbtools.py index c45f56ef0..f9be17c42 100644 --- a/src/werkzeug/debug/tbtools.py +++ b/src/werkzeug/debug/tbtools.py @@ -265,7 +265,9 @@ def all_tracebacks( @cached_property def all_frames(self) -> list[DebugFrameSummary]: return [ - f for _, te in self.all_tracebacks for f in te.stack # type: ignore[misc] + f # type: ignore[misc] + for _, te in self.all_tracebacks + for f in te.stack ] def render_traceback_text(self) -> str: diff --git a/src/werkzeug/formparser.py b/src/werkzeug/formparser.py index 25ef0d61b..4dcaea2f6 100644 --- a/src/werkzeug/formparser.py +++ b/src/werkzeug/formparser.py @@ -225,11 +225,14 @@ def __init__( def get_parse_func( self, mimetype: str, options: dict[str, str] - ) -> None | ( - t.Callable[ - [FormDataParser, t.IO[bytes], str, int | None, dict[str, str]], - t_parse_result, - ] + ) -> ( + None + | ( + t.Callable[ + [FormDataParser, t.IO[bytes], str, int | None, dict[str, str]], + t_parse_result, + ] + ) ): warnings.warn( "The 'get_parse_func' method is deprecated and will be" diff --git a/src/werkzeug/local.py b/src/werkzeug/local.py index fba80e974..525ac0c80 100644 --- a/src/werkzeug/local.py +++ b/src/werkzeug/local.py @@ -556,9 +556,7 @@ def _get_current_object() -> T: # __weakref__ (__getattr__) # __init_subclass__ (proxying metaclass not supported) # __prepare__ (metaclass) - __class__ = _ProxyLookup( - fallback=lambda self: type(self), is_attr=True - ) # type: ignore + __class__ = _ProxyLookup(fallback=lambda self: type(self), is_attr=True) # type: ignore[assignment] __instancecheck__ = _ProxyLookup(lambda self, other: isinstance(other, self)) __subclasscheck__ = _ProxyLookup(lambda self, other: issubclass(other, self)) # __class_getitem__ triggered through __getitem__ diff --git a/src/werkzeug/middleware/lint.py b/src/werkzeug/middleware/lint.py index 462959943..8c858673b 100644 --- a/src/werkzeug/middleware/lint.py +++ b/src/werkzeug/middleware/lint.py @@ -37,7 +37,7 @@ class HTTPWarning(Warning): """Warning class for HTTP warnings.""" -def check_type(context: str, obj: object, need: t.Type = str) -> None: +def check_type(context: str, obj: object, need: type = str) -> None: if type(obj) is not need: warn( f"{context!r} requires {need.__name__!r}, got {type(obj).__name__!r}.", @@ -180,30 +180,44 @@ def close(self) -> None: key ): warn( - f"Entity header {key!r} found in 304 response.", HTTPWarning + f"Entity header {key!r} found in 304 response.", + HTTPWarning, + stacklevel=2, ) if bytes_sent: - warn("304 responses must not have a body.", HTTPWarning) + warn( + "304 responses must not have a body.", + HTTPWarning, + stacklevel=2, + ) elif 100 <= status_code < 200 or status_code == 204: if content_length != 0: warn( f"{status_code} responses must have an empty content length.", HTTPWarning, + stacklevel=2, ) if bytes_sent: - warn(f"{status_code} responses must not have a body.", HTTPWarning) + warn( + f"{status_code} responses must not have a body.", + HTTPWarning, + stacklevel=2, + ) elif content_length is not None and content_length != bytes_sent: warn( "Content-Length and the number of bytes sent to the" " client do not match.", WSGIWarning, + stacklevel=2, ) def __del__(self) -> None: if not self.closed: try: warn( - "Iterator was garbage collected before it was closed.", WSGIWarning + "Iterator was garbage collected before it was closed.", + WSGIWarning, + stacklevel=2, ) except Exception: pass @@ -236,7 +250,7 @@ def __init__(self, app: WSGIApplication) -> None: self.app = app def check_environ(self, environ: WSGIEnvironment) -> None: - if type(environ) is not dict: + if type(environ) is not dict: # noqa: E721 warn( "WSGI environment is not a standard Python dict.", WSGIWarning, @@ -304,14 +318,14 @@ def check_start_response( if status_code < 100: warn("Status code < 100 detected.", WSGIWarning, stacklevel=3) - if type(headers) is not list: + if type(headers) is not list: # noqa: E721 warn("Header list is not a list.", WSGIWarning, stacklevel=3) for item in headers: if type(item) is not tuple or len(item) != 2: warn("Header items must be 2-item tuples.", WSGIWarning, stacklevel=3) name, value = item - if type(name) is not str or type(value) is not str: + if type(name) is not str or type(value) is not str: # noqa: E721 warn( "Header keys and values must be strings.", WSGIWarning, stacklevel=3 ) @@ -402,13 +416,17 @@ def checking_start_response( ) if kwargs: - warn("'start_response' does not take keyword arguments.", WSGIWarning) + warn( + "'start_response' does not take keyword arguments.", + WSGIWarning, + stacklevel=2, + ) status: str = args[0] headers: list[tuple[str, str]] = args[1] exc_info: None | ( tuple[type[BaseException], BaseException, TracebackType] - ) = (args[2] if len(args) == 3 else None) + ) = args[2] if len(args) == 3 else None headers_set[:] = self.check_start_response(status, headers, exc_info) return GuardedWrite(start_response(status, headers, exc_info), chunks) diff --git a/src/werkzeug/routing/rules.py b/src/werkzeug/routing/rules.py index 904a02258..a10fa7365 100644 --- a/src/werkzeug/routing/rules.py +++ b/src/werkzeug/routing/rules.py @@ -108,7 +108,7 @@ def _pythonize(value: str) -> None | bool | int | float | str: return str(value) -def parse_converter_args(argstr: str) -> tuple[t.Tuple, dict[str, t.Any]]: +def parse_converter_args(argstr: str) -> tuple[tuple[t.Any, ...], dict[str, t.Any]]: argstr += "," args = [] kwargs = {} @@ -566,7 +566,7 @@ def get_converter( self, variable_name: str, converter_name: str, - args: t.Tuple, + args: tuple[t.Any, ...], kwargs: t.Mapping[str, t.Any], ) -> BaseConverter: """Looks up the converter for the given parameter. diff --git a/src/werkzeug/sansio/request.py b/src/werkzeug/sansio/request.py index 0bcda90b2..def060553 100644 --- a/src/werkzeug/sansio/request.py +++ b/src/werkzeug/sansio/request.py @@ -1,6 +1,5 @@ from __future__ import annotations -import typing as t import warnings from datetime import datetime from urllib.parse import parse_qsl @@ -174,7 +173,7 @@ def url_charset(self, value: str) -> None: #: (for example for :attr:`access_list`). #: #: .. versionadded:: 0.6 - list_storage_class: type[t.List] = ImmutableList + list_storage_class: type[list] = ImmutableList user_agent_class: type[UserAgent] = UserAgent """The class used and returned by the :attr:`user_agent` property to diff --git a/src/werkzeug/security.py b/src/werkzeug/security.py index 282c4fd8c..359870d7f 100644 --- a/src/werkzeug/security.py +++ b/src/werkzeug/security.py @@ -33,8 +33,8 @@ def _hash_internal(method: str, salt: str, password: str) -> tuple[str, str]: return password, method method, *args = method.split(":") - salt = salt.encode("utf-8") - password = password.encode("utf-8") + salt = salt.encode() + password = password.encode() if method == "scrypt": if not args: diff --git a/src/werkzeug/urls.py b/src/werkzeug/urls.py index f5760eb4c..b3d05ed75 100644 --- a/src/werkzeug/urls.py +++ b/src/werkzeug/urls.py @@ -1123,7 +1123,10 @@ def url_decode( separator = separator.encode(charset or "ascii") # type: ignore return cls( _url_decode_impl( - s.split(separator), charset, include_empty, errors # type: ignore + s.split(separator), # type: ignore[arg-type] + charset, + include_empty, + errors, ) ) diff --git a/src/werkzeug/utils.py b/src/werkzeug/utils.py index 785ac28b9..32ca9dad6 100644 --- a/src/werkzeug/utils.py +++ b/src/werkzeug/utils.py @@ -514,7 +514,7 @@ def send_file( if isinstance(etag, str): rv.set_etag(etag) elif etag and path is not None: - check = adler32(path.encode("utf-8")) & 0xFFFFFFFF + check = adler32(path.encode()) & 0xFFFFFFFF rv.set_etag(f"{mtime}-{size}-{check}") if conditional: diff --git a/src/werkzeug/wrappers/__init__.py b/src/werkzeug/wrappers/__init__.py index b8c45d71c..b36f228f2 100644 --- a/src/werkzeug/wrappers/__init__.py +++ b/src/werkzeug/wrappers/__init__.py @@ -1,3 +1,3 @@ from .request import Request as Request from .response import Response as Response -from .response import ResponseStream +from .response import ResponseStream as ResponseStream diff --git a/tests/live_apps/data_app.py b/tests/live_apps/data_app.py index 561390a1c..9b2e78b91 100644 --- a/tests/live_apps/data_app.py +++ b/tests/live_apps/data_app.py @@ -11,7 +11,7 @@ def app(request: Request) -> Response: { "environ": request.environ, "form": request.form.to_dict(), - "files": {k: v.read().decode("utf8") for k, v in request.files.items()}, + "files": {k: v.read().decode() for k, v in request.files.items()}, }, default=lambda x: str(x), ), diff --git a/tests/sansio/test_multipart.py b/tests/sansio/test_multipart.py index 35109d4bd..cf36fefd6 100644 --- a/tests/sansio/test_multipart.py +++ b/tests/sansio/test_multipart.py @@ -24,11 +24,7 @@ def test_decoder_simple() -> None: asdasd -----------------------------9704338192090380615194531385$-- - """.replace( - "\n", "\r\n" - ).encode( - "utf-8" - ) + """.replace("\n", "\r\n").encode() decoder.receive_data(data) decoder.receive_data(None) events = [decoder.next_event()] @@ -147,11 +143,7 @@ def test_empty_field() -> None: Content-Type: text/plain; charset="UTF-8" --foo-- - """.replace( - "\n", "\r\n" - ).encode( - "utf-8" - ) + """.replace("\n", "\r\n").encode() decoder.receive_data(data) decoder.receive_data(None) events = [decoder.next_event()] diff --git a/tests/sansio/test_utils.py b/tests/sansio/test_utils.py index 04d02e44c..d43de66c2 100644 --- a/tests/sansio/test_utils.py +++ b/tests/sansio/test_utils.py @@ -1,7 +1,5 @@ from __future__ import annotations -import typing as t - import pytest from werkzeug.sansio.utils import get_content_length @@ -28,8 +26,8 @@ ) def test_get_host( scheme: str, - host_header: t.Optional[str], - server: t.Optional[t.Tuple[str, t.Optional[int]]], + host_header: str | None, + server: tuple[str, int | None] | None, expected: str, ) -> None: assert get_host(scheme, host_header, server) == expected diff --git a/tests/test_formparser.py b/tests/test_formparser.py index 1dcb167ef..1ecb01208 100644 --- a/tests/test_formparser.py +++ b/tests/test_formparser.py @@ -273,7 +273,7 @@ def test_basic(self): content_type=f'multipart/form-data; boundary="{boundary}"', content_length=len(data), ) as response: - assert response.get_data() == repr(text).encode("utf-8") + assert response.get_data() == repr(text).encode() @pytest.mark.filterwarnings("ignore::pytest.PytestUnraisableExceptionWarning") def test_ie7_unc_path(self): diff --git a/tests/test_local.py b/tests/test_local.py index 2af69d2d6..2250a5bee 100644 --- a/tests/test_local.py +++ b/tests/test_local.py @@ -170,7 +170,7 @@ class SomeClassWithWrapped: _cv_val.set(42) with pytest.raises(AttributeError): - proxy.__wrapped__ + proxy.__wrapped__ # noqa: B018 ns = local.Local(_cv_ns) ns.foo = SomeClassWithWrapped() @@ -179,7 +179,7 @@ class SomeClassWithWrapped: assert ns("foo").__wrapped__ == "wrapped" with pytest.raises(AttributeError): - ns("bar").__wrapped__ + ns("bar").__wrapped__ # noqa: B018 def test_proxy_doc(): diff --git a/tests/test_routing.py b/tests/test_routing.py index 65d2a5f90..416fb4fc5 100644 --- a/tests/test_routing.py +++ b/tests/test_routing.py @@ -791,7 +791,7 @@ def __init__(self, url_map, *items): self.regex = items[0] # This is a regex pattern with nested groups - DATE_PATTERN = r"((\d{8}T\d{6}([.,]\d{1,3})?)|(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}([.,]\d{1,3})?))Z" # noqa: B950 + DATE_PATTERN = r"((\d{8}T\d{6}([.,]\d{1,3})?)|(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}([.,]\d{1,3})?))Z" # noqa: E501 map = r.Map( [ diff --git a/tests/test_urls.py b/tests/test_urls.py index 0b0f2aeed..bafccd185 100644 --- a/tests/test_urls.py +++ b/tests/test_urls.py @@ -396,5 +396,5 @@ def test_url_parse_does_not_clear_warnings_registry(recwarn): warnings.simplefilter("ignore", DeprecationWarning) for _ in range(2): urls.url_parse("http://example.org/") - warnings.warn("test warning") + warnings.warn("test warning", stacklevel=1) assert len(recwarn) == 1 diff --git a/tests/test_wrappers.py b/tests/test_wrappers.py index 8a91aefc1..d7bc12b95 100644 --- a/tests/test_wrappers.py +++ b/tests/test_wrappers.py @@ -1037,25 +1037,25 @@ class MyRequest(wrappers.Request): parameter_storage_class = dict req = MyRequest.from_values("/?foo=baz", headers={"Cookie": "foo=bar"}) - assert type(req.cookies) is dict + assert type(req.cookies) is dict # noqa: E721 assert req.cookies == {"foo": "bar"} - assert type(req.access_route) is list + assert type(req.access_route) is list # noqa: E721 - assert type(req.args) is dict - assert type(req.values) is CombinedMultiDict + assert type(req.args) is dict # noqa: E721 + assert type(req.values) is CombinedMultiDict # noqa: E721 assert req.values["foo"] == "baz" req = wrappers.Request.from_values(headers={"Cookie": "foo=bar;foo=baz"}) - assert type(req.cookies) is ImmutableMultiDict + assert type(req.cookies) is ImmutableMultiDict # noqa: E721 assert req.cookies.to_dict() == {"foo": "bar"} # it is possible to have multiple cookies with the same name assert req.cookies.getlist("foo") == ["bar", "baz"] - assert type(req.access_route) is ImmutableList + assert type(req.access_route) is ImmutableList # noqa: E721 MyRequest.list_storage_class = tuple req = MyRequest.from_values() - assert type(req.access_route) is tuple + assert type(req.access_route) is tuple # noqa: E721 def test_response_headers_passthrough():