Repository: bids-standard/pybids
Pull request #906 (2022-10-10 23:40:22)

modeling: More helpful __repr__
A lot of objects in the `modeling` module lacked `__reprs__` which make building examples for the model zoo more opaque than was necessary.

Examples:

```
<BIDSRunVariableCollection['loss', 'gain', 'RT', 'participant_response']>

<BIDSStatsModelsNode(level=run, name=run)>

<BIDSStatsModelsNodeOutput(level=run, entities={'run': 2, 'subject': '001'})>

<BIDSStatsModelsGraph({name='NARPS', description='NARPS Analysis model', ... })>

<SparseRunVariable(name='loss', source='events')>
```
The general rule I followed is that if more than one item was necessary to identify the object, we would list the argments individual, but in cases like `BIDSVariableCollection` where the most important piece is the name of the variables, I left that as the only "argument".

A particular helpful example is outputting the specs of a node output:

```
[<BIDSStatsModelsNodeOutput(level=run, entities={'run': 1, 'subject': '001'})>,
 <BIDSStatsModelsNodeOutput(level=run, entities={'run': 2, 'subject': '001'})>,
 <BIDSStatsModelsNodeOutput(level=run, entities={'run': 3, 'subject': '001'})>,
 <BIDSStatsModelsNodeOutput(level=run, entities={'run': 4, 'subject': '001'})>,
 <BIDSStatsModelsNodeOutput(level=run, entities={'run': 1, 'subject': '002'})>,
 <BIDSStatsModelsNodeOutput(level=run, entities={'run': 2, 'subject': '002'})>,
 <BIDSStatsModelsNodeOutput(level=run, entities={'run': 3, 'subject': '002'})>,
 <BIDSStatsModelsNodeOutput(level=run, entities={'run': 4, 'subject': '002'})>,
 <BIDSStatsModelsNodeOutput(level=run, entities={'run': 1, 'subject': '003'})>,
 <BIDSStatsModelsNodeOutput(level=run, entities={'run': 2, 'subject': '003'})>,
 <BIDSStatsModelsNodeOutput(level=run, entities={'run': 3, 'subject': '003'})>,
 <BIDSStatsModelsNodeOutput(level=run, entities={'run': 4, 'subject': '003'})>]
```



Patch:
diff --git a/bids/modeling/model_spec.py b/bids/modeling/model_spec.py
index 0a5c4d6b..74584caa 100644
--- a/bids/modeling/model_spec.py
+++ b/bids/modeling/model_spec.py
@@ -88,6 +88,9 @@ s
         if Z is not None:
             self.build_variance_components(Z, groups, sigma)
 
+    def __repr__(self):
+        return f"<{self.__class__.__name__}{[term.name for term in self.fixed_terms]}'>"
+
     def set_priors(self, fixed=None, random=None):
         raise NotImplementedError("Custom prior use hasn't been implemented yet.")
 
diff --git a/bids/modeling/statsmodels.py b/bids/modeling/statsmodels.py
index 8a0ab325..81c50cc9 100644
--- a/bids/modeling/statsmodels.py
+++ b/bids/modeling/statsmodels.py
@@ -98,6 +98,9 @@ class BIDSStatsModelsGraph:
         self.edges = self._load_edges(self.model, self.nodes)
         self._root_node = self.model.get('root', list(self.nodes.values())[0])
 
+    def __repr__(self):
+        return f"<{self.__class__.__name__}[{{name='{self.model['name']}', description='{self.model['description']}', ... }}]>"
+
     def __getitem__(self, key):
         '''Alias for get_node(key).'''
         return self.get_node(key)
@@ -295,7 +298,7 @@ class BIDSStatsModelsNode:
             pass
 
     def __repr__(self):
-        return f"<{self.__class__.__name__}[{self.level}] {self.name}>"
+        return f"<{self.__class__.__name__}(level={self.level}, name={self.name})>"
 
     @staticmethod
     def _build_groups(objects, group_by):
