Repository: bids-standard/pybids

Issue:
Please return leading . in extension
See https://github.com/bids-standard/bids-specification/pull/152#pullrequestreview-236162714 for more info on bringing more consistency to bids spec and tooling

Discussion:

Patch:
diff --git a/bids/config.py b/bids/config.py
index 298d382f..a4e55b62 100644
--- a/bids/config.py
+++ b/bids/config.py
@@ -4,6 +4,7 @@ import json
 from os.path import join, expanduser, exists
 import os
 from io import open
+import warnings
 
 __all__ = ['set_option', 'set_options', 'get_option']
 
@@ -12,8 +13,11 @@ _config_name = 'pybids_config.json'
 conf_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
                          'layout', 'config', '{}.json')
 _default_settings = {
+    # XXX 0.14: Remove bids-nodot option (and file)
     'config_paths': {
-        name: conf_path.format(name) for name in ['bids', 'derivatives']}
+        name: conf_path.format(name) for name in ['bids', 'derivatives', 'bids-nodot']},
+    # XXX 0.14: Set to True
+    'extension_initial_dot': None,
 }
 
 
@@ -26,6 +30,10 @@ def set_option(key, value):
     """
     if key not in _settings:
         raise ValueError("Invalid pybids setting: '%s'" % key)
+    # XXX 0.14: Raise error
+    if (key, value) == ("extension_initial_dot", False):
+        warnings.warn("Setting 'extension_initial_dot' to False will be disabled in "
+                      "pybids 0.14", FutureWarning)
     _settings[key] = value
 
 
diff --git a/bids/layout/config/bids-nodot.json b/bids/layout/config/bids-nodot.json
new file mode 100644
index 00000000..fd299f06
--- /dev/null
+++ b/bids/layout/config/bids-nodot.json
@@ -0,0 +1,104 @@
+{
+    "name": "bids",
+    "entities": [
+        {
+            "name": "subject",
+            "pattern": "[/\\\\]+sub-([a-zA-Z0-9]+)",
+            "directory": "{subject}"
+        },
+        {
+            "name": "session",
+            "pattern": "[_/\\\\]+ses-([a-zA-Z0-9]+)",
+            "mandatory": false,
+            "directory": "{subject}{session}"
+        },
+        {
+            "name": "task",
+            "pattern": "[_/\\\\]+task-([a-zA-Z0-9]+)"
+        },
+        {
+            "name": "acquisition",
+            "pattern": "[_/\\\\]+acq-([a-zA-Z0-9]+)"
+        },
+        {
+            "name": "ceagent",
+            "pattern": "[_/\\\\]+ce-([a-zA-Z0-9]+)"
+        },
+        {
+            "name": "reconstruction",
+            "pattern": "[_/\\\\]+rec-([a-zA-Z0-9]+)"
+        },
+        {
+            "name": "direction",
+            "pattern": "[_/\\\\]+dir-([a-zA-Z0-9]+)"
+        },
+        {
+            "name": "run",
+            "pattern": "[_/\\\\]+run-0*(\\d+)",
+            "dtype": "int"
+        },
+        {
+            "name": "proc",
+            "pattern": "[_/\\\\]+proc-([a-zA-Z0-9]+)"
+        },
+        {
+            "name": "modality",
+            "pattern": "[_/\\\\]+mod-([a-zA-Z0-9]+)"
+        },
+        {
+            "name": "echo",
+            "pattern": "[_/\\\\]+echo-([0-9]+)"
+        },
+        {
+            "name": "recording",
+            "pattern": "[_/\\\\]+recording-([a-zA-Z0-9]+)"
+        },
+        {
+            "name": "space",
+            "pattern": "[_/\\\\]+space-([a-zA-Z0-9]+)"
+        },
+        {
+            "name": "suffix",
+            "pattern": "[._]*([a-zA-Z0-9]*?)\\.[^/\\\\]+$"
+        },
+        {
+            "name": "scans",
+            "pattern": "(.*\\_scans.tsv)$"
+        },
+        {
+            "name": "fmap",
+            "pattern": "(phasediff|magnitude[1-2]|phase[1-2]|fieldmap|epi)\\.nii"
+        },
+        {
+            "name": "datatype",
+            "pattern": "[/\\\\]+(func|anat|fmap|dwi|meg|eeg)[/\\\\]+"
+        },
+        {
+            "name": "extension",
+            "pattern": "[._]*[a-zA-Z0-9]*?\\.([^/\\\\]+)$"
+        }
+    ],
+
+    "default_path_patterns": [
+        "sub-{subject}[/ses-{session}]/{datatype<anat>|anat}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_ce-{ceagent}][_rec-{reconstruction}]_{suffix<T1w|T2w|T1rho|T1map|T2map|T2star|FLAIR|FLASH|PDmap|PD|PDT2|inplaneT[12]|angio>}.{extension<nii|nii.gz|json>|nii.gz}",
+        "sub-{subject}[/ses-{session}]/{datatype<anat>|anat}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_ce-{ceagent}][_rec-{reconstruction}][_mod-{modality}]_{suffix<defacemask>}.{extension<nii|nii.gz|json>|nii.gz}",
+        "sub-{subject}[/ses-{session}]/{datatype<func>|func}/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_ce-{ceagent}][_dir-{direction}][_rec-{reconstruction}][_run-{run}][_echo-{echo}]_{suffix<bold|cbv|phase|sbref>}.{extension<nii|nii.gz|json>|nii.gz}",
+        "sub-{subject}[/ses-{session}]/{datatype<dwi>|dwi}/sub-{subject}[_ses-{session}][_acq-{acquisition}]_{suffix<dwi>}.{extension<bval|bvec|json|nii.gz|nii>|nii.gz}",
+        "sub-{subject}[/ses-{session}]/{datatype<fmap>|fmap}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_dir-{direction}][_run-{run}]_{fmap<phasediff|magnitude[12]|phase[12]|fieldmap>}.{extension<nii|nii.gz|json>|nii.gz}",
+        "sub-{subject}[/ses-{session}]/{datatype<fmap>|fmap}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_ce-{ceagent}]_dir-{direction}[_run-{run}]_{fmap<epi>}.{extension<nii|nii.gz|json>|nii.gz}",
+        "sub-{subject}[/ses-{session}]/[{datatype<func|meg|beh>|func}/]sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_rec-{reconstruction}][_run-{run}][_echo-{echo}][_recording-{recording}]_{suffix<events>}.{extension<tsv|json>|tsv}",
+        "sub-{subject}[/ses-{session}]/[{datatype<func|beh>|func}/]sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_rec-{reconstruction}][_run-{run}][_echo-{echo}][_recording-{recording}]_{suffix<physio|stim>}.{extension<tsv.gz|json>|tsv.gz}",
+        "sub-{subject}[/ses-{session}]/{datatype<meg>|meg}/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_run-{run}][_proc-{proc}]_{suffix<meg>}.{extension}",
+        "sub-{subject}[/ses-{session}]/{datatype<meg>|meg}/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_run-{run}][_proc-{proc}]_{suffix<channels>}.{extension<tsv|json>|tsv}",
+        "sub-{subject}[/ses-{session}]/{datatype<meg>|meg}/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}]_{suffix<coordsystem>}.{extension<json>|json}",
+        "sub-{subject}[/ses-{session}]/{datatype<meg>|meg}/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}]_{suffix<photo>}.{extension<jpg>|jpg}",
+        "[acq-{acquisition}_][ce-{ceagent}_][rec-{reconstruction}_]{suffix<T1w|T2w|T1rho|T1map|T2map|T2star|FLAIR|FLASH|PDmap|PD|PDT2|inplaneT[12]|angio>}.{extension<json>|json}",
+        "[acq-{acquisition}_][ce-{ceagent}_][rec-{reconstruction}_][mod-{modality}_]{suffix<defacemask>}.{extension<json>|json}",
+        "task-{task}[_acq-{acquisition}][_ce-{ceagent}][_dir-{direction}][_rec-{reconstruction}][_run-{run}][_echo-{echo}]_{suffix<bold|cbv|phase|sbref>}.{extension<json>|json}",
+        "[acq-{acquisition}_]{suffix<dwi>}.{extension<json>|json}",
+        "[acq-{acquisition}_][dir-{direction}_][run-{run}_]{fmap<phasediff|magnitude[1-2]|phase[1-2]|fieldmap>}.{extension<json>|json}",
+        "[acq-{acquisition}_][ce-{ceagent}_]dir-{direction}[_run-{run}]_{fmap<epi>}.{extension<json>|json}",
+        "task-{task}[_acq-{acquisition}][_rec-{reconstruction}][_run-{run}][_echo-{echo}][_recording-{recording}]_{suffix<events>}.{extension<json>|json}",
+        "task-{task}[_acq-{acquisition}][_rec-{reconstruction}][_run-{run}][_echo-{echo}][_recording-{recording}]_{suffix<physio|stim>}.{extension<json>}"
+    ]
+}
diff --git a/bids/layout/config/bids.json b/bids/layout/config/bids.json
index fd299f06..091c55b0 100644
--- a/bids/layout/config/bids.json
+++ b/bids/layout/config/bids.json
@@ -75,30 +75,30 @@
         },
         {
             "name": "extension",
-            "pattern": "[._]*[a-zA-Z0-9]*?\\.([^/\\\\]+)$"
+            "pattern": "[._]*[a-zA-Z0-9]*?(\\.[^/\\\\]+)$"
         }
     ],
 
     "default_path_patterns": [
-        "sub-{subject}[/ses-{session}]/{datatype<anat>|anat}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_ce-{ceagent}][_rec-{reconstruction}]_{suffix<T1w|T2w|T1rho|T1map|T2map|T2star|FLAIR|FLASH|PDmap|PD|PDT2|inplaneT[12]|angio>}.{extension<nii|nii.gz|json>|nii.gz}",
-        "sub-{subject}[/ses-{session}]/{datatype<anat>|anat}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_ce-{ceagent}][_rec-{reconstruction}][_mod-{modality}]_{suffix<defacemask>}.{extension<nii|nii.gz|json>|nii.gz}",
-        "sub-{subject}[/ses-{session}]/{datatype<func>|func}/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_ce-{ceagent}][_dir-{direction}][_rec-{reconstruction}][_run-{run}][_echo-{echo}]_{suffix<bold|cbv|phase|sbref>}.{extension<nii|nii.gz|json>|nii.gz}",
-        "sub-{subject}[/ses-{session}]/{datatype<dwi>|dwi}/sub-{subject}[_ses-{session}][_acq-{acquisition}]_{suffix<dwi>}.{extension<bval|bvec|json|nii.gz|nii>|nii.gz}",
-        "sub-{subject}[/ses-{session}]/{datatype<fmap>|fmap}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_dir-{direction}][_run-{run}]_{fmap<phasediff|magnitude[12]|phase[12]|fieldmap>}.{extension<nii|nii.gz|json>|nii.gz}",
-        "sub-{subject}[/ses-{session}]/{datatype<fmap>|fmap}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_ce-{ceagent}]_dir-{direction}[_run-{run}]_{fmap<epi>}.{extension<nii|nii.gz|json>|nii.gz}",
-        "sub-{subject}[/ses-{session}]/[{datatype<func|meg|beh>|func}/]sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_rec-{reconstruction}][_run-{run}][_echo-{echo}][_recording-{recording}]_{suffix<events>}.{extension<tsv|json>|tsv}",
-        "sub-{subject}[/ses-{session}]/[{datatype<func|beh>|func}/]sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_rec-{reconstruction}][_run-{run}][_echo-{echo}][_recording-{recording}]_{suffix<physio|stim>}.{extension<tsv.gz|json>|tsv.gz}",
-        "sub-{subject}[/ses-{session}]/{datatype<meg>|meg}/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_run-{run}][_proc-{proc}]_{suffix<meg>}.{extension}",
-        "sub-{subject}[/ses-{session}]/{datatype<meg>|meg}/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_run-{run}][_proc-{proc}]_{suffix<channels>}.{extension<tsv|json>|tsv}",
-        "sub-{subject}[/ses-{session}]/{datatype<meg>|meg}/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}]_{suffix<coordsystem>}.{extension<json>|json}",
-        "sub-{subject}[/ses-{session}]/{datatype<meg>|meg}/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}]_{suffix<photo>}.{extension<jpg>|jpg}",
-        "[acq-{acquisition}_][ce-{ceagent}_][rec-{reconstruction}_]{suffix<T1w|T2w|T1rho|T1map|T2map|T2star|FLAIR|FLASH|PDmap|PD|PDT2|inplaneT[12]|angio>}.{extension<json>|json}",
-        "[acq-{acquisition}_][ce-{ceagent}_][rec-{reconstruction}_][mod-{modality}_]{suffix<defacemask>}.{extension<json>|json}",
-        "task-{task}[_acq-{acquisition}][_ce-{ceagent}][_dir-{direction}][_rec-{reconstruction}][_run-{run}][_echo-{echo}]_{suffix<bold|cbv|phase|sbref>}.{extension<json>|json}",
-        "[acq-{acquisition}_]{suffix<dwi>}.{extension<json>|json}",
-        "[acq-{acquisition}_][dir-{direction}_][run-{run}_]{fmap<phasediff|magnitude[1-2]|phase[1-2]|fieldmap>}.{extension<json>|json}",
-        "[acq-{acquisition}_][ce-{ceagent}_]dir-{direction}[_run-{run}]_{fmap<epi>}.{extension<json>|json}",
-        "task-{task}[_acq-{acquisition}][_rec-{reconstruction}][_run-{run}][_echo-{echo}][_recording-{recording}]_{suffix<events>}.{extension<json>|json}",
-        "task-{task}[_acq-{acquisition}][_rec-{reconstruction}][_run-{run}][_echo-{echo}][_recording-{recording}]_{suffix<physio|stim>}.{extension<json>}"
+        "sub-{subject}[/ses-{session}]/{datatype<anat>|anat}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_ce-{ceagent}][_rec-{reconstruction}]_{suffix<T1w|T2w|T1rho|T1map|T2map|T2star|FLAIR|FLASH|PDmap|PD|PDT2|inplaneT[12]|angio>}{extension<.nii|.nii.gz|.json>|.nii.gz}",
+        "sub-{subject}[/ses-{session}]/{datatype<anat>|anat}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_ce-{ceagent}][_rec-{reconstruction}][_mod-{modality}]_{suffix<defacemask>}{extension<.nii|.nii.gz|.json>|.nii.gz}",
+        "sub-{subject}[/ses-{session}]/{datatype<func>|func}/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_ce-{ceagent}][_dir-{direction}][_rec-{reconstruction}][_run-{run}][_echo-{echo}]_{suffix<bold|cbv|phase|sbref>}{extension<.nii|.nii.gz|.json>|.nii.gz}",
+        "sub-{subject}[/ses-{session}]/{datatype<dwi>|dwi}/sub-{subject}[_ses-{session}][_acq-{acquisition}]_{suffix<dwi>}{extension<.bval|.bvec|.json|.nii.gz|.nii>|.nii.gz}",
+        "sub-{subject}[/ses-{session}]/{datatype<fmap>|fmap}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_dir-{direction}][_run-{run}]_{fmap<phasediff|magnitude[12]|phase[12]|fieldmap>}{extension<.nii|.nii.gz|.json>|.nii.gz}",
+        "sub-{subject}[/ses-{session}]/{datatype<fmap>|fmap}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_ce-{ceagent}]_dir-{direction}[_run-{run}]_{fmap<epi>}{extension<.nii|.nii.gz|.json>|.nii.gz}",
+        "sub-{subject}[/ses-{session}]/[{datatype<func|meg|beh>|func}/]sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_rec-{reconstruction}][_run-{run}][_echo-{echo}][_recording-{recording}]_{suffix<events>}{extension<.tsv|.json>|.tsv}",
+        "sub-{subject}[/ses-{session}]/[{datatype<func|beh>|func}/]sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_rec-{reconstruction}][_run-{run}][_echo-{echo}][_recording-{recording}]_{suffix<physio|stim>}{extension<.tsv.gz|.json>|.tsv.gz}",
+        "sub-{subject}[/ses-{session}]/{datatype<meg>|meg}/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_run-{run}][_proc-{proc}]_{suffix<meg>}{extension}",
+        "sub-{subject}[/ses-{session}]/{datatype<meg>|meg}/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_run-{run}][_proc-{proc}]_{suffix<channels>}{extension<.tsv|.json>|.tsv}",
+        "sub-{subject}[/ses-{session}]/{datatype<meg>|meg}/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}]_{suffix<coordsystem>}{extension<.json>|.json}",
+        "sub-{subject}[/ses-{session}]/{datatype<meg>|meg}/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}]_{suffix<photo>}{extension<.jpg>|.jpg}",
+        "[acq-{acquisition}_][ce-{ceagent}_][rec-{reconstruction}_]{suffix<T1w|T2w|T1rho|T1map|T2map|T2star|FLAIR|FLASH|PDmap|PD|PDT2|inplaneT[12]|angio>}{extension<.json>|.json}",
+        "[acq-{acquisition}_][ce-{ceagent}_][rec-{reconstruction}_][mod-{modality}_]{suffix<defacemask>}{extension<.json>|.json}",
+        "task-{task}[_acq-{acquisition}][_ce-{ceagent}][_dir-{direction}][_rec-{reconstruction}][_run-{run}][_echo-{echo}]_{suffix<bold|cbv|phase|sbref>}{extension<.json>|.json}",
+        "[acq-{acquisition}_]{suffix<dwi>}{extension<.json>|.json}",
+        "[acq-{acquisition}_][dir-{direction}_][run-{run}_]{fmap<phasediff|magnitude[1-2]|phase[1-2]|fieldmap>}{extension<.json>|.json}",
+        "[acq-{acquisition}_][ce-{ceagent}_]dir-{direction}[_run-{run}]_{fmap<epi>}{extension<.json>|.json}",
+        "task-{task}[_acq-{acquisition}][_rec-{reconstruction}][_run-{run}][_echo-{echo}][_recording-{recording}]_{suffix<events>}{extension<.json>|.json}",
+        "task-{task}[_acq-{acquisition}][_rec-{reconstruction}][_run-{run}][_echo-{echo}][_recording-{recording}]_{suffix<physio|stim>}{extension<.json>}"
     ]
 }
diff --git a/bids/layout/index.py b/bids/layout/index.py
index 14bb59ac..f533af9b 100644
--- a/bids/layout/index.py
+++ b/bids/layout/index.py
@@ -5,6 +5,7 @@ import json
 from collections import defaultdict
 from pathlib import Path
 from bids_validator import BIDSValidator
+import bids.config
 from .models import Config, Entity, Tag, FileAssociation
 from ..utils import listify, make_bidsfile
 from ..exceptions import BIDSConflictingValuesError
@@ -179,6 +180,8 @@ class BIDSLayoutIndexer(object):
             These keyword arguments define what files get selected
             for metadata indexing.
         """
