Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MRG] Updating make_bids_basename to return BIDSPath, allowing users to dynamically alter the bids file name #446

Merged
merged 51 commits into from Jun 19, 2020

Conversation

adam2392
Copy link
Member

@adam2392 adam2392 commented Jun 8, 2020

PR Description

Closes: #445
Closes: #257

This is a draft to let maintainers see what I mean. Once comments are suggested and made, will iterate. The goal of this PR is to enable a bids filename created via make_bids_basename to be dynamically altered based on its BIDS entity without calling make_bids_basename again and again.

In order to do so, make_bids_basename now returns a BIDSPath object started by @jasmainak. In order to make the PR work w/o changing a lot of code, it was ideal to give BIDSPath as many string functionality as possible, because at the end of the day it is just a structured string, stored as a dictionary.

I override various magic methods. The __str__ and __repr__ of BIDSPath is slightly borrowed from pathlib.Path. Some "magic" method behavior is listed here:

  1. comparison __eq__: makes BIDSPath as a str
  2. __fspath__: same as pathlib.Path
  3. __bytes__: same as pathlib.Path
  4. copy(): returns deepcopy of self
  5. update: updates multiple entities at once, allowing aliases for subject, session, recording, acquisition, processing
  6. property entities: returns an ordered dictionary of all the BIDS entitites.

TODO:

  • figure out how to integrate this change given that almost the entire codebase assumes make_bids_basename outputs a string. Currently, I have a flag as_str=True to make tests pass, but idk if that's the best solution.

Merge checklist

Maintainer, please confirm the following before merging:

  • All comments resolved
  • This is not your own PR
  • All CIs are happy
  • PR title starts with [MRG]
  • whats_new.rst is updated
  • PR description includes phrase "closes <#issue-number>"
  • Commit history does not contain any merge commits

@adam2392 adam2392 marked this pull request as draft June 9, 2020 21:42
@adam2392 adam2392 changed the title [WIP / Draft] An API for updating bids_basename [WIP / Draft] Updating make_bids_basename to return BIDSPath, allowing users to dynamically alter the bids file name Jun 10, 2020
@adam2392 adam2392 marked this pull request as ready for review June 10, 2020 18:34
@adam2392
Copy link
Member Author

@jasmainak can you do just a skim and lmk if this is in line w/ where you were thinking?

I haven't done too many changes yet, but wasn't sure what the best way to integrate BIDSPath after we discussed in #257.

mne_bids/utils.py Outdated Show resolved Hide resolved
@jasmainak
Copy link
Member

@adam2392 I think it might be easier if you cherry-pick my commits, then try to make the tests pass.

@jasmainak
Copy link
Member

Can you also change the codebase to use this everywhere? Show me red lines :)

mne_bids/utils.py Outdated Show resolved Hide resolved
@adam2392
Copy link
Member Author

Can you also change the codebase to use this everywhere? Show me red lines :)

I guess my concern is right now, BIDSPath if printed is a string, but when passed into functions gets seen as a dict. In order to circumvent that issue, either one can call the as_str() function to convert the bids_basename into a string beforehand, or we allow BIDSPath to work inside write_raw_bids, read_raw_bids, etc.

Before making the change throughout the codebase, I figured it would be good to decide which path to take?

@codecov-commenter
Copy link

codecov-commenter commented Jun 10, 2020

Codecov Report

Merging #446 into master will increase coverage by 0.12%.
The diff coverage is 89.41%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #446      +/-   ##
==========================================
+ Coverage   93.36%   93.49%   +0.12%     
==========================================
  Files          12       12              
  Lines        1674     1721      +47     
