Skip to main content

BFS-enhanced method lookup for Python classes

Project description

BFSMRO

⚠️ 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.

BFS-enhanced method lookup for Python classes and instances.

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:

# 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 without knowing about them
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.

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.1.tar.gz (6.7 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.1-py3-none-any.whl (5.8 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: bfs_mro-0.1.1.tar.gz
  • Upload date:
  • Size: 6.7 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.1.tar.gz
Algorithm Hash digest
SHA256 0591ec495741366c474d796e86d4d8b32915d6409b69e2698f02c26e30dd01c8
MD5 aec648c04b632efc57a6b19d203e0860
BLAKE2b-256 ae383fb8b6e309ded00328ddfaf91860c6228b089ea70aa1ce8e92ec2a8b0dc2

See more details on using hashes here.

File details

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

File metadata

  • Download URL: bfs_mro-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 5.8 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.1-py3-none-any.whl
Algorithm Hash digest
SHA256 4c63ce04458444ba9c18cb19163a9e6d002f6363848d088e2471a00fd4f2bf60
MD5 278ebcf945638dce40a3fc2c7a801909
BLAKE2b-256 1580cc23d64dadabe8d60b3e3dd42eb78657408ca7330f5b15ec6ca2f1e6022d

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