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
6 changes: 5 additions & 1 deletion .github/workflows/python-code-quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ jobs:
UV_PYTHON: ${{ matrix.python-version }}
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Set up python and install the project
id: python-setup
uses: ./.github/actions/python-setup
Expand All @@ -46,4 +48,6 @@ jobs:
with:
extra_args: --config python/.pre-commit-config.yaml --all-files
- name: Run Mypy
run: uv run poe mypy
env:
GITHUB_BASE_REF: ${{ github.event.pull_request.base.ref || github.base_ref || 'main' }}
run: uv run poe ci-mypy
1 change: 0 additions & 1 deletion python/.pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 15 additions & 7 deletions python/check_md_code_blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -126,8 +133,9 @@ def check_code_blocks(markdown_file_paths: list[str], exclude_patterns: list[str
# 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)
2 changes: 1 addition & 1 deletion python/packages/anthropic/tests/test_anthropic_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]

Expand Down
4 changes: 2 additions & 2 deletions python/packages/chatkit/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ omit = [
]

[tool.pyright]
extend = "../../pyproject.toml"
extends = "../../pyproject.toml"
exclude = ['tests', 'chatkit-python', 'openai-chatkit-advanced-samples']

[tool.mypy]
Expand Down Expand Up @@ -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"
85 changes: 43 additions & 42 deletions python/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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/**
Expand Down Expand Up @@ -282,50 +279,54 @@ 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"},
{ ref = "install" },
{ ref = "pre-commit-install" }
]
args = [{ name = "python", default = "3.13", options = ['-p', '--python'] }]
[tool.poe.tasks.docs-full]
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]

[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 = "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"]
Expand Down
133 changes: 133 additions & 0 deletions python/run_tasks_in_changed_packages.py
Original file line number Diff line number Diff line change
@@ -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()
8 changes: 4 additions & 4 deletions python/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.