==========================================
+ Hits         1563     1609      +46     
- Misses        111      112       +1     
Impacted Files Coverage Δ
mne_bids/read.py 95.34% <78.94%> (+1.49%) ⬆️
mne_bids/utils.py 95.19% <88.80%> (-0.90%) ⬇️
mne_bids/config.py 95.34% <100.00%> (+0.34%) ⬆️
mne_bids/write.py 96.58% <100.00%> (+0.03%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 6c4d6f2...0a2e89f. Read the comment docs.

@jasmainak
Copy link
Member

I guess my concern is right now, BIDSPath if printed is a string, but when passed into functions gets seen as a dict.

isn't this the desired behavior?

@adam2392
Copy link
Member Author

I guess my concern is right now, BIDSPath if printed is a string, but when passed into functions gets seen as a dict.

isn't this the desired behavior?

Yes true, I was just getting worried about what happens when downstream functions do things tho that assume it's a string, but turns out this was mitigated by just overriding some of the magic methods for BIDSPath.

Copy link
Member

@agramfort agramfort left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not against an object BIDSPath but not with this design. Also why a method make_bids_basename that just calls the constructor of BIDSPath. If we do with the object that we should adopt its API and use the constructor.

mne_bids/utils.py Outdated Show resolved Hide resolved
mne_bids/utils.py Outdated Show resolved Hide resolved
mne_bids/utils.py Outdated Show resolved Hide resolved
mne_bids/utils.py Outdated Show resolved Hide resolved
mne_bids/utils.py Outdated Show resolved Hide resolved
@adam2392
Copy link
Member Author

I am not against an object BIDSPath but not with this design. Also why a method make_bids_basename that just calls the constructor of BIDSPath. If we do with the object that we should adopt its API and use the constructor.

Okay I am refactoring to make it operate as an object instead of dict.

Regarding how it is constructed, do we want to keep the function make_bids_basename? Do we just keep that, but allow people to invoke BIDSPath directly?

@jasmainak
Copy link
Member

I would keep make_bids_basename for now. It can return an instance of BIDSPath

mne_bids/write.py Outdated Show resolved Hide resolved
mne_bids/utils.py Outdated Show resolved Hide resolved
mne_bids/utils.py Outdated Show resolved Hide resolved
mne_bids/utils.py Outdated Show resolved Hide resolved
mne_bids/utils.py Outdated Show resolved Hide resolved
mne_bids/utils.py Outdated Show resolved Hide resolved
@jasmainak
Copy link
Member

@adam2392 your challenge is to get more red lines than green lines. Can it be done? :)

You can also update/simplify code in tests I believe (don't change logic though)

mne_bids/utils.py Outdated Show resolved Hide resolved
@adam2392
Copy link
Member Author

@adam2392 your challenge is to get more red lines than green lines. Can it be done? :)

You can also update/simplify code in tests I believe (don't change logic though)

This turned out to be quite a challenge imo! So the added class turns out to be ~170 lines, so... I think I did neutral :p. I would hope that the changes in a few of the lines are convincing enough for you and @agramfort that it simplifies user code(?)

mne_bids/tests/test_write.py Outdated Show resolved Hide resolved
mne_bids/write.py Outdated Show resolved Hide resolved
mne_bids/utils.py Outdated Show resolved Hide resolved
@agramfort
Copy link
Member

agramfort commented Jun 12, 2020 via email

@adam2392
Copy link
Member Author

adam2392 commented Jun 18, 2020

I've updated the comments above and left some notes to trace back to each decision.

Everything you have commented on, I tried addressing and summarizing above. However, I am not confident in the integration of 'emptyroom' subjects, so hoping to get your input there.

According to codecov report, most lines are covered, with the exception of __repr__ and some warning messages in get_bids_fname, which I can add.

Comment on lines +29 to +33
reader = {'.con': io.read_raw_kit, '.sqd': io.read_raw_kit,
'.fif': io.read_raw_fif, '.pdf': io.read_raw_bti,
'.ds': io.read_raw_ctf, '.vhdr': io.read_raw_brainvision,
'.edf': io.read_raw_edf, '.bdf': io.read_raw_bdf,
'.set': io.read_raw_eeglab}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needed to move this here to prevent circular imports.

"""
return deepcopy(self)

def get_bids_fname(self, kind=None, bids_root=None, extension=None):
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Essentially copied version of _make_bids_fname.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, is BIDSPath by default not the full path? Why is that?

Copy link
Member Author

@adam2392 adam2392 Jun 19, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the BIDSPath object is the full path, then it's not really the bids_basename anymore, which doesn't seem required by the API right now though?

The internals of read_raw_bids and write_raw_bids doesn't require the full fname. This function operates like _make_bids_fname and can "infer" the kind and the extension by searching down the bids_root.

Otherwise, we can alter this back to _make_bids_fname, which accepts the bids_basename as a BIDSPath object instead of a string and don't have it as part of the BIDSPath.

We can either:

  1. Modify this to incorporate a full path at all times? Not sure what this should look like though.
  2. Turn this into a private function?
  3. Add some further edits to this function, but keeping the header as is?
  4. Modify it back to _make_bids_fname?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@adam2392 let's discuss this in a new issue. We have spent too long on this PR. Let's merge this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really sure how to articulate the issue you're thinking of... :| can you make it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reading this again I think @jasmainak has a point here. The fact that get_bids_fname returns BIDSPath and not a str is a code smell. It suggests that BIDSPath can be informed by bids_root and kind.

wdyt?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yeah I see; it does seem like it could be confusing. So would the suggested fix be to:

  1. make it return a str?
    or
  2. add bids_root/kind to BIDSPath explicitly?

Copy link
Member

@jasmainak jasmainak Jun 21, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

started a separate issue @adam2392 see #454

suffix=suffix)


def _get_bids_fname_from_filesystem(*, bids_basename, bids_root, sub, ses,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needed to move this here to prevent circular imports.

return bids_fname


def _infer_kind(*, bids_basename, bids_root, sub, ses):
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needed to move here to prevent circular imports.

return bids_fname


def _make_bids_fname(bids_basename, bids_root=None, kind=None, extension=None,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved whole function into BIDSPath.

return kinds[0]


def _get_bids_fname_from_filesystem(*, bids_basename, bids_root, sub, ses,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copied to utils.py

@@ -264,118 +258,6 @@ def _handle_channels_reading(channels_fname, bids_fname, raw):
return raw


def _infer_kind(*, bids_basename, bids_root, sub, ses):
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copied to utils.py

_gen_bids_basename,
_estimate_line_freq, _get_kinds_for_sub)

reader = {'.con': io.read_raw_kit, '.sqd': io.read_raw_kit,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to config.py

@agramfort
Copy link
Member

this handling of emptyroom and the _gen_bids_basename was really a code smell. I pushed a changed here. Let me know what you think. Basically I made so that BIDSPath does not do any check and you only get checks with make_bids_basename keeping BIDSPath as an internal tool

@agramfort
Copy link
Member

@adam2392 can you check what I did and update what's new? than good to go from my end

@adam2392 adam2392 changed the title [WIP / Draft] Updating make_bids_basename to return BIDSPath, allowing users to dynamically alter the bids file name [MRG] Updating make_bids_basename to return BIDSPath, allowing users to dynamically alter the bids file name Jun 19, 2020
@jasmainak jasmainak merged commit 103ba87 into mne-tools:master Jun 19, 2020
@jasmainak
Copy link
Member

Thanks heaps @adam2392 for the PR!

@adam2392 adam2392 deleted the updatebids branch June 19, 2020 20:25
@agramfort
Copy link
Member

agramfort commented Jun 21, 2020 via email

hoechenberger added a commit to hoechenberger/mne-bids that referenced this pull request Aug 5, 2020
`get_head_mri_trans()` in `master` tries to infer the filename extension
from a BIDS basename; however, the basename, as is created inside that
function, doesn't have a suffix. Correctly, the extension has to be
inferred from `bids_fname`. This is what we used to do before mne-toolsGH-446
got merged. This commit restores the old behavior.

(I realized something was going wrong when MNE-BIDS started looking
 for some .pdf files, while I only have .fif. So beyond what I'm
 fixing here, there seems to be another bug in `parse_ext()`, causing
 it to report `.pdf` when really it shouldn't.)
agramfort pushed a commit that referenced this pull request Aug 5, 2020
`get_head_mri_trans()` in `master` tries to infer the filename extension
from a BIDS basename; however, the basename, as is created inside that
function, doesn't have a suffix. Correctly, the extension has to be
inferred from `bids_fname`. This is what we used to do before GH-446
got merged. This commit restores the old behavior.

(I realized something was going wrong when MNE-BIDS started looking
 for some .pdf files, while I only have .fif. So beyond what I'm
 fixing here, there seems to be another bug in `parse_ext()`, causing
 it to report `.pdf` when really it shouldn't.)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[API Request]: An update function for bids_basename
6 participants