diff --git a/.github/workflows/python-code-quality.yml b/.github/workflows/python-code-quality.yml index 871436509c..dd4c0b57cf 100644 --- a/.github/workflows/python-code-quality.yml +++ b/.github/workflows/python-code-quality.yml @@ -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 @@ -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 diff --git a/python/.pre-commit-config.yaml b/python/.pre-commit-config.yaml index a6274114af..6d5df0b32c 100644 --- a/python/.pre-commit-config.yaml +++ b/python/.pre-commit-config.yaml @@ -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 diff --git a/python/check_md_code_blocks.py b/python/check_md_code_blocks.py index 1015fafdb1..7377a73038 100644 --- a/python/check_md_code_blocks.py +++ b/python/check_md_code_blocks.py @@ -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 @@ 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) diff --git a/python/packages/anthropic/tests/test_anthropic_client.py b/python/packages/anthropic/tests/test_anthropic_client.py index 677cc1e166..11498bfe59 100644 --- a/python/packages/anthropic/tests/test_anthropic_client.py +++ b/python/packages/anthropic/tests/test_anthropic_client.py @@ -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"] diff --git a/python/packages/chatkit/pyproject.toml b/python/packages/chatkit/pyproject.toml index 091147d423..bec671e599 100644 --- a/python/packages/chatkit/pyproject.toml +++ b/python/packages/chatkit/pyproject.toml @@ -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" \ No newline at end of file +build-backend = "flit_core.buildapi" diff --git a/python/pyproject.toml b/python/pyproject.toml index 6b11734d6d..8e35ff288d 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -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] -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"] diff --git a/python/run_tasks_in_changed_packages.py b/python/run_tasks_in_changed_packages.py new file mode 100644 index 0000000000..a0071ceaf8 --- /dev/null +++ b/python/run_tasks_in_changed_packages.py @@ -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() diff --git a/python/uv.lock b/python/uv.lock index 826c75a5b4..8855f2a583 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -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]]