Skip to content

zarr_io warning-suppression context managers leak a process-global filter (never restored) #864

Description

@zahidaramai

Summary

zarr_warnings_suppress_unstable_structs_v3 and zarr_warnings_suppress_unstable_numcodecs_v3 in mdio/core/zarr_io.py call warnings.filterwarnings("ignore", ...) without warnings.catch_warnings(), and their finally block is pass. The filter is therefore never removed: entering either context manager once silences that warning category process-globally for the remainder of the interpreter's life, not just for the block.

Affected code

src/mdio/core/zarr_io.py (v1.2.1, a2895b5):

@contextmanager
def zarr_warnings_suppress_unstable_structs_v3() -> Generator[None, None, None]:
    """Context manager to suppress Zarr V3 unstable structured array warning."""
    warn = r"The data type \((.*?)\) does not have a Zarr V3 specification\."
    warnings.filterwarnings("ignore", message=warn, category=UnstableSpecificationWarning)
    try:
        yield
    finally:
        pass          # <- filter is never restored

zarr_warnings_suppress_unstable_numcodecs_v3 has the same shape for ZarrUserWarning.

By contrast mdio/segy/creation.py:92 does use with warnings.catch_warnings(): and is correctly scoped — so the pattern is already used properly elsewhere in the codebase.

Reproduction

import warnings
from mdio.core.zarr_io import (
    zarr_warnings_suppress_unstable_structs_v3 as structs,
    zarr_warnings_suppress_unstable_numcodecs_v3 as codecs,
)

print(len(warnings.filters))          # 10
with structs():
    pass
with codecs():
    pass
print(len(warnings.filters))          # 12  <- still 12, outside both blocks

Measured on multidimio==1.2.1, segy==0.6.0, zarr==3.3.0, Python 3.12.13.

The two leaked entries are:

action='ignore' category=UnstableSpecificationWarning
  message='The data type \\((.*?)\\) does not have a Zarr V3 specification\\.'
action='ignore' category=ZarrUserWarning
  message='Numcodecs codecs are not in the Zarr version 3 specification'

Why it matters

A caller that touches any code path entering these context managers is permanently deafened to those two warnings, including for its own arrays written later and entirely outside MDIO. There is no way for that caller to opt back in: an outer warnings.catch_warnings(record=True) with simplefilter("always") observes zero of the suppressed warnings, because the leaked filter is inserted at the front of warnings.filters and matches first.

with warnings.catch_warnings(record=True) as caught:
    warnings.simplefilter("always")
    with structs():
        warnings.warn("The data type (struct) does not have a Zarr V3 specification.",
                      UnstableSpecificationWarning)
print(len(caught))   # 0

This matters most for tools that must report, rather than inherit, what a dependency chose to silence — the UnstableSpecificationWarning here is a genuine portability signal (the headers array's data_type: {"name": "struct"} has no Zarr v3 core specification), and a downstream consumer may reasonably need to surface it.

It also affects test suites: a suite running with -W error or filterwarnings = ["error"] will silently stop erroring on these categories after the first ingest, in a way that does not reset between tests in the same process.

Suggested fix

Scope the filter, matching the pattern already used in creation.py:

@contextmanager
def zarr_warnings_suppress_unstable_structs_v3() -> Generator[None, None, None]:
    """Context manager to suppress Zarr V3 unstable structured array warning."""
    warn = r"The data type \((.*?)\) does not have a Zarr V3 specification\."
    with warnings.catch_warnings():
        warnings.filterwarnings("ignore", message=warn, category=UnstableSpecificationWarning)
        yield

Note warnings.catch_warnings() is not thread-safe (it mutates global state), so if these context managers can be entered concurrently that is worth considering separately — though the current code has the same exposure, plus the leak.

A regression test could simply assert len(warnings.filters) is unchanged across the block, or better, assert the warning is observable again afterwards.

Context

Found while building a SEG-Y → MDIO conversion validator that records, rather than inherits, every warning a dependency suppresses. Happy to open a PR with the fix and a regression test if that's useful.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions