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
19 changes: 14 additions & 5 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
---
repos:
- repo: https://github.com/psf/black.git
rev: 24.2.0
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.4
hooks:
- id: black
language_version: python3
exclude: ^(tests\/hooks-abort-render\/hooks|docs\/HelloCookieCutter1)
# Run the linter
- id: ruff
args: [--fix]
# Run the formatter
- id: ruff-format

- repo: https://github.com/pycqa/isort
rev: 5.13.2
hooks:
- id: isort
args: [--profile, plone, --force-alphabetical-sort, --force-single-line, --lines-after-imports, "2"]
additional_dependencies: [setuptools]

- repo: https://github.com/pre-commit/mirrors-mypy
rev: 'v1.9.0' # Use the sha / tag you want to point at
Expand Down
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
- Modernize type hints to use Python 3.10+ syntax (PEP 604: `X | Y` instead of `Union[X, Y]`)
- Use built-in generic types (`list`, `dict`, `tuple`) instead of `typing.List`, `typing.Dict`, `typing.Tuple`
[jensens]
- Replace black with ruff for faster linting and formatting. Configure ruff with line-length=120 and appropriate rule selections. Keep isort for import sorting with plone profile and force-alphabetical-sort. This modernizes the tooling stack for better Python 3.10+ support and faster CI runs.
[jensens]

## 4.1.2 (unreleased)

Expand Down
31 changes: 22 additions & 9 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,14 +117,20 @@ Key settings:
# Run all pre-commit hooks (using uvx with tox-uv)
uvx --with tox-uv tox -e lint

# Run type checking
mypy src/mxdev
# Run ruff linter (with auto-fix)
uvx ruff check --fix src/mxdev tests

# Run ruff formatter
uvx ruff format src/mxdev tests

# Run flake8
flake8 src/mxdev
# Sort imports with isort
uvx isort src/mxdev tests

# Run type checking
uvx mypy src/mxdev

