-
Notifications
You must be signed in to change notification settings - Fork 19
fix shell method being shadowed #382
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
bennyz
wants to merge
1
commit into
jumpstarter-dev:main
Choose a base branch
from
bennyz:fix-shadowing
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: jumpstarter-dev/jumpstarter
Length of output: 8422
🏁 Script executed:
Repository: jumpstarter-dev/jumpstarter
Length of output: 3452
🏁 Script executed:
Repository: jumpstarter-dev/jumpstarter
Length of output: 1440
🏁 Script executed:
Repository: jumpstarter-dev/jumpstarter
Length of output: 711
🏁 Script executed:
Repository: jumpstarter-dev/jumpstarter
Length of output: 53
🏁 Script executed:
Repository: jumpstarter-dev/jumpstarter
Length of output: 525
🏁 Script executed:
Repository: jumpstarter-dev/jumpstarter
Length of output: 870
🏁 Script executed:
Repository: jumpstarter-dev/jumpstarter
Length of output: 1767
🏁 Script executed:
Repository: jumpstarter-dev/jumpstarter
Length of output: 53
🏁 Script executed:
Repository: jumpstarter-dev/jumpstarter
Length of output: 2274
🏁 Script executed:
Repository: jumpstarter-dev/jumpstarter
Length of output: 306
🏁 Script executed:
Repository: jumpstarter-dev/jumpstarter
Length of output: 785
🏁 Script executed:
Repository: jumpstarter-dev/jumpstarter
Length of output: 902
🏁 Script executed:
Repository: jumpstarter-dev/jumpstarter
Length of output: 1780
🏁 Script executed:
Repository: jumpstarter-dev/jumpstarter
Length of output: 435
🏁 Script executed:
Repository: jumpstarter-dev/jumpstarter
Length of output: 160
🏁 Script executed:
Repository: jumpstarter-dev/jumpstarter
Length of output: 53
🏁 Script executed:
Repository: jumpstarter-dev/jumpstarter
Length of output: 5306
🏁 Script executed:
Repository: jumpstarter-dev/jumpstarter
Length of output: 131
🏁 Script executed:
Repository: jumpstarter-dev/jumpstarter
Length of output: 53
🏁 Script executed:
Repository: jumpstarter-dev/jumpstarter
Length of output: 217
🏁 Script executed:
Repository: jumpstarter-dev/jumpstarter
Length of output: 329
Missing safeguard for reserved method names could break client functionality.
The
__getattribute__override intercepts all non-underscore attributes and returns a shell method callable if the name is in_methods. However, if a user configures a shell method with the same name as a critical parent class method (e.g.,"call","streamingcall","close","stream"), the lambda will shadow that method and break the client. This is a real shadowing risk that has been verified.Per the driver code,
get_methods()returns all configured method names without filtering. The proposed approach of adding a reserved names check is sound, but the suggested list needs to be more complete to cover all public methods and properties:call,streamingcall,stream,log_stream,open_stream,closecliportal,stack,children,description,methods_descriptionuuid,labels,namestub,log_level,logger,status🛡️ Suggested reserved names filter
`@dataclass`(kw_only=True) class ShellClient(DriverClient): _methods: set[str] | None = None + _RESERVED_NAMES: frozenset = frozenset({ + "call", "streamingcall", "cli", "close", "stream", + "log_stream", "open_stream", "portal", "stack", "children", + "description", "methods_description", "uuid", "labels", "name", + "stub", "log_level", "logger", "status", + }) ... def __getattribute__(self, name): if not name.startswith("_"): d = object.__getattribute__(self, "__dict__") methods = d.get("_methods") # Lazy-load on first access; guard prevents recursion # since self.call() re-enters __getattribute__ if methods is None and not d.get("_loading_methods"): d["_loading_methods"] = True try: methods = set(object.__getattribute__(self, "call")("get_methods")) + reserved = object.__getattribute__(self, "_RESERVED_NAMES") + if methods & reserved: + logger.warning( + f"Shell methods {methods & reserved} shadow reserved names and will be ignored" + ) + methods -= reserved d["_methods"] = methods except Exception: logger.debug("Failed to lazy-load shell methods", exc_info=True) finally: d.pop("_loading_methods", None) - if methods and name in methods: + if methods and name in methods and name not in object.__getattribute__(self, "_RESERVED_NAMES"): return lambda *args, **kwargs: object.__getattribute__(self, "_call_method")(name, *args, **kwargs) return object.__getattribute__(self, name)🤖 Prompt for AI Agents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mangelajo yeah, this is a difficult problem, I don't know how likely it is to use
callin the exporter config, butclosedoesn't sound very unlikelyThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, probably we should add a checker on the exporter config with a banned list of names that can't be used.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@coderabbitai how can we effectively maintain such a lease without forgetting to add new methods?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bennyzGreat question! The most reliable way to avoid maintaining a manual list is to derive the reserved names programmatically from the MRO at class-definition time. This means the set auto-updates whenever parent classes gain new public methods — no manual bookkeeping required.Here's the approach:
Why this works without ongoing maintenance:
cls.__mro__[1:]skipsShellClientbut walks every ancestor (DriverClient,AsyncDriverClient,object, mixins, etc.).vars(parent)inspects each class's own__dict__, so only directly-defined names are included (no duplicates from inheritance, no surprises).reservedthe next time the code runs — nothing to update.Optional: cache it to avoid recomputing on every
__getattribute__call:Then reference
_RESERVED_NAMESdirectly instead of calling_parent_public_attrs()each time. The tradeoff is that the cache is frozen at import time, but since parent classes don't change at runtime, that's perfectly fine.🧠 Learnings used
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@coderabbitai, but this would break status
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bennyzYou're absolutely right — that's the key flaw in my suggestion. The PR's whole purpose is to allow user-configured shell methods likestatusto shadow parent class methods. Stripping out everything from__mro__[1:]would re-introduce the original bug.The correct distinction is between:
call,streamingcall,close,stream). If these are shadowed, the client breaks entirely.status,name,labels). These should legitimately yield to user-configured shell methods.So the right approach is a small, manually-curated list of only the critical infrastructure names:
This list is actually quite stable — it maps directly to the RPC transport layer. New methods would only be added to it if they're core transport/lifecycle methods, not domain-level methods. The concern about "forgetting to update" is much lower here because:
DriverClient/AsyncDriverClientand changing them is already a breaking API change requiring careful review.The tradeoff (vs. the MRO approach) is intentional: we're trading "auto-complete coverage" for "respects the PR's shadowing intent."
🧠 Learnings used