Skip to content
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

Can't invoke Protocol methods that collide with object properties #333

Open
freakboy3742 opened this issue Jul 7, 2023 · 2 comments
Open
Labels
bug A crash or error in behavior.

Comments

@freakboy3742
Copy link
Member

freakboy3742 commented Jul 7, 2023

Describe the bug

If an Obj C object has a property, and implements a Protocol with selectors whose first argument matches that property, it is not possible to invoke the selectors.

Steps to reproduce

This was discovered when developing beeware/toga#2025; the iOS DetailedList code in v0.3.1 contains a setup that demonstrates the problem; the corresponding code in #2025 (to be included in v0.3.2) contains a workaround.

To demonstrate the problem: on an instance of DetailedList, use Python code to programmatically select a row on the DetailedList. This would be [controller.tableView.delegate tableView:controller.tableView didSelectRowAtIndexPath:...] in ObjC, where controller is the instance of TogaTableViewController, which maps to controller.tableView.delegate(controller.tableView, didSelectRowAtIndexPath:...) in Rubicon.

This will fail on v0.3.1, but pass on the beeware/toga#2025 branch, because of the way the widget is constructed.

The iOS backend uses a UITableViewController with a default UITableView to represent a DetailedList; the UITableViewController has a tableView property describing the view that it is controlling.

A UITableView can specify a UITableViewDelegate; the delegate responds to a number of selectors, including tableView:didSelectRowAtIndexPath:.

In v0.3.1, a subclass of UITableViewController was used (TogaTableViewController) that also acted as the delegate. When an attempt is made to invoke tableView:didSelectRowAtIndexPath:, you get the error:

Traceback (most recent call last):
...
    self.native.delegate.tableView(self.native, didSelectRowAtIndexPath=path)
TypeError: 'ObjCInstance' object is not callable

self.native.delegate returns the delegate, which is a TogaTableViewController instance; since this is a subclass of UITableViewController, it has a tableView property, so the Python tableView attribute is an ObjCInstance, not an ObjCMethod. It doesn't matter if you also explicitly declare the delegate as implementing the UITableViewDelegate protocol. You also can't pass the tableView:didSelectRowAtIndexPath: message directly.

The beeware/toga#2025 branch fixes this by using a raw UITableViewController, and making the delegate a subclass of NSObject. This removes the ambiguity between [UITableViewController tableView] and tableView:didSelectRowAtIndexPath:

This limitation doesn't exist on ObjC code - other bugs notwithstanding, the v0.3.1 branch works, and the UITableView is able to invoke the delegate methods defined in Python, no matter how it's defined. It only emerged in testing when we were programmatically invoking these methods from Python.

This also doesn't affect methods like tableView:cellForRowAtIndexPath: which are defined as part of the UITableViewDataSource protocol - or, at least, there's a different workaround. If you explicitly invoke the method on the controller.dataSource, rather than on the controller object directly, the method resolves. This appears to be because the data source is implicitly set by constructing the UITableViewController; if you explicitly set the dataSource property of the controller, the same problem manifests.

Expected behavior

It should be possible to retrieve the tableView property and invoke the tableView:didSelectRowAtIndexPath: selector on a delegate object.

Screenshots

No response

Environment

  • Operating System: iOS 16.4
  • Python version: 3.10
  • Software versions:
    • Rubicon: 0.4.6
    • Toga: 0.3.1

Logs


Additional context

There's probably an argument to be made that this is a documentation issue. Mapping from ObjC to Python is always going to be leaky, and there's workarounds available.

Another approach would be to require explicit casting to the Protocol in this case.

@freakboy3742 freakboy3742 added the bug A crash or error in behavior. label Jul 7, 2023
@dgelessus
Copy link
Collaborator

dgelessus commented Jul 8, 2023

Hrm, this is tricky. If I'm understanding the issue correctly, there's no way to implement the behavior you're looking for. If obj.tableView gets the value of the property tableView, then there's no way to make obj.tableView(...) call a method that starts with tableView:. Both cases end up calling obj.__getattr__("tableView") with no way to tell if the value will be used on its own or in a method call.

I think this has nothing to do with where the methods and properties are declared - the alternative calls that you've found work because the object in question has no tableView property, not because it implements the protocol differently.

A simple workaround would be to use the "long form" of the call - obj.tableView_didSelectRowAtIndexPath_(tableView, indexPath) - which won't conflict with the tableView property.

A possible nicer solution: allow suppressing the property syntax, with a new method that's an inverse of declare_property. Then the property getting syntax would be obj.tableView() and not obj.tableView, but other obj.tableView(...) method calls would also work.

@freakboy3742
Copy link
Member Author

Yeah - I agree that this is somewhere between very difficult or impossible - hence my suggestion that we might need to "fix" this with documentation that it isn't possible.

I had a vague thought that maybe this is something we could fix by catching implementing __call__() on ObjCInstance. Since ObjC doesn't really have an analog of Python's __call__(), ObjCInstances won't be callable, so a request to make such a call should be an indication that you're looking to pass a message with the attribute name that produced the instance... but there will be some interesting interplay with properties (i.e., things like widget.isHidden vs widget.isHidden()), and the housekeeping required to store the instance that produced the ObjCInstance so that you can pass a message to that instance started to tie my brain in knots.

An inverse of declare_property is also an interesting idea, and I suspect has less brain-bending required to make it work.

I logged this mostly as a warning for others in a similar situation. As I indicated on the ticket, I have a workaround that is complete viable; and your suggestion about invoking the method directly is an even better workaround.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug A crash or error in behavior.
Projects
None yet
Development

No branches or pull requests

2 participants