mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
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:
committed by
GitHub
Unverified
parent
8d7e01e2ee
commit
9e69e66cfe
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
|
||||
|
||||
@@ -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
@@ -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"]
|
||||
|
||||
@@ -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()
|
||||
Generated
+4
-4
@@ -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]]
|
||||
|
||||
Reference in New Issue
Block a user