3 files changed, 70 insertions(+), 26 deletions(-)

M rework/helper.py
M rework/io.py
M tests/test_api.py
M rework/helper.py +9 -6
@@ 360,8 360,10 @@ class BetterCronTrigger(CronTrigger):
 class InputEncoder(json.JSONEncoder):
 
     def default(self, o):
-        if getattr(o, '__json_encode__'):
+        if getattr(o, '__json_encode__', None):
             return o.__json_encode__()
+        if isinstance(o, datetime):
+            return o.isoformat()
         return super().default(o)
 
 

          
@@ 457,7 459,7 @@ def convert_io(spec, args):
     typed = {}
     for field in spec:
         inp = _iobase.from_type(
-            field['type'], field['name'], field['required'], field['choices']
+            field['type'], field['name'], field['required'], field['choices'], field['default']
         )
         val = inp.from_string(args)
         if val is not None:

          
@@ 467,13 469,14 @@ def convert_io(spec, args):
 
 
 def pack_io(spec, args):
+    "Prepare the args for a .prepare or .schedule api call"
     if args is None and not len(spec):
         return
 
     raw = {}
     for field in spec:
         inp = _iobase.from_type(
-            field['type'], field['name'], field['required'], field['choices']
+            field['type'], field['name'], field['required'], field['choices'], field['default']
         )
         val = inp.binary_encode(args)
         if val is not None:

          
