Skip to content

Fix #3011: Handle overlapping submodule promotions gracefully in metaflow_extensions#3013

Open
Mustafa11300 wants to merge 5 commits intoNetflix:masterfrom
Mustafa11300:fix/handle-overlapping-submodule-promotions
Open

Fix #3011: Handle overlapping submodule promotions gracefully in metaflow_extensions#3013
Mustafa11300 wants to merge 5 commits intoNetflix:masterfrom
Mustafa11300:fix/handle-overlapping-submodule-promotions

Conversation

@Mustafa11300
Copy link

Closes #3011.

What is happening?

The __mf_promote_submodules__ mechanism in metaflow/extension_support/__init__.py did not handle the case where multiple extension packages promote the same submodule alias. The existing TODO comment (around line 258) said "For now, don't do this", but there was no proactive handling — conflicting promotions would silently overwrite each other.

Proposed Solution

This PR modifies the alias_submodules function to:

  1. Detect overlapping promotions from different packages via a new module-level _promoted_aliases tracking dictionary.
  2. Emit a UserWarning that clearly identifies:
    • The conflicting alias (e.g. metaflow.plugins.datatools)
    • The overriding package and its target module
    • The overridden package and its target module
    • How to silence the warning
  3. Apply deterministic resolution: the last-loaded package wins, based on the existing topological load order.
  4. Skip warnings for same-package re-promotions (e.g. if a module is loaded twice) since there is no actual conflict.

Additional changes

  • Added get_promoted_aliases() public accessor (returns a copy of the internal tracking dict).
  • Added get_promoted_aliases to __all__.

Tests

15 comprehensive unit tests in test/unit/test_extension_overlap.py:

Test Class Tests Description
TestSinglePackagePromotion 3 Baseline: no warnings for a single package
TestOverlappingPromotions 5 Overlap detection, last-loaded-wins, partial overlap, 3-way overlap
TestSamePackageRepromotion 1 Same package re-promoting is harmless (no false positive)
TestNoPromotionSubmodules 2 Modules without promotions don't affect tracking
TestWarningContent 1 Warning message contains all actionable details
TestGetPromotedAliases 1 Public accessor returns a copy, not a reference
TestExtraIndentFormatting 2 Existing debug-indent parameter still works

Examples

5 runnable example scripts in examples/overlapping_submodule_promotions/:

  1. Single extension, no conflict
  2. Two extensions overlapping — shows the warning
  3. Partial overlap (only some aliases conflict)
  4. Three-way overlap (two warnings)
  5. Same package re-promotion (no warning)

Files Changed

  • metaflow/extension_support/__init__.py — core fix
  • test/unit/test_extension_overlap.py — unit tests
  • examples/overlapping_submodule_promotions/ — 5 examples + README

- Detect when multiple extension packages promote the same alias via
  __mf_promote_submodules__ and emit a UserWarning with full details
  (conflicting packages, alias name, and concrete target modules).
- Apply deterministic resolution: the last-loaded package wins,
  based on the existing topological load order.
- Same-package re-promotions are silently allowed (no false positive).
- Add _promoted_aliases tracking dict and get_promoted_aliases() accessor.
- Add comprehensive unit tests (15 test cases).
- Add practical examples in examples/overlapping_submodule_promotions/.
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 12, 2026

Greptile Summary

This PR fixes issue #3011 by adding explicit overlap detection to alias_submodules in metaflow/extension_support/__init__.py. Previously, when two extension packages both promoted the same alias via __mf_promote_submodules__, the conflict was silently dropped. Now a UserWarning is emitted identifying both packages and the conflicting alias, and a new module-level _promoted_aliases dict tracks every promotion for inspection via the new public get_promoted_aliases() accessor.

