Skip to content

fix(client): prevent ClientFactory.create() from mutating shared config extensions#859

Open
matanweks wants to merge 1 commit intoa2aproject:mainfrom
matanweks:fix/client-factory-config-mutation
Open

fix(client): prevent ClientFactory.create() from mutating shared config extensions#859
matanweks wants to merge 1 commit intoa2aproject:mainfrom
matanweks:fix/client-factory-config-mutation

Conversation

@matanweks
Copy link

Summary

  • Fix: ClientFactory.create() was mutating self._config.extensions when called with per-client extensions, causing extensions to silently accumulate across calls.
  • Solution: Use dataclasses.replace() to create a per-call config copy instead of mutating the shared ClientConfig. This matches the immutability pattern from fix: use default_factory for mutable field defaults in ServerCallContext #744.
  • Test: Added regression test verifying that config.extensions remains unchanged after multiple create() calls with different extensions.

Fixes #858

Test plan

  • New test test_client_factory_create_does_not_mutate_config_extensions verifies no mutation
  • All existing test_client_factory.py tests pass
  • ruff check and ruff format pass
  • mypy passes with no issues

…ig extensions

When `create()` was called with per-client extensions, the merged list
was written back to `self._config.extensions`, permanently mutating the
shared config. Extensions would silently accumulate across calls.

Use `dataclasses.replace()` to create a per-call config copy instead of
mutating the original, matching the immutability pattern from a2aproject#744.

Fixes a2aproject#858
@matanweks matanweks requested a review from a team as a code owner March 17, 2026 20:42
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses a critical bug where the ClientFactory was mutating its internal configuration's extension list when creating new clients. The fix ensures that the factory's base configuration remains immutable, preventing unexpected behavior and maintaining data integrity across client creation calls. This change aligns the client creation process with an immutable pattern, making the system more robust and predictable.

Highlights

  • Bug Fix: Resolved an issue where ClientFactory.create() was inadvertently modifying the shared self._config.extensions when client-specific extensions were provided, leading to unintended accumulation across calls.
  • Solution Implementation: Implemented dataclasses.replace() to generate a per-call copy of the configuration, ensuring that the original ClientConfig remains immutable and preventing side effects.
  • Regression Test: Added a new regression test to explicitly confirm that the config.extensions attribute remains unchanged after multiple create() calls with varying extensions.
Changelog
  • src/a2a/client/client_factory.py
    • Imported the dataclasses module.
    • Modified the create method to use dataclasses.replace() for creating a new ClientConfig instance with updated extensions, rather than directly modifying the factory's shared configuration.
  • tests/client/test_client_factory.py
    • Added a new test test_client_factory_create_does_not_mutate_config_extensions to verify that the factory's configuration extensions are not mutated by create() calls.
Activity
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request effectively resolves a critical bug where ClientFactory.create() mutated the shared configuration's extensions, leading to unintended accumulation across multiple calls. The solution, which involves using dataclasses.replace() to create an immutable, per-call configuration copy, is robust and aligns with best practices for immutability. The addition of a regression test is excellent, as it verifies the fix and prevents future regressions. The changes are correct and well-implemented. I have one minor suggestion to improve the conciseness of the code by using a ternary operator.

@@ -239,15 +240,20 @@ def create(
all_extensions = self._config.extensions.copy()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

low

While the current implementation correctly fixes the mutation bug, the logic for creating the per-call configuration can be simplified for better readability and conciseness. For consistency, prefer using a conditional expression (ternary operator) for this assignment over an if/else block, as per repository guidelines.

config = dataclasses.replace(self._config, extensions=self._config.extensions + extensions) if extensions else self._config
References
  1. For consistency, prefer using conditional expressions (ternary operators) for simple assignments over if/else blocks.

@github-actions
Copy link

🧪 Code Coverage (vs main)

⬇️ Download Full Report

Base PR Delta
src/a2a/client/client_factory.py 89.09% 91.82% 🟢 +2.73%
Total 91.73% 91.79% 🟢 +0.06%

Generated by coverage-comment.yml

@holtskinner holtskinner changed the title fix(client): prevent ClientFactory.create() from mutating shared config extensions fix(client): prevent ClientFactory.create() from mutating shared config extensions Mar 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ClientFactory.create() mutates shared ClientConfig.extensions across calls

1 participant