Skip to content

Commit

Permalink
chore: cleanup project add mypy, update linters and dependencies
Browse files Browse the repository at this point in the history
  • Loading branch information
u8slvn committed Feb 2, 2024
1 parent c8c227d commit 04abc1b
Show file tree
Hide file tree
Showing 10 changed files with 362 additions and 265 deletions.
2 changes: 0 additions & 2 deletions .coveragerc

This file was deleted.

26 changes: 26 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
default_stages: [commit]
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-added-large-files

- repo: https://github.com/psf/black
rev: 23.12.1
hooks:
- id: black

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.1.14
hooks:
- id: ruff

- repo: https://github.com/compilerla/conventional-pre-commit
rev: v3.1.0
hooks:
- id: conventional-pre-commit
stages: [commit-msg]
41 changes: 26 additions & 15 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,25 +1,36 @@
.PHONY: help tests quality coverage coverage-html

.DEFAULT_GOAL := help

.PHONY: help
help: ## List all the command helps.
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)

tests: ## Run tests.
@poetry run pytest tests/ -x -vv
@poetry run mypy sutoppu.py
.PHONY: init-pre-commit
init-pre-commit: ## Init pre-commit.
@pre-commit install
@pre-commit install --hook-type commit-msg

.PHONY: tests
test: ## Run tests.
@pytest -vv
@mypy

quality: ## Check quality.
@poetry run flake8
@poetry run black --check sutoppu.py
.PHONY: lint
lint: ## Check linter.
@pre-commit run --all-files

format: ## Format files.
@poetry run black sutoppu.py
.PHONY: coverage-html
coverage-html: ## Run pytest with html output coverage.
@pytest --cov-report html

coverage: ## Run tests with coverage.
@poetry run pytest tests/ --cov=sutoppu
.PHONY: ci
ci: lint test ## Run CI.

coverage-html: ## Run tests with html output coverage.
@poetry run pytest tests/ --cov=sutoppu --cov-report html
.PHONY: bump-version
bump-version: check-version ## Bump version, define target version with "VERSION=*.*.*".
@sed -i "s/^\(version = \"\)\(.\)*\"/\1${VERSION}\"/" pyproject.toml
@echo "Version replaced by ${VERSION} in 'pyproject.toml'"

ci: quality coverage ## Run CI.
check-version:
ifndef VERSION
$(error VERSION is undefined)
endif
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class FruitIsBitter(Specification):

class FruitIsSweet(Specification):
description = 'The given fruit must be sweet.'

def is_satisfied_by(self, fruit: Fruit) -> bool:
return fruit.sweet is True

Expand Down
327 changes: 151 additions & 176 deletions poetry.lock

Large diffs are not rendered by default.

50 changes: 44 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
[tool.poetry]
name = "sutoppu"

version = "1.0.0"
description = "A simple python implementation of Specification pattern."
authors = ["u8slvn <[email protected]>"]

license = "MIT"
readme = "README.md"
packages = [{include = "sutoppu.py", from = "src"}]
repository = "https://github.com/u8slvn/sutoppu"
homepage = "https://github.com/u8slvn/sutoppu"