Key findings:

  • Missing Tuple import (syntax): The type comment # type: Dict[str, Tuple[str, str]] on the new _promoted_aliases declaration at line 375 references Tuple, which is not in the from typing import Any, Dict statement on line 15. A previous review thread assumed Tuple was already imported — it is not. This will cause mypy to report Name "Tuple" is not defined on this file. Either Tuple must be added to the import, or the annotation should use the lowercase built-in tuple.

  • Last-loaded-wins edge case (logic): The documented "last-loaded package wins" guarantee holds only if the conflicting alias has not yet been resolved and cached in sys.modules when the second package's _LazyFinder is prepended to sys.meta_path. Any code that imports the aliased module between two consecutive multiload_all iterations would lock in the first package's target, producing the opposite of the documented behaviour without any additional warning. A docstring note or guard would make this assumption explicit.

  • Test assertion type fragility (style): Several tests assert aliases["metaflow.plugins.datatools"] == "<string>" without first checking that the value is a str. Since alias_submodules also stores live ModuleType objects in the same dict (from the for n, o in module.__dict__.items() path), a future test that accidentally attaches a real module to the fake module would silently compare incorrectly rather than failing clearly.

  • Example files directly mutate private state: All five example scripts import _promoted_aliases and call .clear() on it to reset between demonstrations (flagged in a previous thread). For reading state, get_promoted_aliases() is the correct public API; for resetting state in examples, each demo should ideally run in a fresh subprocess or the direct internal access should be prominently noted as unsupported.

Confidence Score: 3/5

  • The core logic is sound but has a missing Tuple import that will break mypy, and a subtle last-loaded-wins edge case that could silently produce incorrect alias resolution.
  • Score reflects: the overlap detection and warning mechanism are logically correct for the normal sequential load path; the PR resolves a real silent-overwrite bug and adds good test coverage. Points are deducted for the missing Tuple import (a real CI failure for any project running mypy on this file), the undocumented assumption about sys.modules caching that could silently violate the "last-loaded wins" contract, and the example files directly mutating private state in ways that could mislead extension authors.
  • metaflow/extension_support/__init__.py needs the Tuple import fix and a docstring note about the sys.modules caching assumption; test/unit/test_extension_overlap.py should also restore _aliased_modules in the fixture.

Important Files Changed

Filename Overview
metaflow/extension_support/init.py Core fix: adds _promoted_aliases tracking dict and overlap-warning logic in alias_submodules; also adds get_promoted_aliases() public accessor. Has a missing Tuple import for the type comment and a subtle last-loaded-wins edge case when a module is already cached in sys.modules.
test/unit/test_extension_overlap.py 15 well-structured unit tests covering single-package, overlapping, same-package re-promotion, and warning-content scenarios; fixture correctly isolates _promoted_aliases state but does not restore _aliased_modules (flagged in previous threads).
examples/overlapping_submodule_promotions/01_single_extension_no_conflict.py Baseline example that works correctly but directly imports and mutates the private _promoted_aliases dict (flagged in previous threads); structurally fine for demonstration purposes.
examples/overlapping_submodule_promotions/02_two_extensions_overlap_warning.py Same private-API usage pattern as example 01; otherwise demonstrates the core overlap warning scenario correctly.
examples/overlapping_submodule_promotions/README.md Clear documentation of the fix and examples; accurately describes the last-loaded-wins determinism and the four key behaviors.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["multiload_all(modules, extension_point, dst_globals)"] --> B["for each MFExtModule m"]
    B --> C["alias_submodules(m.module, m.tl_package, extension_point)"]
    C --> D{{"__mf_promote_submodules__ present?"}}
    D -- No --> K["return lazy_load_custom_modules (empty)"]
    D -- Yes --> E["Build new_promotions dict\n(alias → target_module strings)"]
    E --> F["for alias, target in new_promotions"]
    F --> G{{"alias in _promoted_aliases?"}}
    G -- No --> I["_promoted_aliases[alias] = (tl_package, target)"]
    G -- Yes --> H{{"prev_tl_package != tl_package?"}}
    H -- No (same pkg) --> I
    H -- Yes (conflict) --> J["warnings.warn(UserWarning)\n_ext_debug(overlap message)"]
    J --> I
    I --> F
    F --> L["lazy_load_custom_modules.update(new_promotions)"]
    L --> M["return lazy_load_custom_modules"]
    M --> N["lazy_load_aliases(aliases)\nprepend _LazyFinder to sys.meta_path"]
    N --> B