@@ 534,7 537,7 @@ def unpack_io(spec,
                 output.pop(fname, None)
                 continue
         inp = _iobase.from_type(
-            field['type'], fname, field['required'], field['choices']
+            field['type'], fname, field['required'], field['choices'], field['default']
         )
         val = inp.binary_decode(output)
         if val is None:

          
@@ 555,7 558,7 @@ def unpack_iofiles_length(spec, packedby
             continue
 
         inp = _iobase.from_type(
-            'file', fname, field['required'], field['choices']
+            'file', fname, field['required'], field['choices'], None
         )
         val = inp.binary_decode(output)
         if val is None:

          
@@ 578,6 581,6 @@ def unpack_iofile(spec, packedbytes, nam
         assert field['type'] == 'file'
 
         inp = _iobase.from_type(
-            'file', fname, field['required'], field['choices']
+            'file', fname, field['required'], field['choices'], None
         )
         return inp.binary_decode(output)

          
M rework/io.py +7 -4
@@ 11,12 11,13 @@ from psyl import lisp
 
 
 class _iobase:
-    _fields = 'name', 'required', 'choices'
+    _fields = 'name', 'required', 'choices', 'default'
 
-    def __init__(self, name, required=False, choices=None):
+    def __init__(self, name, required=False, choices=None, default=None):
         self.name = name
         self.required = required
         self.choices = choices
+        self.default = default
 
     def __json_encode__(self):
         out = {

          
@@ 27,12 28,14 @@ class _iobase:
         return out
 
     @staticmethod
-    def from_type(atype, name, required, choices):
-        return globals()[atype](name, required, choices)
+    def from_type(atype, name, required, choices, default):
+        return globals()[atype](name, required, choices, default)
 
     def val(self, args):
         val = args.get(self.name)
         if val is None:
+            val = self.default
+        if val is None:
             if self.required:
                 raise ValueError(
                     f'missing required input: `{self.name}`'

          
M tests/test_api.py +54 -16
@@ 56,11 56,11 @@ def register_tasks():
 
     @api.task(inputs=(
         io.file('myfile.txt', required=True),
-        io.number('weight'),
-        io.datetime('birthdate'),
-        io.boolean('happy'),
-        io.moment('sometime'),
-        io.string('name'),
+        io.number('weight', default=42),
+        io.datetime('birthdate', default=dt(2023, 1, 1, 12)),
+        io.boolean('happy', default=True),
+        io.moment('sometime', default='(date "2023-5-20")'),
+        io.string('name', default='Celeste'),
         io.string('option', choices=('foo', 'bar')),
         io.string('ignoreme'))
     )

          
@@ 100,22 100,32 @@ def test_freeze_ops(engine, cleanup):
         ('cheesy', 'cheese', None),
         ('foo', 'default', None),
         ('happy_days', 'default', [
-            {'choices': None, 'name': 'when', 'required': False, 'type': 'moment'}
+            {'choices': None, 'name': 'when', 'default': None,
+             'required': False, 'type': 'moment'}
         ]),
         ('noinput', 'default', []),
         ('yummy', 'default', [
-            {'choices': None, 'name': 'myfile.txt', 'required': True, 'type': 'file'},
-            {'choices': None, 'name': 'weight', 'required': False, 'type': 'number'},
-            {'choices': None, 'name': 'birthdate', 'required': False, 'type': 'datetime'},
-            {'choices': None, 'name': 'happy', 'required': False, 'type': 'boolean'},
-            {'choices': None, 'name': 'sometime', 'required': False, 'type': 'moment'},
-            {'choices': None, 'name': 'name', 'required': False, 'type': 'string'},
-            {'choices': ['foo', 'bar'], 'name': 'option', 'required': False, 'type': 'string'},
-            {'choices': None, 'name': 'ignoreme', 'required': False, 'type': 'string'}
+            {'choices': None, 'default': None, 'name': 'myfile.txt',
+             'required': True, 'type': 'file'},
+            {'choices': None, 'default': 42, 'name': 'weight',
+             'required': False, 'type': 'number'},
+            {'choices': None, 'default': '2023-01-01T12:00:00', 'name': 'birthdate',
+             'required': False, 'type': 'datetime'},
+            {'choices': None, 'default': True, 'name': 'happy',
+             'required': False, 'type': 'boolean'},
+            {'choices': None, 'default': '(date "2023-5-20")', 'name': 'sometime',
+             'required': False, 'type': 'moment'},
+            {'choices': None, 'default': 'Celeste', 'name': 'name',
+             'required': False, 'type': 'string'},
+            {'choices': ['foo', 'bar'], 'default': None, 'name': 'option',
+             'required': False, 'type': 'string'},
+            {'choices': None, 'default': None, 'name': 'ignoreme',
+             'required': False, 'type': 'string'}
         ]),
         ('hammy', 'ham', None),
         ('nr', 'non-default', [
-            {'choices': None, 'name': 'history', 'required': False, 'type': 'number'}
+            {'choices': None, 'default': None, 'name': 'history',
+             'required': False, 'type': 'number'}
         ])
     ]
 

          
@@ 129,7 139,8 @@ def test_freeze_ops(engine, cleanup):
     ]
     assert res == [
         ('hammy', 'ham', [
-            {'choices': None, 'name': 'taste', 'required': False, 'type': 'string'}
+            {'choices': None, 'name': 'taste', 'default': None,
+             'required': False, 'type': 'string'}
         ])
     ]
 

          
@@ 189,6 200,23 @@ def test_with_inputs(engine, cleanup):
         'option': 'foo'
     }
 
+    # test default values
+    args = {
+        'myfile.txt': b'some file',
+        'option': 'foo'
+    }
+    t = api.schedule(engine, 'yummy', args)
+    assert t.input == {
+        'myfile.txt': b'some file',
+        'weight': 42,
+        'birthdate': dt(2023, 1, 1, 12),
+        'happy': True,
+        'sometime': dt(2023, 5, 20, 0, 0),
+        'name': 'Celeste',
+        'option': 'foo'
+    }
+
+
     with pytest.raises(ValueError) as err:
         api.schedule(engine, 'yummy', {'no-such-thing': 42})
     assert err.value.args[0] == 'missing required input: `myfile.txt`'

          
@@ 388,16 416,20 @@ def test_prepare_with_inputs(engine, cle
     unpacked = unpack_io(spec, inputdata)
     assert unpacked == {
         'birthdate': dt(1973, 5, 20, 0, 0),
+        'happy': True,
         'myfile.txt': b'some file',
         'name': 'Babar',
         'option': 'foo',
+        'sometime': dt(2023, 5, 20, 0, 0),
         'weight': 65
     }
     unpacked = unpack_io(spec, inputdata, nofiles=True)
     assert unpacked == {
         'weight': 65,
+        'happy': True,
         'birthdate': dt(1973, 5, 20, 0, 0),
         'name': 'Babar',
+        'sometime': dt(2023, 5, 20, 0, 0),
         'option': 'foo'
     }
 

          
@@ 415,12 447,14 @@ def test_prepare_with_inputs(engine, cle
          (b'myfile.txt',
           b'weight',
           b'birthdate',
+          b'happy',
           b'sometime',
           b'name',
         b'option',
           b'some file',
           b'65',
           b'1973-05-20T09:00:00',
+          b'True',
           b'(date "1973-5-20")',
           b'Babar',
           b'foo'),

          
@@ 430,11 464,15 @@ def test_prepare_with_inputs(engine, cle
          (b'myfile.txt',
           b'weight',
           b'birthdate',
+          b'happy',
+          b'sometime',
           b'name',
           b'option',
           b'some file',
           b'65',
           b'1973-05-20T00:00:00',
+          b'True',
+          b'(date "2023-5-20")',
           b'Babar',
           b'foo'),
          None,