@@ -770,3 +773,6 @@ class BIDSStatsModelsNodeOutput:
     def X(self):
         """Return design matrix via the current ModelSpec."""
         return self.model_spec.X
+
+    def __repr__(self):
+        return f"<{self.__class__.__name__}(level={self.node.name}, entities={self.entities})>"
\ No newline at end of file
diff --git a/bids/variables/collections.py b/bids/variables/collections.py
index 049d6931..7aa11f2e 100644
--- a/bids/variables/collections.py
+++ b/bids/variables/collections.py
@@ -304,6 +304,9 @@ class BIDSVariableCollection(object):
             results.extend(vars_)
         return results
 
+    def __repr__(self):
+        return f"<{self.__class__.__name__}{sorted(list(self.variables.keys()))}>"
+
 
 class BIDSRunVariableCollection(BIDSVariableCollection):
     """A container for one or more RunVariables--i.e., Variables that have a
diff --git a/bids/variables/variables.py b/bids/variables/variables.py
index 380d33a3..e63741f8 100644
--- a/bids/variables/variables.py
+++ b/bids/variables/variables.py
@@ -26,6 +26,9 @@ class BIDSVariable(metaclass=ABCMeta):
         self.source = source
         self.entities = self._extract_entities()
 
+    def __repr__(self):
+        return f"<{self.__class__.__name__}(name='{self.name}', source='{self.source}')>"
+
     def clone(self, data=None, **kwargs):
         """Clone (deep copy) the current column, optionally replacing its
         data and/or any other attributes.


Test patch:
diff --git a/bids/modeling/tests/test_statsmodels.py b/bids/modeling/tests/test_statsmodels.py
index a84c94f6..fed8da80 100644
--- a/bids/modeling/tests/test_statsmodels.py
+++ b/bids/modeling/tests/test_statsmodels.py
@@ -52,6 +52,11 @@ def test_write_graph(graph, tmp_path):
     assert path.exists(tmp_path / "graph.dot")
     assert path.exists(tmp_path / "graph.dot.png")
 
+def test_repr(graph):
+    assert graph.__repr__() == "<BIDSStatsModelsGraph[{name='test_model', description='simple test model', ... }]>"
+    node = graph.nodes['run']
+    assert node.__repr__() == "<BIDSStatsModelsNode(level=run, name=run)>"
+    assert node.run()[0].__repr__() == "<BIDSStatsModelsNodeOutput(level=run, entities={'run': 1, 'subject': '01'})>"
 
 def test_manual_intercept(graph_intercept):
     # Test that a automatic intercept (1) is correct
diff --git a/bids/variables/tests/test_collections.py b/bids/variables/tests/test_collections.py
index 29dfe164..7dbd6ae7 100644
--- a/bids/variables/tests/test_collections.py
+++ b/bids/variables/tests/test_collections.py
@@ -47,6 +47,7 @@ def run_coll_derivs():
 def test_run_variable_collection_init(run_coll):
     assert isinstance(run_coll.variables, dict)
     assert run_coll.sampling_rate == 10
+    assert run_coll.__repr__() == "<BIDSRunVariableCollection['PTval', 'RT', 'gain', 'loss', 'parametric gain', 'respcat', 'respnum', 'trial_type']>"
 
 
 def test_run_variable_collection_sparse_variable_accessors(run_coll):
diff --git a/bids/variables/tests/test_variables.py b/bids/variables/tests/test_variables.py
index 8c54a892..a4a08075 100644
--- a/bids/variables/tests/test_variables.py
+++ b/bids/variables/tests/test_variables.py
@@ -44,6 +44,7 @@ def test_dense_event_variable_init():
     assert dev.run_info[0].duration == 480
     assert dev.source == 'dummy'
     assert len(dev.values) == len(dev.index)
+    assert dev.__repr__() == "<DenseRunVariable(name='test', source='dummy')>"
 
 
 def test_dense_event_variable_resample():