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
30 changes: 15 additions & 15 deletions .github/workflows/python.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,28 +20,28 @@ jobs:

strategy:
matrix:
python-version: [3.8, 3.9, 3.11, 3.12]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]

steps:
- uses: actions/checkout@v4
env:
PYVER: ${{ matrix.python-version }}

- name: Bootstrap poetry
run: pipx install poetry
steps:
- uses: actions/checkout@v7

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
cache: poetry

- name: Configure poetry
run: poetry config virtualenvs.in-project true

- name: Check lock file
run: poetry check --lock

- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
# Install a specific version of uv.
version: "0.11.28"
enable-cache: true

- name: Install dependencies
run: poetry install --with github-actions
- name: Install the project
run: uv sync --locked --all-extras --dev

- name: Run linters
run: make lint
Expand Down
33 changes: 22 additions & 11 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,29 +1,36 @@
PACKAGE = hasher

PYVERS = 3.8 3.9 3.10 3.11 3.12
PYVERS = 3.10 3.11 3.12 3.13 3.14
PYVER = $(firstword $(PYVERS))
UV_PYTHON = $(PYVER)
export UV_PYTHON

VIRTUAL_ENV := $(lastword $(shell poetry env use python$(PYVER)))
VIRTUAL_ENV := .venv
SITE_PACKAGES = $(VIRTUAL_ENV)/lib/python$(PYVER)/site-packages

PYTEST_FLAGS = -vv

.PHONY: all
all: lint test

.PHONY: clean
clean:
rm -rf $(VIRTUAL_ENV) dist/ .mypy_cache/ .pytest_cache/ .ruff_cache/
find . -name '__pycache__' -type d -exec rm -rf '{}' +

.PHONY: format fmt
format fmt: venv
poetry run ruff check --select I --fix
poetry run ruff format .
uv run ruff check --select I --fix
uv run ruff format .

.PHONY: lint
lint: venv
poetry run ruff check
poetry run mypy src/
uv run ruff check
uv run mypy src/

.PHONY: test
test:
poetry run pytest $(PYTEST_FLAGS) tests/
uv run pytest $(PYTEST_FLAGS) tests/

.PHONY: coverage
coverage: PYTEST_FLAGS += --cov --cov-report=term-missing
Expand All @@ -33,9 +40,13 @@ coverage: test
.PHONY: venv
venv: $(SITE_PACKAGES)/$(PACKAGE).pth

$(SITE_PACKAGES)/$(PACKAGE).pth: poetry.lock
poetry install
$(SITE_PACKAGES)/$(PACKAGE).pth: uv.lock | $(VIRTUAL_ENV)/bin/activate
uv sync
touch $@

$(VIRTUAL_ENV)/bin/activate:
uv venv

poetry.lock: pyproject.toml
poetry lock --no-update
uv.lock: pyproject.toml
uv lock
@touch $@
Empty file added README.md
Empty file.
6 changes: 6 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
def main():
print("Hello from hasher!")


if __name__ == "__main__":
main()
63 changes: 23 additions & 40 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,47 +1,37 @@
[tool.poetry]
authors = ["Walter Scheper <walter.scheper@gmail.com>"]
[build-system]
requires = ["uv_build>=0.11.28,<0.12.0"]
build-backend = "uv_build"

[project]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Environment :: Console",
"Intended Audience :: Developers",
"Intended Audience :: Information Technology",
"Intended Audience :: System Administrators",
"License :: OSI Approved :: Apache Software License",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Topic :: Utilities",
]
dependencies = [
"click>=8.1.3,<9",
]
description = "Provide multiple hashing algorithms from a single code base"
homepage = "https://github.com/wfscheper/hasher"
license = "Apache-2.0"
name = "hasher"
packages = [
{include = "hasher", from = "src"},
]
readme = "README.rst"
repository = "https://github.com/wfscheper/hasher.git"
readme = "README.md"
requires-python = ">=3.10"
version = "2.1.0a1"

[tool.poetry.scripts]
hasher = "hasher.app:hasher"

[tool.poetry.dependencies]
click = "^8.1.3"
python = "^3.8"

