Python: pre-commit improvements (#2222)

* pre-commit improvements

* updated lock

* fix for globbing

* reuse logic for mypy

* updated ci-mypy
This commit is contained in:
Eduard van Valkenburg
2025-11-14 19:00:25 +01:00
committed by GitHub
Unverified
parent 8d7e01e2ee
commit 9e69e66cfe
8 changed files with 203 additions and 58 deletions
-1
View File
@@ -47,7 +47,6 @@ repos:
entry: uv --directory ./python run poe pre-commit-check
language: system
files: ^python/
pass_filenames: false
- repo: https://github.com/astral-sh/uv-pre-commit
# uv version.
rev: 0.7.18
+15 -7
View File
@@ -33,13 +33,20 @@ def with_color(text: str, color: Colors) -> str:
return f"{color.value}{text}{Colors.CEND.value}"
def expand_file_patterns(patterns: list[str]) -> list[str]:
def expand_file_patterns(patterns: list[str], skip_glob: bool = False) -> list[str]:
"""Expand glob patterns to actual file paths."""
all_files: list[str] = []
for pattern in patterns:
# Handle both relative and absolute paths
matches = glob.glob(pattern, recursive=True)
all_files.extend(matches)
if skip_glob:
# When skip_glob is True, treat patterns as literal file paths
# Only include if it's a markdown file
if pattern.endswith('.md'):
matches = glob.glob(pattern, recursive=False)
all_files.extend(matches)
else:
# Handle both relative and absolute paths with glob expansion
matches = glob.glob(pattern, recursive=True)
all_files.extend(matches)
return sorted(set(all_files)) # Remove duplicates and sort
@@ -126,8 +133,9 @@ if __name__ == "__main__":
# Argument is a list of markdown files containing glob patterns
parser.add_argument("markdown_files", nargs="+", help="Markdown files to check (supports glob patterns).")
parser.add_argument("--exclude", action="append", help="Exclude files containing this pattern.")
parser.add_argument("--no-glob", action="store_true", help="Treat file arguments as literal paths (no glob expansion).")
args = parser.parse_args()
# Expand glob patterns to actual file paths
expanded_files = expand_file_patterns(args.markdown_files)
# Expand glob patterns to actual file paths (or skip if --no-glob)
expanded_files = expand_file_patterns(args.markdown_files, skip_glob=args.no_glob)
check_code_blocks(expanded_files, args.exclude)
@@ -92,7 +92,7 @@ def test_anthropic_settings_init_with_explicit_values() -> None:
@pytest.mark.parametrize("exclude_list", [["ANTHROPIC_API_KEY"]], indirect=True)
def test_anthropic_settings_missing_api_key(anthropic_unit_test_env: dict[str, str]) -> None:
"""Test AnthropicSettings when API key is missing."""
settings = AnthropicSettings()
settings = AnthropicSettings(env_file_path="test.env")
assert settings.api_key is None
assert settings.chat_model_id == anthropic_unit_test_env["ANTHROPIC_CHAT_MODEL_ID"]
+2 -2
View File
@@ -56,7 +56,7 @@ omit = [
]
[tool.pyright]
extend = "../../pyproject.toml"
extends = "../../pyproject.toml"
exclude = ['tests', 'chatkit-python', 'openai-chatkit-advanced-samples']
[tool.mypy]
@@ -86,4 +86,4 @@ test = "pytest --cov=agent_framework_chatkit --cov-report=term-missing:skip-cove
[build-system]
requires = ["flit-core >= 3.11,<4.0"]
build-backend = "flit_core.buildapi"
build-backend = "flit_core.buildapi"
+43 -42
View File
@@ -215,11 +215,6 @@ executor.type = "uv"
[tool.poe.tasks]
markdown-code-lint = "uv run python check_md_code_blocks.py 'README.md' './packages/**/README.md' './samples/**/*.md' --exclude cookiecutter-agent-framework-lab --exclude tau2 --exclude 'packages/devui/frontend'"
docs-install = "uv sync --all-packages --all-extras --dev -U --prerelease=if-necessary-or-explicit --group=docs"
docs-clean = "rm -rf docs/build"
docs-build = "uv run python ./docs/generate_docs.py"
docs-debug = "uv run python -m debugpy --listen 5678 ./docs/generate_docs.py"
docs-rename = "mv docs/build/agent-framework-core docs/build/agent-framework"
pre-commit-install = "uv run pre-commit install --install-hooks --overwrite"
install = "uv sync --all-packages --all-extras --dev -U --prerelease=if-necessary-or-explicit --no-group=docs"
test = "python run_tasks_in_packages_if_exists.py test"
@@ -239,21 +234,23 @@ build = ["build-packages", "build-meta"]
publish = "uv publish"
# combined checks
check = ["fmt", "lint", "pyright", "mypy", "test", "markdown-code-lint"]
pre-commit-check = ["fmt", "lint", "pyright", "markdown-code-lint"]
[tool.poe.tasks.all-tests-cov]
cmd = """
pytest --import-mode=importlib
--cov=agent_framework
--cov=agent_framework_core
--cov=agent_framework_a2a
--cov=agent_framework_ag_ui
--cov=agent_framework_anthropic
--cov=agent_framework_azure_ai
--cov=agent_framework_azurefunctions
--cov=agent_framework_chatkit
--cov=agent_framework_copilotstudio
--cov=agent_framework_mem0
--cov=agent_framework_redis
--cov=agent_framework_purview
--cov=agent_framework_redis
--cov-config=pyproject.toml
--cov-report=term-missing:skip-covered
--ignore-glob=packages/lab/**
--ignore-glob=packages/devui/**
@@ -282,6 +279,7 @@ packages/azure-ai/tests
[tool.poe.tasks.venv]
cmd = "uv venv --clear --python $python"
args = [{ name = "python", default = "3.13", options = ['-p', '--python'] }]
[tool.poe.tasks.setup]
sequence = [
{ ref = "venv --python $python"},
@@ -289,43 +287,46 @@ sequence = [
{ ref = "pre-commit-install" }
]
args = [{ name = "python", default = "3.13", options = ['-p', '--python'] }]
[tool.poe.tasks.docs-full]
[tool.poe.tasks.pre-commit-markdown-code-lint]
cmd = "uv run python check_md_code_blocks.py ${files} --no-glob --exclude cookiecutter-agent-framework-lab --exclude tau2 --exclude 'packages/devui/frontend'"
args = [{ name = "files", default = ".", positional = true, multiple = true }]
[tool.poe.tasks.pre-commit-pyright]
cmd = "uv run python run_tasks_in_changed_packages.py pyright ${files}"
args = [{ name = "files", default = ".", positional = true, multiple = true }]
[tool.poe.tasks.ci-mypy]
shell = """
# Try multiple strategies to get changed files
if [ -n "$GITHUB_BASE_REF" ]; then
# In GitHub Actions PR context
git fetch origin $GITHUB_BASE_REF --depth=1 2>/dev/null || true
CHANGED_FILES=$(git diff --name-only origin/$GITHUB_BASE_REF...HEAD -- . 2>/dev/null || \
git diff --name-only FETCH_HEAD...HEAD -- . 2>/dev/null || \
git diff --name-only HEAD^...HEAD -- . 2>/dev/null || \
echo ".")
else
# Local development
CHANGED_FILES=$(git diff --name-only origin/main...HEAD -- . 2>/dev/null || \
git diff --name-only main...HEAD -- . 2>/dev/null || \
git diff --name-only HEAD~1 -- . 2>/dev/null || \
echo ".")
fi
echo "Changed files: $CHANGED_FILES"
uv run python run_tasks_in_changed_packages.py mypy $CHANGED_FILES
"""
interpreter = "bash"
[tool.poe.tasks.pre-commit-check]
sequence = [
{ ref = "clean-dist" },
{ ref = 'build' },
{ ref = "docs-clean" },
{ ref = "docs-build" },
{ ref = "docs-rename" }
]
[tool.poe.tasks.docs-full-setup-install]
sequence = [
{ ref = "setup --python 3.11" },
{ ref = "docs-install" },
{ ref = 'build' },
{ ref = "docs-clean" },
{ ref = "docs-build" },
{ ref = "docs-rename" }
]
[tool.poe.tasks.docs-full-install]
sequence = [
{ ref = "docs-install" },
{ ref = 'build' },
{ ref = "docs-clean" },
{ ref = "docs-build" },
{ ref = "docs-rename" }
]
[tool.poe.tasks.docs-rebuild]
sequence = [
{ ref = "docs-clean" },
{ ref = "docs-build" },
{ ref = "docs-rename" }
]
[tool.poe.tasks.docs-rebuild-debug]
sequence = [
{ ref = "docs-clean" },
{ ref = "docs-debug" },
{ ref = "docs-rename" }
{ ref = "fmt" },
{ ref = "lint" },
{ ref = "pre-commit-pyright ${files}" },
{ ref = "pre-commit-markdown-code-lint ${files}" }
]
args = [{ name = "files", default = ".", positional = true, multiple = true }]
[tool.setuptools.packages.find]
where = ["packages"]
+133
View File
@@ -0,0 +1,133 @@
# Copyright (c) Microsoft. All rights reserved.
"""Run a task only in packages that have changed files."""
import argparse
import glob
import sys
from pathlib import Path
import tomli
from poethepoet.app import PoeThePoet
from rich import print
def discover_projects(workspace_pyproject_file: Path) -> list[Path]:
with workspace_pyproject_file.open("rb") as f:
data = tomli.load(f)
projects = data["tool"]["uv"]["workspace"]["members"]
exclude = data["tool"]["uv"]["workspace"].get("exclude", [])
all_projects: list[Path] = []
for project in projects:
if "*" in project:
globbed = glob.glob(str(project), root_dir=workspace_pyproject_file.parent)
globbed_paths = [Path(p) for p in globbed]
all_projects.extend(globbed_paths)
else:
all_projects.append(Path(project))
for project in exclude:
if "*" in project:
globbed = glob.glob(str(project), root_dir=workspace_pyproject_file.parent)
globbed_paths = [Path(p) for p in globbed]
all_projects = [p for p in all_projects if p not in globbed_paths]
else:
all_projects = [p for p in all_projects if p != Path(project)]
return all_projects
def extract_poe_tasks(file: Path) -> set[str]:
with file.open("rb") as f:
data = tomli.load(f)
tasks = set(data.get("tool", {}).get("poe", {}).get("tasks", {}).keys())
# Check if there is an include too
include: str | None = data.get("tool", {}).get("poe", {}).get("include", None)
if include:
include_file = file.parent / include
if include_file.exists():
tasks = tasks.union(extract_poe_tasks(include_file))
return tasks
def get_changed_packages(projects: list[Path], changed_files: list[str], workspace_root: Path) -> set[Path]:
"""Determine which packages have changed files."""
changed_packages: set[Path] = set()
core_package_changed = False
for file_path in changed_files:
# Strip 'python/' prefix if present (when git diff is run from repo root)
file_path_str = str(file_path)
if file_path_str.startswith("python/"):
file_path_str = file_path_str[7:] # Remove 'python/' prefix
# Convert to absolute path if relative
abs_path = Path(file_path_str)
if not abs_path.is_absolute():
abs_path = workspace_root / file_path_str
# Check which package this file belongs to
for project in projects:
project_abs = workspace_root / project
try:
# Check if the file is within this project directory
abs_path.relative_to(project_abs)
changed_packages.add(project)
# Check if the core package was changed
if project == Path("packages/core"):
core_package_changed = True
break
except ValueError:
# File is not in this project
continue
# If core package changed, check all packages
if core_package_changed:
print("[yellow]Core package changed - checking all packages[/yellow]")
return set(projects)
return changed_packages
def main() -> None:
parser = argparse.ArgumentParser(description="Run a task only in packages with changed files.")
parser.add_argument("task", help="The task name to run")
parser.add_argument("files", nargs="*", help="Changed files to determine which packages to run")
args = parser.parse_args()
pyproject_file = Path(__file__).parent / "pyproject.toml"
workspace_root = pyproject_file.parent
projects = discover_projects(pyproject_file)
# If no files specified, run in all packages (default behavior)
if not args.files or args.files == ["."]:
print(f"[yellow]No specific files provided, running {args.task} in all packages[/yellow]")
changed_packages = set(projects)
else:
changed_packages = get_changed_packages(projects, args.files, workspace_root)
if changed_packages:
print(f"[cyan]Detected changes in packages: {', '.join(str(p) for p in sorted(changed_packages))}[/cyan]")
else:
print(f"[yellow]No changes detected in any package, skipping {args.task}[/yellow]")
return
# Run the task in changed packages
for project in sorted(changed_packages):
tasks = extract_poe_tasks(project / "pyproject.toml")
if args.task in tasks:
print(f"Running task {args.task} in {project}")
app = PoeThePoet(cwd=project)
result = app(cli_args=[args.task])
if result:
sys.exit(result)
else:
print(f"Task {args.task} not found in {project}")
if __name__ == "__main__":
main()
+4 -4
View File
@@ -1,5 +1,5 @@
version = 1
revision = 2
revision = 3
requires-python = ">=3.10"
resolution-markers = [
"python_full_version >= '3.13' and sys_platform == 'darwin'",
@@ -2792,7 +2792,7 @@ wheels = [
[[package]]
name = "langfuse"
version = "3.9.3"
version = "3.10.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "backoff", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -2806,9 +2806,9 @@ dependencies = [
{ name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "wrapt", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/dd/fe/80bda024047570ed6d918ee7481888ee572659d04f40ff49b58701e52245/langfuse-3.9.3.tar.gz", hash = "sha256:8081691d40325b7022b07fc4e5884853c4a4ffe03b05c702490788b9a1a125df", size = 206118, upload-time = "2025-11-12T14:51:23.129Z" }
sdist = { url = "https://files.pythonhosted.org/packages/31/97/bacdac633511a1c9895d9e3b1720c3d008a0f5d7d5c0e058d8cf79040773/langfuse-3.10.0.tar.gz", hash = "sha256:7a9254103da50cabcf46a7e7ae9d2cf1f8267f09b6036abdb4a9c6f2f3d15104", size = 220516, upload-time = "2025-11-14T12:15:56.839Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/01/bc/af86791c761b820097e3ec2faa747bc6a507f6bf0cd6d7b7bbe4d087a6d6/langfuse-3.9.3-py3-none-any.whl", hash = "sha256:0afe88773f20fc67636d8a52dd3a5fb4e10f1c6e226f2c99ace8c65190fb5697", size = 374759, upload-time = "2025-11-12T14:51:21.147Z" },
{ url = "https://files.pythonhosted.org/packages/86/05/343e8fa3e5d48035422e1385105aeb986130d3de3264ec732c890dab2d16/langfuse-3.10.0-py3-none-any.whl", hash = "sha256:c7c10607c6fe560990d0973566afd3c5513f7ff97cc31691af72711ad5441d8c", size = 390745, upload-time = "2025-11-14T12:15:54.912Z" },
]
[[package]]