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
1 change: 0 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ jobs:
run: |
poetry run ty --version
poetry run ty check .
poetry run ty check tests/ || true
- name: Run unittests
run: |
Expand Down
7 changes: 7 additions & 0 deletions DEVELOP.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ poetry run pytest -sv tests
make tests
```

Lint the code and run type checking

```bash
make lint
make ty
```

Install git hooks for the automatic linting and code formatting with [pre-commit](https://pre-commit.com/)

```bash
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
![PyPI - Python Version](https://img.shields.io/pypi/pyversions/hapless)


[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
[![ty](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ty/main/assets/badge/v0.json)](https://github.com/astral-sh/ty)
[![EditorConfig](https://img.shields.io/badge/-EditorConfig-grey?logo=editorconfig)](https://editorconfig.org/)
[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit)

Expand Down
6 changes: 3 additions & 3 deletions hapless/cli.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import sys
from shlex import join as shlex_join
from typing import Optional
from typing import Optional, Tuple

import click

Expand Down Expand Up @@ -99,7 +99,7 @@ def errors(hap_alias: str, follow: bool):
default=False,
help="Include failed haps for the removal.",
)
def clean(clean_all):
def clean(clean_all: bool):
hapless.clean(clean_all=clean_all)


Expand All @@ -124,7 +124,7 @@ def cleanall():
default=False,
help="Verify command launched does not fail immediately.",
)
def run(cmd, name, check):
def run(cmd: Tuple[str, ...], name: str, check: bool):
hap = hapless.get_hap(name)
if hap is not None:
console.error(f"Hap with such name already exists: {hap}")
Expand Down
4 changes: 2 additions & 2 deletions hapless/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from hapless.formatters import Formatter
from hapless.hap import Hap, Status
from hapless.ui import ConsoleUI
from hapless.utils import kill_proc_tree, logger, wait_created
from hapless.utils import get_exec_path, kill_proc_tree, logger, wait_created


class Hapless:
Expand Down Expand Up @@ -223,7 +223,7 @@ def run_hap(
self._check_fast_failure(hap)

def _run_via_spawn(self, hap: Hap) -> None:
exec_path = Path(sys.argv[0])
exec_path = get_exec_path()
proc = subprocess.Popen(
[f"{exec_path}", "__internal_wrap_hap", f"{hap.hid}"],
start_new_session=True,
Expand Down
20 changes: 20 additions & 0 deletions hapless/utils.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import logging
import os
import shutil
import signal
import sys
import time
from collections import deque
from contextlib import nullcontext
from functools import wraps
from pathlib import Path
Expand Down Expand Up @@ -122,6 +124,24 @@ def isatty() -> bool:
return sys.stdin.isatty() and sys.stdout.isatty()


def get_exec_path() -> Path:
if str(sys.argv[0]).endswith("hap"):
exec_path = sys.argv[0]
else:
logger.warning("Unusual invocation, checking `hap` in PATH")
exec_path = shutil.which("hap")

if exec_path is None:
raise RuntimeError("Cannot find `hap` executable, please reinstall hapless")

return Path(exec_path)


def tail_lines(filepath: Path, n: int = 20):
with open(filepath) as f:
return deque(f, n)


def configure_logger() -> logging.Logger:
logger = structlog.get_logger()
level = logging.DEBUG if config.DEBUG else logging.CRITICAL
Expand Down
9 changes: 3 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,10 @@ structlog = "^25.4.0"
# Optional dependencies installed as extras
pytest = { version = "^7.4.4", optional = true }
pytest-cov = { version = "^3.0.0", optional = true }
ruff = { version = "^0.9.0", optional = true }
nox = { version = "^2024.10.9", optional = true }
pytest-env = { version = "^1.1.2", optional = true }
ruff = { version = "^0.9.0", optional = true }
ty = { version = "^0.0.1a21", optional = true }

nox = { version = "^2024.10.9", optional = true }

[tool.poetry.extras]
dev = [
Expand Down Expand Up @@ -82,9 +81,7 @@ extend-select = [
]

[tool.ty.src]
exclude = [
"tests/",
]
exclude = []

[tool.pytest.ini_options]
env = [
Expand Down
2 changes: 1 addition & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def runner() -> Generator[CliRunner, None, None]:
with cli_runner.isolated_filesystem() as path:
hapless_test = Hapless(hapless_dir=Path(path))
with patch("hapless.cli.hapless", hapless_test) as hapless_mock:
cli_runner.hapless = hapless_mock
cli_runner.hapless = hapless_mock # ty: ignore[unresolved-attribute]
Copy link

Copilot AI Oct 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type ignore comment contains a typo. It should be type: ignore instead of ty: ignore.

Suggested change
cli_runner.hapless = hapless_mock # ty: ignore[unresolved-attribute]
cli_runner.hapless = hapless_mock # type: ignore[unresolved-attribute]

Copilot uses AI. Check for mistakes.
yield cli_runner


Expand Down
38 changes: 38 additions & 0 deletions tests/test_cli_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from unittest.mock import Mock, PropertyMock, patch

import pytest

from hapless.cli_utils import get_or_exit


@patch("hapless.cli_utils.hapless")
def test_get_or_exit_non_existing(hapless_mock, capsys):
with patch.object(hapless_mock, "get_hap", return_value=None) as get_hap_mock:
with pytest.raises(SystemExit) as e:
get_or_exit("hap-me")

assert e.value.code == 1
get_hap_mock.assert_called_once_with("hap-me")

captured = capsys.readouterr()
assert "No such hap: hap-me" in captured.out


@patch("hapless.cli_utils.hapless")
def test_get_or_exit_not_accessible(hapless_mock, capsys):
hap_mock = Mock(owner="someone-else")
prop_mock = PropertyMock(return_value=False)
type(hap_mock).accessible = prop_mock
with patch.object(hapless_mock, "get_hap", return_value=hap_mock) as get_hap_mock:
with pytest.raises(SystemExit) as e:
get_or_exit("hap-not-mine")

assert e.value.code == 1
get_hap_mock.assert_called_once_with("hap-not-mine")
prop_mock.assert_called_once_with()

captured = capsys.readouterr()
assert (
"Cannot manage hap launched by another user. Owner: someone-else"
in captured.out
)
1 change: 1 addition & 0 deletions tests/test_hap_workdir.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ def blocking_run(*args, **kwargs):
)

restarted_hap = hapless.get_hap("hap-same-name")
assert restarted_hap is not None
assert restarted_hap.rc == 0
assert restarted_hap.stdout_path.exists()
assert "Correct file is being run" in restarted_hap.stdout_path.read_text()
Expand Down
Loading