diff --git a/.github/workflows/python-code-quality.yml b/.github/workflows/python-code-quality.yml index ada8d23738..ef75293f0c 100644 --- a/.github/workflows/python-code-quality.yml +++ b/.github/workflows/python-code-quality.yml @@ -75,7 +75,7 @@ jobs: os: ${{ runner.os }} env: UV_CACHE_DIR: /tmp/.uv-cache - - name: Run fmt, lint, pyright in parallel across packages + - name: Run syntax and pyright across packages run: uv run poe check-packages samples-markdown: @@ -104,10 +104,8 @@ jobs: os: ${{ runner.os }} env: UV_CACHE_DIR: /tmp/.uv-cache - - name: Run samples lint - run: uv run poe samples-lint - - name: Run samples syntax check - run: uv run poe samples-syntax + - name: Run samples checks + run: uv run poe check -S - name: Run markdown code lint run: uv run poe markdown-code-lint @@ -140,4 +138,4 @@ jobs: - name: Run Mypy env: GITHUB_BASE_REF: ${{ github.event.pull_request.base.ref || github.base_ref || 'main' }} - run: uv run poe ci-mypy + run: uv run python scripts/workspace_poe_tasks.py ci-mypy diff --git a/.github/workflows/python-dependency-range-validation.yml b/.github/workflows/python-dependency-range-validation.yml index 2f01552796..1ccf9784fb 100644 --- a/.github/workflows/python-dependency-range-validation.yml +++ b/.github/workflows/python-dependency-range-validation.yml @@ -38,7 +38,7 @@ jobs: id: validate_ranges # Keep workflow running so we can still publish diagnostics from this run. continue-on-error: true - run: uv run poe validate-dependency-bounds-project --mode upper --project "*" + run: uv run poe validate-dependency-bounds-project --mode upper --package "*" working-directory: ./python - name: Upload dependency range report @@ -203,7 +203,7 @@ jobs: cat > "${PR_BODY_FILE}" <<'EOF' This PR was generated by the dependency range validation workflow. - - Ran `uv run poe validate-dependency-bounds-project --mode upper --project "*"` + - Ran `uv run poe validate-dependency-bounds-project --mode upper --package "*"` - Updated package dependency bounds - Refreshed `python/uv.lock` with `uv lock --upgrade` EOF diff --git a/.github/workflows/python-integration-tests.yml b/.github/workflows/python-integration-tests.yml index 913b4325b4..8f17137569 100644 --- a/.github/workflows/python-integration-tests.yml +++ b/.github/workflows/python-integration-tests.yml @@ -48,9 +48,8 @@ jobs: os: ${{ runner.os }} - name: Test with pytest (unit tests only) run: > - uv run poe all-tests + uv run poe test -A -m "not integration" - -n logical --dist worksteal --timeout=120 --session-timeout=900 --timeout_method thread --retries 2 --retry-delay 5 diff --git a/.github/workflows/python-merge-tests.yml b/.github/workflows/python-merge-tests.yml index 0e070463d4..bcf545beac 100644 --- a/.github/workflows/python-merge-tests.yml +++ b/.github/workflows/python-merge-tests.yml @@ -100,9 +100,8 @@ jobs: os: ${{ runner.os }} - name: Test with pytest (unit tests only) run: > - uv run poe all-tests + uv run poe test -A -m "not integration" - -n logical --dist worksteal --timeout=120 --session-timeout=900 --timeout_method thread --retries 2 --retry-delay 5 working-directory: ./python diff --git a/.github/workflows/python-test-coverage.yml b/.github/workflows/python-test-coverage.yml index 7563504b69..0acfeaff44 100644 --- a/.github/workflows/python-test-coverage.yml +++ b/.github/workflows/python-test-coverage.yml @@ -32,13 +32,13 @@ jobs: id: python-setup uses: ./.github/actions/python-setup with: - python-version: ${{ matrix.python-version }} + python-version: ${{ env.UV_PYTHON }} os: ${{ runner.os }} env: # Configure a constant location for the uv cache UV_CACHE_DIR: /tmp/.uv-cache - name: Run all tests with coverage report - run: uv run poe all-tests-cov --cov-report=xml:python-coverage.xml -q --junitxml=pytest.xml + run: uv run poe test -A -C --cov-report=xml:python-coverage.xml -q --junitxml=pytest.xml - name: Check coverage threshold run: python ${{ github.workspace }}/.github/workflows/python-check-coverage.py python-coverage.xml ${{ env.COVERAGE_THRESHOLD }} - name: Upload coverage report diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index ba2796a8f5..3e12773090 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -40,7 +40,7 @@ jobs: UV_CACHE_DIR: /tmp/.uv-cache # Unit tests - name: Run all tests - run: uv run poe all-tests ${{ matrix.python-version == '3.10' && '--ignore-glob=packages/github_copilot/**' || '' }} + run: uv run poe test -A working-directory: ./python # Surface failing tests diff --git a/python/.github/skills/python-code-quality/SKILL.md b/python/.github/skills/python-code-quality/SKILL.md index 9a1ba521b3..29ac63e4fe 100644 --- a/python/.github/skills/python-code-quality/SKILL.md +++ b/python/.github/skills/python-code-quality/SKILL.md @@ -13,26 +13,34 @@ description: > All commands run from the `python/` directory: ```bash -# Format code (ruff format, parallel across packages) -uv run poe fmt - -# Lint and auto-fix (ruff check, parallel across packages) -uv run poe lint +# Syntax formatting + checks (parallel across packages by default) +uv run poe syntax +uv run poe syntax -P core +uv run poe syntax -F # Format only +uv run poe syntax -C # Check only +uv run poe syntax -S # Samples only # Type checking -uv run poe pyright # Pyright (parallel across packages) -uv run poe mypy # MyPy (parallel across packages) +uv run poe pyright # Pyright fan-out across packages +uv run poe pyright -P core +uv run poe pyright -A +uv run poe mypy # MyPy fan-out across packages +uv run poe mypy -P core +uv run poe mypy -A uv run poe typing # Both pyright and mypy +uv run poe typing -P core +uv run poe typing -A -# All package-level checks in parallel (fmt + lint + pyright + mypy) +# All package-level checks in parallel (syntax + pyright) uv run poe check-packages # Full check (packages + samples + tests + markdown) uv run poe check +uv run poe check -P core # Samples only -uv run poe samples-lint # Ruff lint on samples/ -uv run poe samples-syntax # Pyright syntax check on samples/ +uv run poe check -S +uv run poe pyright -S # Markdown code blocks uv run poe markdown-code-lint @@ -40,8 +48,8 @@ uv run poe markdown-code-lint ## Pre-commit Hooks (prek) -Prek hooks run automatically on commit. They check only changed files and run -package-level checks in parallel for affected packages only. +Prek hooks run automatically on commit. They stay lightweight and only check +changed files. ```bash # Install hooks @@ -54,8 +62,10 @@ uv run prek run -a uv run prek run --last-commit ``` -When core package changes, type-checking (mypy, pyright) runs across all packages -since type changes propagate. Format and lint only run in changed packages. +They run changed-package syntax formatting/checking, markdown code lint only +when markdown files change, and sample syntax lint/pyright only when files +under `samples/` change. +They intentionally do not run workspace `pyright` or `mypy` by default. ## Ruff Configuration @@ -80,6 +90,6 @@ in-process with streaming output. CI splits into 4 parallel jobs: 1. **Pre-commit hooks** — lightweight hooks (SKIP=poe-check) -2. **Package checks** — fmt/lint/pyright via check-packages -3. **Samples & markdown** — samples-lint, samples-syntax, markdown-code-lint +2. **Package checks** — syntax/pyright via check-packages +3. **Samples & markdown** — `check -S` plus `markdown-code-lint` 4. **Mypy** — change-detected mypy checks diff --git a/python/.github/skills/python-package-management/SKILL.md b/python/.github/skills/python-package-management/SKILL.md index e954480e75..814410e73d 100644 --- a/python/.github/skills/python-package-management/SKILL.md +++ b/python/.github/skills/python-package-management/SKILL.md @@ -47,17 +47,17 @@ uv run poe upgrade-dev-dependencies # First, run workspace-wide lower/upper compatibility gates uv run poe validate-dependency-bounds-test -# Defaults to --project "*"; pass a package to scope test mode -uv run poe validate-dependency-bounds-test --project +# Defaults to --package "*"; pass a package to scope test mode +uv run poe validate-dependency-bounds-test --package core # Then expand bounds for one dependency in the target package -uv run poe validate-dependency-bounds-project --mode both --project --dependency "" +uv run poe validate-dependency-bounds-project --mode both --package core --dependency "" # Repo-wide automation can reuse the same task -uv run poe validate-dependency-bounds-project --mode upper --project "*" +uv run poe validate-dependency-bounds-project --mode upper --package "*" # Add a dependency to one project and run both validators for that project/dependency -uv run poe add-dependency-and-validate-bounds --project --dependency "" +uv run poe add-dependency-and-validate-bounds --package core --dependency "" ``` ### Dependency Bound Notes @@ -66,7 +66,7 @@ uv run poe add-dependency-and-validate-bounds --project - Prerelease (`dev`/`a`/`b`/`rc`) and `<1.0` dependencies should use hard bounds with an explicit upper cap (avoid open-ended ranges). - For `<1.0` dependencies, prefer the broadest validated range the package can really support. That may be a patch line, a minor line, or multiple minor lines when checks/tests show the broader lane is compatible. - Prefer supporting multiple majors when practical; if APIs diverge across supported majors, use version-conditional imports/paths. -- For dependency changes, run workspace-wide bound gates first, then `validate-dependency-bounds-project --mode both` for the target package/dependency to keep minimum and maximum constraints current. The same task can also drive repo-wide upper-bound automation by using `--project "*"` and omitting `--dependency`. +- For dependency changes, run workspace-wide bound gates first, then `validate-dependency-bounds-project --mode both` for the target package/dependency to keep minimum and maximum constraints current. The same task can also drive repo-wide upper-bound automation by using `--package "*"` and omitting `--dependency`. - Prefer targeted lock updates with `uv lock --upgrade-package ` to reduce `uv.lock` merge conflicts. - Use `add-dependency-and-validate-bounds` for package-scoped dependency additions plus bound validation in one command. - Use `upgrade-dev-dependencies` for repo-wide dev tooling refreshes; it repins dev dependencies, refreshes `uv.lock`, and reruns `check`, `typing`, and `test`. @@ -108,12 +108,12 @@ def __getattr__(name: str) -> Any: Recommended dependency workflow during connector implementation: 1. Add the dependency to the target package: - `uv run poe add-dependency-to-project --project --dependency ""` + `uv run poe add-dependency-to-project --package core --dependency ""` 2. Implement connector code and tests. 3. Validate dependency bounds for that package/dependency: - `uv run poe validate-dependency-bounds-project --mode both --project --dependency ""` + `uv run poe validate-dependency-bounds-project --mode both --package core --dependency ""` 4. If the package has meaningful tests/checks that validate dependency compatibility, you can use the add + validation flow in one command: - `uv run poe add-dependency-and-validate-bounds --project --dependency ""` + `uv run poe add-dependency-and-validate-bounds --package core --dependency ""` If compatibility checks are not in place yet, add the dependency first, then implement tests before running bound validation. ### Promotion to Stable diff --git a/python/.github/skills/python-samples/SKILL.md b/python/.github/skills/python-samples/SKILL.md index b70862eb8a..be992e0771 100644 --- a/python/.github/skills/python-samples/SKILL.md +++ b/python/.github/skills/python-samples/SKILL.md @@ -41,11 +41,14 @@ Do **not** add sample-only dependencies to the root `pyproject.toml` dev group. ## Syntax Checking ```bash +# Format + lint samples +uv run poe syntax -S + # Check samples for syntax errors and missing imports -uv run poe samples-syntax +uv run poe pyright -S -# Lint samples -uv run poe samples-lint +# Lint samples only +uv run poe syntax -S -C ``` ## Documentation diff --git a/python/.github/skills/python-testing/SKILL.md b/python/.github/skills/python-testing/SKILL.md index 4b61f27a55..b9c874a694 100644 --- a/python/.github/skills/python-testing/SKILL.md +++ b/python/.github/skills/python-testing/SKILL.md @@ -17,20 +17,27 @@ We run tests in two stages, for a PR each commit is tested with unit tests only # Run tests for all packages in parallel uv run poe test -# Run tests for a specific package -uv run --directory packages/core poe test +# Run tests for a specific workspace package +uv run poe test -P core -# Run all tests in a single pytest invocation (faster, uses pytest-xdist) -uv run poe all-tests +# Run all selected tests in a single pytest invocation +uv run poe test -A # With coverage -uv run poe all-tests-cov +uv run poe test -A -C +uv run poe test -P core -C # Run only unit tests (exclude integration tests) -uv run poe all-tests -m "not integration" +uv run poe test -A -m "not integration" # Run only integration tests -uv run poe all-tests -m integration +uv run poe test -A -m integration +``` + +Direct package execution still works when you need it: + +```bash +uv run --directory packages/core poe test ``` ## Test Configuration @@ -38,7 +45,7 @@ uv run poe all-tests -m integration - **Async mode**: `asyncio_mode = "auto"` is enabled — do NOT use `@pytest.mark.asyncio`, but do mark tests with `async def` and use `await` for async calls - **Timeout**: Default 60 seconds per test - **Import mode**: `importlib` for cross-package isolation -- **Parallelization**: Large packages (core, ag-ui, orchestrations, anthropic) use `pytest-xdist` (`-n auto --dist worksteal`) in their `poe test` task. The `all-tests` task also uses xdist across all packages. +- **Parallelization**: Large packages (core, ag-ui, orchestrations, anthropic) use `pytest-xdist` (`-n auto --dist worksteal`) in their `poe test` task. The aggregate `uv run poe test -A` sweep also uses xdist across the selected packages. ## Test Directory Structure diff --git a/python/.pre-commit-config.yaml b/python/.pre-commit-config.yaml index dfdf60a61b..bbb2683c5c 100644 --- a/python/.pre-commit-config.yaml +++ b/python/.pre-commit-config.yaml @@ -52,10 +52,10 @@ repos: hooks: - id: poe-check name: Run checks through Poe - entry: uv run poe prek-check + entry: uv run python scripts/workspace_poe_tasks.py prek-check language: system - repo: https://github.com/PyCQA/bandit - rev: 1.9.3 + rev: 1.9.4 hooks: - id: bandit name: Bandit Security Checks @@ -63,7 +63,7 @@ repos: additional_dependencies: ["bandit[toml]"] - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.10.0 + rev: 0.10.10 hooks: # Update the uv lockfile - id: uv-lock diff --git a/python/.vscode/tasks.json b/python/.vscode/tasks.json index fc9ce278b3..ed5ac4997d 100644 --- a/python/.vscode/tasks.json +++ b/python/.vscode/tasks.json @@ -9,9 +9,35 @@ "command": "uv", "args": [ "run", - "prek", + "poe", + "check" + ], + "problemMatcher": { + "owner": "python", + "fileLocation": [ + "relative", + "${workspaceFolder}" + ], + "pattern": { + "regexp": "^(.*):(\\d+):(\\d+):\\s+(.*)$", + "file": 1, + "line": 2, + "column": 3, + "message": 4 + } + }, + "presentation": { + "panel": "shared" + } + }, + { + "label": "Syntax", + "type": "shell", + "command": "uv", + "args": [ "run", - "-a" + "poe", + "syntax", ], "problemMatcher": { "owner": "python", @@ -32,13 +58,14 @@ } }, { - "label": "Format", + "label": "Syntax (format only)", "type": "shell", "command": "uv", "args": [ "run", "poe", - "fmt", + "syntax", + "-F", ], "problemMatcher": { "owner": "python", @@ -59,13 +86,14 @@ } }, { - "label": "Lint", + "label": "Syntax (check only)", "type": "shell", "command": "uv", "args": [ "run", "poe", - "lint", + "syntax", + "-C", ], "problemMatcher": { "owner": "python", @@ -169,7 +197,14 @@ { "label": "Create Venv", "type": "shell", - "command": "uv venv PYTHON=${input:py_version}", + "command": "uv", + "args": [ + "run", + "poe", + "venv", + "-P", + "${input:py_version}" + ], "presentation": { "reveal": "always", "panel": "new" @@ -184,7 +219,8 @@ "run", "poe", "setup", - "--python=${input:py_version}" + "-P", + "${input:py_version}" ], "presentation": { "reveal": "always", @@ -200,11 +236,12 @@ "3.10", "3.11", "3.12", - "3.13" + "3.13", + "3.14" ], "id": "py_version", "description": "Python version", - "default": "3.10" + "default": "3.13" } ] -} \ No newline at end of file +} diff --git a/python/CODING_STANDARD.md b/python/CODING_STANDARD.md index be894dc545..d02b22e088 100644 --- a/python/CODING_STANDARD.md +++ b/python/CODING_STANDARD.md @@ -403,7 +403,7 @@ So we use bounded ranges for external package dependencies in `pyproject.toml`: - For `<1.0.0` dependencies, use a known-good bounded range with an explicit upper cap. Prefer the broadest validated range the package can actually support: that may be a patch line, a minor line, or multiple minor lines (for example: `a2a-sdk>=0.3.5,<0.4.0`, `fastapi>=0.115.0,<0.136.0`, `uvicorn>=0.30.0,<0.39.0`). - For prerelease (`dev`/`a`/`b`/`rc`) dependencies, use a known-good bounded range with a hard upper cap and keep the range only as broad as the package's validation coverage justifies. - Prefer keeping support for multiple major versions when practical. This may mean that the upper bound spans multiple major versions when the dependency maintains backward compatibility; if APIs differ between supported majors, version-conditional imports/branches are acceptable to preserve compatibility. -- When adding or changing an external dependency, first run `uv run poe validate-dependency-bounds-test` to validate workspace-wide lower/upper compatibility, then run `uv run poe validate-dependency-bounds-project --mode both --project --dependency ""` to expand package-scoped bounds. +- When adding or changing an external dependency, first run `uv run poe validate-dependency-bounds-test` to validate workspace-wide lower/upper compatibility, then run `uv run poe validate-dependency-bounds-project --mode both --package --dependency ""` to expand package-scoped bounds. ### Installation Options diff --git a/python/DEV_SETUP.md b/python/DEV_SETUP.md index fa6619b899..d90e29226d 100644 --- a/python/DEV_SETUP.md +++ b/python/DEV_SETUP.md @@ -123,28 +123,39 @@ client = OpenAIChatClient(env_file_path="openai.env") All the tests are located in the `tests` folder of each package. Tests marked with `@pytest.mark.integration` and `@skip_if_..._integration_tests_disabled` are integration tests that require external services (e.g., OpenAI, Azure OpenAI). They are automatically skipped when the required API keys or service endpoints are not configured in your environment or `.env` file. -You can select or exclude integration tests using pytest markers: +The root `test` command now supports both project-scoped fan-out and a single aggregate sweep: ```bash -# Run only unit tests (exclude integration tests) -uv run poe all-tests -m "not integration" +# Run package-local tests across all workspace packages +uv run poe test + +# Run tests for one workspace package +uv run poe test -P core + +# Run an aggregate pytest sweep across the selected packages +uv run poe test -A + +# Run only unit tests in aggregate mode +uv run poe test -A -m "not integration" + +# Run only integration tests in aggregate mode +uv run poe test -A -m integration -# Run only integration tests -uv run poe all-tests -m integration +# Run tests with coverage for one package or an aggregate sweep +uv run poe test -P core -C +uv run poe test -A -C ``` Alternatively, you can run them using VSCode Tasks. Open the command palette (`Ctrl+Shift+P`) and type `Tasks: Run Task`. Select `Test` from the list. -If you want to run the tests for a single package, you can use the `uv run poe test` command with the package name as an argument. For example, to run the tests for the `agent_framework` package, you can use: +Direct package execution still works when you need it: ```bash uv run poe --directory packages/core test ``` -Large packages (core, ag-ui, orchestrations, anthropic) use `pytest-xdist` for parallel test execution within the package. The `all-tests` task also uses xdist across all packages. - -These commands also output the coverage report. +Large packages (core, ag-ui, orchestrations, anthropic) use `pytest-xdist` for parallel test execution within the package. The aggregate `test -A` sweep also uses `pytest-xdist` across the selected packages. ## Code quality checks @@ -158,10 +169,11 @@ Ideally you should run these checks before committing any changes, when you inst ## Code Coverage -We try to maintain a high code coverage for the project. To run the code coverage on the unit tests, you can use the following command: +We try to maintain a high code coverage for the project. To review coverage locally, use either a package-scoped run or the aggregate sweep: ```bash - uv run poe test +uv run poe test -P core -C +uv run poe test -A -C ``` This will show you which files are not covered by the tests, including the specific lines not covered. Make sure to consider the untested lines from the code you are working on, but feel free to add other tests as well, that is always welcome! @@ -213,7 +225,7 @@ Set up the development environment with a virtual environment, install dependenc ```bash uv run poe setup # or with specific Python version -uv run poe setup --python 3.12 +uv run poe setup -P 3.12 ``` #### `install` @@ -230,7 +242,7 @@ Create a virtual environment with specified Python version or switch python vers ```bash uv run poe venv # or with specific Python version -uv run poe venv --python 3.12 +uv run poe venv -P 3.12 ``` #### `prek-install` @@ -239,143 +251,174 @@ Install prek hooks: uv run poe prek-install ``` -### Code Quality and Formatting +### Project-scoped command families + +These commands default to `--package "*"`, so they run across all workspace packages unless you narrow them with `-P/--package`: + +#### `syntax` +Run Ruff formatting plus Ruff lint checks by default: +```bash +uv run poe syntax +uv run poe syntax -P core +uv run poe syntax -F # format only +uv run poe syntax -C # lint/check only +``` -Each of the following tasks run against both the main `agent-framework` package and the extension packages in parallel, ensuring consistent code quality across the project. +#### `build` +Build workspace packages and the root meta package: +```bash +uv run poe build +uv run poe build -P core +``` -#### `fmt` (format) -Format code using ruff (runs in parallel across all packages): +#### `clean-dist` +Clean generated dist artifacts: ```bash -uv run poe fmt +uv run poe clean-dist +uv run poe clean-dist -P core ``` -#### `lint` -Run linting checks and fix issues (runs in parallel across all packages): +### Dual-mode validation and test commands + +These command families share the same selector model: + ```bash -uv run poe lint +uv run poe # project fan-out over --package "*" +uv run poe -P core # one-project fan-out +uv run poe -A # aggregate sweep where supported ``` #### `pyright` -Run Pyright type checking (runs in parallel across all packages): +Run Pyright type checking: ```bash uv run poe pyright +uv run poe pyright -P core +uv run poe pyright -A ``` #### `mypy` -Run MyPy type checking (runs in parallel across all packages): +Run MyPy type checking: ```bash uv run poe mypy +uv run poe mypy -P core +uv run poe mypy -A ``` #### `typing` -Run both Pyright and MyPy type checking: +Run both Pyright and MyPy: ```bash uv run poe typing +uv run poe typing -P core +uv run poe typing -A ``` -### Code Validation - -#### `markdown-code-lint` -Lint markdown code blocks: +#### `test` +Run package-local tests in fan-out mode, or switch to one aggregate pytest sweep with `-A`: ```bash -uv run poe markdown-code-lint +uv run poe test +uv run poe test -P core +uv run poe test -P core -C +uv run poe test -A +uv run poe test -A -C ``` -#### `validate-dependency-bounds-test` -Run workspace-wide dependency compatibility gates at lower and upper resolutions. This runs test + pyright across all packages and stops on first failure: -```bash -uv run poe validate-dependency-bounds-test -# Defaults to --project "*"; pass a package to scope test mode -uv run poe validate-dependency-bounds-test --project -``` +### Sample-target variants -#### `validate-dependency-bounds-project` -Validate and extend dependency bounds for a single dependency in a single package. Use `--mode lower`, `--mode upper`, or the default `--mode both`: -```bash -uv run poe validate-dependency-bounds-project --mode both --project --dependency "" -``` -`--project` defaults to `*`, and `--dependency` is optional. Automation can use `--mode upper --project "*"` to run the upper-bound pass across the workspace. -For `<1.0` dependencies, prefer the broadest validated range the package can really support. That may still be a single patch or minor line, but multi-minor ranges are fine when the package's checks/tests prove they work. +Use `-S/--samples` for sample-only validation instead of separate top-level commands: -#### `add-dependency-and-validate-bounds` -Add an external dependency to a workspace project and run both validators for that same project/dependency: ```bash -uv run poe add-dependency-and-validate-bounds --project --dependency "" +uv run poe syntax -S +uv run poe syntax -S -C +uv run poe pyright -S +uv run poe check -S ``` -#### `upgrade-dev-dependencies` -Refresh exact dev dependency pins across the workspace, run `uv lock --upgrade`, reinstall from the frozen lockfile, then rerun validation, typing, and tests: +### Workspace validation and dependency commands + +#### `markdown-code-lint` +Lint markdown code blocks: ```bash -uv run poe upgrade-dev-dependencies +uv run poe markdown-code-lint ``` -Use this for repo-wide dev tooling refreshes. For targeted runtime dependency upgrades, prefer `uv lock --upgrade-package ` plus the package-scoped bound validation tasks above. - -### Comprehensive Checks #### `check-packages` -Run all package-level quality checks (format, lint, pyright, mypy) in parallel across all packages. This runs the full cross-product of (package × check) concurrently: +Run the package-level syntax sweep (`syntax`) plus `pyright` across the selected projects: ```bash uv run poe check-packages +uv run poe check-packages -P core ``` #### `check` -Run all quality checks including package checks, samples, tests and markdown lint: +Run package syntax, pyright, and tests for the selected project set. Without `-P/--package`, it also includes sample checks and markdown lint: ```bash uv run poe check +uv run poe check -P core +uv run poe check -S ``` -### Testing - -#### `test` -Run unit tests with coverage by invoking the `test` task in each package in parallel: +#### `validate-dependency-bounds-test` +Run workspace-wide dependency compatibility gates at lower and upper resolutions. This runs test + pyright across all packages and stops on first failure: ```bash -uv run poe test +uv run poe validate-dependency-bounds-test +# Defaults to --package "*"; pass a package to scope test mode +uv run poe validate-dependency-bounds-test -P core ``` -To run tests for a specific package only, use the `--directory` flag: +#### `validate-dependency-bounds-project` +Validate and extend dependency bounds for a single dependency in a single package. Use `--mode lower`, `--mode upper`, or the default `--mode both`: ```bash -# Run tests for the core package -uv run --directory packages/core poe test - -# Run tests for the azure-ai package -uv run --directory packages/azure-ai poe test +uv run poe validate-dependency-bounds-project -M both -P core -D "" ``` +`--package` defaults to `*`, and `--dependency` is optional. Automation can use `--mode upper --package "*"` to run the upper-bound pass across the workspace. +For `<1.0` dependencies, prefer the broadest validated range the package can really support. That may still be a single patch or minor line, but multi-minor ranges are fine when the package's checks/tests prove they work. -#### `all-tests` -Run all tests in a single pytest invocation across all packages in parallel (excluding lab and devui). This is faster than `test` as it uses pytest's parallel execution: +#### `add-dependency-and-validate-bounds` +Add an external dependency to a workspace project and run both validators for that same project/dependency: ```bash -uv run poe all-tests +uv run poe add-dependency-and-validate-bounds -P core -D "" ``` -#### `all-tests-cov` -Same as `all-tests` but with coverage reporting enabled: +#### `upgrade-dev-dependencies` +Refresh exact dev dependency pins across the workspace, run `uv lock --upgrade`, reinstall from the frozen lockfile, then rerun validation, typing, and tests: ```bash -uv run poe all-tests-cov +uv run poe upgrade-dev-dependencies ``` +Use this for repo-wide dev tooling refreshes. For targeted runtime dependency upgrades, prefer `uv lock --upgrade-package ` plus the package-scoped bound validation tasks above. ### Building and Publishing -#### `build` -Build all packages: +#### `publish` +Publish packages to PyPI: ```bash -uv run poe build +uv run poe publish ``` -#### `clean-dist` -Clean the dist directories: -```bash -uv run poe clean-dist -``` +### Compatibility aliases + +These legacy commands still work during the transition, but prefer the newer forms above: -#### `publish` -Publish packages to PyPI: ```bash -uv run poe publish +uv run poe fmt # prefer: uv run poe syntax -F +uv run poe format # prefer: uv run poe syntax -F +uv run poe lint # prefer: uv run poe syntax -C +uv run poe all-tests # prefer: uv run poe test -A +uv run poe all-tests-cov # prefer: uv run poe test -A -C +uv run poe samples-lint # prefer: uv run poe syntax -S -C +uv run poe samples-syntax # prefer: uv run poe pyright -S ``` ## Prek Hooks -Prek hooks run automatically on commit and execute a subset of the checks on changed files only. Package-level checks (fmt, lint, pyright) run in parallel but only for packages with changed files. Markdown and sample checks are skipped when no relevant files were changed. If the `core` package is changed, all packages are checked. You can also run all checks using prek directly: +Prek hooks run automatically on commit and stay intentionally lightweight: + +- changed-package syntax formatting +- changed-package syntax lint/check +- markdown code lint only when markdown files change +- sample lint + sample pyright only when files under `samples/` change + +They do **not** run workspace `pyright` or `mypy` by default. Use `uv run poe pyright`, `uv run poe mypy`, `uv run poe typing`, `uv run poe check-packages`, or `uv run poe check` when you want deeper validation. + +You can run the installed hooks directly with: ```bash uv run prek run -a diff --git a/python/packages/a2a/pyproject.toml b/python/packages/a2a/pyproject.toml index fece606606..b26aa58e25 100644 --- a/python/packages/a2a/pyproject.toml +++ b/python/packages/a2a/pyproject.toml @@ -85,9 +85,13 @@ exclude_dirs = ["tests"] executor.type = "uv" include = "../../shared_tasks.toml" -[tool.poe.tasks] -mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_a2a" -test = 'pytest -m "not integration" --cov=agent_framework_a2a --cov-report=term-missing:skip-covered tests' +[tool.poe.tasks.mypy] +help = "Run MyPy for this package." +cmd = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_a2a" + +[tool.poe.tasks.test] +help = "Run the default unit test suite for this package." +cmd = 'pytest -m "not integration" --cov=agent_framework_a2a --cov-report=term-missing:skip-covered tests' [build-system] requires = ["flit-core >= 3.11,<4.0"] diff --git a/python/packages/ag-ui/pyproject.toml b/python/packages/ag-ui/pyproject.toml index 74b04669b4..38aa31a377 100644 --- a/python/packages/ag-ui/pyproject.toml +++ b/python/packages/ag-ui/pyproject.toml @@ -72,6 +72,10 @@ typeCheckingMode = "basic" executor.type = "uv" include = "../../shared_tasks.toml" -[tool.poe.tasks] -mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_ag_ui" -test = 'pytest -m "not integration" --cov=agent_framework_ag_ui --cov-report=term-missing:skip-covered -n auto --dist worksteal tests/ag_ui' +[tool.poe.tasks.mypy] +help = "Run MyPy for this package." +cmd = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_ag_ui" + +[tool.poe.tasks.test] +help = "Run the default unit test suite for this package." +cmd = 'pytest -m "not integration" --cov=agent_framework_ag_ui --cov-report=term-missing:skip-covered -n auto --dist worksteal tests/ag_ui' diff --git a/python/packages/anthropic/pyproject.toml b/python/packages/anthropic/pyproject.toml index 92522a9c50..6212ed93a0 100644 --- a/python/packages/anthropic/pyproject.toml +++ b/python/packages/anthropic/pyproject.toml @@ -85,9 +85,13 @@ exclude_dirs = ["tests"] executor.type = "uv" include = "../../shared_tasks.toml" -[tool.poe.tasks] -mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_anthropic" -test = 'pytest -m "not integration" --cov=agent_framework_anthropic --cov-report=term-missing:skip-covered -n auto --dist worksteal tests' +[tool.poe.tasks.mypy] +help = "Run MyPy for this package." +cmd = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_anthropic" + +[tool.poe.tasks.test] +help = "Run the default unit test suite for this package." +cmd = 'pytest -m "not integration" --cov=agent_framework_anthropic --cov-report=term-missing:skip-covered -n auto --dist worksteal tests' [build-system] requires = ["flit-core >= 3.11,<4.0"] diff --git a/python/packages/azure-ai-search/pyproject.toml b/python/packages/azure-ai-search/pyproject.toml index 66b5689acf..71ef0bf88b 100644 --- a/python/packages/azure-ai-search/pyproject.toml +++ b/python/packages/azure-ai-search/pyproject.toml @@ -87,9 +87,13 @@ exclude_dirs = ["tests"] executor.type = "uv" include = "../../shared_tasks.toml" -[tool.poe.tasks] -mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_azure_ai_search" -test = 'pytest -m "not integration" --cov=agent_framework_azure_ai_search --cov-report=term-missing:skip-covered tests' +[tool.poe.tasks.mypy] +help = "Run MyPy for this package." +cmd = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_azure_ai_search" + +[tool.poe.tasks.test] +help = "Run the default unit test suite for this package." +cmd = 'pytest -m "not integration" --cov=agent_framework_azure_ai_search --cov-report=term-missing:skip-covered tests' [build-system] requires = ["flit-core >= 3.11,<4.0"] diff --git a/python/packages/azure-ai/pyproject.toml b/python/packages/azure-ai/pyproject.toml index 76c37fdbea..40f849f630 100644 --- a/python/packages/azure-ai/pyproject.toml +++ b/python/packages/azure-ai/pyproject.toml @@ -85,11 +85,16 @@ exclude_dirs = ["tests"] executor.type = "uv" include = "../../shared_tasks.toml" -[tool.poe.tasks] -mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_azure_ai" -test = 'pytest -m "not integration" --cov=agent_framework_azure_ai --cov-report=term-missing:skip-covered tests' +[tool.poe.tasks.mypy] +help = "Run MyPy for this package." +cmd = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_azure_ai" + +[tool.poe.tasks.test] +help = "Run the default unit test suite for this package." +cmd = 'pytest -m "not integration" --cov=agent_framework_azure_ai --cov-report=term-missing:skip-covered tests' [tool.poe.tasks.integration-tests] +help = "Run the package integration test suite." cmd = """ pytest --import-mode=importlib -n logical --dist worksteal diff --git a/python/packages/azure-cosmos/pyproject.toml b/python/packages/azure-cosmos/pyproject.toml index 9566c53c09..4de7f1029c 100644 --- a/python/packages/azure-cosmos/pyproject.toml +++ b/python/packages/azure-cosmos/pyproject.toml @@ -84,10 +84,17 @@ exclude_dirs = ["tests"] [tool.poe] executor.type = "uv" include = "../../shared_tasks.toml" -[tool.poe.tasks] -mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_azure_cosmos" -test = "pytest -m \"not integration\" --cov=agent_framework_azure_cosmos --cov-report=term-missing:skip-covered tests" -integration-tests = "pytest tests/test_cosmos_history_provider.py -m integration" +[tool.poe.tasks.mypy] +help = "Run MyPy for this package." +cmd = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_azure_cosmos" + +[tool.poe.tasks.test] +help = "Run the default unit test suite for this package." +cmd = "pytest -m \"not integration\" --cov=agent_framework_azure_cosmos --cov-report=term-missing:skip-covered tests" + +[tool.poe.tasks.integration-tests] +help = "Run the package integration test suite." +cmd = "pytest tests/test_cosmos_history_provider.py -m integration" [build-system] requires = ["flit-core >= 3.11,<4.0"] diff --git a/python/packages/azurefunctions/pyproject.toml b/python/packages/azurefunctions/pyproject.toml index 78b38541dd..c1de71aa3b 100644 --- a/python/packages/azurefunctions/pyproject.toml +++ b/python/packages/azurefunctions/pyproject.toml @@ -91,9 +91,13 @@ exclude_dirs = ["tests"] executor.type = "uv" include = "../../shared_tasks.toml" -[tool.poe.tasks] -mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_azurefunctions" -test = 'pytest -m "not integration" --cov=agent_framework_azurefunctions --cov-report=term-missing:skip-covered tests' +[tool.poe.tasks.mypy] +help = "Run MyPy for this package." +cmd = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_azurefunctions" + +[tool.poe.tasks.test] +help = "Run the default unit test suite for this package." +cmd = 'pytest -m "not integration" --cov=agent_framework_azurefunctions --cov-report=term-missing:skip-covered tests' [build-system] requires = ["flit-core >= 3.11,<4.0"] diff --git a/python/packages/bedrock/pyproject.toml b/python/packages/bedrock/pyproject.toml index 201ae0e80f..e85f3e2ac4 100644 --- a/python/packages/bedrock/pyproject.toml +++ b/python/packages/bedrock/pyproject.toml @@ -84,9 +84,13 @@ exclude_dirs = ["tests"] executor.type = "uv" include = "../../shared_tasks.toml" -[tool.poe.tasks] -mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_bedrock" -test = 'pytest -m "not integration" --cov=agent_framework_bedrock --cov-report=term-missing:skip-covered tests' +[tool.poe.tasks.mypy] +help = "Run MyPy for this package." +cmd = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_bedrock" + +[tool.poe.tasks.test] +help = "Run the default unit test suite for this package." +cmd = 'pytest -m "not integration" --cov=agent_framework_bedrock --cov-report=term-missing:skip-covered tests' [build-system] requires = ["hatchling"] diff --git a/python/packages/chatkit/pyproject.toml b/python/packages/chatkit/pyproject.toml index 9bb2bdbce3..a5cc39a594 100644 --- a/python/packages/chatkit/pyproject.toml +++ b/python/packages/chatkit/pyproject.toml @@ -86,9 +86,13 @@ exclude_dirs = ["tests"] executor.type = "uv" include = "../../shared_tasks.toml" -[tool.poe.tasks] -mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_chatkit" -test = 'pytest -m "not integration" --cov=agent_framework_chatkit --cov-report=term-missing:skip-covered tests' +[tool.poe.tasks.mypy] +help = "Run MyPy for this package." +cmd = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_chatkit" + +[tool.poe.tasks.test] +help = "Run the default unit test suite for this package." +cmd = 'pytest -m "not integration" --cov=agent_framework_chatkit --cov-report=term-missing:skip-covered tests' [build-system] requires = ["flit-core >= 3.11,<4.0"] diff --git a/python/packages/claude/pyproject.toml b/python/packages/claude/pyproject.toml index 1ee7e0cd50..cf836caa5c 100644 --- a/python/packages/claude/pyproject.toml +++ b/python/packages/claude/pyproject.toml @@ -86,9 +86,13 @@ exclude_dirs = ["tests"] executor.type = "uv" include = "../../shared_tasks.toml" -[tool.poe.tasks] -mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_claude" -test = 'pytest -m "not integration" --cov=agent_framework_claude --cov-report=term-missing:skip-covered tests' +[tool.poe.tasks.mypy] +help = "Run MyPy for this package." +cmd = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_claude" + +[tool.poe.tasks.test] +help = "Run the default unit test suite for this package." +cmd = 'pytest -m "not integration" --cov=agent_framework_claude --cov-report=term-missing:skip-covered tests' [build-system] requires = ["flit-core >= 3.11,<4.0"] diff --git a/python/packages/copilotstudio/pyproject.toml b/python/packages/copilotstudio/pyproject.toml index 8756ec40bd..4c6ea7a292 100644 --- a/python/packages/copilotstudio/pyproject.toml +++ b/python/packages/copilotstudio/pyproject.toml @@ -85,9 +85,13 @@ exclude_dirs = ["tests"] executor.type = "uv" include = "../../shared_tasks.toml" -[tool.poe.tasks] -mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_copilotstudio" -test = 'pytest -m "not integration" --cov=agent_framework_copilotstudio --cov-report=term-missing:skip-covered tests' +[tool.poe.tasks.mypy] +help = "Run MyPy for this package." +cmd = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_copilotstudio" + +[tool.poe.tasks.test] +help = "Run the default unit test suite for this package." +cmd = 'pytest -m "not integration" --cov=agent_framework_copilotstudio --cov-report=term-missing:skip-covered tests' [build-system] requires = ["flit-core >= 3.11,<4.0"] diff --git a/python/packages/core/pyproject.toml b/python/packages/core/pyproject.toml index e1c79ddb53..a6eb902ccd 100644 --- a/python/packages/core/pyproject.toml +++ b/python/packages/core/pyproject.toml @@ -121,9 +121,13 @@ exclude_dirs = ["tests"] executor.type = "uv" include = "../../shared_tasks.toml" -[tool.poe.tasks] -mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework" -test = 'pytest -m "not integration" --cov=agent_framework --cov-report=term-missing:skip-covered -n auto --dist worksteal tests' +[tool.poe.tasks.mypy] +help = "Run MyPy for this package." +cmd = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework" + +[tool.poe.tasks.test] +help = "Run the default unit test suite for this package." +cmd = 'pytest -m "not integration" --cov=agent_framework --cov-report=term-missing:skip-covered -n auto --dist worksteal tests' [tool.flit.module] name = "agent_framework" diff --git a/python/packages/declarative/pyproject.toml b/python/packages/declarative/pyproject.toml index 8eff06022b..aedf2617a2 100644 --- a/python/packages/declarative/pyproject.toml +++ b/python/packages/declarative/pyproject.toml @@ -92,9 +92,13 @@ exclude_dirs = ["tests"] executor.type = "uv" include = "../../shared_tasks.toml" -[tool.poe.tasks] -mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_declarative" -test = 'pytest -m "not integration" --cov=agent_framework_declarative --cov-report=term-missing:skip-covered tests' +[tool.poe.tasks.mypy] +help = "Run MyPy for this package." +cmd = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_declarative" + +[tool.poe.tasks.test] +help = "Run the default unit test suite for this package." +cmd = 'pytest -m "not integration" --cov=agent_framework_declarative --cov-report=term-missing:skip-covered tests' [build-system] requires = ["flit-core >= 3.11,<4.0"] diff --git a/python/packages/devui/pyproject.toml b/python/packages/devui/pyproject.toml index 396dd22e5d..5ab31b16fa 100644 --- a/python/packages/devui/pyproject.toml +++ b/python/packages/devui/pyproject.toml @@ -98,9 +98,13 @@ exclude_dirs = ["tests"] executor.type = "uv" include = "../../shared_tasks.toml" -[tool.poe.tasks] -mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_devui" -test = 'pytest -m "not integration" --cov=agent_framework_devui --cov-report=term-missing:skip-covered tests' +[tool.poe.tasks.mypy] +help = "Run MyPy for this package." +cmd = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_devui" + +[tool.poe.tasks.test] +help = "Run the default unit test suite for this package." +cmd = 'pytest -m "not integration" --cov=agent_framework_devui --cov-report=term-missing:skip-covered tests' [build-system] requires = ["flit-core >= 3.11,<4.0"] diff --git a/python/packages/durabletask/pyproject.toml b/python/packages/durabletask/pyproject.toml index 44f87918d1..eff4d7825d 100644 --- a/python/packages/durabletask/pyproject.toml +++ b/python/packages/durabletask/pyproject.toml @@ -97,9 +97,13 @@ exclude_dirs = ["tests"] executor.type = "uv" include = "../../shared_tasks.toml" -[tool.poe.tasks] -mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_durabletask" -test = 'pytest -m "not integration" --cov=agent_framework_durabletask --cov-report=term-missing:skip-covered tests' +[tool.poe.tasks.mypy] +help = "Run MyPy for this package." +cmd = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_durabletask" + +[tool.poe.tasks.test] +help = "Run the default unit test suite for this package." +cmd = 'pytest -m "not integration" --cov=agent_framework_durabletask --cov-report=term-missing:skip-covered tests' [build-system] requires = ["flit-core >= 3.11,<4.0"] diff --git a/python/packages/foundry_local/pyproject.toml b/python/packages/foundry_local/pyproject.toml index 6e21997c97..a67a4302d8 100644 --- a/python/packages/foundry_local/pyproject.toml +++ b/python/packages/foundry_local/pyproject.toml @@ -84,9 +84,13 @@ exclude_dirs = ["tests"] executor.type = "uv" include = "../../shared_tasks.toml" -[tool.poe.tasks] -mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_foundry_local" -test = 'pytest -m "not integration" --cov=agent_framework_foundry_local --cov-report=term-missing:skip-covered tests' +[tool.poe.tasks.mypy] +help = "Run MyPy for this package." +cmd = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_foundry_local" + +[tool.poe.tasks.test] +help = "Run the default unit test suite for this package." +cmd = 'pytest -m "not integration" --cov=agent_framework_foundry_local --cov-report=term-missing:skip-covered tests' [build-system] requires = ["flit-core >= 3.11,<4.0"] diff --git a/python/packages/github_copilot/pyproject.toml b/python/packages/github_copilot/pyproject.toml index abf4a5680d..18f81026c1 100644 --- a/python/packages/github_copilot/pyproject.toml +++ b/python/packages/github_copilot/pyproject.toml @@ -85,14 +85,17 @@ exclude_dirs = ["tests"] executor.type = "uv" include = "../../shared_tasks.toml" -[tool.poe.tasks] -test = "pytest -m \"not integration\" --cov=agent_framework_github_copilot --cov-report=term-missing:skip-covered tests" +[tool.poe.tasks.test] +help = "Run the default unit test suite for this package." +cmd = "pytest -m \"not integration\" --cov=agent_framework_github_copilot --cov-report=term-missing:skip-covered tests" [tool.poe.tasks.pyright] +help = "Run Pyright for this package, skipping automatically on unsupported Python versions." shell = "python -c \"import sys; exit(0 if sys.version_info < (3,11) else 1)\" || pyright" interpreter = "posix" [tool.poe.tasks.mypy] +help = "Run MyPy for this package, skipping automatically on unsupported Python versions." shell = "python -c \"import sys; exit(0 if sys.version_info < (3,11) else 1)\" || mypy --config-file $POE_ROOT/pyproject.toml agent_framework_github_copilot" interpreter = "posix" diff --git a/python/packages/lab/README.md b/python/packages/lab/README.md index a46893c2d1..f505b4527f 100644 --- a/python/packages/lab/README.md +++ b/python/packages/lab/README.md @@ -71,10 +71,10 @@ uv run --directory packages/lab poe test uv run --directory packages/lab pytest -q -m "not integration" ``` -When you need to run package tasks from the repository root, use sequential mode to avoid launching all package tests in parallel: +When you need to run lab tests from the repository root, scope the root task to the lab package: ```bash -uv run poe test --seq +uv run poe test -P lab ``` Lightning observability tests intentionally exercise heavier tracing paths and are marked as `resource_intensive`: diff --git a/python/packages/lab/pyproject.toml b/python/packages/lab/pyproject.toml index 170b1d4b78..8345843d2b 100644 --- a/python/packages/lab/pyproject.toml +++ b/python/packages/lab/pyproject.toml @@ -146,17 +146,45 @@ exclude_dirs = ["gaia/tests", "lightning/tests", "tau2/tests"] [tool.poe] include = "../../shared_tasks.toml" -[tool.poe.tasks] -mypy-gaia = "mypy --config-file $POE_ROOT/pyproject.toml gaia/agent_framework_lab_gaia" -mypy-lightning = "mypy --config-file $POE_ROOT/pyproject.toml lightning/agent_framework_lab_lightning" -mypy-tau2 = "mypy --config-file $POE_ROOT/pyproject.toml tau2/agent_framework_lab_tau2" -mypy = ["mypy-gaia", "mypy-lightning", "mypy-tau2"] -test = 'pytest -m "not integration and not resource_intensive" --cov-report=term-missing:skip-covered --junitxml=test-results.xml' -test-gaia = "pytest gaia/tests --cov=agent_framework_lab_gaia --cov-report=term-missing:skip-covered" -test-lightning = "pytest lightning/tests --cov=agent_framework_lab_lightning --cov-report=term-missing:skip-covered" -test-tau2 = "pytest tau2/tests --cov=agent_framework_lab_tau2 --cov-report=term-missing:skip-covered" -build = "echo 'Skipping build'" -publish = "echo 'Skipping publish'" +[tool.poe.tasks.mypy-gaia] +help = "Run MyPy for the lab GAIA package." +cmd = "mypy --config-file $POE_ROOT/pyproject.toml gaia/agent_framework_lab_gaia" + +[tool.poe.tasks.mypy-lightning] +help = "Run MyPy for the lab Lightning package." +cmd = "mypy --config-file $POE_ROOT/pyproject.toml lightning/agent_framework_lab_lightning" + +[tool.poe.tasks.mypy-tau2] +help = "Run MyPy for the lab Tau2 package." +cmd = "mypy --config-file $POE_ROOT/pyproject.toml tau2/agent_framework_lab_tau2" + +[tool.poe.tasks.mypy] +help = "Run MyPy across all lab subpackages." +sequence = ["mypy-gaia", "mypy-lightning", "mypy-tau2"] + +[tool.poe.tasks.test] +help = "Run the default lab unit test suite." +cmd = 'pytest -m "not integration and not resource_intensive" --cov-report=term-missing:skip-covered --junitxml=test-results.xml' + +[tool.poe.tasks.test-gaia] +help = "Run the GAIA lab test suite." +cmd = "pytest gaia/tests --cov=agent_framework_lab_gaia --cov-report=term-missing:skip-covered" + +[tool.poe.tasks.test-lightning] +help = "Run the Lightning lab test suite." +cmd = "pytest lightning/tests --cov=agent_framework_lab_lightning --cov-report=term-missing:skip-covered" + +[tool.poe.tasks.test-tau2] +help = "Run the Tau2 lab test suite." +cmd = "pytest tau2/tests --cov=agent_framework_lab_tau2 --cov-report=term-missing:skip-covered" + +[tool.poe.tasks.build] +help = "Skip build for the lab package." +cmd = "echo 'Skipping build'" + +[tool.poe.tasks.publish] +help = "Skip publish for the lab package." +cmd = "echo 'Skipping publish'" [tool.pytest.ini_options] pythonpath = ["."] diff --git a/python/packages/mem0/pyproject.toml b/python/packages/mem0/pyproject.toml index ea697ca046..081843b4aa 100644 --- a/python/packages/mem0/pyproject.toml +++ b/python/packages/mem0/pyproject.toml @@ -85,9 +85,13 @@ exclude_dirs = ["tests"] executor.type = "uv" include = "../../shared_tasks.toml" -[tool.poe.tasks] -mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_mem0" -test = 'pytest -m "not integration" --cov=agent_framework_mem0 --cov-report=term-missing:skip-covered tests' +[tool.poe.tasks.mypy] +help = "Run MyPy for this package." +cmd = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_mem0" + +[tool.poe.tasks.test] +help = "Run the default unit test suite for this package." +cmd = 'pytest -m "not integration" --cov=agent_framework_mem0 --cov-report=term-missing:skip-covered tests' [build-system] requires = ["flit-core >= 3.11,<4.0"] diff --git a/python/packages/ollama/pyproject.toml b/python/packages/ollama/pyproject.toml index 52259f297a..17e1540cbd 100644 --- a/python/packages/ollama/pyproject.toml +++ b/python/packages/ollama/pyproject.toml @@ -88,9 +88,13 @@ exclude_dirs = ["tests"] executor.type = "uv" include = "../../shared_tasks.toml" -[tool.poe.tasks] -mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_ollama" -test = 'pytest -m "not integration" --cov=agent_framework_ollama --cov-report=term-missing:skip-covered tests' +[tool.poe.tasks.mypy] +help = "Run MyPy for this package." +cmd = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_ollama" + +[tool.poe.tasks.test] +help = "Run the default unit test suite for this package." +cmd = 'pytest -m "not integration" --cov=agent_framework_ollama --cov-report=term-missing:skip-covered tests' [tool.uv.build-backend] module-name = "agent_framework_ollama" diff --git a/python/packages/orchestrations/pyproject.toml b/python/packages/orchestrations/pyproject.toml index bd7d1c8ac5..d4a49e10ae 100644 --- a/python/packages/orchestrations/pyproject.toml +++ b/python/packages/orchestrations/pyproject.toml @@ -83,9 +83,13 @@ exclude_dirs = ["tests"] executor.type = "uv" include = "../../shared_tasks.toml" -[tool.poe.tasks] -mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_orchestrations" -test = 'pytest -m "not integration" --cov=agent_framework_orchestrations --cov-report=term-missing:skip-covered -n auto --dist worksteal tests' +[tool.poe.tasks.mypy] +help = "Run MyPy for this package." +cmd = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_orchestrations" + +[tool.poe.tasks.test] +help = "Run the default unit test suite for this package." +cmd = 'pytest -m "not integration" --cov=agent_framework_orchestrations --cov-report=term-missing:skip-covered -n auto --dist worksteal tests' [build-system] requires = ["flit-core >= 3.11,<4.0"] diff --git a/python/packages/purview/pyproject.toml b/python/packages/purview/pyproject.toml index 05f3f4b828..5974ef0275 100644 --- a/python/packages/purview/pyproject.toml +++ b/python/packages/purview/pyproject.toml @@ -84,9 +84,13 @@ exclude_dirs = ["tests"] executor.type = "uv" include = "../../shared_tasks.toml" -[tool.poe.tasks] -mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_purview" -test = 'pytest -m "not integration" --cov=agent_framework_purview --cov-report=term-missing:skip-covered tests' +[tool.poe.tasks.mypy] +help = "Run MyPy for this package." +cmd = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_purview" + +[tool.poe.tasks.test] +help = "Run the default unit test suite for this package." +cmd = 'pytest -m "not integration" --cov=agent_framework_purview --cov-report=term-missing:skip-covered tests' [build-system] requires = ["flit-core >= 3.9,<4.0"] diff --git a/python/packages/redis/pyproject.toml b/python/packages/redis/pyproject.toml index e1bb352696..70abe25bf9 100644 --- a/python/packages/redis/pyproject.toml +++ b/python/packages/redis/pyproject.toml @@ -87,9 +87,13 @@ exclude_dirs = ["tests"] executor.type = "uv" include = "../../shared_tasks.toml" -[tool.poe.tasks] -mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_redis" -test = 'pytest -m "not integration" --cov=agent_framework_redis --cov-report=term-missing:skip-covered tests' +[tool.poe.tasks.mypy] +help = "Run MyPy for this package." +cmd = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_redis" + +[tool.poe.tasks.test] +help = "Run the default unit test suite for this package." +cmd = 'pytest -m "not integration" --cov=agent_framework_redis --cov-report=term-missing:skip-covered tests' [build-system] requires = ["flit-core >= 3.11,<4.0"] diff --git a/python/pyproject.toml b/python/pyproject.toml index 5a413fef67..ff7d73e745 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -196,94 +196,137 @@ exclude_dirs = ["tests", "scripts", "samples"] [tool.poe] executor.type = "uv" -[tool.poe.tasks] -markdown-code-lint = "uv run python scripts/check_md_code_blocks.py 'README.md' './packages/**/README.md' './samples/**/*.md' --exclude cookiecutter-agent-framework-lab --exclude tau2 --exclude 'packages/devui/frontend' --exclude context_providers/azure_ai_search" -prek-install = "prek install --overwrite" -install = "uv sync --all-packages --all-extras --dev --frozen --prerelease=if-necessary-or-explicit" -test = "python scripts/run_tasks_in_packages_if_exists.py test" -fmt = "python scripts/run_tasks_in_packages_if_exists.py fmt" -format.ref = "fmt" -lint = "python scripts/run_tasks_in_packages_if_exists.py lint" -samples-lint = "ruff check samples --fix --exclude samples/autogen-migration,samples/semantic-kernel-migration --ignore E501,ASYNC,B901,TD002" -pyright = "python scripts/run_tasks_in_packages_if_exists.py pyright" -mypy = "python scripts/run_tasks_in_packages_if_exists.py mypy" -typing = "python scripts/run_tasks_in_packages_if_exists.py mypy pyright" -samples-syntax.shell = "pyright -p $(python -c \"import sys; print('pyrightconfig.samples.py310.json' if sys.version_info < (3,11) else 'pyrightconfig.samples.json')\") --warnings" -samples-syntax.interpreter = "posix" -# cleaning -clean-dist-packages = "python scripts/run_tasks_in_packages_if_exists.py clean-dist" -clean-dist-meta = "rm -rf dist" -clean-dist = ["clean-dist-packages", "clean-dist-meta"] -# build and publish -build-packages = "python scripts/run_tasks_in_packages_if_exists.py build" -build-meta = "python -m flit build" -build = ["build-packages", "build-meta"] -publish = "uv publish" -# combined checks -check-packages = "python scripts/run_tasks_in_packages_if_exists.py fmt lint pyright" -check = ["check-packages", "samples-lint", "samples-syntax", "test", "markdown-code-lint"] - -[tool.poe.tasks.all-tests-cov] -cmd = """ -pytest --import-mode=importlib --m "not integration" ---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_azure_ai_search ---cov=agent_framework_azurefunctions ---cov=agent_framework_chatkit ---cov=agent_framework_copilotstudio ---cov=agent_framework_mem0 ---cov=agent_framework_purview ---cov=agent_framework_redis ---cov=agent_framework_orchestrations ---cov=agent_framework_declarative ---cov-config=pyproject.toml ---cov-report=term-missing:skip-covered ---ignore-glob=packages/lab/** ---ignore-glob=packages/devui/** --rs --n logical --dist worksteal - packages/**/tests -""" - -[tool.poe.tasks.all-tests] -cmd = """ -pytest --import-mode=importlib --m "not integration" ---ignore-glob=packages/lab/** ---ignore-glob=packages/devui/** --rs --n logical --dist worksteal - packages/**/tests -""" - -[tool.poe.tasks.venv] -cmd = "uv venv --clear --python $python" -args = [{ name = "python", default = "3.13", options = ['-p', '--python'] }] +# Workspace setup +[tool.poe.tasks.install] +help = "Install all workspace packages, extras, and dev dependencies from the lockfile." +cmd = "uv sync --all-packages --all-extras --dev --frozen --prerelease=if-necessary-or-explicit" [tool.poe.tasks.setup] +help = "Create the workspace virtual environment for -P/--python, install dependencies, and install prek hooks." sequence = [ { ref = "venv --python $python"}, { ref = "install" }, { ref = "prek-install" } ] -args = [{ name = "python", default = "3.13", options = ['-p', '--python'] }] +args = [{ name = "python", default = "3.13", options = ['-P', '-p', '--python'] }] + +[tool.poe.tasks.venv] +help = "Create or recreate the workspace virtual environment for -P/--python." +cmd = "uv venv --clear --python $python" +args = [{ name = "python", default = "3.13", options = ['-P', '-p', '--python'] }] + +[tool.poe.tasks.prek-install] +help = "Install or refresh the prek git hooks." +cmd = "prek install --overwrite" + +# Syntax, typing, and validation +[tool.poe.tasks.syntax] +help = "Run Ruff formatting and Ruff checks for -P/--package packages, or use -S/--samples; add -F/--format or -C/--check to narrow the mode." +cmd = "python scripts/workspace_poe_tasks.py syntax" + +[tool.poe.tasks.fmt] +help = "DEPRECATED: Use `syntax --format` instead." +cmd = "python scripts/workspace_poe_tasks.py syntax --format" + +[tool.poe.tasks.format] +help = "DEPRECATED: Use `syntax --format` instead." +cmd = "python scripts/workspace_poe_tasks.py syntax --format" + +[tool.poe.tasks.lint] +help = "DEPRECATED: Use `syntax --check` instead." +cmd = "python scripts/workspace_poe_tasks.py syntax --check" + +[tool.poe.tasks.samples-lint] +help = "DEPRECATED: Use `syntax --samples --check` instead." +cmd = "python scripts/workspace_poe_tasks.py syntax --samples --check" + +[tool.poe.tasks.pyright] +help = "Run Pyright for -P/--package packages, use -A/--all for one aggregate sweep, or use -S/--samples for sample checks." +cmd = "python scripts/workspace_poe_tasks.py pyright" + +[tool.poe.tasks.mypy] +help = "Run MyPy for -P/--package packages, or use -A/--all for one aggregate sweep." +cmd = "python scripts/workspace_poe_tasks.py mypy" + +[tool.poe.tasks.typing] +help = "Run both MyPy and Pyright for -P/--package packages, or use -A/--all for aggregate mode." +cmd = "python scripts/workspace_poe_tasks.py typing" + +[tool.poe.tasks.samples-syntax] +help = "DEPRECATED: Use `pyright --samples` instead." +cmd = "python scripts/workspace_poe_tasks.py pyright --samples" + +[tool.poe.tasks.check-packages] +help = "Run `syntax` and `pyright` for -P/--package packages." +cmd = "python scripts/workspace_poe_tasks.py check-packages" + +[tool.poe.tasks.check] +help = "Run package syntax, pyright, and tests for -P/--package packages; without -P also include sample checks and markdown code lint, or use -S/--samples for sample-only checks." +cmd = "python scripts/workspace_poe_tasks.py check" + +[tool.poe.tasks.markdown-code-lint] +help = "Lint Python code blocks embedded in README and sample markdown files." +cmd = "uv run python scripts/check_md_code_blocks.py 'README.md' './packages/**/README.md' './samples/**/*.md' --exclude cookiecutter-agent-framework-lab --exclude tau2 --exclude 'packages/devui/frontend' --exclude context_providers/azure_ai_search" + +# Testing +[tool.poe.tasks.test] +help = "Run tests for -P/--package packages, or use -A/--all for one aggregate sweep; add -C/--cov for coverage." +cmd = "python scripts/workspace_poe_tasks.py test" + +[tool.poe.tasks.all-tests] +help = "DEPRECATED: Use `test --all` instead." +cmd = "python scripts/workspace_poe_tasks.py test --all" + +[tool.poe.tasks.all-tests-cov] +help = "DEPRECATED: Use `test --all --cov` instead." +cmd = "python scripts/workspace_poe_tasks.py test --all --cov" + +# Build and publishing +[tool.poe.tasks._clean-dist-packages] +cmd = "python scripts/workspace_poe_tasks.py clean-dist" + +[tool.poe.tasks._clean-dist-meta] +cmd = "rm -rf dist" + +[tool.poe.tasks.clean-dist] +help = "Remove generated dist artifacts for -P/--package packages and the root meta package." +sequence = [ + { ref = "_clean-dist-packages --package ${project}" }, + { ref = "_clean-dist-meta" }, +] +args = [{ name = "project", default = "*", options = ["-P", "--package"] }] + +[tool.poe.tasks._build-packages] +cmd = "python scripts/workspace_poe_tasks.py build" + +[tool.poe.tasks._build-meta] +cmd = "python -m flit build" +[tool.poe.tasks.build] +help = "Build -P/--package packages and the root meta package." +sequence = [ + { ref = "_build-packages --package ${project}" }, + { ref = "_build-meta" }, +] +args = [{ name = "project", default = "*", options = ["-P", "--package"] }] + +[tool.poe.tasks.publish] +help = "Publish built distributions with uv." +cmd = "uv publish" + +# Dependency maintenance [tool.poe.tasks.upgrade-dev-dependency-pins] +help = "Repin the workspace dev dependency versions used in pyproject.toml." cmd = "python -m scripts.dependencies.upgrade_dev_dependencies" -[tool.poe.tasks.upgrade-lockfile] +[tool.poe.tasks._upgrade-lockfile] cmd = "uv lock --upgrade" [tool.poe.tasks.upgrade-dev-dependencies] +help = "Repin dev dependencies, refresh uv.lock, reinstall, and rerun validation commands." sequence = [ { ref = "upgrade-dev-dependency-pins" }, - { ref = "upgrade-lockfile" }, + { ref = "_upgrade-lockfile" }, { ref = "install" }, { ref = "check" }, { ref = "typing" }, @@ -291,17 +334,20 @@ sequence = [ ] [tool.poe.tasks.add-dependency-to-project] -cmd = "uv add --package ${project} ${dependency}" +help = "Add a dependency to a -P/--package workspace package selected by short name such as `core`." +cmd = "python -m scripts.dependencies.add_dependency_to_project --package ${project} --dependency ${dependency}" args = [ - { name = "project", options = ["-p", "--project"] }, - { name = "dependency", options = ["-d", "--dependency"] }, + { name = "project", options = ["-P", "--package"] }, + { name = "dependency", options = ["-D", "-d", "--dependency"] }, ] [tool.poe.tasks.validate-dependency-bounds-test] +help = "Run workspace dependency-bound validation in test mode, optionally scoped with -P/--package short names such as `core`." shell = "python -m scripts.dependencies.validate_dependency_bounds --mode test --package \"$project\"" -args = [{ name = "project", default = "*", options = ["-p", "--project"] }] +args = [{ name = "project", default = "*", options = ["-P", "--package"] }] [tool.poe.tasks.validate-dependency-bounds-project] +help = "Validate lower and upper dependency bounds for a -P/--package workspace package, optionally narrowed with -M/--mode and -D/--dependency." shell = """ command=(python -m scripts.dependencies.validate_dependency_bounds --mode "${mode}" --package "${project}") if [ -n "${dependency}" ]; then @@ -311,84 +357,21 @@ fi """ interpreter = "bash" args = [ - { name = "mode", default = "both", options = ["-m", "--mode"] }, - { name = "project", default = "*", options = ["-p", "--project"] }, - { name = "dependency", default = "", options = ["-d", "--dependency"] }, + { name = "mode", default = "both", options = ["-M", "-m", "--mode"] }, + { name = "project", default = "*", options = ["-P", "--package"] }, + { name = "dependency", default = "", options = ["-D", "-d", "--dependency"] }, ] [tool.poe.tasks.add-dependency-and-validate-bounds] +help = "Add a dependency to a -P/--package workspace package selected by short name such as `core`, then validate its dependency bounds with -D/--dependency." sequence = [ - { ref = "add-dependency-to-project --project ${project} --dependency ${dependency}" }, - { ref = "validate-dependency-bounds-project --mode both --project ${project} --dependency ${dependency}" }, + { ref = "add-dependency-to-project --package ${project} --dependency ${dependency}" }, + { ref = "validate-dependency-bounds-project --mode both --package ${project} --dependency ${dependency}" }, ] args = [ - { name = "project", options = ["-p", "--project"] }, - { name = "dependency", options = ["-d", "--dependency"] }, -] - -[tool.poe.tasks.prek-pyright] -cmd = "uv run python scripts/run_tasks_in_changed_packages.py pyright --files ${files}" -args = [{ name = "files", default = ".", positional = true, multiple = true }] - -[tool.poe.tasks.prek-check-packages] -cmd = "uv run python scripts/run_tasks_in_changed_packages.py fmt lint pyright --files ${files}" -args = [{ name = "files", default = ".", positional = true, multiple = true }] - -[tool.poe.tasks.prek-markdown-code-lint] -cmd = """uv run python scripts/check_md_code_blocks.py ${files} --no-glob - --exclude cookiecutter-agent-framework-lab --exclude tau2 - --exclude packages/devui/frontend --exclude context_providers/azure_ai_search""" -args = [{ name = "files", default = ".", positional = true, multiple = true }] - -[tool.poe.tasks.prek-samples-check] -shell = """ -HAS_SAMPLES=false -for f in ${files}; do - case "$f" in - samples/*) HAS_SAMPLES=true; break ;; - esac -done -if [ "$HAS_SAMPLES" = true ]; then - echo "Sample files changed, running samples checks..." - uv run ruff check samples --fix --exclude samples/autogen-migration,samples/semantic-kernel-migration --ignore E501,ASYNC,B901,TD002 - uv run pyright -p pyrightconfig.samples.json --warnings -else - echo "No sample files changed, skipping samples checks" -fi -""" -interpreter = "bash" -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 scripts/run_tasks_in_changed_packages.py mypy --files $CHANGED_FILES -""" -interpreter = "bash" - -[tool.poe.tasks.prek-check] -sequence = [ - { ref = "prek-check-packages ${files}" }, - { ref = "prek-markdown-code-lint ${files}" }, - { ref = "prek-samples-check ${files}" } + { name = "project", options = ["-P", "--package"] }, + { name = "dependency", options = ["-D", "-d", "--dependency"] }, ] -args = [{ name = "files", default = ".", positional = true, multiple = true }] [tool.setuptools.packages.find] where = ["packages"] diff --git a/python/samples/02-agents/chat_client/chat_response_cancellation.py b/python/samples/02-agents/chat_client/chat_response_cancellation.py index dd32379443..aff3536602 100644 --- a/python/samples/02-agents/chat_client/chat_response_cancellation.py +++ b/python/samples/02-agents/chat_client/chat_response_cancellation.py @@ -29,7 +29,9 @@ async def main() -> None: client = OpenAIChatClient() try: - task = asyncio.create_task(client.get_response(messages=[Message(role="user", text="Tell me a fantasy story.")])) + task = asyncio.create_task( + client.get_response(messages=[Message(role="user", text="Tell me a fantasy story.")]) + ) await asyncio.sleep(1) task.cancel() await task diff --git a/python/samples/02-agents/chat_client/custom_chat_client.py b/python/samples/02-agents/chat_client/custom_chat_client.py index cb63c74597..5adcf50d15 100644 --- a/python/samples/02-agents/chat_client/custom_chat_client.py +++ b/python/samples/02-agents/chat_client/custom_chat_client.py @@ -94,9 +94,7 @@ def _inner_get_response( response_text = f"{response_text} {suffix}" stream_delay_seconds = float(options.get("stream_delay_seconds", 0.05)) - response_message = Message( - role="assistant", contents=[Content.from_text(response_text)] - ) + response_message = Message(role="assistant", text=response_text) response = ChatResponse( messages=[response_message], diff --git a/python/samples/02-agents/compaction/custom.py b/python/samples/02-agents/compaction/custom.py index ea9647b9ae..da6acb066b 100644 --- a/python/samples/02-agents/compaction/custom.py +++ b/python/samples/02-agents/compaction/custom.py @@ -27,15 +27,9 @@ async def __call__(self, messages: list[Message]) -> bool: group_annotation = message.additional_properties.get(GROUP_ANNOTATION_KEY) group_id = group_annotation.get("id") if isinstance(group_annotation, dict) else None kind = group_annotation.get("kind") if isinstance(group_annotation, dict) else None - if ( - isinstance(group_id, str) - and isinstance(kind, str) - and group_id not in group_kinds - ): + if isinstance(group_id, str) and isinstance(kind, str) and group_id not in group_kinds: group_kinds[group_id] = kind - user_group_ids = [ - group_id for group_id in group_ids if group_kinds.get(group_id) == "user" - ] + user_group_ids = [group_id for group_id in group_ids if group_kinds.get(group_id) == "user"] if not user_group_ids: return False keep_user_group_id = user_group_ids[-1] diff --git a/python/samples/02-agents/compaction/tiktoken_tokenizer.py b/python/samples/02-agents/compaction/tiktoken_tokenizer.py index ac282db338..7e2a5a7665 100644 --- a/python/samples/02-agents/compaction/tiktoken_tokenizer.py +++ b/python/samples/02-agents/compaction/tiktoken_tokenizer.py @@ -33,9 +33,7 @@ class TiktokenTokenizer(TokenizerProtocol): """TokenizerProtocol implementation backed by tiktoken's o200k_base (gpt-4.1 and up default) encoding.""" - def __init__( - self, *, encoding_name: str = "o200k_base", model_name: str | None = None - ) -> None: + def __init__(self, *, encoding_name: str = "o200k_base", model_name: str | None = None) -> None: if model_name is not None: self._encoding = tiktoken.encoding_for_model(model_name) else: @@ -62,10 +60,7 @@ def _build_messages() -> list[Message]: ), Message( role="user", - text=( - "Now provide a detailed checklist with owners, rollback " - "gates, and validation criteria." - ), + text=("Now provide a detailed checklist with owners, rollback gates, and validation criteria."), ), Message( role="assistant", diff --git a/python/samples/02-agents/devui/in_memory_mode.py b/python/samples/02-agents/devui/in_memory_mode.py index 8914bf8e8e..b1e43a9a7f 100644 --- a/python/samples/02-agents/devui/in_memory_mode.py +++ b/python/samples/02-agents/devui/in_memory_mode.py @@ -37,9 +37,7 @@ def get_weather( """Get the weather for a given location.""" conditions = ["sunny", "cloudy", "rainy", "stormy"] temperature = 53 - return ( - f"The weather in {location} is {conditions[0]} with a high of {temperature}°C." - ) + return f"The weather in {location} is {conditions[0]} with a high of {temperature}°C." @tool(approval_mode="never_require") @@ -68,9 +66,7 @@ class AddExclamation(Executor): """Add exclamation mark to text.""" @handler - async def add_exclamation( - self, text: str, ctx: WorkflowContext[Never, str] - ) -> None: + async def add_exclamation(self, text: str, ctx: WorkflowContext[Never, str]) -> None: """Add exclamation and yield as workflow output.""" result = f"{text}!" await ctx.yield_output(result) diff --git a/python/samples/02-agents/providers/github_copilot/github_copilot_with_file_operations.py b/python/samples/02-agents/providers/github_copilot/github_copilot_with_file_operations.py index fbfdc34f43..145702591f 100644 --- a/python/samples/02-agents/providers/github_copilot/github_copilot_with_file_operations.py +++ b/python/samples/02-agents/providers/github_copilot/github_copilot_with_file_operations.py @@ -19,9 +19,7 @@ from copilot.types import PermissionRequestResult -def prompt_permission( - request: PermissionRequest, context: dict[str, str] -) -> PermissionRequestResult: +def prompt_permission(request: PermissionRequest, context: dict[str, str]) -> PermissionRequestResult: """Permission handler that prompts the user for approval.""" print(f"\n[Permission Request: {request.kind}]") diff --git a/python/samples/02-agents/skills/code_defined_skill/code_defined_skill.py b/python/samples/02-agents/skills/code_defined_skill/code_defined_skill.py index e9b4757bb6..aeef342a06 100644 --- a/python/samples/02-agents/skills/code_defined_skill/code_defined_skill.py +++ b/python/samples/02-agents/skills/code_defined_skill/code_defined_skill.py @@ -75,7 +75,9 @@ # --------------------------------------------------------------------------- # 2. Dynamic Resources — callable function via @skill.resource # --------------------------------------------------------------------------- -@unit_converter_skill.resource(name="conversion-policy", description="Current conversion formatting and rounding policy") +@unit_converter_skill.resource( + name="conversion-policy", description="Current conversion formatting and rounding policy" +) def conversion_policy(**kwargs: Any) -> Any: """Return the current conversion policy. @@ -148,8 +150,7 @@ async def main() -> None: print("Converting units") print("-" * 60) response = await agent.run( - "How many kilometers is a marathon (26.2 miles)? " - "And how many pounds is 75 kilograms?", + "How many kilometers is a marathon (26.2 miles)? And how many pounds is 75 kilograms?", precision=2, ) print(f"Agent: {response}\n") diff --git a/python/samples/02-agents/skills/file_based_skill/file_based_skill.py b/python/samples/02-agents/skills/file_based_skill/file_based_skill.py index 044514e7b7..de712c4fb1 100644 --- a/python/samples/02-agents/skills/file_based_skill/file_based_skill.py +++ b/python/samples/02-agents/skills/file_based_skill/file_based_skill.py @@ -70,8 +70,7 @@ async def main() -> None: print("Converting units") print("-" * 60) response = await agent.run( - "How many kilometers is a marathon (26.2 miles)? " - "And how many pounds is 75 kilograms?" + "How many kilometers is a marathon (26.2 miles)? And how many pounds is 75 kilograms?" ) print(f"Agent: {response}\n") diff --git a/python/samples/02-agents/skills/mixed_skills/mixed_skills.py b/python/samples/02-agents/skills/mixed_skills/mixed_skills.py index 4e0d9173b7..9916b430b6 100644 --- a/python/samples/02-agents/skills/mixed_skills/mixed_skills.py +++ b/python/samples/02-agents/skills/mixed_skills/mixed_skills.py @@ -137,8 +137,7 @@ async def main() -> None: print("Converting units") print("-" * 60) response = await agent.run( - "How many kilometers is a marathon (26.2 miles)? " - "And how many liters is a 5-gallon bucket?" + "How many kilometers is a marathon (26.2 miles)? And how many liters is a 5-gallon bucket?" ) print(f"Agent: {response}\n") diff --git a/python/samples/02-agents/tools/control_total_tool_executions.py b/python/samples/02-agents/tools/control_total_tool_executions.py index eaad6e225b..ae19d1a077 100644 --- a/python/samples/02-agents/tools/control_total_tool_executions.py +++ b/python/samples/02-agents/tools/control_total_tool_executions.py @@ -135,8 +135,7 @@ async def scenario_max_function_calls(): ) response = await agent.run( - "Search for the weather in Paris, London, Tokyo, " - "New York, and Sydney, and also search for best travel tips." + "Search for the weather in Paris, London, Tokyo, New York, and Sydney, and also search for best travel tips." ) print(f" Response: {response.text[:200]}...") print() @@ -236,8 +235,12 @@ def _do_lookup(query: Annotated[str, "Search query."]) -> str: session_b = agent_b.create_session() await agent_b.run("Look up quantum computing", session=session_b) - print(f" agent_a_lookup.invocation_count = {agent_a_lookup.invocation_count} (limit {agent_a_lookup.max_invocations})") - print(f" agent_b_lookup.invocation_count = {agent_b_lookup.invocation_count} (limit {agent_b_lookup.max_invocations})") + print( + f" agent_a_lookup.invocation_count = {agent_a_lookup.invocation_count} (limit {agent_a_lookup.max_invocations})" + ) + print( + f" agent_b_lookup.invocation_count = {agent_b_lookup.invocation_count} (limit {agent_b_lookup.max_invocations})" + ) print(" → Agent A hit its limit; Agent B used 1 of 5.") print() @@ -254,8 +257,8 @@ async def scenario_combined(): client = OpenAIResponsesClient() # 1. Configure the client with both iteration and function call limits. - client.function_invocation_configuration["max_iterations"] = 5 # max 5 LLM roundtrips - client.function_invocation_configuration["max_function_calls"] = 8 # max 8 total tool calls + client.function_invocation_configuration["max_iterations"] = 5 # max 5 LLM roundtrips + client.function_invocation_configuration["max_function_calls"] = 8 # max 8 total tool calls print(f" max_iterations = {client.function_invocation_configuration['max_iterations']}") print(f" max_function_calls = {client.function_invocation_configuration['max_function_calls']}") diff --git a/python/samples/02-agents/tools/function_tool_with_session_injection.py b/python/samples/02-agents/tools/function_tool_with_session_injection.py index 53cc63c2c0..282a79c1fb 100644 --- a/python/samples/02-agents/tools/function_tool_with_session_injection.py +++ b/python/samples/02-agents/tools/function_tool_with_session_injection.py @@ -47,12 +47,8 @@ async def main() -> None: session = agent.create_session() # Run the agent with the session; tools receive it via ctx.session. - print( - f"Agent: {await agent.run('What is the weather in London?', session=session)}" - ) - print( - f"Agent: {await agent.run('What is the weather in Amsterdam?', session=session)}" - ) + print(f"Agent: {await agent.run('What is the weather in London?', session=session)}") + print(f"Agent: {await agent.run('What is the weather in Amsterdam?', session=session)}") print(f"Agent: {await agent.run('What cities did I ask about?', session=session)}") diff --git a/python/samples/04-hosting/a2a/invoice_data.py b/python/samples/04-hosting/a2a/invoice_data.py index 877a00b4d2..d37cde84c8 100644 --- a/python/samples/04-hosting/a2a/invoice_data.py +++ b/python/samples/04-hosting/a2a/invoice_data.py @@ -72,56 +72,116 @@ def _random_date_within_last_two_months() -> datetime: def _build_invoices() -> list[Invoice]: """Build 10 mock invoices.""" return [ - Invoice("TICKET-XYZ987", "INV789", "Contoso", _random_date_within_last_two_months(), [ - Product("T-Shirts", 150, 10.00), - Product("Hats", 200, 15.00), - Product("Glasses", 300, 5.00), - ]), - Invoice("TICKET-XYZ111", "INV111", "XStore", _random_date_within_last_two_months(), [ - Product("T-Shirts", 2500, 12.00), - Product("Hats", 1500, 8.00), - Product("Glasses", 200, 20.00), - ]), - Invoice("TICKET-XYZ222", "INV222", "Cymbal Direct", _random_date_within_last_two_months(), [ - Product("T-Shirts", 1200, 14.00), - Product("Hats", 800, 7.00), - Product("Glasses", 500, 25.00), - ]), - Invoice("TICKET-XYZ333", "INV333", "Contoso", _random_date_within_last_two_months(), [ - Product("T-Shirts", 400, 11.00), - Product("Hats", 600, 15.00), - Product("Glasses", 700, 5.00), - ]), - Invoice("TICKET-XYZ444", "INV444", "XStore", _random_date_within_last_two_months(), [ - Product("T-Shirts", 800, 10.00), - Product("Hats", 500, 18.00), - Product("Glasses", 300, 22.00), - ]), - Invoice("TICKET-XYZ555", "INV555", "Cymbal Direct", _random_date_within_last_two_months(), [ - Product("T-Shirts", 1100, 9.00), - Product("Hats", 900, 12.00), - Product("Glasses", 1200, 15.00), - ]), - Invoice("TICKET-XYZ666", "INV666", "Contoso", _random_date_within_last_two_months(), [ - Product("T-Shirts", 2500, 8.00), - Product("Hats", 1200, 10.00), - Product("Glasses", 1000, 6.00), - ]), - Invoice("TICKET-XYZ777", "INV777", "XStore", _random_date_within_last_two_months(), [ - Product("T-Shirts", 1900, 13.00), - Product("Hats", 1300, 16.00), - Product("Glasses", 800, 19.00), - ]), - Invoice("TICKET-XYZ888", "INV888", "Cymbal Direct", _random_date_within_last_two_months(), [ - Product("T-Shirts", 2200, 11.00), - Product("Hats", 1700, 8.50), - Product("Glasses", 600, 21.00), - ]), - Invoice("TICKET-XYZ999", "INV999", "Contoso", _random_date_within_last_two_months(), [ - Product("T-Shirts", 1400, 10.50), - Product("Hats", 1100, 9.00), - Product("Glasses", 950, 12.00), - ]), + Invoice( + "TICKET-XYZ987", + "INV789", + "Contoso", + _random_date_within_last_two_months(), + [ + Product("T-Shirts", 150, 10.00), + Product("Hats", 200, 15.00), + Product("Glasses", 300, 5.00), + ], + ), + Invoice( + "TICKET-XYZ111", + "INV111", + "XStore", + _random_date_within_last_two_months(), + [ + Product("T-Shirts", 2500, 12.00), + Product("Hats", 1500, 8.00), + Product("Glasses", 200, 20.00), + ], + ), + Invoice( + "TICKET-XYZ222", + "INV222", + "Cymbal Direct", + _random_date_within_last_two_months(), + [ + Product("T-Shirts", 1200, 14.00), + Product("Hats", 800, 7.00), + Product("Glasses", 500, 25.00), + ], + ), + Invoice( + "TICKET-XYZ333", + "INV333", + "Contoso", + _random_date_within_last_two_months(), + [ + Product("T-Shirts", 400, 11.00), + Product("Hats", 600, 15.00), + Product("Glasses", 700, 5.00), + ], + ), + Invoice( + "TICKET-XYZ444", + "INV444", + "XStore", + _random_date_within_last_two_months(), + [ + Product("T-Shirts", 800, 10.00), + Product("Hats", 500, 18.00), + Product("Glasses", 300, 22.00), + ], + ), + Invoice( + "TICKET-XYZ555", + "INV555", + "Cymbal Direct", + _random_date_within_last_two_months(), + [ + Product("T-Shirts", 1100, 9.00), + Product("Hats", 900, 12.00), + Product("Glasses", 1200, 15.00), + ], + ), + Invoice( + "TICKET-XYZ666", + "INV666", + "Contoso", + _random_date_within_last_two_months(), + [ + Product("T-Shirts", 2500, 8.00), + Product("Hats", 1200, 10.00), + Product("Glasses", 1000, 6.00), + ], + ), + Invoice( + "TICKET-XYZ777", + "INV777", + "XStore", + _random_date_within_last_two_months(), + [ + Product("T-Shirts", 1900, 13.00), + Product("Hats", 1300, 16.00), + Product("Glasses", 800, 19.00), + ], + ), + Invoice( + "TICKET-XYZ888", + "INV888", + "Cymbal Direct", + _random_date_within_last_two_months(), + [ + Product("T-Shirts", 2200, 11.00), + Product("Hats", 1700, 8.50), + Product("Glasses", 600, 21.00), + ], + ), + Invoice( + "TICKET-XYZ999", + "INV999", + "Contoso", + _random_date_within_last_two_months(), + [ + Product("T-Shirts", 1400, 10.50), + Product("Hats", 1100, 9.00), + Product("Glasses", 950, 12.00), + ], + ), ] diff --git a/python/samples/SAMPLE_GUIDELINES.md b/python/samples/SAMPLE_GUIDELINES.md index a40312614f..b93f5bdd14 100644 --- a/python/samples/SAMPLE_GUIDELINES.md +++ b/python/samples/SAMPLE_GUIDELINES.md @@ -62,7 +62,7 @@ Users can create a `.env` file in the `python/` directory based on `.env.example ## Syntax Checking -Run `uv run poe samples-syntax` to check samples for syntax errors and missing imports from `agent_framework`. This uses a relaxed pyright configuration that validates imports without strict type checking. +Run `uv run poe pyright -S` to check samples for syntax errors and missing imports from `agent_framework`. This uses a relaxed pyright configuration that validates imports without strict type checking. Some samples depend on external packages (e.g., `azure.ai.agentserver.agentframework`, `microsoft_agents`) that are not installed in the dev environment. These are excluded in `pyrightconfig.samples.json`. When adding or modifying these excluded samples, add them to the exclude list and manually verify they have no import errors from `agent_framework` packages by temporarily removing them from the exclude list and running the check. diff --git a/python/scripts/dependencies/README.md b/python/scripts/dependencies/README.md index 5ce410d766..e129d67eed 100644 --- a/python/scripts/dependencies/README.md +++ b/python/scripts/dependencies/README.md @@ -46,14 +46,14 @@ These are the normal user-facing entrypoints: uv run poe upgrade-dev-dependency-pins uv run poe upgrade-dev-dependencies uv run poe validate-dependency-bounds-test -uv run poe validate-dependency-bounds-test --project -uv run poe validate-dependency-bounds-project --mode both --project --dependency "" +uv run poe validate-dependency-bounds-test --package core +uv run poe validate-dependency-bounds-project --mode both --package core --dependency "" ``` - `upgrade-dev-dependency-pins` only refreshes exact dev pins in `pyproject.toml` files. - `upgrade-dev-dependencies` refreshes dev pins (using task above), runs `uv lock --upgrade`, reinstalls from the frozen lockfile, then runs `check`, `typing`, and `test`. - `validate-dependency-bounds-test` runs the repo-wide lower/upper smoke gate. -- `validate-dependency-bounds-project` is the single package-scoped task; use `--mode lower`, `--mode upper`, or `--mode both` for the target package/dependency pair. Its `--project` argument defaults to `*`, and `--dependency` is optional, so automation can also use it for repo-wide upper-bound runs. +- `validate-dependency-bounds-project` is the single package-scoped task; use `--mode lower`, `--mode upper`, or `--mode both` for the target package/dependency pair. Its `--package` argument defaults to `*`, and `--dependency` is optional, so automation can also use it for repo-wide upper-bound runs. ### GitHub Actions workflows @@ -61,7 +61,7 @@ These workflows call the Poe tasks: - `.github/workflows/python-dependency-range-validation.yml` - Trigger: `workflow_dispatch` - - Runs `uv run poe validate-dependency-bounds-project --mode upper --project "*"` + - Runs `uv run poe validate-dependency-bounds-project --mode upper --package "*"` - Uploads `python/scripts/dependencies/dependency-range-results.json` - Creates issues for failing candidate versions and opens/updates a PR for passing range updates @@ -76,10 +76,10 @@ These are useful for debugging or targeted manual runs: ```bash python -m scripts.dependencies.upgrade_dev_dependencies --dry-run --version-source lock -python -m scripts.dependencies.validate_dependency_bounds --mode test --package packages/core --dry-run -python -m scripts.dependencies.validate_dependency_bounds --mode both --package packages/core --dependencies openai --dry-run -python -m scripts.dependencies._dependency_bounds_lower_impl --packages packages/core --dependencies openai --dry-run -python -m scripts.dependencies._dependency_bounds_upper_impl --packages packages/core --dependencies openai --dry-run +python -m scripts.dependencies.validate_dependency_bounds --mode test --package core --dry-run +python -m scripts.dependencies.validate_dependency_bounds --mode both --package core --dependencies openai --dry-run +python -m scripts.dependencies._dependency_bounds_lower_impl --packages core --dependencies openai --dry-run +python -m scripts.dependencies._dependency_bounds_upper_impl --packages core --dependencies openai --dry-run ``` Use the direct lower/upper implementation modules mainly for debugging or development of the optimizers themselves. For normal usage, prefer the Poe tasks or `validate_dependency_bounds.py`. diff --git a/python/scripts/dependencies/_dependency_bounds_lower_impl.py b/python/scripts/dependencies/_dependency_bounds_lower_impl.py index ad259bd6e6..308e206350 100644 --- a/python/scripts/dependencies/_dependency_bounds_lower_impl.py +++ b/python/scripts/dependencies/_dependency_bounds_lower_impl.py @@ -1,5 +1,5 @@ # Copyright (c) Microsoft. All rights reserved. -# ruff: noqa: INP001, S404, S603 +# ruff: noqa: S404, S603 """Lower dependency bounds, validate, and persist the oldest passing set.""" @@ -21,14 +21,15 @@ from urllib import request as urllib_request import tomli +from packaging.requirements import InvalidRequirement, Requirement +from packaging.version import InvalidVersion, Version +from rich import print + from scripts.dependencies._dependency_bounds_runtime import ( extend_command_with_runtime_tools, extend_command_with_task, ) -from packaging.requirements import InvalidRequirement, Requirement -from packaging.version import InvalidVersion, Version -from rich import print -from scripts.task_runner import discover_projects, extract_poe_tasks +from scripts.task_runner import discover_projects, extract_poe_tasks, project_filter_matches CHECK_TASK_PRIORITY = ("check", "typing", "pyright", "mypy", "lint") REQ_PATTERN = r"^\s*([A-Za-z0-9_.-]+(?:\[[^\]]+\])?)\s*(.*?)\s*$" @@ -937,7 +938,7 @@ def main() -> None: "--packages", nargs="*", default=None, - help="Optional package filters by workspace path (e.g., packages/core) or package name.", + help="Optional package filters by short name (for example core), workspace path, or package name.", ) parser.add_argument( "--dependencies", @@ -1001,7 +1002,11 @@ def main() -> None: project_section = package_config.get("project", {}) optional_dependencies = project_section.get("optional-dependencies", {}) or {} dependency_groups = package_config.get("dependency-groups", {}) or {} - if package_filters and str(project_path) not in package_filters and package_name not in package_filters: + # Reuse the shared selector matcher so direct optimizer runs accept the + # same short-name package filters as the contributor-facing Poe tasks. + if package_filters and not any( + project_filter_matches(project_path, package_filter, [package_name]) for package_filter in package_filters + ): continue plans.append( PackagePlan( diff --git a/python/scripts/dependencies/_dependency_bounds_upper_impl.py b/python/scripts/dependencies/_dependency_bounds_upper_impl.py index a92d16cd7e..239e7dd04a 100644 --- a/python/scripts/dependencies/_dependency_bounds_upper_impl.py +++ b/python/scripts/dependencies/_dependency_bounds_upper_impl.py @@ -1,5 +1,5 @@ # Copyright (c) Microsoft. All rights reserved. -# ruff: noqa: INP001, S404, S603 +# ruff: noqa: S404, S603 """Raise dependency upper bounds, validate, and persist the latest passing set.""" @@ -22,15 +22,16 @@ from urllib import request as urllib_request import tomli +from packaging.requirements import InvalidRequirement, Requirement +from packaging.version import InvalidVersion, Version +from rich import print + from scripts.dependencies._dependency_bounds_runtime import ( extend_command_with_runtime_tools, extend_command_with_task, next_zero_major_minor_boundary, ) -from packaging.requirements import InvalidRequirement, Requirement -from packaging.version import InvalidVersion, Version -from rich import print -from scripts.task_runner import discover_projects, extract_poe_tasks +from scripts.task_runner import discover_projects, extract_poe_tasks, project_filter_matches CHECK_TASK_PRIORITY = ("check", "typing", "pyright", "mypy", "lint") REQ_PATTERN = r"^\s*([A-Za-z0-9_.-]+(?:\[[^\]]+\])?)\s*(.*?)\s*$" @@ -1088,7 +1089,7 @@ def main() -> None: "--packages", nargs="*", default=None, - help="Optional package filters by workspace path (e.g., packages/core) or package name.", + help="Optional package filters by short name (for example core), workspace path, or package name.", ) parser.add_argument( "--dependencies", @@ -1153,7 +1154,11 @@ def main() -> None: project_section = package_config.get("project", {}) optional_dependencies = project_section.get("optional-dependencies", {}) or {} dependency_groups = package_config.get("dependency-groups", {}) or {} - if package_filters and str(project_path) not in package_filters and package_name not in package_filters: + # Reuse the shared selector matcher so direct optimizer runs accept the + # same short-name package filters as the contributor-facing Poe tasks. + if package_filters and not any( + project_filter_matches(project_path, package_filter, [package_name]) for package_filter in package_filters + ): continue plans.append( PackagePlan( diff --git a/python/scripts/dependencies/add_dependency_to_project.py b/python/scripts/dependencies/add_dependency_to_project.py new file mode 100644 index 0000000000..c595faa887 --- /dev/null +++ b/python/scripts/dependencies/add_dependency_to_project.py @@ -0,0 +1,118 @@ +# Copyright (c) Microsoft. All rights reserved. +# ruff: noqa: S603 + +"""Add a dependency to one workspace package selected by short name or path. + +``uv add --package`` expects the published workspace distribution name, while +the root Poe surface intentionally speaks in short repo package names such as +``core``. This wrapper keeps the user-facing selector stable and translates it +just before delegating to uv. +""" + +from __future__ import annotations + +import argparse +import subprocess +from dataclasses import dataclass +from pathlib import Path + +import tomli +from rich import print + +from scripts.task_runner import discover_projects, project_filter_matches + + +@dataclass(frozen=True) +class WorkspacePackage: + """Workspace package metadata needed for `uv add --package`.""" + + short_name: str + project_path: Path + distribution_name: str + + +def _load_distribution_name(pyproject_file: Path) -> str: + with pyproject_file.open("rb") as f: + data = tomli.load(f) + return str(data.get("project", {}).get("name", "")).strip() + + +def _discover_workspace_packages(workspace_root: Path) -> list[WorkspacePackage]: + workspace_pyproject = workspace_root / "pyproject.toml" + packages: list[WorkspacePackage] = [] + for project_path in sorted(discover_projects(workspace_pyproject), key=str): + pyproject_file = workspace_root / project_path / "pyproject.toml" + if not pyproject_file.exists(): + continue + distribution_name = _load_distribution_name(pyproject_file) + if not distribution_name: + continue + packages.append( + WorkspacePackage( + short_name=project_path.name, + project_path=project_path, + distribution_name=distribution_name, + ) + ) + return packages + + +def _resolve_workspace_package(workspace_root: Path, project_filter: str) -> WorkspacePackage: + """Resolve one workspace package from a user-facing selector. + + The wrapper accepts the same short-name/path/distribution-name vocabulary as + the other root tasks, but errors on ambiguous matches so dependency edits + never hit the wrong package. + """ + matches = [ + package + for package in _discover_workspace_packages(workspace_root) + if project_filter_matches(package.project_path, project_filter, [package.short_name, package.distribution_name]) + ] + if not matches: + raise SystemExit(f"No workspace package matched selector '{project_filter}'.") + if len(matches) > 1: + names = ", ".join(sorted(package.short_name for package in matches)) + raise SystemExit( + f"Package selector '{project_filter}' matched multiple workspace packages: {names}. " + "Use a more specific short name or path." + ) + return matches[0] + + +def main() -> None: + """Resolve a workspace project selector, then delegate to `uv add`.""" + parser = argparse.ArgumentParser( + description="Add a dependency to a single workspace package selected by short name, path, or package name." + ) + parser.add_argument( + "-P", + "--package", + dest="project", + metavar="PACKAGE", + required=True, + help="Workspace package selector, such as `core`.", + ) + # Keep the old long flag as a silent alias while downstream automation + # finishes moving to the user-facing ``--package`` spelling. + parser.add_argument("--project", dest="project", help=argparse.SUPPRESS) + parser.add_argument("-D", "--dependency", required=True, help="Dependency specifier to add.") + args = parser.parse_args() + + workspace_root = Path(__file__).resolve().parents[2] + package = _resolve_workspace_package(workspace_root, args.project) + print( + f"[cyan]Adding {args.dependency} to {package.short_name} " + f"({package.distribution_name})[/cyan]" + ) + result = subprocess.run( + ["uv", "add", "--package", package.distribution_name, args.dependency], + cwd=workspace_root, + check=False, + ) + if result.returncode: + raise SystemExit(result.returncode) + + +if __name__ == "__main__": + main() diff --git a/python/scripts/dependencies/validate_dependency_bounds.py b/python/scripts/dependencies/validate_dependency_bounds.py index 8563cb36da..5c6a9e74e7 100644 --- a/python/scripts/dependencies/validate_dependency_bounds.py +++ b/python/scripts/dependencies/validate_dependency_bounds.py @@ -1,5 +1,5 @@ # Copyright (c) Microsoft. All rights reserved. -# ruff: noqa: INP001, S404, S603 +# ruff: noqa: S404, S603 """Unified dependency-bound validation entrypoint. @@ -8,6 +8,10 @@ - lower: run lower-bound expansion for one package. - upper: run upper-bound expansion for one package. - both: run lower then upper expansion for one package. + +Package filters intentionally reuse the root task selector semantics so the +same short package names (for example ``core``) work in both contributor +commands and direct debugging entrypoints. """ from __future__ import annotations @@ -23,6 +27,7 @@ import tomli from rich import print + from scripts.dependencies._dependency_bounds_runtime import ( extend_command_with_runtime_tools, extend_command_with_task, @@ -33,7 +38,7 @@ _load_package_name, _resolve_internal_editables, ) -from scripts.task_runner import discover_projects, extract_poe_tasks +from scripts.task_runner import discover_projects, extract_poe_tasks, project_filter_matches _LOWER_IMPL_MODULE = "scripts.dependencies._dependency_bounds_lower_impl" _UPPER_IMPL_MODULE = "scripts.dependencies._dependency_bounds_upper_impl" @@ -76,10 +81,10 @@ def _coerce_subprocess_output(output: str | bytes | None) -> str: def _build_test_plans(workspace_root: Path, package_filter: str | None) -> list[PackageTestPlan]: + """Build per-package test plans for the requested workspace selector.""" workspace_pyproject = workspace_root / "pyproject.toml" package_map = _build_workspace_package_map(workspace_root) internal_graph = _build_internal_graph(workspace_root, package_map) - normalized_filter = None if package_filter in {None, "", "*"} else package_filter plans: list[PackageTestPlan] = [] missing_tasks: list[str] = [] @@ -89,7 +94,14 @@ def _build_test_plans(workspace_root: Path, package_filter: str | None) -> list[ continue package_name = _load_package_name(pyproject_file) - if normalized_filter and str(project_path) != normalized_filter and package_name != normalized_filter: + # Reuse the shared matcher so dependency-bound test mode accepts the + # same short names and legacy path-style selectors as the root Poe + # commands. + if ( + package_filter + and package_filter != "*" + and not project_filter_matches(project_path, package_filter, [package_name]) + ): continue available_tasks = extract_poe_tasks(pyproject_file) @@ -366,7 +378,10 @@ def main() -> None: parser.add_argument( "--package", default=None, - help="Optional workspace package path/name filter for all modes. Use '*' or omit it for the whole workspace.", + help=( + "Optional workspace package selector for all modes, such as `core`. " + "Use '*' or omit it for the whole workspace." + ), ) parser.add_argument( "--dependencies", diff --git a/python/scripts/task_runner.py b/python/scripts/task_runner.py index a6e14ccaaa..617b4d58b0 100644 --- a/python/scripts/task_runner.py +++ b/python/scripts/task_runner.py @@ -1,6 +1,11 @@ # Copyright (c) Microsoft. All rights reserved. -"""Shared utilities for running poe tasks across workspace packages in parallel.""" +"""Shared utilities for running Poe tasks across workspace packages. + +These helpers centralize workspace discovery, selector matching, and execution +mode so the root task dispatcher and dependency tooling interpret package +filters the same way. +""" import concurrent.futures import glob @@ -8,6 +13,8 @@ import subprocess import sys import time +from collections.abc import Sequence +from fnmatch import fnmatch from pathlib import Path import tomli @@ -70,12 +77,67 @@ def build_work_items(projects: list[Path], task_names: list[str]) -> list[tuple[ return work_items -def _run_task_subprocess(project: Path, task: str, workspace_root: Path) -> tuple[Path, str, int, str, str, float]: +def normalize_project_filter(value: str) -> str: + """Normalize a user-supplied workspace selector. + + Strip presentation differences so short names, relative paths, and globs can + be compared with one matcher. + """ + normalized = value.strip().strip("/").replace("\\", "/") + return normalized or "." + + +def build_project_filter_candidates(project: Path | str, aliases: Sequence[str] = ()) -> set[str]: + """Return accepted selector values for one workspace project. + + We accept the workspace path, short package name, and any supplied aliases + so user-facing ``--package core`` stays stable even when underlying tools + still need paths or distribution names. + """ + normalized_path = normalize_project_filter(str(project)) + candidates = {normalized_path} + if normalized_path == ".": + candidates.update({"./", "root"}) + else: + # Accept bare short names like ``core`` alongside ``packages/core`` and + # ``./packages/core`` so callers do not have to care which form a + # downstream script prefers. + path = Path(normalized_path) + candidates.add(path.name) + candidates.add(f"./{normalized_path}") + + for alias in aliases: + normalized_alias = normalize_project_filter(alias) + if normalized_alias and normalized_alias != ".": + candidates.add(normalized_alias) + + return {candidate.lower() for candidate in candidates} + + +def project_filter_matches(project: Path | str, pattern: str, aliases: Sequence[str] = ()) -> bool: + """Return whether a project matches a user-supplied selector or glob. + + Matching happens against the normalized candidate set so CLI callers can use + the same selector vocabulary everywhere. + """ + normalized_pattern = normalize_project_filter(pattern).lower() + return any( + fnmatch(candidate, normalized_pattern) + for candidate in build_project_filter_candidates(project, aliases) + ) + + +def _run_task_subprocess( + project: Path, + task: str, + workspace_root: Path, + task_args: Sequence[str] = (), +) -> tuple[Path, str, int, str, str, float]: """Run a single poe task in a project directory via subprocess.""" start = time.monotonic() cwd = workspace_root / project result = subprocess.run( - ["uv", "run", "poe", task], + ["uv", "run", "poe", task, *task_args], cwd=cwd, capture_output=True, text=True, @@ -84,20 +146,20 @@ def _run_task_subprocess(project: Path, task: str, workspace_root: Path) -> tupl return (project, task, result.returncode, result.stdout, result.stderr, elapsed) -def _run_sequential(work_items: list[tuple[Path, str]]) -> None: +def _run_sequential(work_items: list[tuple[Path, str]], task_args: Sequence[str] = ()) -> None: """Run tasks sequentially using in-process PoeThePoet (streaming output).""" from poethepoet.app import PoeThePoet for project, task in work_items: print(f"Running task {task} in {project}") app = PoeThePoet(cwd=project) - result = app(cli_args=[task]) + result = app(cli_args=[task, *task_args]) if result: sys.exit(result) -def _run_parallel(work_items: list[tuple[Path, str]], workspace_root: Path) -> None: - """Run all (package × task) combinations in parallel via subprocesses.""" +def _run_parallel(work_items: list[tuple[Path, str]], workspace_root: Path, task_args: Sequence[str] = ()) -> None: + """Run all (package x task) combinations in parallel via subprocesses.""" max_workers = min(len(work_items), os.cpu_count() or 4) failures: list[tuple[Path, str, str, str]] = [] completed = 0 @@ -107,7 +169,7 @@ def _run_parallel(work_items: list[tuple[Path, str]], workspace_root: Path) -> N with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor: futures = { - executor.submit(_run_task_subprocess, project, task, workspace_root): (project, task) + executor.submit(_run_task_subprocess, project, task, workspace_root, task_args): (project, task) for project, task in work_items } for future in concurrent.futures.as_completed(futures): @@ -123,7 +185,7 @@ def _run_parallel(work_items: list[tuple[Path, str]], workspace_root: Path) -> N if failures: print(f"\n[red]{len(failures)} task(s) failed:[/red]") for project, task, stdout, stderr in failures: - print(f"\n[red]{'='*60}[/red]") + print(f"\n[red]{'=' * 60}[/red]") print(f"[red]FAILED: {task} in {project}[/red]") if stdout.strip(): print(stdout) @@ -134,7 +196,13 @@ def _run_parallel(work_items: list[tuple[Path, str]], workspace_root: Path) -> N print(f"\n[green]All {total} task(s) passed ✓[/green]") -def run_tasks(work_items: list[tuple[Path, str]], workspace_root: Path, *, sequential: bool = False) -> None: +def run_tasks( + work_items: list[tuple[Path, str]], + workspace_root: Path, + *, + sequential: bool = False, + task_args: Sequence[str] = (), +) -> None: """Run work items either in parallel or sequentially. Single items use in-process PoeThePoet for streaming output. @@ -145,6 +213,6 @@ def run_tasks(work_items: list[tuple[Path, str]], workspace_root: Path, *, seque return if sequential or len(work_items) == 1: - _run_sequential(work_items) + _run_sequential(work_items, task_args) else: - _run_parallel(work_items, workspace_root) + _run_parallel(work_items, workspace_root, task_args) diff --git a/python/scripts/workspace_poe_tasks.py b/python/scripts/workspace_poe_tasks.py new file mode 100644 index 0000000000..d9e55e001d --- /dev/null +++ b/python/scripts/workspace_poe_tasks.py @@ -0,0 +1,698 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""Dispatch contributor-facing workspace tasks with consistent scope flags. + +This script is the single root-task entrypoint used by ``python/pyproject.toml``. +It keeps selector semantics, aggregate-vs-fan-out behaviour, and compatibility +aliases in one place so docs and automation can share the same command surface. +""" + +from __future__ import annotations + +import argparse +import os +import subprocess +import sys +from dataclasses import dataclass +from pathlib import Path + +import tomli +from packaging.specifiers import SpecifierSet +from packaging.version import Version +from rich import print +from task_runner import build_work_items, discover_projects, project_filter_matches, run_tasks + +WORKSPACE_ROOT = Path(__file__).resolve().parent.parent +WORKSPACE_PYPROJECT = WORKSPACE_ROOT / "pyproject.toml" +CURRENT_PYTHON = Version(f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}") +SAMPLE_EXCLUDES = "samples/autogen-migration,samples/semantic-kernel-migration" +SAMPLE_RUFF_IGNORE = "E501,ASYNC,B901,TD002" +MARKDOWN_EXCLUDES = [ + "cookiecutter-agent-framework-lab", + "tau2", + "packages/devui/frontend", + "context_providers/azure_ai_search", +] +DEFAULT_AGGREGATE_TEST_EXCLUDES = {"devui", "lab"} + + +@dataclass(frozen=True) +class WorkspaceProject: + """Metadata about a workspace package.""" + + path: Path + name: str + distribution_name: str + requires_python: str | None + + +def parse_args(argv: list[str]) -> tuple[argparse.Namespace, list[str]]: + """Parse the workspace command and return any pass-through arguments.""" + parser = argparse.ArgumentParser(description="Dispatch workspace Poe tasks with consistent scope flags.") + subparsers = parser.add_subparsers(dest="command", required=True) + + def add_project_option(command: argparse.ArgumentParser) -> None: + command.add_argument( + "-P", + "--package", + dest="project", + default="*", + metavar="PACKAGE", + help="Workspace package selector or glob pattern, such as `core`.", + ) + # Keep a hidden compatibility alias while old automation and local + # muscle memory migrate from ``--project`` to ``--package``. + command.add_argument("--project", dest="project", help=argparse.SUPPRESS) + + def add_syntax_mode_options(command: argparse.ArgumentParser) -> None: + command.add_argument("-F", "--format", action="store_true", help="Run formatting only.") + command.add_argument("-C", "--check", action="store_true", help="Run lint checks only.") + + def add_all_option(command: argparse.ArgumentParser) -> None: + command.add_argument("-A", "--all", action="store_true", help="Run a single aggregate workspace sweep.") + + def add_samples_option(command: argparse.ArgumentParser) -> None: + command.add_argument("-S", "--samples", action="store_true", help="Target samples/ instead of packages.") + + def add_cov_option(command: argparse.ArgumentParser) -> None: + command.add_argument("-C", "--cov", action="store_true", help="Enable coverage output.") + + syntax = subparsers.add_parser("syntax") + add_project_option(syntax) + add_samples_option(syntax) + add_syntax_mode_options(syntax) + + for command_name in ("fmt", "build", "clean-dist", "check-packages"): + command = subparsers.add_parser(command_name) + add_project_option(command) + + lint = subparsers.add_parser("lint") + add_project_option(lint) + add_samples_option(lint) + + pyright = subparsers.add_parser("pyright") + add_project_option(pyright) + add_all_option(pyright) + add_samples_option(pyright) + + mypy = subparsers.add_parser("mypy") + add_project_option(mypy) + add_all_option(mypy) + + typing = subparsers.add_parser("typing") + add_project_option(typing) + add_all_option(typing) + + test = subparsers.add_parser("test") + add_project_option(test) + add_all_option(test) + add_cov_option(test) + + check = subparsers.add_parser("check") + add_project_option(check) + add_samples_option(check) + + prek_check = subparsers.add_parser("prek-check") + prek_check.add_argument("files", nargs="*", default=["."], help="Files reported by pre-commit.") + + subparsers.add_parser("ci-mypy") + + return parser.parse_known_args(argv) + + +def load_toml(file_path: Path) -> dict: + """Load a TOML file.""" + with file_path.open("rb") as file: + return tomli.load(file) + + +def discover_workspace_projects() -> list[WorkspaceProject]: + """Return workspace packages together with their Python-version metadata.""" + projects: list[WorkspaceProject] = [] + for project_path in discover_projects(WORKSPACE_PYPROJECT): + pyproject = load_toml(WORKSPACE_ROOT / project_path / "pyproject.toml") + requires_python = pyproject.get("project", {}).get("requires-python") + distribution_name = str(pyproject.get("project", {}).get("name", "")).strip() + projects.append( + WorkspaceProject( + path=project_path, + name=project_path.name, + distribution_name=distribution_name, + requires_python=requires_python, + ) + ) + return projects + + +def supports_current_python(project: WorkspaceProject) -> bool: + """Return whether the current interpreter satisfies the project's Python requirement.""" + if not project.requires_python: + return True + return SpecifierSet(project.requires_python).contains(CURRENT_PYTHON, prereleases=True) + + +def select_projects(pattern: str) -> list[WorkspaceProject]: + """Select supported workspace projects that match the supplied pattern. + + The shared matcher accepts short names such as ``core``, legacy path-style + values, and distribution names so every root task family speaks the same + selector dialect. + """ + matched_projects = [ + project + for project in discover_workspace_projects() + if project_filter_matches(project.path, pattern, aliases=[project.name, project.distribution_name]) + ] + if not matched_projects: + print(f"[red]No workspace projects matched pattern '{pattern}'.[/red]") + raise SystemExit(2) + + supported_projects = [project for project in matched_projects if supports_current_python(project)] + unsupported_projects = [project.name for project in matched_projects if not supports_current_python(project)] + if unsupported_projects: + version = f"{sys.version_info.major}.{sys.version_info.minor}" + print( + "[yellow]Skipping packages not supported by " + f"Python {version}: {', '.join(sorted(unsupported_projects))}[/yellow]" + ) + + return supported_projects + + +def relative_path(path: Path) -> str: + """Convert a workspace path to a stable relative string.""" + return path.relative_to(WORKSPACE_ROOT).as_posix() + + +def collect_source_dirs(projects: list[WorkspaceProject]) -> list[Path]: + """Collect top-level import package directories for the selected projects.""" + source_dirs: set[Path] = set() + for project in projects: + project_root = WORKSPACE_ROOT / project.path + for init_file in project_root.rglob("__init__.py"): + package_dir = init_file.parent + if package_dir.name.startswith("agent_framework"): + source_dirs.add(package_dir) + return sorted(source_dirs) + + +def collect_test_dirs(projects: list[WorkspaceProject]) -> list[Path]: + """Collect test directories for the selected projects.""" + test_dirs: set[Path] = set() + for project in projects: + project_root = WORKSPACE_ROOT / project.path + for directory_name in ("tests", "ag_ui_tests"): + for test_dir in project_root.rglob(directory_name): + relative_test_dir = test_dir.relative_to(project_root) + # Ignore hidden/generated trees such as ``.mypy_cache`` so the + # aggregate sweep only targets real repository test directories. + if test_dir.is_dir() and not any(part.startswith(".") for part in relative_test_dir.parts): + test_dirs.add(test_dir) + return sorted(test_dirs) + + +def run_command(command: list[str]) -> None: + """Run a subprocess from the workspace root and stream its output.""" + result = subprocess.run(command, cwd=WORKSPACE_ROOT, check=False) + if result.returncode: + raise SystemExit(result.returncode) + + +def run_fan_out(task_names: list[str], project_pattern: str, task_args: list[str]) -> None: + """Run package-local Poe tasks across the selected projects.""" + selected_projects = select_projects(project_pattern) + if not selected_projects: + print("[yellow]No selected projects support the current Python version, skipping.[/yellow]") + return + + work_items = build_work_items([project.path for project in selected_projects], task_names) + run_tasks(work_items, WORKSPACE_ROOT, task_args=task_args) + + +def sample_pyright_config() -> str: + """Return the sample Pyright configuration for the current interpreter.""" + if sys.version_info < (3, 11): + return "pyrightconfig.samples.py310.json" + return "pyrightconfig.samples.json" + + +def run_sample_lint(extra_args: list[str]) -> None: + """Run linting against samples/.""" + command = [ + "uv", + "run", + "ruff", + "check", + "samples", + "--fix", + "--exclude", + SAMPLE_EXCLUDES, + "--ignore", + SAMPLE_RUFF_IGNORE, + *extra_args, + ] + run_command(command) + + +def run_sample_format(extra_args: list[str]) -> None: + """Run formatting against samples/.""" + command = [ + "uv", + "run", + "ruff", + "format", + "samples", + "--exclude", + SAMPLE_EXCLUDES, + *extra_args, + ] + run_command(command) + + +def run_sample_pyright(extra_args: list[str]) -> None: + """Run sample syntax/import validation.""" + command = ["uv", "run", "pyright", "-p", sample_pyright_config(), "--warnings", *extra_args] + run_command(command) + + +def run_markdown_code_lint(files: list[str] | None = None) -> None: + """Run markdown code-block linting globally or for the changed markdown files only.""" + command = [ + "uv", + "run", + "python", + "scripts/check_md_code_blocks.py", + ] + if files is None: + command.extend([ + "README.md", + "./packages/**/README.md", + "./samples/**/*.md", + ]) + else: + if not files: + print("[yellow]No markdown files changed, skipping markdown code lint.[/yellow]") + return + command.extend(files) + command.append("--no-glob") + + for excluded_path in MARKDOWN_EXCLUDES: + command.extend(["--exclude", excluded_path]) + run_command(command) + + +def run_aggregate_pyright(project_pattern: str, extra_args: list[str]) -> None: + """Run a single Pyright sweep across the selected project roots.""" + projects = select_projects(project_pattern) + if not projects: + print("[yellow]No selected projects support the current Python version, skipping.[/yellow]") + return + + project_paths = [relative_path(WORKSPACE_ROOT / project.path) for project in projects] + run_command(["uv", "run", "pyright", *extra_args, *project_paths]) + + +def run_aggregate_mypy(project_pattern: str, extra_args: list[str]) -> None: + """Run a single MyPy sweep across the selected project import roots.""" + projects = select_projects(project_pattern) + if not projects: + print("[yellow]No selected projects support the current Python version, skipping.[/yellow]") + return + + source_dirs = [relative_path(path) for path in collect_source_dirs(projects)] + if not source_dirs: + print("[yellow]No import roots found for the selected projects, skipping MyPy.[/yellow]") + return + + run_command(["uv", "run", "mypy", "--config-file", "pyproject.toml", *extra_args, *source_dirs]) + + +def run_aggregate_test(project_pattern: str, cov: bool, extra_args: list[str]) -> None: + """Run a single pytest sweep across the selected project test directories.""" + projects = select_projects(project_pattern) + if not projects: + print("[yellow]No selected projects support the current Python version, skipping.[/yellow]") + return + + if project_pattern == "*": + # Preserve the legacy ``all-tests`` contract when ``test --all`` runs with + # the default selector: experimental packages stay opt-in instead of + # suddenly joining every PR unit-test sweep. + projects = [project for project in projects if project.name not in DEFAULT_AGGREGATE_TEST_EXCLUDES] + if not projects: + print("[yellow]No aggregate-test projects remain after applying default exclusions.[/yellow]") + return + + test_dirs = [relative_path(path) for path in collect_test_dirs(projects)] + if not test_dirs: + print("[yellow]No test directories found for the selected projects, skipping pytest.[/yellow]") + return + + command = [ + "uv", + "run", + "pytest", + "--import-mode=importlib", + "-m", + "not integration", + "-rs", + "-n", + "logical", + "--dist", + "worksteal", + ] + if cov: + for source_dir in collect_source_dirs(projects): + command.append(f"--cov={source_dir.name}") + command.extend(["--cov-config=pyproject.toml", "--cov-report=term-missing:skip-covered"]) + + command.extend(extra_args) + command.extend(test_dirs) + run_command(command) + + +def normalize_changed_file(file_path: str) -> str: + """Normalize changed-file paths passed from git or pre-commit.""" + normalized = file_path.replace("\\", "/") + if normalized.startswith("python/"): + return normalized[7:] + return normalized + + +def has_changed_sample_files(files: list[str]) -> bool: + """Return whether any changed file lives under samples/.""" + return any(normalize_changed_file(file_path).startswith("samples/") for file_path in files) + + +def changed_markdown_files(files: list[str]) -> list[str]: + """Return markdown files from the provided change list.""" + markdown_files = [normalize_changed_file(file_path) for file_path in files] + return sorted({file_path for file_path in markdown_files if file_path.endswith(".md")}) + + +def run_changed_package_tasks(task_names: list[str], files: list[str]) -> None: + """Run package-local tasks only in packages affected by the provided file list.""" + command = [ + "uv", + "run", + "python", + "scripts/run_tasks_in_changed_packages.py", + *task_names, + "--files", + *files, + ] + run_command(command) + + +def run_prek_check(files: list[str]) -> None: + """Run the lightweight pre-commit task surface.""" + normalized_files = [normalize_changed_file(file_path) for file_path in files] or ["."] + run_changed_package_tasks(["fmt", "lint"], normalized_files) + run_markdown_code_lint(changed_markdown_files(normalized_files)) + if has_changed_sample_files(normalized_files): + print("[cyan]Sample files changed, running sample checks.[/cyan]") + run_sample_lint([]) + run_sample_pyright([]) + else: + print("[yellow]No sample files changed, skipping sample checks.[/yellow]") + + +def git_diff_name_only(*revisions: str) -> list[str] | None: + """Try a git diff strategy and return changed files if it succeeds.""" + result = subprocess.run( + ["git", "diff", "--name-only", *revisions, "--", "."], + cwd=WORKSPACE_ROOT, + capture_output=True, + text=True, + check=False, + ) + if result.returncode != 0: + return None + return [line for line in result.stdout.splitlines() if line] + + +def detect_ci_changed_files() -> list[str]: + """Detect changed files for change-based mypy runs.""" + base_ref = os.environ.get("GITHUB_BASE_REF") + if base_ref: + subprocess.run( + ["git", "fetch", "origin", base_ref, "--depth=1"], + cwd=WORKSPACE_ROOT, + capture_output=True, + text=True, + check=False, + ) + strategies = [ + (f"origin/{base_ref}...HEAD",), + ("FETCH_HEAD...HEAD",), + ("HEAD^...HEAD",), + ] + else: + strategies = [ + ("origin/main...HEAD",), + ("main...HEAD",), + ("HEAD~1",), + ] + + for strategy in strategies: + changed_files = git_diff_name_only(*strategy) + if changed_files is not None: + return changed_files or ["."] + + return ["."] + + +def run_ci_mypy() -> None: + """Run MyPy only where changes require it, mirroring CI behaviour.""" + changed_files = detect_ci_changed_files() + print("[cyan]Changed files for CI mypy:[/cyan]") + for file_path in changed_files: + print(f" {file_path}") + run_changed_package_tasks(["mypy"], changed_files) + + +def ensure_no_extra_args(command_name: str, extra_args: list[str]) -> None: + """Reject unsupported pass-through arguments for commands that do not forward them.""" + if extra_args: + joined_args = " ".join(extra_args) + print(f"[red]Command '{command_name}' does not accept extra arguments: {joined_args}[/red]") + raise SystemExit(2) + + +def resolve_syntax_modes(*, format_selected: bool, check_selected: bool) -> tuple[bool, bool]: + """Resolve which syntax steps to run.""" + if not format_selected and not check_selected: + return True, True + return format_selected, check_selected + + +def run_syntax( + *, + project_pattern: str, + samples: bool, + format_selected: bool, + check_selected: bool, + extra_args: list[str], +) -> None: + """Run formatting and/or lint checking for packages or samples. + + Combined package mode deliberately dispatches ``fmt`` and ``lint`` together + so the shared task runner can start both legs in parallel. + """ + run_format, run_check = resolve_syntax_modes( + format_selected=format_selected, + check_selected=check_selected, + ) + if run_format and run_check and extra_args: + joined_args = " ".join(extra_args) + print( + "[red]Extra arguments are only supported when syntax runs a single mode; " + f"use either --format or --check with: {joined_args}[/red]" + ) + raise SystemExit(2) + + if samples and project_pattern != "*": + print("[red]--samples cannot be combined with --package.[/red]") + raise SystemExit(2) + + format_args = extra_args if run_format and not run_check else [] + check_args = extra_args if run_check and not run_format else [] + + if samples: + if run_format: + run_sample_format(format_args) + if run_check: + run_sample_lint(check_args) + return + + if run_format and run_check: + # Fan out both legs in one call so task_runner can parallelize format + # and lint work across the same selected package set. + run_fan_out(["fmt", "lint"], project_pattern, []) + return + + if run_format: + run_fan_out(["fmt"], project_pattern, format_args) + if run_check: + run_fan_out(["lint"], project_pattern, check_args) + + +def main() -> None: + """Dispatch the requested workspace task.""" + args, extra_args = parse_args(sys.argv[1:]) + + if args.command == "syntax": + run_syntax( + project_pattern=args.project, + samples=args.samples, + format_selected=args.format, + check_selected=args.check, + extra_args=extra_args, + ) + return + + if args.command == "fmt": + run_syntax( + project_pattern=args.project, + samples=False, + format_selected=True, + check_selected=False, + extra_args=extra_args, + ) + return + + if args.command == "lint": + if args.samples: + run_syntax( + project_pattern=args.project, + samples=True, + format_selected=False, + check_selected=True, + extra_args=extra_args, + ) + return + run_syntax( + project_pattern=args.project, + samples=False, + format_selected=False, + check_selected=True, + extra_args=extra_args, + ) + return + + if args.command == "pyright": + if args.samples: + if args.all or args.project != "*": + print("[red]--samples cannot be combined with --all or --package.[/red]") + raise SystemExit(2) + run_sample_pyright(extra_args) + return + if args.all: + run_aggregate_pyright(args.project, extra_args) + return + run_fan_out(["pyright"], args.project, extra_args) + return + + if args.command == "mypy": + if args.all: + run_aggregate_mypy(args.project, extra_args) + return + run_fan_out(["mypy"], args.project, extra_args) + return + + if args.command == "typing": + ensure_no_extra_args(args.command, extra_args) + if args.all: + # Start MyPy first so combined typing runs follow the requested + # ordering even though completion still depends on runtime duration. + run_aggregate_mypy(args.project, []) + run_aggregate_pyright(args.project, []) + return + # Preserve the same "MyPy first" ordering for the per-package fan-out + # path as well. + run_fan_out(["mypy", "pyright"], args.project, []) + return + + if args.command == "test": + if args.all: + run_aggregate_test(args.project, args.cov, extra_args) + return + run_fan_out(["test"], args.project, extra_args) + return + + if args.command == "build": + ensure_no_extra_args(args.command, extra_args) + run_fan_out(["build"], args.project, []) + return + + if args.command == "clean-dist": + ensure_no_extra_args(args.command, extra_args) + run_fan_out(["clean-dist"], args.project, []) + return + + if args.command == "check-packages": + ensure_no_extra_args(args.command, extra_args) + run_syntax( + project_pattern=args.project, + samples=False, + format_selected=False, + check_selected=False, + extra_args=[], + ) + run_fan_out(["pyright"], args.project, []) + return + + if args.command == "check": + ensure_no_extra_args(args.command, extra_args) + if args.samples: + if args.project != "*": + print("[red]--samples cannot be combined with --package.[/red]") + raise SystemExit(2) + run_syntax( + project_pattern="*", + samples=True, + format_selected=False, + check_selected=False, + extra_args=[], + ) + run_sample_pyright([]) + return + run_syntax( + project_pattern=args.project, + samples=False, + format_selected=False, + check_selected=False, + extra_args=[], + ) + run_fan_out(["pyright"], args.project, []) + run_fan_out(["test"], args.project, []) + # Sample validation and markdown lint are intentionally workspace-wide; + # a package-scoped check should stay focused on the selected package set. + if args.project == "*": + run_syntax( + project_pattern="*", + samples=True, + format_selected=False, + check_selected=False, + extra_args=[], + ) + run_sample_pyright([]) + run_markdown_code_lint() + return + + if args.command == "prek-check": + ensure_no_extra_args(args.command, extra_args) + run_prek_check(args.files) + return + + if args.command == "ci-mypy": + ensure_no_extra_args(args.command, extra_args) + run_ci_mypy() + return + + print(f"[red]Unsupported command: {args.command}[/red]") + raise SystemExit(2) + + +if __name__ == "__main__": + main() diff --git a/python/shared_tasks.toml b/python/shared_tasks.toml index 775cb6ec0a..427cc1c5a7 100644 --- a/python/shared_tasks.toml +++ b/python/shared_tasks.toml @@ -1,10 +1,39 @@ -[tool.poe.tasks] -fmt = "ruff format" -format.ref = "fmt" -lint = "ruff check" -pyright = "pyright" -publish = "uv publish" -clean-dist = "rm -rf dist" -build-package = "uv build" -move-dist = "sh -c 'mkdir -p ../../dist && mv dist/* ../../dist/ 2>/dev/null || true'" -build = ["build-package", "move-dist"] +[tool.poe.tasks.syntax] +help = "Run Ruff formatting and Ruff checks for this package." +sequence = ["fmt", "lint"] + +[tool.poe.tasks.fmt] +help = "DEPRECATED: Use `syntax --format` instead." +cmd = "ruff format" + +[tool.poe.tasks.format] +help = "DEPRECATED: Use `syntax --format` instead." +ref = "fmt" + +[tool.poe.tasks.lint] +help = "DEPRECATED: Use `syntax --check` instead." +cmd = "ruff check" + +[tool.poe.tasks.pyright] +help = "Run Pyright for this package." +cmd = "pyright" + +[tool.poe.tasks.publish] +help = "Publish this package with uv." +cmd = "uv publish" + +[tool.poe.tasks.clean-dist] +help = "Remove generated dist artifacts for this package." +cmd = "rm -rf dist" + +[tool.poe.tasks.build-package] +help = "Build distribution artifacts for this package." +cmd = "uv build" + +[tool.poe.tasks.move-dist] +help = "Move built package artifacts into the workspace dist directory." +cmd = "sh -c 'mkdir -p ../../dist && mv dist/* ../../dist/ 2>/dev/null || true'" + +[tool.poe.tasks.build] +help = "Build this package and move its artifacts into the workspace dist directory." +sequence = ["build-package", "move-dist"]