[tool.poetry.group.dev.dependencies]
ruff = "^0.3.1"
pytest = "^8.0.2"
mypy = "^1.8.0"
pytest-cov = "^4.1.0"
pytest-mock = "^3.12.0"

# only used in github actions
[tool.poetry.group.github-actions]
optional = true

[tool.poetry.group.github-actions.dependencies]
pytest-github-actions-annotate-failures = "^0.2.0"
[dependency-groups]
dev = [
"mypy>=2.2.0",
"pytest>=9.1.1",
"pytest-cov>=7.1.0",
"pytest-mock>=3.15.1",
"ruff>=0.15.20",
]

[tool.coverage.run]
branch = true
Expand All @@ -61,20 +51,13 @@ show_missing = true
[tool.ruff.lint]
select = [
# pycodestyle
"E", "W",
# pyflakes
"F",
# isort
"I",
# bugbear
"B",
# pyupgrade
"E",
"W", # pyflakes
"F", # isort
"I", # bugbear
"B", # pyupgrade
"UP",
]

[tool.ruff.lint.isort]
from-first = true

[build-system]
build-backend = "poetry.masonry.api"
requires = ["poetry>=0.12"]
7 changes: 3 additions & 4 deletions src/hasher/hashes.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,13 @@

from __future__ import annotations

from collections.abc import Callable, Iterator
from re import Pattern
from typing import (
IO,
TYPE_CHECKING,
Any,
Callable,
ClassVar,
Iterator,
Pattern,
Protocol,
cast,
)
Expand Down Expand Up @@ -107,7 +106,7 @@ def check_hash(self, fname: str, args: Args) -> int:
if not m:
if args.warn:
self.stderr(
f"hasher {self.name}: {fname}: {idx+1}: improperly formatted "
f"hasher {self.name}: {fname}: {idx + 1}: improperly formatted "
f"{self.name.upper()} checksum line"
)
format_errors += 1
Expand Down
33 changes: 16 additions & 17 deletions tests/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,15 @@
from pathlib import Path

from click.testing import CliRunner
from hasher.app import hasher
import pytest

from hasher.app import hasher


def test_hasher_usage():
runner = CliRunner()
result = runner.invoke(
hasher,
)
assert result.exit_code == 0
result = runner.invoke(hasher)
assert 2 == result.exit_code
assert (
"""Usage: hasher [OPTIONS] COMMAND [ARGS]...

Expand All @@ -28,7 +27,7 @@ def test_hasher_usage():
sha1 Generate or check sha1 hashes
sha256 Generate or check sha256 hashes
"""
== result.output
== result.stderr
)


Expand All @@ -42,7 +41,7 @@ def test_hasher_missing_command():

