Skip to content

Fix coordinator data mutation in YouTube diagnostics#170183

Closed
TomFilsell wants to merge 4 commits into
home-assistant:devfrom
TomFilsell:fix/youtube-diagnostics-mutation
Closed

Fix coordinator data mutation in YouTube diagnostics#170183
TomFilsell wants to merge 4 commits into
home-assistant:devfrom
TomFilsell:fix/youtube-diagnostics-mutation

Conversation

@TomFilsell
Copy link
Copy Markdown
Contributor

@TomFilsell TomFilsell commented May 9, 2026

Proposed change

The async_get_config_entry_diagnostics function was mutating the
coordinator's live data on every diagnostics fetch.

The previous code called .pop(ATTR_DESCRIPTION) directly on
channel_data, which is a reference into coordinator.data — not a
copy. This permanently removed description from ATTR_LATEST_VIDEO
for the lifetime of the config entry, meaning any code reading that
field after a diagnostics fetch would find it missing.

Additionally, when ATTR_LATEST_VIDEO is None, the previous code
called .get(ATTR_LATEST_VIDEO, {}).pop(...) on a throwaway empty
dict, silently doing nothing rather than handling the None case
explicitly.

This fix builds a shallow copy of each channel's data and constructs
a new ATTR_LATEST_VIDEO dict excluding description, leaving
coordinator data untouched.

Type of change

  • Dependency upgrade
  • Bugfix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix/feature causing existing functionality to break)
  • Code quality improvements to existing code or addition of tests

Additional information

  • No functional changes to the diagnostics output (description remains
    correctly excluded)
  • Three new tests added: coordinator data immutability, description
    redaction, and explicit None latest video handling
  • Snapshot updated to reflect the corrected output structure

Checklist

  • The code change is tested and works locally
  • Local tests pass
  • There is no commented out code in this PR
  • I have followed the development checklist
  • I have followed the perfect PR recommendations
  • The code has been formatted using Ruff
  • Tests have been added to verify that the new code works

@home-assistant
Copy link
Copy Markdown
Contributor

home-assistant Bot commented May 9, 2026

Hey there @joostlek, mind taking a look at this pull request as it has been labeled with an integration (youtube) you are listed as a code owner for? Thanks!

Code owner commands

Code owners of youtube can trigger bot actions by commenting:

  • @home-assistant close Closes the pull request.
  • @home-assistant mark-draft Mark the pull request as draft.
  • @home-assistant ready-for-review Remove the draft status from the pull request.
  • @home-assistant rename Awesome new title Renames the pull request.
  • @home-assistant reopen Reopen the pull request.
  • @home-assistant unassign youtube Removes the current integration label and assignees on the pull request, add the integration domain after the command.
  • @home-assistant update-branch Update the pull request branch with the base branch.
  • @home-assistant add-label needs-more-information Add a label (needs-more-information, problem in dependency, problem in custom component, problem in config, problem in device, feature-request) to the pull request.
  • @home-assistant remove-label needs-more-information Remove a label (needs-more-information, problem in dependency, problem in custom component, problem in config, problem in device, feature-request) on the pull request.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes a bug in the YouTube integration diagnostics where diagnostics generation could mutate (or previously even break on) the coordinator’s live data, by ensuring diagnostics operate on copies and safely handle latest_video=None.

Changes:

  • Avoid mutating coordinator.data by copying per-channel data and rebuilding latest_video without description.
  • Add diagnostics tests to verify coordinator data immutability, description redaction, and correct handling of latest_video=None.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
homeassistant/components/youtube/diagnostics.py Stops in-place mutation of coordinator data and safely redacts latest_video.description without altering live state.
tests/components/youtube/test_diagnostics.py Adds regression tests ensuring diagnostics don’t mutate coordinator data and handle latest_video=None while keeping description out of output.

Copy link
Copy Markdown
Contributor

@jbouwh jbouwh left a comment

Choose a reason for hiding this comment

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

Please fill i the PR description.

Split the fixed code and the Config Entry typing improvements

@home-assistant
Copy link
Copy Markdown
Contributor

home-assistant Bot commented May 9, 2026

Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍

Learn more about our pull request process.

@home-assistant home-assistant Bot marked this pull request as draft May 9, 2026 16:55
Copilot AI review requested due to automatic review settings May 10, 2026 04:24
@TomFilsell
Copy link
Copy Markdown
Contributor Author

Please fill i the PR description.

Split the fixed code and the Config Entry typing improvements

No, worries. Thanks for the feedback! I've updated the PR to contain only the diagnostics mutation bug fix. coordinator.py and init.py changes have been removed. The YouTubeConfigEntry typing improvements will be submitted as a separate PR.
I've also tidied up the PR description to remove the unused template sections.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

Comment thread homeassistant/components/youtube/__init__.py
Comment thread homeassistant/components/youtube/diagnostics.py Outdated
Comment on lines +42 to +45
await setup_integration()
entry = hass.config_entries.async_entries(DOMAIN)[0]
coordinator = hass.data[DOMAIN][entry.entry_id][COORDINATOR]

Comment on lines +103 to +106
await setup_integration()
entry = hass.config_entries.async_entries(DOMAIN)[0]
coordinator = hass.data[DOMAIN][entry.entry_id][COORDINATOR]

@TomFilsell
Copy link
Copy Markdown
Contributor Author

Please fill i the PR description.

Split the fixed code and the Config Entry typing improvements

Hi. Take 2.

I think the Config Entry typing changes in init.py and coordinator.py are actually a prerequisite for this fix — diagnostics.py needs to access the coordinator via entry.runtime_data because the updated async_setup_entry no longer populates hass.data. Would it be acceptable to include both changes in this PR, or would you prefer I submit the typing refactor first and rebase this on top of it?

