Repository: bids-standard/pybids

Issue:
Improved reprs for SQLAlchemy model objects
This should help provide more useful exception messages for obscure SQLAlchemy error conditions. And just be generally more readable in a REPL.

Related to #465.
Including a .json sidecar for events.tsv files causes a SQLAlchemy Tag conflict
We have an fMRIPrep user experiencing the following issue (more details here https://neurostars.org/t/naming-of-bids-events-tsv-files-seems-to-disrupt-fmriprep-1-5-0rc1/4771):

```
  File "/usr/local/miniconda/lib/python3.7/site-packages/fmriprep/cli/run.py", line 524, in build_workflow
    layout = BIDSLayout(str(bids_dir), validate=False)
  File "/usr/local/miniconda/lib/python3.7/site-packages/bids/layout/layout.py", line 212, in __init__
    indexer.index_metadata()
  File "/usr/local/miniconda/lib/python3.7/site-packages/bids/layout/index.py", line 338, in index_metadata
    self.session.commit()
  File "/usr/local/miniconda/lib/python3.7/site-packages/sqlalchemy/orm/session.py", line 1027, in commit
    self.transaction.commit()
  File "/usr/local/miniconda/lib/python3.7/site-packages/sqlalchemy/orm/session.py", line 494, in commit
    self._prepare_impl()
  File "/usr/local/miniconda/lib/python3.7/site-packages/sqlalchemy/orm/session.py", line 473, in _prepare_impl
    self.session.flush()
  File "/usr/local/miniconda/lib/python3.7/site-packages/sqlalchemy/orm/session.py", line 2459, in flush
    self._flush(objects)
  File "/usr/local/miniconda/lib/python3.7/site-packages/sqlalchemy/orm/session.py", line 2597, in _flush
    transaction.rollback(_capture_exception=True)
  File "/usr/local/miniconda/lib/python3.7/site-packages/sqlalchemy/util/langhelpers.py", line 68, in __exit__
    compat.reraise(exc_type, exc_value, exc_tb)
  File "/usr/local/miniconda/lib/python3.7/site-packages/sqlalchemy/util/compat.py", line 153, in reraise
    raise value
  File "/usr/local/miniconda/lib/python3.7/site-packages/sqlalchemy/orm/session.py", line 2557, in _flush
    flush_context.execute()
  File "/usr/local/miniconda/lib/python3.7/site-packages/sqlalchemy/orm/unitofwork.py", line 422, in execute
    rec.execute(self)
  File "/usr/local/miniconda/lib/python3.7/site-packages/sqlalchemy/orm/unitofwork.py", line 589, in execute
    uow,
  File "/usr/local/miniconda/lib/python3.7/site-packages/sqlalchemy/orm/persistence.py", line 213, in save_obj
    ) in _organize_states_for_save(base_mapper, states, uowtransaction):
  File "/usr/local/miniconda/lib/python3.7/site-packages/sqlalchemy/orm/persistence.py", line 408, in _organize_states_for_save
    % (state_str(state), instance_key, state_str(existing))
sqlalchemy.orm.exc.FlushError: New instance <Tag at 0x2b66784f7ba8> with identity key (<class 'bids.layout.models.Tag'>, ('/inp/sub-02/func/sub-02_task-Emotion_run-1_events.tsv', 'run'), None) conflicts with persistent instance <Tag at 0x2b66784c6630>
```


Discussion:

It's hard to know if this is a bug without knowing what other files are in the BIDS project. My sense is it could be a genuine conflict between file names induced by some minor departure from the BIDS standard. It would be helpful if the user could provide a list of all of the `events.tsv` files in the project.
If it is not a bug, I'd say a more descriptive error message would improve user experience a lot.
Well, if we had a more descriptive error message, it would be easier for me to tell you if it's a bug or not. :p
Per discussion in the [linked NeuroStars thread](https://neurostars.org/t/naming-of-bids-events-tsv-files-seems-to-disrupt-fmriprep-1-5-0rc1/4771), this seems like a bug induced by the inclusion of a `.json` file with the same filename (except for extension) as an `events.tsv` file.

Resolving patch:
diff --git a/bids/layout/models.py b/bids/layout/models.py
--- a/bids/layout/models.py
+++ b/bids/layout/models.py
@@ -491,6 +491,10 @@ def __init__(self, file, entity, value, dtype=None):
         self._dtype = dtype
 
         self._init_on_load()
+    
+    def __repr__(self):
+        msg = "<Tag file:{!r} entity:{!r} value:{!r}>"
+        return msg.format(self.file_path, self.entity_name, self.value)
 
     @reconstructor
     def _init_on_load(self):


Test patch:
diff --git a/bids/layout/tests/test_models.py b/bids/layout/tests/test_models.py
--- a/bids/layout/tests/test_models.py
+++ b/bids/layout/tests/test_models.py
@@ -97,6 +97,13 @@ def test_file_associations():
     assert set(results) == {md1, md2}
 
 
+def test_tag_init(sample_bidsfile, subject_entity):
+    f, e = sample_bidsfile, subject_entity
+    tag = Tag(f, e, 'zzz')
+    rep = str(tag)
+    assert rep.startswith("<Tag file:") and f.path in rep and 'zzz' in rep
+
+
 def test_tag_dtype(sample_bidsfile, subject_entity):
     f, e = sample_bidsfile, subject_entity
     # Various ways of initializing--should all give same result