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
15 changes: 15 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions .github/workflows/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
102 changes: 88 additions & 14 deletions Makefile
Original file line number Diff line number Diff line change
@@ -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
16 changes: 9 additions & 7 deletions l9format/l9format.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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

Expand All @@ -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):
Expand Down
25 changes: 25 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"]
4 changes: 3 additions & 1 deletion tests/test_l9format.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
]


Expand Down