@jbouwh
Copy link
Copy Markdown
Contributor

jbouwh commented May 10, 2026

I'd like the fix (mutation in the test) to be as small as possible, as this gets in to the patch release. Yeah is the first one. The second one is about code quality, and adds the typing.

@TomFilsell
Copy link
Copy Markdown
Contributor Author

I'd like the fix (mutation in the test) to be as small as possible, as this gets in to the patch release. Yeah is the first one. The second one is about code quality, and adds the typing.

Thank you!
I've investigated further and it looks like the old hass.data[DOMAIN][entry.entry_id][COORDINATOR] pattern is no longer compatible with the current init.py on dev, which already uses entry.runtime_data and no longer populates hass.data. Using the old pattern causes a KeyError at runtime and in tests.

Given this, I see two options for your consideration:
either 1. Include both the typing update and the bug fix together in this PR, or
2. Submit the typing refactor as a separate PR first, then rebase this fix on top of it once merged

Happy to go whichever route you prefer just let me know!

@jbouwh
Copy link
Copy Markdown
Contributor

jbouwh commented May 10, 2026

I just rebased (you need to do a git pull locally. And it seems the changes you want to make are invalid. May be your checkout branch was outdated.

@jbouwh
Copy link
Copy Markdown
Contributor

jbouwh commented May 10, 2026

I don't think the additional tests are needed. Just copy the coordinator data like:

async def async_get_config_entry_diagnostics(
    hass: HomeAssistant, entry: YouTubeConfigEntry
) -> dict[str, Any]:
    """Return diagnostics for a config entry."""
    coordinator = entry.runtime_data
    sensor_data: dict[str, Any] = {}
    for channel_id, channel_data in dict(coordinator.data).items():
        channel_data.get(ATTR_LATEST_VIDEO, {}).pop(ATTR_DESCRIPTION)
        sensor_data[channel_id] = channel_data
    return sensor_data

Comment on lines +5 to +10
from homeassistant.components.youtube.const import (
ATTR_DESCRIPTION,
ATTR_LATEST_VIDEO,
COORDINATOR,
DOMAIN,
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
from homeassistant.components.youtube.const import (
ATTR_DESCRIPTION,
ATTR_LATEST_VIDEO,
COORDINATOR,
DOMAIN,
)
from homeassistant.components.youtube.const import DOMAIN

) -> None:
"""Test diagnostics."""
await setup_integration()
entry = hass.config_entries.async_entries(DOMAIN)[0]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
entry = hass.config_entries.async_entries(DOMAIN)[0]
entry = hass.config_entries.async_entries(DOMAIN)[0]

Comment on lines +29 to +45


async def test_diagnostics_does_not_mutate_coordinator_data(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
setup_integration: ComponentSetup,
) -> None:
"""Test that fetching diagnostics does not mutate the coordinator's data.

Previously, diagnostics called .pop(ATTR_DESCRIPTION) directly on the
coordinator's data dict, permanently removing the description from the
live data after the first diagnostics fetch.
"""
await setup_integration()
entry = hass.config_entries.async_entries(DOMAIN)[0]
coordinator = hass.data[DOMAIN][entry.entry_id][COORDINATOR]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
async def test_diagnostics_does_not_mutate_coordinator_data(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
setup_integration: ComponentSetup,
) -> None:
"""Test that fetching diagnostics does not mutate the coordinator's data.
Previously, diagnostics called .pop(ATTR_DESCRIPTION) directly on the
coordinator's data dict, permanently removing the description from the
live data after the first diagnostics fetch.
"""
await setup_integration()
entry = hass.config_entries.async_entries(DOMAIN)[0]
coordinator = hass.data[DOMAIN][entry.entry_id][COORDINATOR]

Comment on lines +46 to +67
descriptions_before = {
channel_id: channel_data[ATTR_LATEST_VIDEO][ATTR_DESCRIPTION]
for channel_id, channel_data in coordinator.data.items()
if channel_data.get(ATTR_LATEST_VIDEO) is not None
}

assert descriptions_before, (
"Test setup should include at least one channel with a latest video"
)

await get_diagnostics_for_config_entry(hass, hass_client, entry)

for channel_id, description in descriptions_before.items():
assert (
coordinator.data[channel_id][ATTR_LATEST_VIDEO][ATTR_DESCRIPTION]
== description
), (
f"Coordinator data was mutated for channel {channel_id}: "
"description was removed by diagnostics fetch"
)


Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
descriptions_before = {
channel_id: channel_data[ATTR_LATEST_VIDEO][ATTR_DESCRIPTION]
for channel_id, channel_data in coordinator.data.items()
if channel_data.get(ATTR_LATEST_VIDEO) is not None
}
assert descriptions_before, (
"Test setup should include at least one channel with a latest video"
)
await get_diagnostics_for_config_entry(hass, hass_client, entry)
for channel_id, description in descriptions_before.items():
assert (
coordinator.data[channel_id][ATTR_LATEST_VIDEO][ATTR_DESCRIPTION]
== description
), (
f"Coordinator data was mutated for channel {channel_id}: "
"description was removed by diagnostics fetch"
)

@TomFilsell TomFilsell force-pushed the fix/youtube-diagnostics-mutation branch from 6a007e6 to 5733314 Compare May 11, 2026 01:19
Copilot AI review requested due to automatic review settings May 11, 2026 01:19
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

Comment on lines +49 to 50
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
@TomFilsell
Copy link
Copy Markdown
Contributor Author

Closing in favour of a cleaner PR based on current upstream dev: (#170300). Thanks again @jbouwh for the guidance.

@TomFilsell TomFilsell closed this May 11, 2026
@github-actions github-actions Bot locked and limited conversation to collaborators May 12, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants