Skip to content

Bind extension methods to the extended type so they are callable from the instance - #150

Open
jhonabreul wants to merge 2 commits into
QuantConnect:masterfrom
jhonabreul:feature-bind-extension-methods
Open

jhonabreul wants to merge 2 commits into
QuantConnect:masterfrom
jhonabreul:feature-bind-extension-methods

Conversation

@jhonabreul

@jhonabreul jhonabreul commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

What does this implement/fix? Explain your changes.

C# extension methods were only reachable from Python through their static class, so the instance-style call every C# example uses failed:

AttributeError: 'OrderStatus' object has no attribute 'is_open'

Cause: ClassManager builds a type's Python class from its real members only, and extension methods are static methods on another type.

The change:

  • ExtensionMethods indexes the public extension methods of each scanned assembly by the type they extend, inside AssemblyManager.ScanAssembly. Framework assemblies and Python.Runtime are skipped so LINQ is not grafted onto every collection.
  • ClassManager.GetClassInfo binds the extensions of the type and of the interfaces it introduces as members, so snake-case aliases come for free. A real member with the same name wins, as in C#.
  • MethodBinder.Bind treats an extension called through an instance like a bound operator: it matches the Python arguments against the remaining parameters and prepends the instance.
  • Base-class extensions reach subclasses through the Python type hierarchy. Open generic targets (this T, this IEnumerable<T>) and by-ref targets are skipped because the binder cannot infer them from the instance.
  • The index is cleared on AssemblyManager.Initialize so a re-initialized engine rebuilds it.

Limits: targets converted to Python natives (string, decimal, DateTime, double) cannot be bound, and their extensions stay available through the static class.

Does this close any currently open issues?

No.

Any other comments?

Tests, in TestExtensionMethods:

  • Enum extension callable from the value in both snake and Pascal case, with positional and keyword arguments.
  • Class extension callable from the instance, including a default parameter.
  • Subclass inherits a base-class extension; interface extension is bound on the implementing class.
  • Real member with the same name wins over the extension.
  • Closed generic target (List<int>) is bound; open generic target is not.
  • Static call forms through the extension class and through the extended class keep working.
  • Python-native target (string) is not bound but its static form works.
  • Full embedding suite: 1003 passed, 0 failed.

… the instance

C# extension methods were only reachable from Python through their static
class, so instance-style calls such as order_status.is_open() failed with
AttributeError. Extension methods declared in non-framework assemblies are
now indexed during the assembly scan and bound as members of the type they
extend, including interface targets on implementing classes. The binder
passes the instance as the first argument when an extension method is called
through it. Real members with the same name win, and open generic targets are
skipped since the binder cannot infer them from the instance.

This branch has not been deployed

No deployments
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.

1 participant