Skip to main content

Context manager that extends MRO with BFS lookup in subclasses

Project description

BFSMRO

GitHub PyPI

⚠️ Note: This is a pet project for practicing advanced Python concepts like metaprogramming, context managers, and dynamic method resolution.
It is not intended for production use. Features like __slots__, property, and full descriptor support are not implemented.

Context manager that extends MRO with BFS lookup in subclasses.

This context manager allows a class or instance to access @classmethod, @staticmethod, and instance methods from its subclasses — even if they’re not in the MRO.

It uses Breadth-First Search (BFS) to find methods downward in the inheritance tree, while preserving normal MRO lookup (upward) as the first priority.

Ideal for dynamic plugin systems, framework extensions, and exploratory programming.

🔧 Features

  • ✅ Works on classes and instances
  • ✅ Supports @classmethod, @staticmethod, and instance methods
  • ✅ Lookup order: MRO first (up), then BFS in subclasses (down)
  • ✅ Opt-in debug mode: logs lookup and enhances error messages
  • ✅ Thread-safe mode available
  • ✅ Zero changes to existing classes

💡 Why BFSMRO?

Python's standard MRO only searches upward in the inheritance tree. BFSMRO adds downward lookup while maintaining:

  1. Backward Compatibility: Normal MRO has priority
  2. Binding Consistency: Methods use caller's context (self/cls)
  3. Dynamic Discovery: Find methods added after class definition

This enables novel patterns like:

from bfs_mro import BFSMRO

# Framework core remains unchanged
class Core: pass

class PluginA(Core):
    def feature_a(self):
        return f"Feature A from plugin, used by {self.__class__.__name__}"

class PluginB(Core):
    def feature_b(self):
        return f"Feature B from plugin, used by {self.__class__.__name__}"

# Core can now access plugin features
with BFSMRO(Core()) as enhanced:
    print(enhanced.feature_a())  # → "Feature A from plugin, used by Core"
    print(enhanced.feature_b())  # → "Feature B from plugin, used by Core"

Perfect for plugin architectures, testing scenarios, and exploratory programming where you want to access subclass functionality dynamically

🚀 Usage Guide

⚠️ Critical Limitation: Name Shadowing

Due to Python's scoping rules, using the same name in both the expression and as clause causes issues:

Classes for following examples:

from bfs_mro import BFSMRO

class Wizard: pass
class WhiteWizard(Wizard):
    @classmethod
    def cast_light(cls):
        return f"{cls.__name__} casts light"
    
    def heal(self):
        return f"A {self.__class__.__name__} heals"

Outside functions: works once, then breaks subsequent usage:

with BFSMRO(Wizard) as Wizard:
    print(Wizard.cast_light())  # This works
# But now Wizard is the proxy object, not the class!

wizard = Wizard() # ❌ TypeError: 'UniversalProxy' object is not callable
with BFSMRO(wizard) as wizard:
    print(wizard.heal())

Inside functions: fails immediately:

def bad_example():
  with BFSMRO(Wizard) as Wizard: # "UnboundLocalError: cannot access local variable 'Wizard' where it is not associated with a value"
    Wizard.cast_light()

bad_example()

Solution 1: Use Different Names

Use different name in the as clause:

def good_example():
    with BFSMRO(Wizard) as EnhancedWizard:
        assert EnhancedWizard.cast_light() == "Wizard casts light"
        
    wizard = Wizard()
    with BFSMRO(wizard) as enhanced_wizard:
        assert enhanced_wizard.heal() == "A Wizard heals"

Solution 2: Preserve Original Name

Store the original class/instance in a temporary variable:

_Wizard = Wizard

def good_example():
    with BFSMRO(_Wizard) as Wizard:
        assert Wizard.cast_light() == "Wizard casts light"
        
    wizard = _Wizard()
    with BFSMRO(wizard) as wizard:
        assert wizard.heal() == "A Wizard heals"

🔍 Method Binding Semantics

BFSMRO preserves Python's standard method binding behavior - the calling object determines cls/self, not the defining class:

class Wizard: pass

class WhiteWizard(Wizard):
    @classmethod
    def whoami(cls):
        return f"Method called on {cls.__name__}"
    
    def instance_class(self):
        return f"Instance of {self.__class__.__name__}"

# Class method: cls = Wizard (the enhanced class, not WhiteWizard!)
with BFSMRO(Wizard) as EnhancedWizard:
    print(EnhancedWizard.whoami())  # → "Method called on Wizard"

# Instance method: self = Wizard() (the enhanced instance)
wizard = Wizard()
with BFSMRO(wizard) as enhanced_wizard:
    print(enhanced_wizard.instance_class())  # → "Instance of Wizard"

This matches Python's normal behavior. Just like super().__init__() passes the current self to the parent method, BFSMRO passes the enhanced object to borrowed methods.

📦 Installation

pip install bfs-mro

🛠 Development

# Clone and install in dev mode
git clone https://github.com/AlexShchW/bfs_mro.git
cd bfs_mro
pip install -e .[dev]

# Run tests
pytest

📄 License

MIT — see LICENSE file.

🌐 Links

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

bfs_mro-0.1.3.tar.gz (6.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

bfs_mro-0.1.3-py3-none-any.whl (6.0 kB view details)

Uploaded Python 3

File details

Details for the file bfs_mro-0.1.3.tar.gz.

File metadata

  • Download URL: bfs_mro-0.1.3.tar.gz
  • Upload date:
  • Size: 6.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.11.0

File hashes

Hashes for bfs_mro-0.1.3.tar.gz
Algorithm Hash digest
SHA256 75a202dae11644e4830215eb957ef5f5dd990a3c204123d8ea15ca76fc453d35
MD5 52a71305c5b5e9b71c4402e52d5f98ef
BLAKE2b-256 79dbad5dae6ab9ad877395c64d5c690484b8390b2628710b3b501fd449653ee3

See more details on using hashes here.

File details

Details for the file bfs_mro-0.1.3-py3-none-any.whl.

File metadata

  • Download URL: bfs_mro-0.1.3-py3-none-any.whl
  • Upload date:
  • Size: 6.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.11.0

File hashes

Hashes for bfs_mro-0.1.3-py3-none-any.whl
Algorithm Hash digest
SHA256 e4f414c43931cd25dd50371671a3b7d739bb837047110f075413be0fca2c2324
MD5 9fe39ccd7b198a2ca4c558a21f1136bd
BLAKE2b-256 93e06ba96bc63754aee1b1c03ac9d2479028c193b762b918c7de703eb0dc9770

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page