nucleic / enaml

Declarative User Interfaces for Python
http://enaml.readthedocs.io/en/latest/
Other
1.53k stars 130 forks source link

Can the expression engine provide context for errors somehow? #493

Open frmdstryr opened 2 years ago

frmdstryr commented 2 years ago

If an error occurs in an expression during the initialization it provides very little context as to where the error is making it difficult to debug in more complex applications.

For example this script:

from enaml.core.api import Conditional
from enaml.widgets.api import Window, Container, Label

enamldef Main(Window):
    Container:
        Conditional:
            condition = None
            Label:
                text = "Hello"

Will generate this error:

Traceback (most recent call last):
  File "/home/user/micromamba/envs/enaml/bin/enaml-run", line 33, in <module>
    sys.exit(load_entry_point('enaml', 'console_scripts', 'enaml-run')())
  File "/home/user/projects/enaml/enaml/runner.py", line 71, in main
    window.show()
  File "/home/user/projects/enaml/enaml/widgets/window.py", line 380, in show
    self.initialize()
  File "/home/user/projects/enaml/enaml/widgets/window.py", line 158, in initialize
    super(Window, self).initialize()
  File "/home/user/projects/enaml/enaml/widgets/toolkit_object.py", line 160, in initialize
    super(ToolkitObject, self).initialize()
  File "/home/user/projects/enaml/enaml/core/declarative.py", line 135, in initialize
    child.initialize()
  File "/home/user/projects/enaml/enaml/widgets/toolkit_object.py", line 160, in initialize
    super(ToolkitObject, self).initialize()
  File "/home/user/projects/enaml/enaml/core/declarative.py", line 135, in initialize
    child.initialize()
  File "/home/user/projects/enaml/enaml/core/pattern.py", line 38, in initialize
    self.refresh_items()
  File "/home/user/projects/enaml/enaml/core/conditional.py", line 74, in refresh_items
    if self.condition:
TypeError: The 'condition' member on the 'Conditional' object must be of type 'bool'. Got object of type 'NoneType' instead.

The trace shows the initialization stack but no context as to where the Conditional is in the tree. Since the Conditional is an instance of a node in the tree can you think of any way to provide some sort of context?

One thought that comes to mind is having declarative do child initialization in a try except and do some sort of inspection?

MatthieuDartiailh commented 2 years ago

Error within declarative widgets are indeed an area where we can improve much. In this specific case, having some try except in initialization may be the best solution since the error is not in the declarative part but simply in the validation of the member. However we could also try to improve error reported in the expression engine for cases when the error occurs at runtime and not at initialization time.

I do not have a plan to address this (and right now it is not my top priority) so feel free to investigate if you have time to do so.

As a side note, I am considering switching the Bool member for a Coerced bool in widgets because having to explicitly cast to bool feels very unpythonic.

tstordyallison commented 2 years ago

+1

I've played around with this sort of thing a few times in the past, but never quite had the time to put something together that was comprehensive.

As you point out - there's a few different points we'd want to better report the 'declarative stack' as we encounter an error. activation/init being one of them, but also at runtime when for whatever reason we fail to update the state managed by a proxy.

frmdstryr commented 2 years ago

I pushed a branch https://github.com/nucleic/enaml/compare/main...frmdstryr:declarative-context?expand=1 which helps with the initialization. Would be nice to be able to add lines into the normal traceback if anyone knows how to do that. I'm not sure what to think about saving the fileno/line for each node cost wise, will have to do some testing.

Eg

from enaml.core.api import Conditional
from enaml.widgets.api import Window, Container, Label, PushButton

enamldef MyContainer(Container): inside_my_container:
    Label: my_label:
        text = "Hello"
    Conditional: cond:
        condition = None
        PushButton: btn:
            text = "Blow up"
            clicked :: cond.condition = None

enamldef Main(Window): main:
    Container: container:
        Label: label1:
            text = "Test"
        MyContainer: my_container:
            pass

Will produce this:

Traceback (most recent call last):
  File "/home/user/projects/enaml/enaml/core/declarative.py", line 185, in initialize
    child.initialize()
  File "/home/user/projects/enaml/enaml/core/pattern.py", line 38, in initialize
    self.refresh_items()
  File "/home/user/projects/enaml/enaml/core/conditional.py", line 74, in refresh_items
    if self.condition:
TypeError: The 'condition' member on the 'Conditional' object must be of type 'bool'. Got object of type 'NoneType' instead.

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/home/user/micromamba/envs/enaml/bin/enaml-run", line 33, in <module>
    sys.exit(load_entry_point('enaml', 'console_scripts', 'enaml-run')())
  File "/home/user/projects/enaml/enaml/runner.py", line 71, in main
    window.show()
  File "/home/user/projects/enaml/enaml/widgets/window.py", line 380, in show
    self.initialize()
  File "/home/user/projects/enaml/enaml/widgets/window.py", line 158, in initialize
    super(Window, self).initialize()
  File "/home/user/projects/enaml/enaml/widgets/toolkit_object.py", line 160, in initialize
    super(ToolkitObject, self).initialize()
  File "/home/user/projects/enaml/enaml/core/declarative.py", line 185, in initialize
    child.initialize()
  File "/home/user/projects/enaml/enaml/widgets/toolkit_object.py", line 160, in initialize
    super(ToolkitObject, self).initialize()
  File "/home/user/projects/enaml/enaml/core/declarative.py", line 185, in initialize
    child.initialize()
  File "/home/user/projects/enaml/enaml/widgets/toolkit_object.py", line 160, in initialize
    super(ToolkitObject, self).initialize()
  File "/home/user/projects/enaml/enaml/core/declarative.py", line 189, in initialize
    raise InitializationError(child, e) from e
enaml.core.declarative.InitializationError: The 'condition' member on the 'Conditional' object must be of type 'bool'. Got object of type 'NoneType' instead.
  File "tests/example.enaml", line 14, in Main
  File "tests/example.enaml", line 15, in Container
  File "tests/example.enaml", line 18, in MyContainer
  File "tests/example.enaml", line 8, in Conditional