+
+        dot = '.' if bids.config.get_option('extension_initial_dot') else ''
         
         if filters:
             # ensure we are returning objects
@@ -188,8 +191,10 @@ class BIDSLayoutIndexer(object):
             if filters.get(ext_key):
                 filters[ext_key] = listify(filters[ext_key])
                 # ensure json files are being indexed
-                if 'json' not in filters[ext_key]:
-                    filters[ext_key].append('json')
+                # XXX 0.14: dot always == '.'
+                json_ext = dot + 'json'
+                if json_ext not in filters[ext_key]:
+                    filters[ext_key].append(json_ext)
 
         # Process JSON files first if we're indexing metadata
         all_files = self.layout.get(absolute_paths=True, **filters)
@@ -223,7 +228,7 @@ class BIDSLayoutIndexer(object):
                 if key not in file_data:
                     file_data[key] = defaultdict(list)
 
-                if ext == 'json':
+                if ext == dot + 'json':
                     with open(bf.path, 'r') as handle:
                         try:
                             payload = json.load(handle)
@@ -272,7 +277,7 @@ class BIDSLayoutIndexer(object):
             # add the payload to the stack. Finally, we invert the
             # stack and merge the payloads in order.
             ext_key = "{}/{}".format(ext, suffix)
-            json_key = "json/{}".format(suffix)
+            json_key = dot + "json/{}".format(suffix)
             dirname = bf.dirname
 
             payloads = []
