From fac243d1c4c8da99f0d93fbe8df7b2e71f522568 Mon Sep 17 00:00:00 2001 From: Danny Willems Date: Sun, 1 Feb 2026 14:34:41 -0300 Subject: [PATCH] Add mypy type checking and improve tooling - Add mypy ^1.19.0 for type checking with serde-compatible config - Add .editorconfig for consistent editor settings - Expand Makefile with help, typecheck, trailing whitespace targets - Add tool configuration sections for ruff, black, isort in pyproject.toml - Fix type annotations in Decimal class and improve exception handling - Add typecheck step to CI workflow --- .editorconfig | 15 ++++++ .github/workflows/action.yml | 2 + Makefile | 102 ++++++++++++++++++++++++++++++----- l9format/l9format.py | 16 +++--- pyproject.toml | 25 +++++++++ tests/test_l9format.py | 4 +- 6 files changed, 142 insertions(+), 22 deletions(-) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..73ed817 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +charset = utf-8 +indent_style = space +indent_size = 4 + +[*.{json,yaml,yml,md}] +indent_size = 2 + +[Makefile] +indent_style = tab diff --git a/.github/workflows/action.yml b/.github/workflows/action.yml index 04c3fea..b8ba468 100644 --- a/.github/workflows/action.yml +++ b/.github/workflows/action.yml @@ -27,6 +27,8 @@ jobs: run: poetry run black --check . - name: sort-check run: poetry run isort --check-only . + - name: typecheck + run: poetry run mypy l9format - name: test run: poetry run pytest - name: security-audit diff --git a/Makefile b/Makefile index dbbf7a8..82a23ca 100644 --- a/Makefile +++ b/Makefile @@ -1,27 +1,101 @@ -# Makefile for pygins project +# Makefile for l9format-python project -.PHONY: test format sort lint security-check all +UNAME_S := $(shell uname -s) +ifeq ($(UNAME_S),Darwin) + SED := $(shell command -v gsed 2>/dev/null) + ifeq ($(SED),) + $(error GNU sed (gsed) not found on macOS. \ + Install with: brew install gnu-sed) + endif +else + SED := sed +endif -# Run all quality checks -all: test format sort lint security-check +.PHONY: help +help: ## Show this help + @grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | \ + awk 'BEGIN {FS = ":.*?## "}; \ + {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' -# Run tests using pytest -test: +.PHONY: all +all: format sort lint typecheck test security-check ## Run all quality checks + +.PHONY: test +test: ## Run tests using pytest poetry run pytest -# Format code using black -format: +.PHONY: format +format: ## Format code using black poetry run black . -# Sort imports using isort -sort: +.PHONY: check-format +check-format: ## Check code formatting with black + poetry run black --check . + +.PHONY: sort +sort: ## Sort imports using isort poetry run isort . -# Lint code using ruff -lint: +.PHONY: check-sort +check-sort: ## Check import sorting with isort + poetry run isort --check-only . + +.PHONY: lint +lint: ## Lint code using ruff poetry run ruff check . -# Check for vulnerable dependencies using pip-audit -security-check: +.PHONY: lint-fix +lint-fix: ## Lint and fix code using ruff + poetry run ruff check --fix . + +.PHONY: typecheck +typecheck: ## Run mypy type checker + poetry run mypy l9format + +.PHONY: security-check +security-check: ## Check for vulnerable dependencies using pip-audit poetry run pip-audit +.PHONY: fix-trailing-whitespace +fix-trailing-whitespace: ## Remove trailing whitespaces from all files + @echo "Removing trailing whitespaces from all files..." + @find . -type f \( \ + -name "*.py" -o -name "*.toml" -o -name "*.md" -o -name "*.yaml" \ + -o -name "*.yml" -o -name "*.json" \) \ + -not -path "./.git/*" \ + -not -path "./.venv/*" \ + -not -path "./__pycache__/*" \ + -exec sh -c \ + 'echo "Processing: $$1"; $(SED) -i -e "s/[[:space:]]*$$//" "$$1"' \ + _ {} \; && \ + echo "Trailing whitespaces removed." + +.PHONY: check-trailing-whitespace +check-trailing-whitespace: ## Check for trailing whitespaces in source files + @echo "Checking for trailing whitespaces..." + @files_with_trailing_ws=$$(find . -type f \( \ + -name "*.py" -o -name "*.toml" -o -name "*.md" -o -name "*.yaml" \ + -o -name "*.yml" -o -name "*.json" \) \ + -not -path "./.git/*" \ + -not -path "./.venv/*" \ + -not -path "./__pycache__/*" \ + -exec grep -l '[[:space:]]$$' {} + 2>/dev/null || true); \ + if [ -n "$$files_with_trailing_ws" ]; then \ + echo "Files with trailing whitespaces found:"; \ + echo "$$files_with_trailing_ws" | sed 's/^/ /'; \ + echo ""; \ + echo "Run 'make fix-trailing-whitespace' to fix automatically."; \ + exit 1; \ + else \ + echo "No trailing whitespaces found."; \ + fi + +.PHONY: install +install: ## Install dependencies with Poetry + poetry install + +.PHONY: clean +clean: ## Clean build artifacts and caches + rm -rf __pycache__ .pytest_cache .mypy_cache .ruff_cache + find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true + find . -type f -name "*.pyc" -delete 2>/dev/null || true diff --git a/l9format/l9format.py b/l9format/l9format.py index b4bdb44..a8a60e1 100644 --- a/l9format/l9format.py +++ b/l9format/l9format.py @@ -8,7 +8,9 @@ def round_decimal( decimal_obj: decimal.Decimal, num_of_places: int = 6 ) -> decimal.Decimal: - return decimal_obj.quantize(decimal.Decimal(10) ** -num_of_places).normalize() + return decimal_obj.quantize( + decimal.Decimal(10) ** -num_of_places + ).normalize() class Decimal(fields.Instance): @@ -32,7 +34,7 @@ class Decimal(fields.Instance): ty = decimal.Decimal - def __init__(self, resolution=None, **kwargs): + def __init__(self, resolution: int | None = None, **kwargs: object) -> None: super(Decimal, self).__init__(self.__class__.ty, **kwargs) self.resolution = resolution @@ -41,16 +43,16 @@ def serialize(self, value: decimal.Decimal) -> str: value = round_decimal(value, num_of_places=self.resolution) return "{0:f}".format(value) - def deserialize(self, value) -> decimal.Decimal: + def deserialize(self, value: object) -> decimal.Decimal: try: if self.resolution is not None: return round_decimal( - decimal.Decimal(value), num_of_places=self.resolution + decimal.Decimal(str(value)), num_of_places=self.resolution ) - return decimal.Decimal(value) - except decimal.DecimalException: - raise Exception("invalid decimal", value=value) + return decimal.Decimal(str(value)) + except decimal.DecimalException as e: + raise ValueError(f"invalid decimal: {value}") from e class L9HttpEvent(Model): diff --git a/pyproject.toml b/pyproject.toml index 22315be..100c963 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,7 @@ serde = "^0.9.0" black = "^26.1.0" fire = "^0.7.1" isort = "^7.0.0" +mypy = "^1.19.0" pip-audit = "^2.10.0" pytest = "^9.0.2" ruff = "^0.14.14" @@ -28,3 +29,27 @@ build-backend = "poetry.core.masonry.api" [tool.poetry.urls] "Bug Tracker" = "https://github.com/leakix/l9format-python/issues" + +[tool.ruff] +line-length = 80 + +[tool.black] +line-length = 80 + +[tool.isort] +profile = "black" +line_length = 80 + +[tool.mypy] +python_version = "3.13" +warn_return_any = true +warn_unused_configs = true +disallow_untyped_defs = true +disallow_incomplete_defs = true +# serde library lacks type stubs, so we need to ignore import errors +ignore_missing_imports = true + +[[tool.mypy.overrides]] +module = "l9format.l9format" +# serde Model/Instance classes appear as Any, causing false positives +disable_error_code = ["misc", "valid-type"] diff --git a/tests/test_l9format.py b/tests/test_l9format.py index 6ce2e2a..153d7b3 100644 --- a/tests/test_l9format.py +++ b/tests/test_l9format.py @@ -7,7 +7,9 @@ TESTS_DIR = Path(os.path.dirname(__file__)) IP4SCOUT_FILES = [ - f for f in Path.iterdir(TESTS_DIR) if Path.is_file(f) and "ip4scout" in f.name + f + for f in Path.iterdir(TESTS_DIR) + if Path.is_file(f) and "ip4scout" in f.name ]