Loading

Comments Outside Diff (1)

  1. test/unit/test_extension_overlap.py, line 604-616 (link)

    alias_submodules return value also contains module objects — assertion may be fragile

    alias_submodules returns lazy_load_custom_modules, which is populated from two sources:

    1. new_promotions — string-to-string mappings built from __mf_promote_submodules__.
    2. The for n, o in module.__dict__.items() loop — which adds any ModuleType attributes directly.

    The tests only supply __mf_promote_submodules__, so path 2 is empty in these tests. However, assert aliases["metaflow.plugins.datatools"] == "%s.org_a.plugins.datatools" % EXT_PKG implicitly assumes the value is always a str. If a future test accidentally adds a real module object to the fake module, the assertion would fail silently (wrong type). Consider asserting isinstance(aliases["metaflow.plugins.datatools"], str) alongside the equality check to make the intent explicit.

Last reviewed commit: 154be83

Comment on lines +46 to +54
@pytest.fixture(autouse=True)
def _reset_promoted_aliases():
"""Ensure each test starts with a clean ``_promoted_aliases`` state."""
saved = dict(_promoted_aliases)
_promoted_aliases.clear()
yield
_promoted_aliases.clear()
_promoted_aliases.update(saved)

Copy link
Contributor

Choose a reason for hiding this comment

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

Test fixture leaks _aliased_modules global state

The fixture saves and restores _promoted_aliases, but alias_submodules also appends to the module-level _aliased_modules list (via _aliased_modules.extend(lazy_load_custom_modules.keys())). This list is never cleared between tests, causing it to grow unboundedly over the test run. While none of the tests in this file check _aliased_modules directly, other tests in the suite that rely on get_aliased_modules() returning a clean list could be affected.

Consider also saving and restoring _aliased_modules:

@pytest.fixture(autouse=True)
def _reset_promoted_aliases():
    """Ensure each test starts with a clean ``_promoted_aliases`` and
    ``_aliased_modules`` state."""
    from metaflow.extension_support import _aliased_modules

    saved_aliases = dict(_promoted_aliases)
    saved_modules = list(_aliased_modules)
    _promoted_aliases.clear()
    _aliased_modules.clear()
    yield
    _promoted_aliases.clear()
    _promoted_aliases.update(saved_aliases)
    _aliased_modules.clear()
    _aliased_modules.extend(saved_modules)

Comment on lines +28 to +32

def main():
# Clean slate
_promoted_aliases.clear()

Copy link
Contributor

Choose a reason for hiding this comment

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

Examples import and mutate a private internal variable

All five example scripts import the private _promoted_aliases dict directly and call .clear() on it to reset state between demonstrations. This pattern:

  1. Teaches users that it is safe/normal to manipulate private module internals directly.
  2. Mutates global interpreter state, which is fragile if the examples are ever run in the same process or adapted into notebooks.
  3. Bypasses the public get_promoted_aliases() accessor that was introduced specifically for safe access.

The same concern applies to the same import pattern in 02_two_extensions_overlap_warning.py, 03_partial_overlap.py, 04_three_way_overlap.py, and 05_same_package_repromotion.py.

For reading the final state, the examples should use the public accessor. For clearing state between demos, consider either structuring each demo as a fresh subprocess call, or at minimum adding a prominent comment explaining that this is internal API only and must not be done in user code.

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Comment on lines +296 to +309
if prev is not None:
prev_tl_package, prev_target = prev
if prev_tl_package != tl_package:
warnings.warn(
"Overlapping submodule promotion detected for alias "
"'%s': package '%s' (target '%s') is overriding "
"package '%s' (target '%s'). The last-loaded package "
"wins. To silence this warning, ensure only one "
"extension promotes this alias via "
"__mf_promote_submodules__."
% (alias, tl_package, target, prev_tl_package, prev_target),
UserWarning,
stacklevel=2,
)
Copy link
Contributor