@@ -343,7 +348,7 @@ class BIDSLayoutIndexer(object):
             # Link files to BOLD runs
             if suffix in ['physio', 'stim', 'events', 'sbref']:
                 images = self.layout.get(
-                    extension=['nii', 'nii.gz'], suffix='bold',
+                    extension=['.nii', '.nii.gz'], suffix='bold',
                     return_type='filename', **file_ents)
                 for img in images:
                     create_association_pair(bf.path, img, 'IntendedFor',
@@ -352,7 +357,7 @@ class BIDSLayoutIndexer(object):
             # Link files to DWI runs
             if suffix == 'sbref' or ext in ['bvec', 'bval']:
                 images = self.layout.get(
-                    extension=['nii', 'nii.gz'], suffix='dwi',
+                    extension=['.nii', '.nii.gz'], suffix='dwi',
                     return_type='filename', **file_ents)
                 for img in images:
                     create_association_pair(bf.path, img, 'IntendedFor',
diff --git a/bids/layout/layout.py b/bids/layout/layout.py
index 68b15648..9fa32d4d 100644
--- a/bids/layout/layout.py
+++ b/bids/layout/layout.py
@@ -956,9 +956,16 @@ class BIDSLayout(object):
 
         # Strip leading periods if extensions were passed
         if 'extension' in filters:
+            # XXX 0.14: Disable drop_dot option
+            drop_dot = (self.config['bids'].entities['extension'].pattern ==
+                        '[._]*[a-zA-Z0-9]*?\\.([^/\\\\]+)$')
             exts = listify(filters['extension'])
-            filters['extension'] = [x.lstrip('.') if isinstance(x, str) else x
-                                    for x in exts]
+            if drop_dot:
+                filters['extension'] = [x.lstrip('.') if isinstance(x, str) else x
+                                        for x in exts]
+            else:
+                filters['extension'] = ['.' + x.lstrip('.') if isinstance(x, str) else x
+                                        for x in exts]
 
         if invalid_filters != 'allow':
             bad_filters = set(filters.keys()) - set(entities.keys())
@@ -1343,13 +1350,13 @@ class BIDSLayout(object):
 
     def get_bvec(self, path, **kwargs):
         """Get bvec file for passed path."""
-        result = self.get_nearest(path, extension='bvec', suffix='dwi',
+        result = self.get_nearest(path, extension='.bvec', suffix='dwi',
                                   all_=True, **kwargs)
         return listify(result)[0]
 
     def get_bval(self, path, **kwargs):
         """Get bval file for passed path."""
-        result = self.get_nearest(path, suffix='dwi', extension='bval',
+        result = self.get_nearest(path, suffix='dwi', extension='.bval',
                                   all_=True, **kwargs)
         return listify(result)[0]
 
@@ -1377,7 +1384,7 @@ class BIDSLayout(object):
         fieldmap_set = []
         suffix = '(phase1|phasediff|epi|fieldmap)'
         files = self.get(subject=sub, suffix=suffix, regex_search=True,
-                         extension=['nii.gz', 'nii'])
+                         extension=['.nii.gz', '.nii'])
         for file in files:
             metadata = self.get_metadata(file.path)
             if metadata and "IntendedFor" in metadata.keys():
@@ -1438,7 +1445,7 @@ class BIDSLayout(object):
         # Constrain search to functional images
         filters.update(suffix='bold', datatype='func')
         scope = 'all' if derivatives else 'raw'
-        images = self.get(extension=['nii', 'nii.gz'], scope=scope,
+        images = self.get(extension=['.nii', '.nii.gz'], scope=scope,
                           **filters)
         if not images:
             raise NoMatchError("No functional images that match criteria found.")
diff --git a/bids/layout/models.py b/bids/layout/models.py
index e483d45c..e5266eb0 100644
--- a/bids/layout/models.py
+++ b/bids/layout/models.py
@@ -91,6 +91,17 @@ class Config(Base):
         A Config instance.
         """
 
+        # XXX 0.14: Disable extension_initial_dot branching
+        extension_initial_dot = get_option('extension_initial_dot')
+        if config == "bids" and not extension_initial_dot:
+            if extension_initial_dot is None:
+                warnings.warn("The 'extension' entity currently excludes the leading dot ('.'). "
+                              "As of version 0.14.0, it will include the leading dot. To suppress "
+                              "this warning and include the leading dot, use "
+                              "`bids.config.set_option('extension_initial_dot', True)`.",
+                              FutureWarning)
+            config = "bids-nodot"
+
         if isinstance(config, str):
             config_paths = get_option('config_paths')
             if config in config_paths:
@@ -103,9 +114,17 @@ class Config(Base):
 
         # Return existing Config record if one exists
         if session is not None:
-            result = (session.query(Config)
-                      .filter_by(name=config['name']).first())
+            result = session.query(Config).filter_by(name=config['name']).first()
             if result:
+                # XXX 0.14: Remove check
+                if config["name"] == "bids":
+                    old_pattern = result.entities["extension"].pattern
+                    new_pattern = next(ent['pattern'] for ent in config['entities']
+                                       if ent['name'] == 'extension')
+                    if new_pattern != old_pattern:
+                        warnings.warn(
+                            "Cannot modify extension_initial_dot option after initialization. "
+                            "Set option immediately after ``import bids``.")
                 return result
 
         return Config(session=session, **config)
@@ -352,7 +371,8 @@ class BIDSDataFile(BIDSFile):
 
         data = self.data.copy()
 
-        if self.entities['extension'] == 'tsv.gz':
+        # XXX 0.14: ".tsv.gz" only
+        if self.entities['extension'] in ('tsv.gz', '.tsv.gz'):
             md = self.get_metadata()
             # We could potentially include some validation here, but that seems
             # like a job for the BIDS Validator.
diff --git a/bids/layout/writing.py b/bids/layout/writing.py
index 46935c49..bf970c3d 100644
--- a/bids/layout/writing.py
+++ b/bids/layout/writing.py
@@ -53,7 +53,7 @@ def build_path(entities, path_patterns, strict=False):
     Examples
     --------
     >>> entities = {
-    ...     'extension': 'nii',
+    ...     'extension': '.nii',
     ...     'space': 'MNI',
     ...     'subject': '001',
     ...     'suffix': 'inplaneT2',
@@ -61,12 +61,12 @@ def build_path(entities, path_patterns, strict=False):
     >>> patterns = ['sub-{subject}[/ses-{session}]/anat/sub-{subject}[_ses-{session}]'
     ...             '[_acq-{acquisition}][_ce-{ceagent}][_rec-{reconstruction}]_'
     ...             '{suffix<T[12]w|T1rho|T[12]map|T2star|FLAIR|FLASH|PDmap|PD|PDT2|'
-    ...             'inplaneT[12]|angio>}.{extension<nii|nii.gz|json>|nii.gz}',
+    ...             'inplaneT[12]|angio>}{extension<.nii|.nii.gz|.json>|.nii.gz}',
     ...             'sub-{subject}[/ses-{session}]/anat/sub-{subject}[_ses-{session}]'
     ...             '[_acq-{acquisition}][_ce-{ceagent}][_rec-{reconstruction}]'
     ...             '[_space-{space}][_desc-{desc}]_{suffix<T1w|T2w|T1rho|T1map|T2map|'
-    ...             'T2star|FLAIR|FLASH|PDmap|PD|PDT2|inplaneT[12]|angio>}.'
-    ...             '{extension<nii|nii.gz|json>|nii.gz}']
+    ...             'T2star|FLAIR|FLASH|PDmap|PD|PDT2|inplaneT[12]|angio>}'
+    ...             '{extension<.nii|.nii.gz|.json>|.nii.gz}']
     >>> build_path(entities, patterns)
     'sub-001/anat/sub-001_inplaneT2.nii'
 
@@ -103,19 +103,19 @@ def build_path(entities, path_patterns, strict=False):
     True
 
     >>> entities = {
-    ...     'extension': 'bvec',
+    ...     'extension': '.bvec',
     ...     'subject': '001',
     ... }
     >>> patterns = (
     ...     "sub-{subject}[/ses-{session}]/{datatype|dwi}/sub-{subject}[_ses-{session}]"
-    ...     "[_acq-{acquisition}]_{suffix|dwi}.{extension<bval|bvec|json|nii.gz|nii>|nii.gz}"
+    ...     "[_acq-{acquisition}]_{suffix|dwi}{extension<.bval|.bvec|.json|.nii.gz|.nii>|.nii.gz}"
     ... )
     >>> build_path(entities, patterns, strict=True)
     'sub-001/dwi/sub-001_dwi.bvec'
 
     >>> # Lists of entities are expanded
     >>> entities = {
-    ...     'extension': 'bvec',
+    ...     'extension': '.bvec',
     ...     'subject': ['%02d' % i for i in range(1, 4)],
     ... }
     >>> build_path(entities, patterns, strict=True)
@@ -127,10 +127,6 @@ def build_path(entities, path_patterns, strict=False):
     # Drop None and empty-strings, keep zeros, and listify
     entities = {k: listify(v) for k, v in entities.items() if v or v == 0}
 
-    # One less source of confusion
-    if 'extension' in entities:
-        entities['extension'] = [e.lstrip('.') for e in entities['extension']]
-
     # Loop over available patherns, return first one that matches all
     for pattern in path_patterns:
         entities_matched = list(_PATTERN_FIND.findall(pattern))
@@ -147,6 +143,16 @@ def build_path(entities, path_patterns, strict=False):
         # Expand options within valid values and
         # check whether entities provided have acceptable value
         tmp_entities = entities.copy()  # Do not modify the original query
+
+        # Accept extensions with and without leading dot
+        if 'extension' in tmp_entities:
+            exts = [e.lstrip('.') for e in tmp_entities['extension']]
+            # Does this pattern place a dot before the extension, or expect it inside?
+            if re.search(r'\.\{extension', pattern):
+                tmp_entities['extension'] = exts
+            else:
+                tmp_entities['extension'] = ['.' + e for e in exts]
+
         for fmt, name, valid, defval in entities_matched:
             valid_expanded = [v for val in valid.split('|') if val
                               for v in _expand_options(val)]
@@ -154,10 +160,11 @@ def build_path(entities, path_patterns, strict=False):
                 warnings.warn(
                     'Pattern "%s" is inconsistent as it defines an invalid default value.' % fmt
                 )
+
             if (
                 valid_expanded
-                and name in entities
-                and set(entities[name]) - set(valid_expanded)
+                and name in tmp_entities
+                and set(tmp_entities[name]) - set(valid_expanded)
             ):
                 continue
 
diff --git a/bids/reports/report.py b/bids/reports/report.py
index e23f4e16..c677f9fb 100644
--- a/bids/reports/report.py
+++ b/bids/reports/report.py
@@ -75,7 +75,7 @@ class BIDSReport(object):
         >>> from bids.tests import get_test_data_path
         >>> layout = BIDSLayout(join(get_test_data_path(), 'synthetic'))
         >>> report = BIDSReport(layout)
-        >>> files = layout.get(session='01', extension=['nii.gz', 'nii'])
+        >>> files = layout.get(session='01', extension=['.nii.gz', '.nii'])
         >>> counter = report.generate_from_files(files)
         Number of patterns detected: 1
         Remember to double-check everything and to replace <deg> with a degree symbol.
diff --git a/bids/variables/io.py b/bids/variables/io.py
index b41ae25a..026333b5 100644
--- a/bids/variables/io.py
+++ b/bids/variables/io.py
@@ -158,7 +158,7 @@ def _load_time_variables(layout, dataset=None, columns=None, scan_length=None,
 
     selectors['datatype'] = 'func'
     selectors['suffix'] = 'bold'
-    images = layout.get(return_type='object', extension='nii.gz',
+    images = layout.get(return_type='object', extension=['.nii', '.nii.gz'],
                         scope=scope, **selectors)
 
     if not images:
@@ -212,28 +212,29 @@ def _load_time_variables(layout, dataset=None, columns=None, scan_length=None,
                 raise ValueError("More than one existing Node matches the "
                                  "specified entities! You may need to pass "
                                  "additional selectors to narrow the search.")
-            return result[0]
-
-        # Otherwise create a new node and use that.
-        # We first convert any entity values that are currently collections to
-        # JSON strings to prevent nasty hashing problems downstream. Note that
-        # isinstance() isn't as foolproof as actually trying to hash the
-        # value, but the latter is likely to be slower, and since values are
-        # coming from JSON or filenames, there's no real chance of encountering
-        # anything but a list or dict.
-        entities = {
-            k: (json.dumps(v) if isinstance(v, (list, dict)) else v)
-            for (k, v) in entities.items()
-        }
-
-        run = dataset.create_node('run', entities, image_file=img_f,
-                                  duration=duration, repetition_time=tr)
-        run_info = run.get_info()
+            run_info = result[0].get_info()
+
+        else:
+            # Otherwise create a new node and use that.
+            # We first convert any entity values that are currently collections to
+            # JSON strings to prevent nasty hashing problems downstream. Note that
+            # isinstance() isn't as foolproof as actually trying to hash the
+            # value, but the latter is likely to be slower, and since values are
+            # coming from JSON or filenames, there's no real chance of encountering
+            # anything but a list or dict.
+            entities = {
+                k: (json.dumps(v) if isinstance(v, (list, dict)) else v)
+                for (k, v) in entities.items()
+            }
+
+            run = dataset.create_node('run', entities, image_file=img_f,
+                                      duration=duration, repetition_time=tr)
+            run_info = run.get_info()
 
         # Process event files
         if events:
             dfs = layout.get_nearest(
-                img_f, extension='tsv', suffix='events', all_=True,
+                img_f, extension='.tsv', suffix='events', all_=True,
                 full_search=True, ignore_strict_entities=['suffix', 'extension'])
             for _data in dfs:
                 _data = pd.read_csv(_data, sep='\t')
@@ -304,7 +305,7 @@ def _load_time_variables(layout, dataset=None, columns=None, scan_length=None,
 
         if rec_types:
             rec_files = layout.get_nearest(
-                img_f, extension='tsv.gz', all_=True, suffix=rec_types,
+                img_f, extension='.tsv.gz', all_=True, suffix=rec_types,
                 ignore_strict_entities=['suffix', 'extension'], full_search=True)
             for rf in rec_files:
                 metadata = layout.get_metadata(rf)
@@ -395,7 +396,7 @@ def _load_tsv_variables(layout, suffix, dataset=None, columns=None,
     if dataset is None:
         dataset = NodeIndex()
 
-    files = layout.get(extension='tsv', suffix=suffix, scope=scope,
+    files = layout.get(extension='.tsv', suffix=suffix, scope=scope,
                        **layout_kwargs)
 
     for f in files:


Test patch:
diff --git a/bids/conftest.py b/bids/conftest.py
new file mode 100644
index 00000000..56fe1aaf
--- /dev/null
+++ b/bids/conftest.py
@@ -0,0 +1,36 @@
+"""
+This module allows you to mock the config file as needed.
+A default fixture that simply returns a safe-to-modify copy of
+the default value is provided.
+This can be overridden by parametrizing over the option you wish to
+mock.
+
+e.g.
+
+>>> @pytest.mark.parametrize("extension_initial_dot", (True, False))
+... def test_fixture(mock_config, extension_initial_dot):
+...     import bids
+...     assert bids.config.get_option("extension_initial_dot") == extension_initial_dot
+"""
+
+
+from unittest.mock import patch
+import pytest
+
+@pytest.fixture
+def config_paths():
+    import bids.config
+    return bids.config.get_option('config_paths').copy()
+
+@pytest.fixture
+def extension_initial_dot():
+    import bids.config
+    return bids.config.get_option('extension_initial_dot')
+
+@pytest.fixture
+def mock_config(config_paths, extension_initial_dot):
+    import bids.config
+    with patch.dict('bids.config._settings'):
+        bids.config._settings['config_paths'] = config_paths
+        bids.config._settings['extension_initial_dot'] = extension_initial_dot
+        yield
diff --git a/bids/layout/tests/test_layout.py b/bids/layout/tests/test_layout.py
index 4318cb3c..b98e3c47 100644
--- a/bids/layout/tests/test_layout.py
+++ b/bids/layout/tests/test_layout.py
@@ -30,6 +30,7 @@ def test_layout_init(layout_7t_trt):
     assert isinstance(layout_7t_trt.files, dict)
 
 
+@pytest.mark.parametrize("extension_initial_dot", (True, False))
 @pytest.mark.parametrize(
     'index_metadata,query,result',
     [
@@ -37,17 +38,17 @@ def test_layout_init(layout_7t_trt):
         (False, None, None),
         (False, {}, 3.0),
         (False, {'task': 'rest'}, 3.0),
-        (False, {'task': 'rest', 'extension': ['nii.gz']}, 3.0),
-        (False, {'task': 'rest', 'extension': 'nii.gz'}, 3.0),
-        (False, {'task': 'rest', 'extension': ['nii.gz', 'json'], 'return_type': 'file'}, 3.0),
+        (False, {'task': 'rest', 'extension': ['.nii.gz']}, 3.0),
+        (False, {'task': 'rest', 'extension': '.nii.gz'}, 3.0),
+        (False, {'task': 'rest', 'extension': ['.nii.gz', '.json'], 'return_type': 'file'}, 3.0),
     ])
-def test_index_metadata(index_metadata, query, result):
+def test_index_metadata(index_metadata, query, result, mock_config):
     data_dir = join(get_test_data_path(), '7t_trt')
     layout = BIDSLayout(data_dir, index_metadata=index_metadata)
     if not index_metadata and query is not None:
         indexer = BIDSLayoutIndexer(layout)
         indexer.index_metadata(**query)
-    sample_file = layout.get(task='rest', extension='nii.gz',
+    sample_file = layout.get(task='rest', extension='.nii.gz',
                              acquisition='fullbrain')[0]
     metadata = sample_file.get_metadata()
     assert metadata.get('RepetitionTime') == result
@@ -407,17 +408,17 @@ def test_layout_with_multi_derivs(layout_ds005_multi_derivs):
 
 def test_query_derivatives(layout_ds005_derivs):
     result = layout_ds005_derivs.get(suffix='events', return_type='object',
-                                     extension='tsv')
+                                     extension='.tsv')
     result = [f.filename for f in result]
     assert len(result) == 49
     assert 'sub-01_task-mixedgamblestask_run-01_desc-extra_events.tsv' in result
     result = layout_ds005_derivs.get(suffix='events', return_type='object',
-                                     scope='raw', extension='tsv')
+                                     scope='raw', extension='.tsv')
     assert len(result) == 48
     result = [f.filename for f in result]
     assert 'sub-01_task-mixedgamblestask_run-01_desc-extra_events.tsv' not in result
     result = layout_ds005_derivs.get(suffix='events', return_type='object',
-                                     desc='extra', extension='tsv')
+                                     desc='extra', extension='.tsv')
     assert len(result) == 1
     result = [f.filename for f in result]
     assert 'sub-01_task-mixedgamblestask_run-01_desc-extra_events.tsv' in result
@@ -477,19 +478,22 @@ def test_to_df(layout_ds117):
                 set(df.columns))
 
 
-def test_parse_file_entities():
+@pytest.mark.parametrize("extension_initial_dot", (True, False))
+def test_parse_file_entities(mock_config, extension_initial_dot):
     filename = '/sub-03_ses-07_run-4_desc-bleargh_sekret.nii.gz'
 
+    dot = '.' if extension_initial_dot else ''
+
     # Test with entities taken from bids config
     target = {'subject': '03', 'session': '07', 'run': 4, 'suffix': 'sekret',
-              'extension': 'nii.gz'}
+              'extension': dot + 'nii.gz'}
     assert target == parse_file_entities(filename, config='bids')
     config = Config.load('bids')
     assert target == parse_file_entities(filename, config=[config])
 
     # Test with entities taken from bids and derivatives config
     target = {'subject': '03', 'session': '07', 'run': 4, 'suffix': 'sekret',
-              'desc': 'bleargh', 'extension': 'nii.gz'}
+              'desc': 'bleargh', 'extension': dot + 'nii.gz'}
     assert target == parse_file_entities(filename)
     assert target == parse_file_entities(
         filename, config=['bids', 'derivatives'])
@@ -506,6 +510,7 @@ def test_parse_file_entities():
     assert target == parse_file_entities(filename, entities=entities)
 
 
+# XXX 0.14: Add dot to extension (difficult to parametrize with module-scoped fixture)
 def test_parse_file_entities_from_layout(layout_synthetic):
     layout = layout_synthetic
     filename = '/sub-03_ses-07_run-4_desc-bleargh_sekret.nii.gz'
@@ -600,7 +605,7 @@ def test_get_dataset_description(layout_ds005_multi_derivs):
 
 def test_indexed_file_associations(layout_7t_trt):
     img = layout_7t_trt.get(subject='01', run=1, suffix='bold', session='1',
-                            acquisition='fullbrain', extension='nii.gz')[0]
+                            acquisition='fullbrain', extension='.nii.gz')[0]
     assocs = img.get_associations()
     assert len(assocs) == 3
     targets = [
@@ -654,13 +659,13 @@ def test_get_with_regex_search(layout_7t_trt):
 
     # subject matches both '10' and '01'
     results = l.get(subject='1', session='1', task='rest', suffix='bold',
-                    acquisition='fron.al', extension='nii.gz',
+                    acquisition='fron.al', extension='.nii.gz',
                     regex_search=True)
     assert len(results) == 2
 
     # subject matches '10'
     results = l.get(subject='^1', session='1', task='rest', suffix='bold',
-                    acquisition='fron.al', extension='nii.gz',
+                    acquisition='fron.al', extension='.nii.gz',
                     regex_search=True, return_type='filename')
     assert len(results) == 1
     assert results[0].endswith('sub-10_ses-1_task-rest_acq-prefrontal_bold.nii.gz')
@@ -672,7 +677,7 @@ def test_get_with_regex_search_bad_dtype(layout_7t_trt):
     appropriately). """
     l = layout_7t_trt
     results = l.get(subject='1', run=1, task='rest', suffix='bold',
-                    acquisition='fullbrain', extension='nii.gz',
+                    acquisition='fullbrain', extension='.nii.gz',
                     regex_search=True)
     # Two runs (1 per session) for each of subjects '10' and '01'
     assert len(results) == 4
diff --git a/bids/layout/tests/test_writing.py b/bids/layout/tests/test_writing.py
index 476b59df..ac14026c 100644
--- a/bids/layout/tests/test_writing.py
+++ b/bids/layout/tests/test_writing.py
@@ -66,6 +66,9 @@ class TestWritableFile:
         assert _PATTERN_FIND.findall('{extension<nii|nii.gz|json>|nii.gz}') == [
             ('{extension<nii|nii.gz|json>|nii.gz}', 'extension', 'nii|nii.gz|json', 'nii.gz')
         ]
+        assert _PATTERN_FIND.findall('{extension<.nii|.nii.gz|.json>|.nii.gz}') == [
+            ('{extension<.nii|.nii.gz|.json>|.nii.gz}', 'extension', '.nii|.nii.gz|.json', '.nii.gz')
+        ]
         assert _PATTERN_FIND.findall('{extension<json|jsld>|json}') == [
             ('{extension<json|jsld>|json}', 'extension', 'json|jsld', 'json')
         ]
diff --git a/bids/reports/tests/test_report.py b/bids/reports/tests/test_report.py
index 798eafad..ebb1afbd 100644
--- a/bids/reports/tests/test_report.py
+++ b/bids/reports/tests/test_report.py
@@ -39,7 +39,7 @@ def test_report_gen_from_files(testlayout):
     descriptions in the dataset.
     """
     report = BIDSReport(testlayout)
-    files = testlayout.get(extension=['nii.gz', 'nii'])
+    files = testlayout.get(extension=['.nii.gz', '.nii'])
     descriptions = report.generate_from_files(files)
     assert isinstance(descriptions, Counter)
 
diff --git a/bids/tests/test_config.py b/bids/tests/test_config.py
index 874e0a51..3bcb3406 100644
--- a/bids/tests/test_config.py
+++ b/bids/tests/test_config.py
@@ -4,6 +4,7 @@ import os
 import json
 import pytest
 from bids.config import reset_options
+from bids.tests import get_test_data_path
 
 
 def test_load_from_standard_paths():
@@ -49,3 +50,28 @@ def test_set_option():
 # def test_get_option():
 #     reset_options(False)
 #     assert not bids.config.get_option('loop_preproc')
+
+
+def test_extension_initial_dot(mock_config):
+    ds117 = os.path.join(get_test_data_path(), 'ds000117')
+
+    # Warn if creating a layout without declaring a mode
+    bids.config.set_option('extension_initial_dot', None)
+    with pytest.warns(FutureWarning, match='To suppress this warning'):
+        layout = bids.BIDSLayout(ds117)
+    assert layout.get(extension='nii.gz')[0].entities['extension'] == 'nii.gz'
+
+    # Warn if setting the mode to False... this isn't sticking around
+    with pytest.warns(FutureWarning, match='will be disabled'):
+        bids.config.set_option('extension_initial_dot', False)
+    with pytest.warns(None) as record:
+        layout = bids.BIDSLayout(ds117)
+    assert len(record) == 0
+    assert layout.get(extension='nii.gz')[0].entities['extension'] == 'nii.gz'
+
+    # No warnings to move to dot mode
+    with pytest.warns(None) as record:
+        bids.config.set_option('extension_initial_dot', True)
+        layout = bids.BIDSLayout(ds117)
+    assert len(record) == 0
+    assert layout.get(extension='nii.gz')[0].entities['extension'] == '.nii.gz'
diff --git a/bids/variables/tests/test_io.py b/bids/variables/tests/test_io.py
index ab99661a..c4690ce8 100644
--- a/bids/variables/tests/test_io.py
+++ b/bids/variables/tests/test_io.py
@@ -2,6 +2,7 @@ from bids.layout import BIDSLayout
 from bids.variables import (SparseRunVariable, SimpleVariable,
                             DenseRunVariable, load_variables)
 from bids.variables.entities import Node, RunNode, NodeIndex
+from unittest.mock import patch
 import pytest
 from os.path import join
 from bids.tests import get_test_data_path
@@ -17,15 +18,19 @@ def layout1():
 
 @pytest.fixture(scope="module", params=["events", "preproc"])
 def synthetic(request):
-    root = join(get_test_data_path(), 'synthetic')
-    if request.param == 'preproc':
-        layout = BIDSLayout(root, derivatives=True)
-        dataset = load_variables(layout, skip_empty=True, desc='preproc',
-                                 space='T1w')
-    else:
-        layout = BIDSLayout(root)
-        dataset = load_variables(layout, skip_empty=True)
-    yield request.param, dataset
+    import bids.config
+    # Not testing with/without here
+    with patch.dict('bids.config._settings'):
+        bids.config.set_option('extension_initial_dot', True)
+        root = join(get_test_data_path(), 'synthetic')
+        if request.param == 'preproc':
+            layout = BIDSLayout(root, derivatives=True)
+            dataset = load_variables(layout, skip_empty=True, desc='preproc',
+                                     space='T1w')
+        else:
+            layout = BIDSLayout(root)
+            dataset = load_variables(layout, skip_empty=True)
+        yield request.param, dataset
 
 
 def test_load_events(layout1):