classifiers = [
"Intended Audience :: Developers",
"Development Status :: 5 - Production/Stable",
Expand All @@ -32,7 +30,6 @@ keywords=[
"business-rules",
"verification",
]

include = [
"LICENSE",
"CHANGELOG.md",
Expand All @@ -45,11 +42,52 @@ python = "^3.8.1"
[tool.poetry.dev-dependencies]
pytest-cov = "^4.0.0"
pytest-mock = "^3.10.0"
flake8 = "^6.0.0"
black = "^23.1.0"
pytest = "^7.2.2"
mypy = "^1.1.1"

[tool.poetry.group.dev.dependencies]
mypy = "^1.8.0"

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"

[tool.mypy]
files = "src/"
mypy_path = "src/"
namespace_packages = true
show_error_codes = true
ignore_missing_imports = true
strict = true

[tool.black]
line_length = 88

[tool.ruff]
fix = true
line-length = 88
extend-select = [
"I", # isort
"N", # pep8-naming
]

[tool.ruff.isort]
force-single-line = true
lines-between-types = 1
lines-after-imports = 2
required-imports = [
"from __future__ import annotations",
]

[tool.coverage.report]
exclude_lines = [
"raise NotImplementedError",
]

[tool.pytest.ini_options]
pythonpath = "src/"
testpaths = ["tests"]
addopts = [
"--cov=src/",
"--import-mode=importlib",
]
30 changes: 18 additions & 12 deletions sutoppu.py → src/sutoppu.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@
from __future__ import annotations

import functools
from abc import ABCMeta, abstractmethod

from abc import ABCMeta
from abc import abstractmethod


__all__ = ["Specification"]
__version__ = "1.0.0"

from typing import Callable, Any
from typing import Any
from typing import Callable


class _SpecificationMeta(ABCMeta):
Expand All @@ -25,15 +29,15 @@ class method '_report_errors' as decorator for the 'is_satisfied_by'
"""

def __new__(
mcs,
cls,
name: str,
bases: tuple[type, ...],
namespace: dict[str, Any],
) -> _SpecificationMeta:
cls = super().__new__(mcs, name, bases, namespace)
if hasattr(cls, "is_satisfied_by") and hasattr(cls, "_report_errors"):
cls.is_satisfied_by = cls._report_errors(cls.is_satisfied_by)
return cls
class_ = super().__new__(cls, name, bases, namespace)
if hasattr(class_, "is_satisfied_by") and hasattr(class_, "_report_errors"):
class_.is_satisfied_by = class_._report_errors(class_.is_satisfied_by)
return class_


class Specification(metaclass=_SpecificationMeta):
Expand All @@ -44,14 +48,16 @@ class Specification(metaclass=_SpecificationMeta):
description = "No description provided."

def __init__(self) -> None:
self.errors: dict = {}
self.errors: dict[str, str] = {}

@classmethod
def _report_errors(cls, func) -> Callable[[Any], bool]:
def _report_errors(
cls, func: Callable[[Specification, Any], bool]
) -> Callable[[Specification, Any], bool]:
@functools.wraps(func)
def wrapper(self, *args, **kwargs) -> bool:
def wrapper(self: Specification, candidate: Any) -> bool:
self.errors = {} # reset the errors dict
result = func(self, *args, **kwargs)
result = func(self, candidate)
self._report_error(result)
return result

Expand Down Expand Up @@ -96,7 +102,7 @@ def __init__(self, spec_a: Specification, spec_b: Specification) -> None:
super().__init__()
self._specs = (spec_a, spec_b)

def _report_error(self, _) -> None:
def _report_error(self, _: bool) -> None:
"""Gets the children spec errors and merge them into its own.
The goal behind this it to propagate the errors through all the
parents specifications.
Expand Down
12 changes: 7 additions & 5 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

from sutoppu import Specification


Expand All @@ -12,7 +14,7 @@ class FruitIsYellow(Specification):
description = "Fruit must be yellow."

def is_satisfied_by(self, fruit):
return fruit.color == 'yellow'
return fruit.color == "yellow"


class FruitIsSweet(Specification):
Expand All @@ -29,7 +31,7 @@ def is_satisfied_by(self, fruit):
return fruit.bitter is True


lemon = Fruit(color='yellow', sweet=False, bitter=True)
orange = Fruit(color='orange', sweet=True, bitter=True)
apple = Fruit(color='red', sweet=True, bitter=False)
avocado = Fruit(color='green', sweet=False, bitter=False)
lemon = Fruit(color="yellow", sweet=False, bitter=True)
orange = Fruit(color="orange", sweet=True, bitter=True)
apple = Fruit(color="red", sweet=True, bitter=False)
avocado = Fruit(color="green", sweet=False, bitter=False)
38 changes: 26 additions & 12 deletions tests/test_error_report.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,29 @@
import pytest

from tests.conftest import (FruitIsBitter, FruitIsSweet, avocado, lemon,
orange)
from __future__ import annotations

import pytest

@pytest.mark.parametrize('fruit, expected, failed', [
(lemon, False, {'FruitIsSweet': 'Fruit must be sweet.',
'FruitIsBitter': 'Not ~ Fruit must be bitter.'}),
(orange, False, {'FruitIsBitter': 'Not ~ Fruit must be bitter.'}),
(avocado, False, {'FruitIsSweet': 'Fruit must be sweet.'}),
])
from tests.conftest import FruitIsBitter
from tests.conftest import FruitIsSweet
from tests.conftest import avocado
from tests.conftest import lemon
from tests.conftest import orange


@pytest.mark.parametrize(
"fruit, expected, failed",
[
(
lemon,
False,
{
"FruitIsSweet": "Fruit must be sweet.",
"FruitIsBitter": "Not ~ Fruit must be bitter.",
},
),
(orange, False, {"FruitIsBitter": "Not ~ Fruit must be bitter."}),
(avocado, False, {"FruitIsSweet": "Fruit must be sweet."}),
],
)
def test_basic_report_specification(fruit, expected, failed):
specification = FruitIsSweet() & ~FruitIsBitter()
result = specification.is_satisfied_by(fruit)
Expand All @@ -23,12 +37,12 @@ def test_report_reset_after_two_uses():

result = specification.is_satisfied_by(orange)

expected_failed = {'FruitIsBitter': 'Not ~ Fruit must be bitter.'}
expected_failed = {"FruitIsBitter": "Not ~ Fruit must be bitter."}
assert result is False
assert specification.errors == expected_failed

result = specification.is_satisfied_by(avocado)

expected_failed = {'FruitIsSweet': 'Fruit must be sweet.'}
expected_failed = {"FruitIsSweet": "Fruit must be sweet."}
assert result is False
assert specification.errors == expected_failed
Loading

0 comments on commit 04abc1b

Please sign in to comment.