Choose a reason for hiding this comment

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

stacklevel=2 points into Metaflow internals rather than user extension code

With stacklevel=2, the warning's reported origin is the caller of alias_submodules. In normal production use that caller is multiload_all (line 365 of this same file), so users will see a location inside Metaflow's own source rather than anything pointing at their extension config module. This makes it hard to use warnings.filterwarnings("ignore", …, module="…") to silence just one specific extension's conflict.

Using stacklevel=1 (the default) would at least point to the for alias, target loop inside alias_submodules, which is still an internal location but keeps the reported frame inside the function that detected the issue. Alternatively, keeping stacklevel=2 is fine because the message itself is fully self-contained (alias name + both package names), but consider adding guidance on silencing via warnings.filterwarnings to the message itself, for example:

"To silence this warning for this specific alias, use: "
"warnings.filterwarnings('ignore', message=\"Overlapping.*%s.*\")" % alias,

Mustafa11300 and others added 2 commits March 12, 2026 12:51
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

# Tracks which package promoted each alias so overlapping promotions can be
# detected and warned about. Mapping: alias -> (tl_package, target_module)
_promoted_aliases = {} # type: Dict[str, Tuple[str, str]]
Copy link
Contributor

Choose a reason for hiding this comment

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

Tuple is not imported — type comment will fail mypy

The type comment # type: Dict[str, Tuple[str, str]] references Tuple, but line 15 only imports Any and Dict from typing:

from typing import Any, Dict

Tuple is not in scope, so any mypy run over this file will report Name "Tuple" is not defined. A previous thread noted the annotation was imprecise and assumed Tuple was already imported — it isn't.

Fix by adding Tuple to the import:

Suggested change
_promoted_aliases = {} # type: Dict[str, Tuple[str, str]]
_promoted_aliases = {} # type: Dict[str, Tuple[str, str]]

And update line 15:

from typing import Any, Dict, Tuple

Alternatively, use the lowercase built-in (Python 3.9+):

_promoted_aliases = {}  # type: dict[str, tuple[str, str]]

or simply use Dict[str, tuple] with the existing imports if adding Tuple is not desirable.

)
_promoted_aliases[alias] = (tl_package, target)

lazy_load_custom_modules.update(new_promotions)
Copy link
Contributor

Choose a reason for hiding this comment

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

lazy_load_custom_modules always updated even when conflicting alias previously registered

lazy_load_custom_modules.update(new_promotions) unconditionally overwrites any prior alias in the local map, which is fine — the intent is last-loaded wins. However, note that the global resolution of metaflow.plugins.datatools ultimately depends on the order entries are prepended to sys.meta_path in lazy_load_aliases. When two packages both promote the same alias:

  1. Package A's _LazyFinder is prepended first.
  2. Package B's _LazyFinder is prepended second (so it appears earlier in sys.meta_path).

Python's import machinery queries finders in order, so Package B's finder is hit first and wins — consistent with "last-loaded wins." But if metaflow.plugins.datatools was already resolved and cached in sys.modules by Package A's finder before Package B's finder was registered, Package B's finder would never be consulted and Package A would silently win — the opposite of what is documented and what _promoted_aliases records.

In multiload_all the calls are sequential (lazy_load_aliases(alias_submodules(...))) so a lazy module that has not been accessed yet should be safe. However, any code path that imports metaflow.plugins.datatools between the two multiload_all iterations would produce the undocumented behavior. A defensive guard or a note in the docstring about this assumption would prevent future surprises.

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.

[Feature] Handle overlapping submodules gracefully in metaflow_extensions promotion

1 participant