diff --git a/.chronus/changes/python-update-contributing-docs-2026-04-23-10-47-41.md b/.chronus/changes/python-update-contributing-docs-2026-04-23-10-47-41.md new file mode 100644 index 00000000000..fcf3177a6d8 --- /dev/null +++ b/.chronus/changes/python-update-contributing-docs-2026-04-23-10-47-41.md @@ -0,0 +1,7 @@ +--- +changeKind: internal +packages: + - "@typespec/http-client-python" +--- + +Fix transient mypy CI failures on Windows with retry logic and update CONTRIBUTING docs to reflect the new end-to-end release workflow diff --git a/packages/http-client-python/CONTRIBUTING.md b/packages/http-client-python/CONTRIBUTING.md index 321e726098c..66d913fe4ff 100644 --- a/packages/http-client-python/CONTRIBUTING.md +++ b/packages/http-client-python/CONTRIBUTING.md @@ -105,37 +105,58 @@ Before creating a pull request: ## Downstream Testing -Due to the integration with `@azure-tools/typespec-python`, we require downstream testing to ensure compatibility. +This package (`@typespec/http-client-python`) is the **unbranded emitter**. It is wrapped by the **branded emitter** (`@azure-tools/typespec-python`), which lives in [Azure/typespec-azure](https://github.com/Azure/typespec-azure/tree/main/packages/typespec-python). -### Automatic Downstream PR Creation +### How CI Works -After your PR is created and CI passes: +When you open a PR against this package: -1. **Get the build artifact URL**: - - In your PR's CI results, click on "5 published; 1 consumed" (or similar) - - Navigate to: `Published artifacts` → `build_artifacts_python` → `packages` → `typespec-http-client-python-x.x.x.tgz` - - Click the three dots and select "Copy download url" +1. **Unbranded emitter CI** runs automatically (build, lint, test, regenerate). +2. **Branded emitter CI** also runs automatically — it builds `@azure-tools/typespec-python` from [`Azure/typespec-azure`](https://github.com/Azure/typespec-azure/tree/main/packages/typespec-python) against your PR's version of `@typespec/http-client-python` to verify compatibility. -2. **Trigger downstream testing**: - - Run [this pipeline](https://dev.azure.com/azure-sdk/internal/_build?definitionId=7257) with: - - `PULL-REQUEST-URL`: Your PR URL from step 1 - - `ARTIFACTS_URL`: The artifact URL from step 1 +Both must pass before your PR can be merged. -3. **Review downstream changes**: - - The pipeline will create a PR in [autorest.python](https://github.com/Azure/autorest.python) - - Follow the [autorest.python CONTRIBUTING.md](https://github.com/Azure/autorest.python/blob/main/CONTRIBUTING.md) for any additional changes needed +### Manual Regeneration Testing -4. **Merge process**: - - Ensure the downstream PR passes all tests - - Merge your original TypeSpec PR once downstream testing is complete +You can manually trigger the [TypeSpec Python Regenerate Tests](https://github.com/Azure/azure-sdk-for-python/actions/workflows/typespec-python-regenerate.yml) workflow in `azure-sdk-for-python` to regenerate tests with either emitter: -### Post-Release Updates +- **Branded** (`@azure-tools/typespec-python`): Select "branded" and optionally specify a version. If no version is given, it uses the version from [`eng/emitter-package.json`](https://github.com/Azure/azure-sdk-for-python/blob/main/eng/emitter-package.json). +- **Unbranded** (`@typespec/http-client-python`): Select "unbranded" and optionally specify a version. If no version is given, it uses the latest published version on npm. -After your changes are released: +The workflow checks out `microsoft/typespec` (at the ref you specify, defaulting to `main`), builds the regeneration infrastructure, installs the target emitter from npm, and runs the full regeneration. -1. Update the [autorest.python](https://github.com/Azure/autorest.python) repository to use the released version -2. Run `pnpm install` to update dependency mappings -3. Release the autorest emitters with your changes +### Post-Release: Updating azure-sdk-for-python + +Once a new version of the branded emitter (`@azure-tools/typespec-python`) is released, follow these steps to update `azure-sdk-for-python`: + +1. **Update `eng/emitter-package.json`** in [Azure/azure-sdk-for-python](https://github.com/Azure/azure-sdk-for-python): + + Update the `@azure-tools/typespec-python` version to the newly released version: + + ```json + { + "dependencies": { + "@azure-tools/typespec-python": "" + } + } + ``` + +2. **Regenerate config files** using `tsp-client`: + + ```bash + tsp-client generate-config-files \ + --package-json= < path-to-local-typespec-azure > /packages/typespec-python/package.json + ``` + + This updates the `devDependencies` in `eng/emitter-package.json` to match the branded emitter's peer dependencies. + +3. **Create a PR** with the updated `eng/emitter-package.json` and submit it to `azure-sdk-for-python`. + +4. **Automatic regeneration**: Once the PR merges to `main`, the [TypeSpec Python Regenerate Tests](https://github.com/Azure/azure-sdk-for-python/actions/workflows/typespec-python-regenerate.yml) workflow triggers automatically (it watches for changes to `eng/emitter-package.json`). It regenerates all test code and creates a follow-up PR with the updated generated files. + +5. **Generated code location**: The regenerated tests are checked in at [`eng/tools/azure-sdk-tools/emitter/generated/`](https://github.com/Azure/azure-sdk-for-python/tree/main/eng/tools/azure-sdk-tools/emitter/generated) in `azure-sdk-for-python`, split into: + - `azure/` — Tests generated with the branded emitter (Azure SDK specs) + - `unbranded/` — Tests generated with the unbranded emitter (TypeSpec HTTP specs) ## Getting Help diff --git a/packages/http-client-python/eng/scripts/ci/run_mypy.py b/packages/http-client-python/eng/scripts/ci/run_mypy.py index fb6210f72a2..ef99a326381 100644 --- a/packages/http-client-python/eng/scripts/ci/run_mypy.py +++ b/packages/http-client-python/eng/scripts/ci/run_mypy.py @@ -26,27 +26,34 @@ def get_config_file_location(): return os.path.join(os.path.dirname(__file__), "config/mypy.ini") -def _single_dir_mypy(mod): +def _single_dir_mypy(mod, retries=2): inner_class = get_package_namespace_dir(mod) if not inner_class: logging.info(f"No package directory found in {mod}, skipping") return True - try: - check_call( - [ - sys.executable, - "-m", - "mypy", - "--config-file", - get_config_file_location(), - "--ignore-missing", - str(inner_class.absolute()), - ] - ) - return True - except CalledProcessError as e: - logging.error("{} exited with mypy error {}".format(inner_class.stem, e.returncode)) - return False + for attempt in range(1, retries + 2): + try: + check_call( + [ + sys.executable, + "-m", + "mypy", + "--config-file", + get_config_file_location(), + "--ignore-missing", + str(inner_class.absolute()), + ] + ) + return True + except CalledProcessError as e: + if attempt <= retries: + logging.warning( + "{} mypy attempt {} failed (exit {}), retrying...".format(inner_class.stem, attempt, e.returncode) + ) + else: + logging.error("{} exited with mypy error {}".format(inner_class.stem, e.returncode)) + return False + return False if __name__ == "__main__":