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:

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.

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.2.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.2-py3-none-any.whl (5.8 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: bfs_mro-0.1.2.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.2.tar.gz
Algorithm Hash digest
SHA256 1506e423e793ca316420fb7f39c4eb68ee4724eef8db27502784c9bdf23d0eb4
MD5 5f64fc62fac92fc17ff3f531654facdb
BLAKE2b-256 949f6ae10d2acd805c4dea923730ef9b867b4e91defd1e1c6774d73ba6513bc1

See more details on using hashes here.

File details

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

File metadata

  • Download URL: bfs_mro-0.1.2-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.2-py3-none-any.whl
Algorithm Hash digest
SHA256 5b53338cf929a5960d9c15b503db2161b0d19001ca6eeff3c4183975e19d0f1a
MD5 a0bc8fb3300c1e4ea73de2e2f492bb95
BLAKE2b-256 e0d762b163ab9ad86670a2bd29f5b6484f6a501c3c28d6c43f9468d13cd66148

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