Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
ec3a2d6
Fix implicitly None-able types
NMertsch Oct 19, 2025
5aacfa8
Prefix unused unpacked variables with underscore
NMertsch Oct 19, 2025
88afbc9
Replace collection concatenations with unpacking
NMertsch Oct 19, 2025
d35c8b3
Normalize ambiguous non-ASCII characters in comments
NMertsch Oct 19, 2025
2b890ea
Simplify dict checks
NMertsch Oct 19, 2025
90c6c33
Don't create lists only to access the first item
NMertsch Oct 19, 2025
1595e70
Mark ambiguous non-ASCII characters
NMertsch Oct 19, 2025
08f6b7c
Escape '.' in Regex patterns
NMertsch Oct 19, 2025
d7ff87f
Remove unused noqa directives
NMertsch Oct 19, 2025
fad6a61
Annotate mutable class attributes as immutable
NMertsch Oct 19, 2025
1d3aef8
Enable RUF ruff rules
NMertsch Oct 19, 2025
94a218e
Add towncrier fragment
NMertsch Oct 19, 2025
dfbe982
Remove unnecessary parentheses on raised exceptions
NMertsch Oct 19, 2025
938fb5b
Enable RSE ruff rules
NMertsch Oct 19, 2025
ffdb57c
Update towncrier fragments
NMertsch Oct 19, 2025
be79a8c
Replace `yield` in pytest fixture without teardown with `return`
NMertsch Oct 19, 2025
296fa2a
Remove empty parentheses on `pytest.fixture` calls
NMertsch Oct 19, 2025
563d61c
Simplify `pytest.raises()` blocks
NMertsch Oct 19, 2025
1479207
Split `assert ... and ...` statements
NMertsch Oct 19, 2025
574a4fb
Remove duplicate test cases in `pytest.mark.parametrize`
NMertsch Oct 19, 2025
a7add3a
Use list instead of tuple for `pytest.mark.parametrize` cases
NMertsch Oct 19, 2025
29da6fc
Use tuple instead of str for `pytest.mark.parametrize` parameter list
NMertsch Oct 19, 2025
92d7bfc
Fix typo
NMertsch Oct 19, 2025
e1661bd
Check for error messages in `pytest.raises()`
NMertsch Oct 19, 2025
485a5bb
Enable PT ruff rules
NMertsch Oct 19, 2025
e9479a6
Add towncrier fragment
NMertsch Oct 19, 2025
004d924
Revert "Remove unnecessary parentheses on raised exceptions"
NMertsch Oct 20, 2025
274bca1
Revert activation of RSE ruff rules
NMertsch Oct 20, 2025
1e60e7e
Merge branch 'main' into enable-RUF-rules
NMertsch Oct 20, 2025
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: 1 addition & 0 deletions changes/2520.misc.1.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The `RUF ruff rule set <https://docs.astral.sh/ruff/rules/#ruff-specific-rules-ruf>`_ was enabled and all findings were fixed.
1 change: 1 addition & 0 deletions changes/2520.misc.3.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The `PT ruff rule set <https://docs.astral.sh/ruff/rules/#flake8-pytest-style-pt>`_ was enabled and all findings were fixed.
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,8 @@ extend-select = [
"ASYNC", # flake8-async
"C4", # flake8-comprehensions
"I", # isort
"RUF", # ruff-specific rules
"PT", # flake8-pytest-style
# The SIM rules are *very* opinionated, and don't necessarily make for better code.
# They may be worth occasionally turning on just to see if something could actually
# use improvement.
Expand Down
3 changes: 2 additions & 1 deletion src/briefcase/bootstraps/base.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

from collections.abc import Collection
from pathlib import Path
from typing import Any, TypedDict

Expand Down Expand Up @@ -29,7 +30,7 @@ class BaseGuiBootstrap:
# Any fields defined here must be implemented as methods that return a ``str``
# or ``None``. Returning ``None`` omits the field as a key in the context, thereby
# deferring the value for the field to the cookiecutter template.
fields: list[str] = [
fields: Collection[str] = [
"app_source",
"app_start_source",
"pyproject_table_briefcase_extra_content",
Expand Down
2 changes: 1 addition & 1 deletion src/briefcase/cmdline.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ def parse_known_args(args):
args, passthrough = split_passthrough(args)
options, extra = parser.parse_known_args(args)
if passthrough:
extra += ["--"] + passthrough
extra += ["--", *passthrough]
return options, extra

# Use parse_known_args to ensure any extra arguments can be ignored,
Expand Down
9 changes: 5 additions & 4 deletions src/briefcase/commands/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import sys
from abc import ABC, abstractmethod
from argparse import RawDescriptionHelpFormatter
from collections.abc import Collection
from datetime import datetime
from pathlib import Path
from typing import Any
Expand Down Expand Up @@ -125,7 +126,7 @@ def parse_config_overrides(config_overrides: list[str] | None) -> dict[str, Any]

class BaseCommand(ABC):
cmd_line = "briefcase {command} {platform} {output_format}"
supported_host_os = {"Darwin", "Linux", "Windows"}
supported_host_os: Collection[str] = {"Darwin", "Linux", "Windows"}
supported_host_os_reason = f"This command is not supported on {platform.system()}."

# defined by platform-specific subclasses
Expand All @@ -144,9 +145,9 @@ def __init__(
self,
console: Console,
tools: ToolCache = None,
apps: dict[str, AppConfig] = None,
base_path: Path = None,
data_path: Path = None,
apps: dict[str, AppConfig] | None = None,
base_path: Path | None = None,
data_path: Path | None = None,
is_clone: bool = False,
):
"""Base for all Commands.
Expand Down
2 changes: 1 addition & 1 deletion src/briefcase/commands/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -782,7 +782,7 @@ def __call__(
self,
template: str | None = None,
template_branch: str | None = None,
project_overrides: list[str] = None,
project_overrides: list[str] | None = None,
**options,
):
# Confirm host compatibility, and that all required tools are available.
Expand Down
34 changes: 28 additions & 6 deletions src/briefcase/commands/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import shutil
import subprocess
import sys
from collections.abc import Collection
from datetime import date, datetime
from pathlib import Path

Expand Down Expand Up @@ -96,7 +97,7 @@ def add_options(self, parser):
)

# app properties that won't be exposed to the context
hidden_app_properties = {"permission"}
hidden_app_properties: Collection[str] = {"permission"}

@property
def app_template_url(self) -> str:
Expand Down Expand Up @@ -1043,11 +1044,32 @@ def _has_url(requirement):
return any(
f"{scheme}:" in requirement
for scheme in (
["http", "https", "file", "ftp"]
+ ["git+file", "git+https", "git+ssh", "git+http", "git+git", "git"]
+ ["hg+file", "hg+http", "hg+https", "hg+ssh", "hg+static-http"]
+ ["svn", "svn+svn", "svn+http", "svn+https", "svn+ssh"]
+ ["bzr+http", "bzr+https", "bzr+ssh", "bzr+sftp", "bzr+ftp", "bzr+lp"]
"http",
"https",
"file",
"ftp",
"git+file",
"git+https",
"git+ssh",
"git+http",
"git+git",
"git",
"hg+file",
"hg+http",
"hg+https",
"hg+ssh",
"hg+static-http",
"svn",
"svn+svn",
"svn+http",
"svn+https",
"svn+ssh",
"bzr+http",
"bzr+https",
"bzr+ssh",
"bzr+sftp",
"bzr+ftp",
"bzr+lp",
)
)

Expand Down
5 changes: 3 additions & 2 deletions src/briefcase/commands/dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import os
import subprocess
import sys
from collections.abc import Mapping
from pathlib import Path

from briefcase.commands.run import RunAppMixin
Expand All @@ -27,7 +28,7 @@ class DevCommand(RunAppMixin, BaseCommand):
# rather patently, Not Good.
# To avoid this causing unwanted hilarity, we use environment variables to configure the
# Python interpreter rather than command-line options.
DEV_ENVIRONMENT = {
DEV_ENVIRONMENT: Mapping[str, str] = {
# Equivalent of passing "-u"
"PYTHONUNBUFFERED": "1",
# Equivalent of passing "-X dev"
Expand Down Expand Up @@ -230,7 +231,7 @@ def __call__(
# in pyproject.toml, then we can use it as a default;
# otherwise look for a -a/--app option.
if len(self.apps) == 1:
app = list(self.apps.values())[0]
app = next(iter(self.apps.values()))
elif appname:
try:
app = self.apps[appname]
Expand Down
2 changes: 1 addition & 1 deletion src/briefcase/commands/new.py
Original file line number Diff line number Diff line change
Expand Up @@ -598,7 +598,7 @@ def __call__(
self,
template: str | None = None,
template_branch: str | None = None,
project_overrides: list[str] = None,
project_overrides: list[str] | None = None,
**options,
):
# Confirm host compatibility, and that all required tools are available.
Expand Down
2 changes: 1 addition & 1 deletion src/briefcase/commands/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ def __call__(
# in pyproject.toml, then we can use it as a default;
# otherwise look for a -a/--app option.
if len(self.apps) == 1:
app = list(self.apps.values())[0]
app = next(iter(self.apps.values()))
elif appname:
try:
app = self.apps[appname]
Expand Down
2 changes: 1 addition & 1 deletion src/briefcase/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -660,7 +660,7 @@ def parse_config(config_file, platform, output_format, console):
except KeyError as e:
raise BriefcaseConfigError("No Briefcase apps defined in pyproject.toml") from e

for name, config in [("project", global_config)] + list(all_apps.items()):
for name, config in [("project", global_config), *all_apps.items()]:
if isinstance(config.get("license"), str):
section_name = "the Project" if name == "project" else f"{name!r}"
console.warning(
Expand Down
6 changes: 3 additions & 3 deletions src/briefcase/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import textwrap
import time
import traceback
from collections.abc import Callable, Iterable, Mapping
from collections.abc import Callable, Collection, Iterable, Mapping
from contextlib import contextmanager
from datetime import datetime
from enum import IntEnum
Expand Down Expand Up @@ -68,7 +68,7 @@ class RichConsoleHighlighter(RegexHighlighter):
"""

base_style = "repr."
highlights = [
highlights: Collection[str] = [
r"(?P<url>(file|https|http|ws|wss)://[-0-9a-zA-Z$_+!`(),.?/;:&=%#~]*)"
]

Expand Down Expand Up @@ -100,7 +100,7 @@ class LogLevel(IntEnum):


class NotDeadYet:
# Im getting better! No youre not, youll be stone dead in a minute.
# I'm getting better! No, you're not, you'll be stone dead in a minute.

def __init__(self, console: Console):
"""A keep-alive spinner for long-running processes without console output.
Expand Down
4 changes: 2 additions & 2 deletions src/briefcase/integrations/android_sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -1282,7 +1282,7 @@ def start_emulator(

# Start the emulator
emulator_popen = self.tools.subprocess.Popen(
[self.emulator_path, f"@{avd}", "-dns-server", "8.8.8.8"] + extra_args,
[self.emulator_path, f"@{avd}", "-dns-server", "8.8.8.8", *extra_args],
env=self.env,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
Expand Down Expand Up @@ -1471,7 +1471,7 @@ def run(self, *arguments: SubprocessArgT, quiet: int = 0) -> str:
# This keeps performance good in the success case.
try:
output = self.tools.subprocess.check_output(
[self.tools.android_sdk.adb_path, "-s", self.device] + list(arguments),
[self.tools.android_sdk.adb_path, "-s", self.device, *arguments],
quiet=quiet,
)
# add returns status code 0 in the case of failure. The only tangible evidence
Expand Down
4 changes: 2 additions & 2 deletions src/briefcase/integrations/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import sys
from abc import ABC, abstractmethod
from collections import defaultdict
from collections.abc import Mapping
from collections.abc import Collection, Mapping
from functools import cached_property
from pathlib import Path
from typing import TYPE_CHECKING, TypeVar
Expand Down Expand Up @@ -57,7 +57,7 @@ class Tool(ABC):

name: str
full_name: str
supported_host_os: set[str] = {"Darwin", "Linux", "Windows"}
supported_host_os: Collection[str] = {"Darwin", "Linux", "Windows"}

def __init__(self, tools: ToolCache, **kwargs):
self.tools = tools
Expand Down
2 changes: 1 addition & 1 deletion src/briefcase/integrations/docker.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ class Docker(Tool):
"""

# Platform-specific template context dictionary for Docker installation details
DOCKER_INSTALL_URL = {
DOCKER_INSTALL_URL: Mapping[str, str] = {
"Windows": "https://docs.docker.com/docker-for-windows/install/",
"Darwin": "https://docs.docker.com/docker-for-mac/install/",
"Linux": "https://docs.docker.com/engine/install/#server",
Expand Down
2 changes: 1 addition & 1 deletion src/briefcase/integrations/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ def download(self, url: str, download_path: Path, role: str | None = None) -> Pa
:returns: The filename of the downloaded (or cached) file.
"""
download_path.mkdir(parents=True, exist_ok=True)
filename: Path = None
filename: Path | None = None
try:
with self.tools.httpx.stream(
"GET",
Expand Down
3 changes: 2 additions & 1 deletion src/briefcase/integrations/flatpak.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import subprocess
from collections.abc import Collection
from pathlib import Path

from briefcase.exceptions import BriefcaseCommandError
Expand All @@ -11,7 +12,7 @@
class Flatpak(Tool):
name = "flatpak"
full_name = "Flatpak"
supported_host_os = {"Linux"}
supported_host_os: Collection[str] = {"Linux"}

DEFAULT_REPO_ALIAS = "flathub"
DEFAULT_REPO_URL = "https://flathub.org/repo/flathub.flatpakrepo"
Expand Down
3 changes: 2 additions & 1 deletion src/briefcase/integrations/linuxdeploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import hashlib
import shlex
from abc import ABC, abstractmethod
from collections.abc import Collection
from pathlib import Path
from typing import TypeVar
from urllib.parse import urlparse
Expand Down Expand Up @@ -32,7 +33,7 @@ class LinuxDeployBase(ABC):
# because verification only requires downloading and permission checks, not
# execution. The commands where the LinuxDeploy tool is actually used do the
# additional check to ensure that if we're on macOS, we're also using Docker.
supported_host_os = {"Darwin", "Linux"}
supported_host_os: Collection[str] = {"Darwin", "Linux"}

@property
@abstractmethod
Expand Down
3 changes: 2 additions & 1 deletion src/briefcase/integrations/rcedit.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

from collections.abc import Collection
from pathlib import Path

from briefcase.exceptions import MissingToolError
Expand All @@ -9,7 +10,7 @@
class RCEdit(ManagedTool):
name = "rcedit"
full_name = "RCEdit"
supported_host_os = {"Windows"}
supported_host_os: Collection[str] = {"Windows"}

@property
def download_url(self) -> str:
Expand Down
7 changes: 4 additions & 3 deletions src/briefcase/integrations/visualstudio.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import subprocess
from collections.abc import Collection
from pathlib import Path

from briefcase.exceptions import BriefcaseCommandError, CommandOutputParseError
Expand All @@ -11,7 +12,7 @@
class VisualStudio(Tool):
name = "visualstudio"
full_name = "Visual Studio"
supported_host_os = {"Windows"}
supported_host_os: Collection[str] = {"Windows"}
VSCODE_REQUIRED_COMPONENTS = """
* .NET Desktop Development
- Default packages
Expand All @@ -24,7 +25,7 @@ def __init__(
self,
tools: ToolCache,
msbuild_path: Path,
install_metadata: dict[str, str | int | bool] = None,
install_metadata: dict[str, str | int | bool] | None = None,
):
super().__init__(tools=tools)
self._msbuild_path = msbuild_path
Expand Down Expand Up @@ -79,7 +80,7 @@ def verify_install(cls, tools: ToolCache, **kwargs) -> VisualStudio:
# Look for an %MSBUILD% environment variable
try:
msbuild_path = Path(tools.os.environ["MSBUILD"])
install_metadata: dict[str, str | int | bool] = None
install_metadata: dict[str, str | int | bool] | None = None

if not msbuild_path.exists():
# The location referenced by %MSBUILD% doesn't exist
Expand Down
6 changes: 3 additions & 3 deletions src/briefcase/integrations/windows_sdk.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from __future__ import annotations

import subprocess
from collections.abc import Iterator
from collections.abc import Collection, Iterator
from pathlib import Path

# winreg can only be imported on Windows
Expand All @@ -17,7 +17,7 @@
class WindowsSDK(Tool):
name = "windows_sdk"
full_name = "Windows SDK"
supported_host_os = {"Windows"}
supported_host_os: Collection[str] = {"Windows"}

SDK_VERSION = "10.0"
# Oldest supported SDK version is 10.0.15063.0
Expand All @@ -29,7 +29,7 @@ class WindowsSDK(Tool):
# Subkey for "latest" installed SDK version
SDK_VERSION_KEY = "ProductVersion"
# As a fallback, possible default locations for SDK
DEFAULT_SDK_DIRS = [
DEFAULT_SDK_DIRS: Collection[Path] = [
Path(rf"C:\Program Files (x86)\Windows Kits\{SDK_VERSION.split('.')[0]}")
]
# Installing parts of the SDK for UWP apps is not inherently required; however,
Expand Down
3 changes: 2 additions & 1 deletion src/briefcase/integrations/wix.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import re
from collections.abc import Collection
from pathlib import Path
from subprocess import CalledProcessError

Expand All @@ -13,7 +14,7 @@
class WiX(ManagedTool):
name = "wix"
full_name = "WiX"
supported_host_os = {"Windows"}
supported_host_os: Collection[str] = {"Windows"}

# WARNING: version 6 and later have licensing issues: see
# https://github.com/beeware/briefcase/issues/1185.
Expand Down
Loading