Error: Missing command.
"""
== result.output
== result.stderr
)


Expand All @@ -58,7 +57,7 @@ def test_stdin(hash: str, expected: str):
runner = CliRunner()
result = runner.invoke(hasher, [hash], input="test\n")
assert 0 == result.exit_code
assert f"{expected} -\n" == result.output
assert f"{expected} -\n" == result.stdout


@pytest.mark.parametrize("hash,expected", test_inputs)
Expand All @@ -68,12 +67,12 @@ def test_file(hash: str, expected: str):
Path("test.txt").write_text("test\n")
result = runner.invoke(hasher, [hash, "test.txt"])
assert 0 == result.exit_code, result.output
assert f"{expected} test.txt\n" == result.output
assert f"{expected} test.txt\n" == result.stdout


@pytest.mark.parametrize("hash,checksum", test_inputs)
def test_check(hash: str, checksum: str):
runner = CliRunner(mix_stderr=False)
runner = CliRunner()
with runner.isolated_filesystem():
Path("test1.txt").write_text("test\n")
Path("test2.txt").write_text("test\n")
Expand All @@ -82,7 +81,7 @@ def test_check(hash: str, checksum: str):
)
result = runner.invoke(hasher, [hash, "--check", "checksums.txt"])
assert 0 == result.exit_code, result.output
assert "test1.txt: OK\ntest2.txt: FAILED\n" == result.output
assert "test1.txt: OK\ntest2.txt: FAILED\n" == result.stdout
assert (
f"hasher {hash}: WARNING: 1 computed checksum did NOT match\n"
== result.stderr
Expand All @@ -91,35 +90,35 @@ def test_check(hash: str, checksum: str):

@pytest.mark.parametrize("hash,checksum", test_inputs)
def test_check_bad_format(hash: str, checksum: str):
runner = CliRunner(mix_stderr=False)
runner = CliRunner()
with runner.isolated_filesystem():
Path("test1.txt").write_text("test\n")
Path("checksums.txt").write_text(f"{checksum} test1.txt\n")
result = runner.invoke(hasher, [hash, "--check", "checksums.txt"])
assert 0 == result.exit_code, result.output
assert "" == result.output
assert "" == result.stdout
assert (
f"hasher {hash}: WARNING: 1 line is improperly formatted\n" == result.stderr
)


@pytest.mark.parametrize("hash,checksum", test_inputs)
def test_check_bad_format_strict(hash: str, checksum: str):
runner = CliRunner(mix_stderr=False)
runner = CliRunner()
with runner.isolated_filesystem():
Path("test1.txt").write_text("test\n")
Path("checksums.txt").write_text(f"{checksum} test1.txt\n")
result = runner.invoke(hasher, [hash, "--check", "--strict", "checksums.txt"])
assert 0 == result.exit_code, result.output
assert "" == result.output
assert "" == result.stdout
assert (
f"hasher {hash}: WARNING: 1 line is improperly formatted\n" == result.stderr
)


@pytest.mark.parametrize("hash,checksum", test_inputs)
def test_check_strict(hash: str, checksum: str):
runner = CliRunner(mix_stderr=False)
runner = CliRunner()
with runner.isolated_filesystem():
Path("test1.txt").write_text("test\n")
Path("test2.txt").write_text("test\n")
Expand All @@ -128,7 +127,7 @@ def test_check_strict(hash: str, checksum: str):
)
result = runner.invoke(hasher, [hash, "--check", "--strict", "checksums.txt"])
assert 0 == result.exit_code, result.output
assert "test1.txt: OK\ntest2.txt: FAILED\n" == result.output
assert "test1.txt: OK\ntest2.txt: FAILED\n" == result.stdout
assert (
f"hasher {hash}: WARNING: 1 computed checksum did NOT match\n"
== result.stderr
Expand Down
8 changes: 3 additions & 5 deletions tests/test_hasher.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ def test_generate_display_text(self, mocker, md5hasher, args):
md5hasher.generate_hash("foo", args)

_open.assert_called_once_with("foo", "r")
md5hasher.stdout.assert_called_with("%s foo" % self.data_md5)
md5hasher.stdout.assert_called_with(f"{self.data_md5} foo")

def test_generate_display_text_binary(self, mocker, md5hasher, args):
_open = mocker.patch(
Expand All @@ -70,7 +70,7 @@ def test_generate_display_text_binary(self, mocker, md5hasher, args):
md5hasher.generate_hash("foo", args)

_open.assert_called_once_with("foo", "rb")
md5hasher.stdout.assert_called_with("%s *foo" % self.data_md5)
md5hasher.stdout.assert_called_with(f"{self.data_md5} *foo")

def test_checkresult_display(self, mocker, md5hasher, args):
_open = mocker.patch("hasher.hashes.open", mocker.mock_open())
Expand Down Expand Up @@ -299,9 +299,7 @@ def test_checkresult_warn_formaterror(self, mocker, md5hasher, args):
assert expected_stdout_calls == md5hasher.stdout.call_args_list

expected_stderr_calls = [
mocker.call(
"hasher md5: foo: 2: improperly formatted MD5 checksum" " line"
),
mocker.call("hasher md5: foo: 2: improperly formatted MD5 checksum line"),
mocker.call("hasher md5: WARNING: 1 line is improperly formatted"),
]
assert expected_stderr_calls == md5hasher.stderr.call_args_list
Expand Down
Loading