Repository: skypyproject/skypy
Pull request #463 (2021-04-27 11:29:30)

ENH: Config syntax for importing objects
## Description
This is a proposal for an extension to the config syntax that allows objects to be imported directly from external (sub)modules using the `!` tag and providing neither a list of args or a dict of kwargs. (N.B. to call a function without any arguments pass and empty list of args or an empty dict of kwargs). For example this enables:

```yaml
cosmo1: !astropy.cosmology.Planck18 # import the Planck18 object and bind to cosmo1
cosmo2: !astropy.cosmology.default_cosmology.get [] # call default_cosmology.get() and bind to cosmo2
```

Note that as a result of the design, it is no longer possible to call a function with a single argument using a scalar node; the argument must be put into a list:
```yaml
old_syntax: !len 'hello world!' # invalid
new_syntax: !len ['hello world!'] # correctly define an explicit list of args
```

## Checklist
- [x] Follow the [Contributor Guidelines](https://github.com/skypyproject/skypy/blob/main/CONTRIBUTING.rst)
- [x] Write unit tests
- [ ] Write documentation strings
- [x] Assign someone from your working team to review this pull request
- [x] Assign someone from the infrastructure team to review this pull request


rrjbca: The syntax will be documented in #454

Patch:
diff --git a/examples/galaxies/sdss_photometry.yml b/examples/galaxies/sdss_photometry.yml
index 5d370bf..651e2e5 100644
--- a/examples/galaxies/sdss_photometry.yml
+++ b/examples/galaxies/sdss_photometry.yml
@@ -1,4 +1,4 @@
-cosmology: !astropy.cosmology.default_cosmology.get
+cosmology: !astropy.cosmology.default_cosmology.get []
 z_range: !numpy.linspace [0, 2, 21]
 M_star: !astropy.modeling.models.Linear1D [-0.9408582, -20.40492365]
 phi_star: !astropy.modeling.models.Exponential1D [0.00370253, -9.73858]
diff --git a/skypy/pipeline/_config.py b/skypy/pipeline/_config.py
index 2b839d4..656402b 100644
--- a/skypy/pipeline/_config.py
+++ b/skypy/pipeline/_config.py
@@ -52,23 +52,23 @@ class SkyPyLoader(yaml.SafeLoader):
         return Ref(ref)
 
     def construct_call(self, name, node):
-        if isinstance(node, yaml.ScalarNode):
-            arg = self.construct_scalar(node)
-            args = [arg] if arg != '' else []
-            kwargs = {}
-        elif isinstance(node, yaml.SequenceNode):
-            args = self.construct_sequence(node)
-            kwargs = {}
-        elif isinstance(node, yaml.MappingNode):
-            args = []
-            kwargs = self.construct_mapping(node)
-
         try:
-            function = import_function(name)
+            object = import_function(name)
         except (ModuleNotFoundError, AttributeError) as e:
             raise ImportError(f'{e}\n{node.start_mark}') from e
 
-        return Call(function, args, kwargs)
+        if isinstance(node, yaml.ScalarNode):
+            if node.value:
+                raise ValueError(f'{node.value}: ScalarNode should be empty to import an object')
+            return object
+        else:
+            if isinstance(node, yaml.SequenceNode):
+                args = self.construct_sequence(node)
+                kwargs = {}
+            if isinstance(node, yaml.MappingNode):
+                args = []
+                kwargs = self.construct_mapping(node)
+            return Call(object, args, kwargs)
 
     def construct_quantity(self, node):
         value = self.construct_scalar(node)


Test patch:
diff --git a/skypy/pipeline/tests/data/bad_object.yml b/skypy/pipeline/tests/data/bad_object.yml
new file mode 100644
index 0000000..a9ff708
--- /dev/null
+++ b/skypy/pipeline/tests/data/bad_object.yml
@@ -0,0 +1,1 @@
+bad_object: !astropy.cosmology.Planck15 "bad value"
diff --git a/skypy/pipeline/tests/data/test_config.yml b/skypy/pipeline/tests/data/test_config.yml
index fda28a7..e9f4ce9 100644
--- a/skypy/pipeline/tests/data/test_config.yml
+++ b/skypy/pipeline/tests/data/test_config.yml
@@ -1,8 +1,9 @@
 test_int: 1
 test_float: 1.0
 test_str: hello world
-test_func: !numpy.random.uniform
-test_func_with_arg: !len 'hello world'
+test_func: !numpy.random.uniform []
+test_func_with_arg: !len ['hello world']
+test_object: !astropy.cosmology.Planck15
 cosmology: !astropy.cosmology.FlatLambdaCDM
   H0: 67.74
   Om0: 0.3075
diff --git a/skypy/pipeline/tests/test_config.py b/skypy/pipeline/tests/test_config.py
index 5d2289f..16b0b03 100644
--- a/skypy/pipeline/tests/test_config.py
+++ b/skypy/pipeline/tests/test_config.py
@@ -3,6 +3,7 @@ import pytest
 from skypy.pipeline import load_skypy_yaml
 from skypy.pipeline._items import Call
 from astropy import units
+from astropy.cosmology.core import Cosmology
 
 
 def test_load_skypy_yaml():
@@ -18,6 +19,8 @@ def test_load_skypy_yaml():
     assert isinstance(config['test_float'], float)
     assert isinstance(config['test_str'], str)
     assert isinstance(config['test_func'], Call)
+    assert isinstance(config['test_func_with_arg'], Call)
+    assert isinstance(config['test_object'], Cosmology)
     assert isinstance(config['cosmology'], Call)
     assert isinstance(config['tables']['test_table_1']['test_column_3'], Call)
 
@@ -31,6 +34,11 @@ def test_load_skypy_yaml():
     with pytest.raises(ImportError):
         load_skypy_yaml(filename)
 
+    # Bad object
+    filename = get_pkg_data_filename('data/bad_object.yml')
+    with pytest.raises(ValueError):
+        load_skypy_yaml(filename)
+
 
 def test_empty_ref():
     filename = get_pkg_data_filename('data/test_empty_ref.yml')