Skip to content

Conversation

@richban
Copy link

@richban richban commented Aug 15, 2025

Refactor: Replace exec() usage with GeneratedCallable classes

Problem

The current dynamic console method generation uses exec() to create string-based Python code, causing invalid syntax with args/kwargs parameters and breaking SQLMesh 0.209.0 compatibility.

Solution

Replace exec() approach with GeneratedCallable and UnknownEventCallable classes that use Python's callable protocol to handle method invocations directly.

Changes

  • Add GeneratedCallable and UnknownEventCallable classes
  • Update IntrospectingConsole.create_event_handler() to use callables
  • Remove create_signatures_and_params() string generation method
  • Remove unused textwrap import

Benefits

  • ✅ Fixes *args/**kwargs syntax errors
  • ✅ Eliminates exec() security concerns
  • ✅ Improves type safety and debugging
  • ✅ SQLMesh 0.209.0 compatible
  • ✅ All tests pass (8/8)

Dependencies

  • SQLMesh: >=0.1880.209.0
  • All other dependencies remain unchanged
  • Python 3.11-3.12 support maintained
image

@ravenac95
Copy link
Member

@richban thank you so much for the PR! I'll review this hopefully in the next day or so. I actually would like to relieve the version pinning now. In the meantime, we had to fix a bug is it possible to rebase?

- Add proper *args handling in IntrospectingConsole.create_signatures_and_params()
- Convert *args to _unknown_args_from_varargs dict to bridge incompatible interfaces
- Update publish_known_event() to process converted *args data
- Fix LogTestResults field ordering to prevent syntax errors
- Update SQLMesh constraint from <0.188 to >=0.188

Fixes compatibility issue with SQLMesh 0.209.0 which added *args/**kwargs
to log_error() and log_warning() abstract methods. Without this fix, the
dynamic method generation creates invalid Python syntax that prevents import.

The solution preserves all argument data in unknown_args while maintaining
the clean **kwargs-only interface of the event system.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
@richban richban reopened this Aug 19, 2025
@ravenac95
Copy link
Member

ravenac95 commented Sep 4, 2025

@richban I've been thinking about this a bunch mostly cause I didn't like my current solution that relies on exec. I'd actually like to refactor that section to instead use a class that simply returns a callable.

Something like:

class GeneratedCallable(t.Generic[EventType]):
    def __init__(self, console: IntrospectingConsole, event_cls: type[EventType], original_signature: inspect.Signature):
        self.console = console
        self.event_cls = event_cls
        self.original_signature = original_signature

    def __call__(self, *args: t.Any, **kwargs: t.Any) -> None:
        """Create an instance of the event class with the provided arguments."""
        ... generic call logic here that validates the type... 

I think this would satisfy a few things:

  • Remove the exec usage (which feels wrong)
  • We would inherently handle *args and **kwargs.

Is this something you think you'd be able to change here? If not, I could also fix this as well, I just haven't had a moment recently.

@richban
Copy link
Author

richban commented Sep 7, 2025

Related: dagster-io/dagster#32092

@ravenac95
Copy link
Member

Ah shoot, the changes don’t look bad but commits aren’t passing the gitlint ci check. This needs to use conventional commits to pass.

@richban
Copy link
Author

richban commented Sep 8, 2025

Got it!

@ravenac95
Copy link
Member

@richban i just noticed, refactor: is not a valid commit prefix. I'd just use fix: refactor...etc

Replace dynamic exec() generation with proper callable class pattern
for better maintainability, type safety, and debugging capability.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
func_signature, call_params = cls.create_signatures_and_params(signature)
def create_unknown_event_handler(cls, method_name: str, signature: inspect.Signature) -> t.Callable[..., None]:
"""Create an UnknownEventCallable for unknown events."""
def handler(self: IntrospectingConsole, *args: t.Any, **kwargs: t.Any) -> None:
Copy link
Member

Choose a reason for hiding this comment

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

We can clean this up a bit and set the generated callable as the response. No need to wrap this in the another callable.

func_signature, call_params = cls.create_signatures_and_params(signature)
def create_event_handler(cls, method_name: str, event_cls: type[BaseConsoleEvent], signature: inspect.Signature) -> t.Callable[..., None]:
"""Create a GeneratedCallable for known events."""
def handler(self: IntrospectingConsole, *args: t.Any, **kwargs: t.Any) -> None:
Copy link
Member

Choose a reason for hiding this comment

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

Same issue no need to wrap the callable in a callable

Copy link
Author

Choose a reason for hiding this comment

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

Let me know if the new changes make sense.

# Create methods now that we have self
for method_type, method_name, event_cls, signature in self._method_info:
if method_type == 'known':
handler = GeneratedCallable(self, event_cls, signature, method_name)
Copy link
Member

Choose a reason for hiding this comment

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

I am a bit old school this way, but I don't like to do things in constructors that might fail if I can really help it. Can we instead put the logic to handle the methods back. I think perhaps telling you not to wrap in that handler made it not possible to use __init_subclass__? If so, let's just keep that wrapped handler then.

Copy link
Author

Choose a reason for hiding this comment

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

when I tried to eliminate the wrapper handler, it made __init_subclass__ impossible to use because we needed self

@ravenac95
Copy link
Member

@richban some additional issues on the git commit lint. Apologies, I'm just gonna close this and fix that up myself so I can get this across the finish line!

@richban
Copy link
Author

richban commented Sep 17, 2025

Thanks for the feedback and patience.

brunoalano added a commit to brunoalano/dagster-sqlmesh that referenced this pull request Sep 28, 2025
… console and backfilling integration\n- Support latest Dagster and SQLMesh versions\n- Exclude private README changes; keep upstream README\n\nRefs: opensource-observer#49, closes opensource-observer#43; follow-up to opensource-observer#52
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.

2 participants