Repository: zoofIO/flexx (python)

Issue:
Provide models with initial data
Sometimes a model needs information at initialization time, that you want to provide in a flexible way, something like:

```py
class MyModel(app.Model):

    def init(self, the_info):
         ...

...

m = MyModel(the_info=...)
```

Currently, the `init()` is always argumentless. The required behavior can be achieved by making a property `info`, the value of which can be used inside the `init()`. However, a property suggests that the info can be modified/reset after the model is instantiated, which is not always the case.

In other words, there are situations where a `Model` needs information at initialization time, but for which a property does not make sense conceptually.

Some possible solutions:

A. Any keyword arguments used to instantiate a `Model` which do not correspond to properties are passed to the `init()` as kwargs. You'd thus need to write it like `def init(self, info=None)` to make it a keyword arg. In theory it could also map to positional args by inspecting the signature, but that feels a bit too magical, I think. 

I am a bit worried about error reporting if a user makes a type in a property name. We would need to inspect the `init()` sig to see what kwargs it accepts, so we can be sure that a kwarg is intended for the `init()`. This should thus be disallowed: `def init(self, **kwargs)`.

B. Provide a new kind of property, similar to the current `readonly` that *must* be provided at initialization and is readonly afterwards. This adds less conceptual stuff for the user to learn, as it's just a kind of property.

C. Use positional arguments. The `Model.__init__()` could catch the positional arguments and pass them to the `init()`. The only downside is that for Models run as an app via `app.launch` or `app.serve` we'd need to provide a way to set these args, but we already do that for the kwargs, so I guess that could work ...


cc @korijn @Maxyme

Pull request description:
Model.init() can now have args
Fixes #228. Relates to closed issue #140 and PR #199.

So you can do:
```py

class MyModel(app.Model):
    def init(foo, bar=3):
        ...

...
m = MyModel(7, 8, **properties_are_passed_as_kwargs_as_usual)
```

Patch:
diff --git a/flexx/app/model.py b/flexx/app/model.py
index 92806c9..b0ceb37 100644
--- a/flexx/app/model.py
+++ b/flexx/app/model.py
@@ -331,10 +331,12 @@ class Model(with_metaclass(ModelMeta, event.HasEvents)):
     # CSS for this class (no css in the base class)
     CSS = ""
     
-    def __init__(self, session=None, is_app=False, **kwargs):
+    def __init__(self, *init_args, **kwargs):
         
-        # Param "is_app" is not used, but we "take" the argument so it
-        # is not mistaken for a property value.
+        # Pop args that we need from the kwargs (because legacy Python does
+        # not support keyword args after *args).
+        session = kwargs.pop('session', None)
+        kwargs.pop('is_app', None)  # not used here, but need to "take" it 
         
         # Set id and register this instance
         Model._counter += 1
@@ -394,7 +396,7 @@ class Model(with_metaclass(ModelMeta, event.HasEvents)):
         # subwidgets etc. This is done here, at the point where the
         # properties are initialized, but the handlers not yet.
         with self:
-            self.init()
+            self.init(*init_args)
         self._session._exec('flexx.instances.%s.init();' % self._id)
         
         # Initialize handlers for Python and for JS. Done after init()
diff --git a/flexx/ui/_widget.py b/flexx/ui/_widget.py
index 63c1a52..1092110 100644
--- a/flexx/ui/_widget.py
+++ b/flexx/ui/_widget.py
@@ -102,7 +102,7 @@ class Widget(Model):
     }
     """
 
-    def __init__(self, **kwargs):
+    def __init__(self, *init_args, **kwargs):
 
         # Handle parent
         parent = kwargs.pop('parent', None)
@@ -121,7 +121,7 @@ class Widget(Model):
             kwargs['container'] = 'body'
 
         # Init - pass signal values via kwargs
-        Model.__init__(self, **kwargs)
+        Model.__init__(self, *init_args, **kwargs)
         
         # All widgets need phosphor
         self._session.use_global_asset('phosphor-all.js', before='flexx-ui.css')


Test patch:
diff --git a/flexx/app/tests/test_model.py b/flexx/app/tests/test_model.py
index 3f3d929..295e09e 100644
--- a/flexx/app/tests/test_model.py
+++ b/flexx/app/tests/test_model.py
@@ -85,6 +85,13 @@ class Foo6(Foo1):
             return {}
 
 
+class Foo7(Model):
+    
+    def init(self, foo, bar=0):
+        self.foo = foo
+        self.bar = bar
+
+
 def test_pairing1():
     
     assert isinstance(Foo1.title, event._emitters.Property)
@@ -120,6 +127,31 @@ def test_overloading():
     assert Foo4.JS.red is not Foo2.JS.red
 
 
+def test_init_args():
+    
+    # This test needs a default session
+    session = app.manager.get_default_session()
+    if session is None:
+        app.manager.create_default_session()
+    
+    with raises(TypeError):
+        m = Model(2, 3)
+    
+    m = Foo7(2, 3)
+    assert m.foo == 2
+    assert m.bar == 3
+    
+    m = Foo7(2)
+    assert m.foo == 2
+    assert m.bar == 0
+    
+    with raises(TypeError):
+        m = Foo7()
+    
+    with raises(TypeError):
+        m = Foo7(2, 3, 4)
+
+
 def test_both():
     # New prop
     assert Foo5.purple is Foo5.JS.purple