# Sort imports
isort src/mxdev
# Run all pre-commit hooks manually
uvx pre-commit run --all-files
```

### Testing Multiple Python Versions (using uvx tox with uv)
Expand Down Expand Up @@ -401,9 +407,16 @@ myext-package_setting = value

## Code Style

- **Formatting**: Black-compatible (max line length: 120)
- **Import sorting**: isort with `force_alphabetical_sort = true`, `force_single_line = true`
- **Type hints**: Use throughout (Python 3.10+ compatible)
- **Formatting**: Ruff formatter (max line length: 120, target Python 3.10+)
- Configured in [pyproject.toml](pyproject.toml) under `[tool.ruff]`
- Rules: E, W, F, UP, D (with selective ignores for docstrings)
- Automatically enforced via pre-commit hooks
- **Import sorting**: isort with plone profile, `force_alphabetical_sort = true`, `force_single_line = true`
- Configured in [pyproject.toml](pyproject.toml) under `[tool.isort]`
- Runs after ruff in pre-commit pipeline
- **Type hints**: Use throughout (Python 3.10+ syntax)
- Use `X | Y` instead of `Union[X, Y]`
- Use `list[T]`, `dict[K, V]` instead of `List[T]`, `Dict[K, V]`
- **Path handling**: Prefer `pathlib.Path` over `os.path` for path operations
- Use `pathlib.Path().as_posix()` for cross-platform path comparison
- Use `/` operator for path joining: `Path("dir") / "file.txt"`
Expand Down
153 changes: 93 additions & 60 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
#: core.mxenv
#: core.mxfiles
#: core.packages
#: qa.black
#: qa.isort
#: qa.mypy
#: qa.ruff
#: qa.test
#
# SETTINGS (ALL CHANGES MADE BELOW SETTINGS WILL BE LOST)
Expand Down Expand Up @@ -54,18 +54,19 @@ PRIMARY_PYTHON?=3.14
PYTHON_MIN_VERSION?=3.10

# Install packages using the given package installer method.
# Supported are `pip` and `uv`. If uv is used, its global availability is
# checked. Otherwise, it is installed, either in the virtual environment or
# using the `PRIMARY_PYTHON`, dependent on the `VENV_ENABLED` setting. If
# `VENV_ENABLED` and uv is selected, uv is used to create the virtual
# environment.
# Supported are `pip` and `uv`. When `uv` is selected, a global installation
# is auto-detected and used if available. Otherwise, uv is installed in the
# virtual environment or using `PRIMARY_PYTHON`, depending on the
# `VENV_ENABLED` setting.
# Default: pip
PYTHON_PACKAGE_INSTALLER?=uv

# Flag whether to use a global installed 'uv' or install
# it in the virtual environment.
# Default: false
MXENV_UV_GLOBAL?=true
# Python version for UV to install/use when creating virtual
# environments with global UV. Passed to `uv venv -p VALUE`. Supports version
# specs like `3.11`, `3.14`, `cpython@3.14`. Defaults to PRIMARY_PYTHON value
# for backward compatibility.
# Default: $(PRIMARY_PYTHON)
UV_PYTHON?=$(PRIMARY_PYTHON)

# Flag whether to use virtual environment. If `false`, the
# interpreter according to `PRIMARY_PYTHON` found in `PATH` is used.
Expand Down Expand Up @@ -94,17 +95,17 @@ MXDEV?=mxdev
# Default: mxmake
MXMAKE?=mxmake

## qa.isort
## qa.ruff

# Source folder to scan for Python files to run isort on.
# Source folder to scan for Python files to run ruff on.
# Default: src
ISORT_SRC?=src
RUFF_SRC?=src

## qa.black
## qa.isort

# Source folder to scan for Python files to run black on.
# Source folder to scan for Python files to run isort on.
# Default: src
BLACK_SRC?=src
ISORT_SRC?=src

## core.mxfiles

Expand Down Expand Up @@ -199,30 +200,57 @@ else
MXENV_PYTHON=$(PRIMARY_PYTHON)
endif

# Determine the package installer
# Determine the package installer with non-interactive flags
ifeq ("$(PYTHON_PACKAGE_INSTALLER)","uv")
PYTHON_PACKAGE_COMMAND=uv pip
PYTHON_PACKAGE_COMMAND=uv pip --quiet --no-progress
else
PYTHON_PACKAGE_COMMAND=$(MXENV_PYTHON) -m pip
endif

# Auto-detect global uv availability (simple existence check)
ifeq ("$(PYTHON_PACKAGE_INSTALLER)","uv")
UV_AVAILABLE:=$(shell command -v uv >/dev/null 2>&1 && echo "true" || echo "false")
else
UV_AVAILABLE:=false
endif

# Determine installation strategy
USE_GLOBAL_UV:=$(shell [[ "$(PYTHON_PACKAGE_INSTALLER)" == "uv" && "$(UV_AVAILABLE)" == "true" ]] && echo "true" || echo "false")
USE_LOCAL_UV:=$(shell [[ "$(PYTHON_PACKAGE_INSTALLER)" == "uv" && "$(UV_AVAILABLE)" == "false" ]] && echo "true" || echo "false")

# Check if global UV is outdated (non-blocking warning)
ifeq ("$(USE_GLOBAL_UV)","true")
UV_OUTDATED:=$(shell uv self update --dry-run 2>&1 | grep -q "Would update" && echo "true" || echo "false")
else
UV_OUTDATED:=false
endif

MXENV_TARGET:=$(SENTINEL_FOLDER)/mxenv.sentinel
$(MXENV_TARGET): $(SENTINEL)
ifneq ("$(PYTHON_PACKAGE_INSTALLER)$(MXENV_UV_GLOBAL)","uvfalse")
# Validation: Check Python version if not using global uv
ifneq ("$(USE_GLOBAL_UV)","true")
@$(PRIMARY_PYTHON) -c "import sys; vi = sys.version_info; sys.exit(1 if (int(vi[0]), int(vi[1])) >= tuple(map(int, '$(PYTHON_MIN_VERSION)'.split('.'))) else 0)" \
&& echo "Need Python >= $(PYTHON_MIN_VERSION)" && exit 1 || :
else
@echo "Use Python $(PYTHON_MIN_VERSION) over uv"
@echo "Using global uv for Python $(UV_PYTHON)"
endif
# Validation: Check VENV_FOLDER is set if venv enabled
@[[ "$(VENV_ENABLED)" == "true" && "$(VENV_FOLDER)" == "" ]] \
&& echo "VENV_FOLDER must be configured if VENV_ENABLED is true" && exit 1 || :
@[[ "$(VENV_ENABLED)$(PYTHON_PACKAGE_INSTALLER)" == "falseuv" ]] \
# Validation: Check uv not used with system Python
@[[ "$(VENV_ENABLED)" == "false" && "$(PYTHON_PACKAGE_INSTALLER)" == "uv" ]] \
&& echo "Package installer uv does not work with a global Python interpreter." && exit 1 || :
# Warning: Notify if global UV is outdated
ifeq ("$(UV_OUTDATED)","true")
@echo "WARNING: A newer version of uv is available. Run 'uv self update' to upgrade."
endif

# Create virtual environment
ifeq ("$(VENV_ENABLED)", "true")
ifeq ("$(VENV_CREATE)", "true")
ifeq ("$(PYTHON_PACKAGE_INSTALLER)$(MXENV_UV_GLOBAL)","uvtrue")
@echo "Setup Python Virtual Environment using package 'uv' at '$(VENV_FOLDER)'"
@uv venv -p $(PRIMARY_PYTHON) --seed $(VENV_FOLDER)
ifeq ("$(USE_GLOBAL_UV)","true")
@echo "Setup Python Virtual Environment using global uv at '$(VENV_FOLDER)'"
@uv venv --quiet --no-progress -p $(UV_PYTHON) --seed $(VENV_FOLDER)
else
@echo "Setup Python Virtual Environment using module 'venv' at '$(VENV_FOLDER)'"
@$(PRIMARY_PYTHON) -m venv $(VENV_FOLDER)
Expand All @@ -232,10 +260,14 @@ endif
else
@echo "Using system Python interpreter"
endif
ifeq ("$(PYTHON_PACKAGE_INSTALLER)$(MXENV_UV_GLOBAL)","uvfalse")
@echo "Install uv"

# Install uv locally if needed
ifeq ("$(USE_LOCAL_UV)","true")
@echo "Install uv in virtual environment"
@$(MXENV_PYTHON) -m pip install uv
endif

# Install/upgrade core packages
@$(PYTHON_PACKAGE_COMMAND) install -U pip setuptools wheel
@echo "Install/Update MXStack Python packages"
@$(PYTHON_PACKAGE_COMMAND) install -U $(MXDEV) $(MXMAKE)
Expand Down Expand Up @@ -263,6 +295,41 @@ INSTALL_TARGETS+=mxenv
DIRTY_TARGETS+=mxenv-dirty
CLEAN_TARGETS+=mxenv-clean

##############################################################################
# ruff
##############################################################################

RUFF_TARGET:=$(SENTINEL_FOLDER)/ruff.sentinel
$(RUFF_TARGET): $(MXENV_TARGET)
@echo "Install Ruff"
@$(PYTHON_PACKAGE_COMMAND) install ruff
@touch $(RUFF_TARGET)

.PHONY: ruff-check
ruff-check: $(RUFF_TARGET)
@echo "Run ruff check"
@ruff check $(RUFF_SRC)

.PHONY: ruff-format
ruff-format: $(RUFF_TARGET)
@echo "Run ruff format"
@ruff format $(RUFF_SRC)

.PHONY: ruff-dirty
ruff-dirty:
@rm -f $(RUFF_TARGET)

.PHONY: ruff-clean
ruff-clean: ruff-dirty
@test -e $(MXENV_PYTHON) && $(MXENV_PYTHON) -m pip uninstall -y ruff || :
@rm -rf .ruff_cache

INSTALL_TARGETS+=$(RUFF_TARGET)
CHECK_TARGETS+=ruff-check
FORMAT_TARGETS+=ruff-format
DIRTY_TARGETS+=ruff-dirty
CLEAN_TARGETS+=ruff-clean

##############################################################################
# isort
##############################################################################
Expand Down Expand Up @@ -297,40 +364,6 @@ FORMAT_TARGETS+=isort-format
DIRTY_TARGETS+=isort-dirty
CLEAN_TARGETS+=isort-clean

##############################################################################
# black
##############################################################################

BLACK_TARGET:=$(SENTINEL_FOLDER)/black.sentinel
$(BLACK_TARGET): $(MXENV_TARGET)
@echo "Install Black"
@$(PYTHON_PACKAGE_COMMAND) install black
@touch $(BLACK_TARGET)

.PHONY: black-check
black-check: $(BLACK_TARGET)
@echo "Run black checks"
@black --check $(BLACK_SRC)

.PHONY: black-format
black-format: $(BLACK_TARGET)
@echo "Run black format"
@black $(BLACK_SRC)

.PHONY: black-dirty
black-dirty:
@rm -f $(BLACK_TARGET)

.PHONY: black-clean
black-clean: black-dirty
@test -e $(MXENV_PYTHON) && $(MXENV_PYTHON) -m pip uninstall -y black || :

INSTALL_TARGETS+=$(BLACK_TARGET)
CHECK_TARGETS+=black-check
FORMAT_TARGETS+=black-format
DIRTY_TARGETS+=black-dirty
CLEAN_TARGETS+=black-clean

##############################################################################
# mxfiles
##############################################################################
Expand Down
Loading