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
9 changes: 3 additions & 6 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,11 @@ repos:
entry: .venv/bin/ruff format
language: system
types: [python]
- id: mypy
name: mypy
entry: .venv/bin/mypy
- id: ty
name: ty check
entry: .venv/bin/ty check
language: system
types: [python]
# parallel mypy processes share .mypy_cache, and mypy 2 can crash
# with an INTERNAL ERROR when they race on it
require_serial: true
- id: shellcheck
name: shellcheck
entry: .venv/bin/shellcheck
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Co-Authored-By: <agent and model> <email>
- Set up: `uv sync`
- Test: `uv run pytest tests/`
- Test with coverage, as CI runs it: `uv run coverage erase && uv run coverage run -m pytest && uv run coverage report`
- Lint, format, and type-check (ruff, mypy, shellcheck): `uv run prek run --all-files`
- Lint, format, and type-check (ruff, ty, shellcheck): `uv run prek run --all-files`
- Run your checkout against another repo: `uv run --project ~/code/devenv devenv sync` from that repo's root
- Run the `bootstrap` workflow locally with `gh act`: `make test`

Expand Down
2 changes: 1 addition & 1 deletion devenv/doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class Check:
check: Callable[[], tuple[bool, str]]
fix: Callable[[], tuple[bool, str]]

def __init__(self, module: ModuleType):
def __init__(self, module: ModuleType) -> None:
# Check that the module has the required attributes.
if not hasattr(module, "name"):
raise ValueError("missing the `name` attribute")
Expand Down
6 changes: 5 additions & 1 deletion devenv/lib/archive.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import urllib.request
from collections.abc import Generator
from collections.abc import Sequence
from typing import BinaryIO
from typing import cast
from urllib.error import HTTPError

from devenv.constants import home
Expand Down Expand Up @@ -59,9 +61,10 @@ def download(
req = urllib.request.Request(url, headers=headers)

retry_sleep = 1.0
resp: BinaryIO | None = None
while retries >= 0:
try:
resp = urllib.request.urlopen(req)
resp = cast(BinaryIO, urllib.request.urlopen(req))
break
except HTTPError as e:
if retries == 0:
Expand All @@ -72,6 +75,7 @@ def download(
retries -= 1
retry_sleep *= retry_exp

assert resp is not None
dest_dir = os.path.dirname(dest)
os.makedirs(dest_dir, exist_ok=True)

Expand Down
3 changes: 2 additions & 1 deletion devenv/lib/colima.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from collections.abc import Collection
from dataclasses import dataclass
from enum import Enum
from typing import cast

from devenv import constants
from devenv.constants import SYSTEM_MACHINE
Expand Down Expand Up @@ -310,7 +311,7 @@ def _legacy_root_disk(config: colima_config.ColimaConfig) -> bool:
f"Colima store metadata {store_path} has an unsupported "
"format; inspect the existing VM before starting it."
)
store = parsed
store = cast(dict[str, object], parsed)

if store is not None:
formatted = store.get("disk_formatted")
Expand Down
13 changes: 9 additions & 4 deletions devenv/lib/colima_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,11 @@ def _mapping(line: str, indent: int) -> tuple[str, str] | None:
match = _MAPPING.match(_line_text(line))
if match is None or len(match.group("indent")) != indent:
return None
return match.group("key"), _commentless(match.group("rest"))
key = match.group("key")
value = match.group("rest")
if not isinstance(key, str) or not isinstance(value, str):
return None
return key, _commentless(value)


def _quoted_key(line: str, indent: int) -> bool:
Expand Down Expand Up @@ -392,7 +396,7 @@ def set_snapshotter(path: str, snapshotter: bool) -> bool:
"starting Colima."
) from error

temporary_path = ""
temporary_path: str | None = None
try:
descriptor, temporary_path = tempfile.mkstemp(
prefix=".colima.yaml.", dir=os.path.dirname(path)
Expand All @@ -402,8 +406,9 @@ def set_snapshotter(path: str, snapshotter: bool) -> bool:
file.write(replacement)
file.flush()
os.fsync(file.fileno())
assert temporary_path is not None
os.replace(temporary_path, path)
temporary_path = ""
temporary_path = None
except OSError as error:
raise StorageMigrationBlocked(
f"Unable to update Colima config {path}: {error}. The original "
Expand All @@ -412,7 +417,7 @@ def set_snapshotter(path: str, snapshotter: bool) -> bool:
"starting Colima."
) from error
finally:
if temporary_path:
if temporary_path is not None:
try:
os.unlink(temporary_path)
except FileNotFoundError:
Expand Down
4 changes: 2 additions & 2 deletions devenv/lib/repository.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from __future__ import annotations

import importlib
import os.path
from configparser import ConfigParser
from importlib import metadata

from devenv import constants
from devenv.lib.config import get_config
Expand Down Expand Up @@ -34,7 +34,7 @@ def config(self) -> ConfigParser:
return get_config(f"{self.config_path}/config.ini")

def check_minimum_version(self) -> None:
version = importlib.metadata.version("sentry-devenv")
version = metadata.version("sentry-devenv")

cfg = self.config()
try:
Expand Down
8 changes: 4 additions & 4 deletions devenv/lib/venv.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def sync(
editable_paths: Optional[tuple[str, ...]] = None,
bins: Optional[tuple[str, ...]] = None,
) -> None:
cmd: tuple[str, ...] = (
cmd = [
uv.find(),
"pip",
"install",
Expand All @@ -66,11 +66,11 @@ def sync(
f"{venv_dir}/bin/python",
"-r",
requirements,
)
]
if editable_paths is not None:
for path in editable_paths:
cmd = (*cmd, "-e", path)
proc.run(cmd)
cmd.extend(("-e", path))
proc.run(tuple(cmd))

if bins is not None:
binroot = fs.ensure_binroot(reporoot)
Expand Down
3 changes: 2 additions & 1 deletion devenv/pin_gha.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import subprocess
from collections.abc import Sequence
from functools import lru_cache
from typing import cast

from devenv.lib.context import Context
from devenv.lib.modules import DevModuleInfo
Expand Down Expand Up @@ -44,7 +45,7 @@ def main(context: Context, argv: Sequence[str] | None = None) -> int:
"files", nargs="+", type=str, help="path to github actions file"
)
args = parser.parse_args(argv)
files: list[str] = args.files
files = cast(list[str], args.files)

ACTION_VERSION_RE = re.compile(
r"(?<=uses: )(?P<action>.*)@(?P<ref>[^#\s]+)"
Expand Down
56 changes: 17 additions & 39 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ line-length = 80

[tool.ruff.lint]
select = ["E", "F", "I"]
extend-select = ["ANN", "PYI"]
preview = true
ignore = ["E501"]

[tool.ruff.lint.isort]
Expand All @@ -48,53 +50,29 @@ skip-magic-trailing-comma = true
[project.scripts]
devenv = "devenv.main:main"

[tool.mypy]
python_version = "3.13"
strict = true

# disallow Any typing:
disallow_any_unimported = true
# there's too many Any's in typeshed:
disallow_any_expr = false
disallow_any_decorated = true
disallow_any_explicit = true

implicit_reexport = false

warn_no_return = true
warn_unreachable = true

allow_redefinition = true

[tool.pyright]
include = ["."]
pythonVersion = "3.13"

typeCheckingMode = "strict"

# turn on all the checks not already enabled by "strict":
reportCallInDefaultInitializer = "error"
reportImplicitStringConcatenation = "error"
reportMissingSuperCall = "error"
reportPropertyTypeMismatch = "error"
reportUninitializedInstanceVariable = "error"
reportUnnecessaryTypeIgnoreComment = "error"
reportUnusedCallResult = "none" # too noisy

# don't flag arguments as long as their type is known
reportMissingParameterType = "none"
reportUnknownParameterType = "error"
#### don't flag missing typestubs, as long as everything is typed
###reportMissingTypeStubs = false
[tool.ty.analysis]
strict-equality-semantics = true
strict-generic-narrowing = true

[tool.ty.rules]
dynamic-function-decorator-return = "error"
missing-type-argument = "error"
possibly-unresolved-reference = "warn"
unsound-assignment = "error"
unsound-return-statement = "error"
unsound-yield = "error"
unsupported-dynamic-base = "warn"
possibly-missing-attribute = "warn"
possibly-missing-import = "warn"

[dependency-groups]
dev = [
"coverage",
"covdefaults",
"mypy",
"prek",
"pytest",
"re-assert",
"ruff",
"shellcheck-py",
"ty",
]
20 changes: 13 additions & 7 deletions tests/lib/test_archive.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
import tarfile
import time
import typing
import urllib.error
import urllib.request
from http.client import HTTPMessage
from unittest import mock

import pytest
Expand Down Expand Up @@ -150,12 +152,16 @@ def tar5(tmp_path: pathlib.Path) -> pathlib.Path:


@pytest.fixture
def mock_sleep() -> typing.Generator[mock.MagicMock, None, None]:
def mock_sleep() -> typing.Generator[
mock.MagicMock | mock.AsyncMock, None, None
]:
with mock.patch.object(time, "sleep", autospec=True) as mock_sleep:
yield mock_sleep


def test_download(tmp_path: pathlib.Path, mock_sleep: mock.MagicMock) -> None:
def test_download(
tmp_path: pathlib.Path, mock_sleep: mock.MagicMock | mock.AsyncMock
) -> None:
data = b"foo\n"
data_sha256 = (
"b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c"
Expand All @@ -165,7 +171,7 @@ def test_download(tmp_path: pathlib.Path, mock_sleep: mock.MagicMock) -> None:
"https://example.com/foo",
503,
"Service Unavailable",
"", # type: ignore
HTTPMessage(),
io.BytesIO(b""),
)

Expand Down Expand Up @@ -195,7 +201,7 @@ def test_download(tmp_path: pathlib.Path, mock_sleep: mock.MagicMock) -> None:


def test_download_exceeded_retries(
tmp_path: pathlib.Path, mock_sleep: mock.MagicMock
tmp_path: pathlib.Path, mock_sleep: mock.MagicMock | mock.AsyncMock
) -> None:
data_sha256 = (
"b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c"
Expand All @@ -205,7 +211,7 @@ def test_download_exceeded_retries(
"https://example.com/foo",
503,
"Service Unavailable",
"", # type: ignore
HTTPMessage(),
io.BytesIO(b""),
)
dest = f"{tmp_path}/a"
Expand All @@ -226,7 +232,7 @@ def test_download_exceeded_retries(


def test_download_dest_is_broken_symlink(
tmp_path: pathlib.Path, mock_sleep: mock.MagicMock
tmp_path: pathlib.Path, mock_sleep: mock.MagicMock | mock.AsyncMock
) -> None:
data = b"foo\n"
data_sha256 = (
Expand All @@ -251,7 +257,7 @@ def test_download_dest_is_broken_symlink(


def test_download_wrong_sha(
tmp_path: pathlib.Path, mock_sleep: mock.MagicMock
tmp_path: pathlib.Path, mock_sleep: mock.MagicMock | mock.AsyncMock
) -> None:
data = b"foo\n"
data_sha256 = (
Expand Down
6 changes: 3 additions & 3 deletions tests/lib/test_colima_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ def test_stopped_legacy_vm_ignores_standalone_lima_disk(tmp_path: Path) -> None:
limactl = root / "bin/limactl"
limactl.parent.mkdir(parents=True)
limactl.touch()
probe_environments: list[dict[str, str] | None] = []
probe_environments: list[object | None] = []

def run(command: tuple[str, ...], **kwargs: object) -> str:
if command[0].endswith("limactl"):
Expand Down Expand Up @@ -352,7 +352,7 @@ def test_stopped_modern_vm_disk_prevents_overlay2_override(
limactl = root / "bin/limactl"
limactl.parent.mkdir(parents=True)
limactl.touch()
probe_environments: list[dict[str, str] | None] = []
probe_environments: list[object | None] = []

def run(command: tuple[str, ...], **kwargs: object) -> str:
if command[0].endswith("limactl"):
Expand Down Expand Up @@ -394,7 +394,7 @@ def test_explicit_lima_home_is_used_for_colima_instance_and_disk_probe(
limactl = root / "bin/limactl"
limactl.parent.mkdir(parents=True)
limactl.touch()
probe_environments: list[dict[str, str] | None] = []
probe_environments: list[object | None] = []

def run(command: tuple[str, ...], **kwargs: object) -> str:
if command[0].endswith("limactl"):
Expand Down
Loading
Loading