We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
replace_entities() modifies entities
I guess it is by design, but replace_entities() modifies the input entities as it goes. I find any function that modifies the input values surprising, but it also means that previous path_patterns can affect the entities as they are iterated.

I think the function should return a new entities with the correct entities if this is useful. However, this failing, the function definitely shouldn't modify entities unless it actually returns something other than None.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `bids/layout/writing.py`
Content:
```
1 '''
2 Contains helper functions that involve writing operations.
3 '''
4 
5 import warnings
6 import os
7 import re
8 import sys
9 from ..utils import splitext, listify
10 from os.path import join, dirname, exists, islink, isabs, isdir
11 
12 
13 __all__ = ['replace_entities', 'build_path', 'write_contents_to_file']
14 
15 
16 def replace_entities(entities, pattern):
17     """
18     Replaces all entity names in a given pattern with the corresponding
19     values provided by entities.
20 
21     Args:
22         entities (dict): A dictionary mapping entity names to entity values.
23         pattern (str): A path pattern that contains entity names denoted
24             by curly braces. Optional portions denoted by square braces.
25             For example: 'sub-{subject}/[var-{name}/]{id}.csv'
26             Accepted entity values, using regex matching, denoted within angle
27             brackets.
28             For example: 'sub-{subject<01|02>}/{task}.csv'
29 
30     Returns:
31         A new string with the entity values inserted where entity names
32         were denoted in the provided pattern.
33     """
34     ents = re.findall(r'\{(.*?)\}', pattern)
35     new_path = pattern
36     for ent in ents:
37         match = re.search(r'([^|<]+)(<.*?>)?(\|.*)?', ent)
38         if match is None:
39             return None
40         name, valid, default = match.groups()
41         default = default[1:] if default is not None else default
42 
43         if name in entities and valid is not None:
44             ent_val = str(entities[name])
45             if not re.match(valid[1:-1], ent_val):
46                 if default is None:
47                     return None
48                 entities[name] = default
49 
50         ent_val = entities.get(name, default)
51         if ent_val is None:
52             return None
53         new_path = new_path.replace('{%s}' % ent, str(ent_val))
54 
55     return new_path
56 
57 
58 def build_path(entities, path_patterns, strict=False):
59     """
60     Constructs a path given a set of entities and a list of potential
61     filename patterns to use.
62 
63     Args:
64         entities (dict): A dictionary mapping entity names to entity values.
65         path_patterns (str, list): One or more filename patterns to write
66             the file to. Entities should be represented by the name
67             surrounded by curly braces. Optional portions of the patterns
68             should be denoted by square brackets. Entities that require a
69             specific value for the pattern to match can pass them inside
70             carets. Default values can be assigned by specifying a string after
71             the pipe operator. E.g., (e.g., {type<image>|bold} would only match
72             the pattern if the entity 'type' was passed and its value is
73             "image", otherwise the default value "bold" will be used).
74                 Example 1: 'sub-{subject}/[var-{name}/]{id}.csv'
75                 Result 2: 'sub-01/var-SES/1045.csv'
76         strict (bool): If True, all passed entities must be matched inside a
77             pattern in order to be a valid match. If False, extra entities will
78             be ignored so long as all mandatory entities are found.
79 
80     Returns:
81         A constructed path for this file based on the provided patterns.
82     """
83     path_patterns = listify(path_patterns)
84 
85     # Loop over available patherns, return first one that matches all
86     for pattern in path_patterns:
87         # If strict, all entities must be contained in the pattern
88         if strict:
89             defined = re.findall(r'\{(.*?)(?:<[^>]+>)?\}', pattern)
90             if set(entities.keys()) - set(defined):
91                 continue
92         # Iterate through the provided path patterns
93         new_path = pattern
94         optional_patterns = re.findall(r'\[(.*?)\]', pattern)
95         # First build from optional patterns if possible
96         for optional_pattern in optional_patterns:
97             optional_chunk = replace_entities(entities, optional_pattern) or ''
98             new_path = new_path.replace('[%s]' % optional_pattern,
99                                         optional_chunk)
100         # Replace remaining entities
101         new_path = replace_entities(entities, new_path)
102 
103         if new_path:
104             return new_path
105 
106     return None
107 
108 
109 def write_contents_to_file(path, contents=None, link_to=None,
110                            content_mode='text', root=None, conflicts='fail'):
111     """
112     Uses provided filename patterns to write contents to a new path, given
113     a corresponding entity map.
114 
115     Args:
116         path (str): Destination path of the desired contents.
117         contents (str): Raw text or binary encoded string of contents to write
118             to the new path.
119         link_to (str): Optional path with which to create a symbolic link to.
120             Used as an alternative to and takes priority over the contents
121             argument.
122         content_mode (str): Either 'text' or 'binary' to indicate the writing
123             mode for the new file. Only relevant if contents is provided.
124         root (str): Optional root directory that all patterns are relative
125             to. Defaults to current working directory.
126         conflicts (str): One of 'fail', 'skip', 'overwrite', or 'append'
127             that defines the desired action when the output path already
128             exists. 'fail' raises an exception; 'skip' does nothing;
129             'overwrite' overwrites the existing file; 'append' adds  a suffix
130             to each file copy, starting with 1. Default is 'fail'.
131     """
132 
133     if root is None and not isabs(path):
134         root = os.getcwd()
135 
136     if root:
137         path = join(root, path)
138 
139     if exists(path) or islink(path):
140         if conflicts == 'fail':
141             msg = 'A file at path {} already exists.'
142             raise ValueError(msg.format(path))
143         elif conflicts == 'skip':
144             msg = 'A file at path {} already exists, skipping writing file.'
145             warnings.warn(msg.format(path))
146             return
147         elif conflicts == 'overwrite':
148             if isdir(path):
149                 warnings.warn('New path is a directory, not going to '
150                              'overwrite it, skipping instead.')
151                 return
152             os.remove(path)
153         elif conflicts == 'append':
154             i = 1
155             while i < sys.maxsize:
156                 path_splits = splitext(path)
157                 path_splits[0] = path_splits[0] + '_%d' % i
158                 appended_filename = os.extsep.join(path_splits)
159                 if not exists(appended_filename) and \
160                    not islink(appended_filename):
161                     path = appended_filename
162                     break
163                 i += 1
164         else:
165             raise ValueError('Did not provide a valid conflicts parameter')
166 
167     if not exists(dirname(path)):
168         os.makedirs(dirname(path))
169 
170     if link_to:
171         os.symlink(link_to, path)
172     elif contents:
173         mode = 'wb' if content_mode == 'binary' else 'w'
174         with open(path, mode) as f:
175             f.write(contents)
176     else:
177         raise ValueError('One of contents or link_to must be provided.')
178
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/bids/layout/writing.py b/bids/layout/writing.py
--- a/bids/layout/writing.py
+++ b/bids/layout/writing.py
@@ -31,6 +31,7 @@
         A new string with the entity values inserted where entity names
         were denoted in the provided pattern.
     """
+    entities = entities.copy()  # make a local copy, since dicts are mutable
     ents = re.findall(r'\{(.*?)\}', pattern)
     new_path = pattern
     for ent in ents:

{"golden_diff": "diff --git a/bids/layout/writing.py b/bids/layout/writing.py\n--- a/bids/layout/writing.py\n+++ b/bids/layout/writing.py\n@@ -31,6 +31,7 @@\n         A new string with the entity values inserted where entity names\n         were denoted in the provided pattern.\n     \"\"\"\n+    entities = entities.copy()  # make a local copy, since dicts are mutable\n     ents = re.findall(r'\\{(.*?)\\}', pattern)\n     new_path = pattern\n     for ent in ents:\n", "issue": "replace_entities() modifies entities\nI guess it is by design, but replace_entities() modifies the input entities as it goes. I find any function that modifies the input values surprising, but it also means that previous path_patterns can affect the entities as they are iterated.\r\n\r\nI think the function should return a new entities with the correct entities if this is useful. However, this failing, the function definitely shouldn't modify entities unless it actually returns something other than None.\n", "before_files": [{"content": "'''\nContains helper functions that involve writing operations.\n'''\n\nimport warnings\nimport os\nimport re\nimport sys\nfrom ..utils import splitext, listify\nfrom os.path import join, dirname, exists, islink, isabs, isdir\n\n\n__all__ = ['replace_entities', 'build_path', 'write_contents_to_file']\n\n\ndef replace_entities(entities, pattern):\n    \"\"\"\n    Replaces all entity names in a given pattern with the corresponding\n    values provided by entities.\n\n    Args:\n        entities (dict): A dictionary mapping entity names to entity values.\n        pattern (str): A path pattern that contains entity names denoted\n            by curly braces. Optional portions denoted by square braces.\n            For example: 'sub-{subject}/[var-{name}/]{id}.csv'\n            Accepted entity values, using regex matching, denoted within angle\n            brackets.\n            For example: 'sub-{subject<01|02>}/{task}.csv'\n\n    Returns:\n        A new string with the entity values inserted where entity names\n        were denoted in the provided pattern.\n    \"\"\"\n    ents = re.findall(r'\\{(.*?)\\}', pattern)\n    new_path = pattern\n    for ent in ents:\n        match = re.search(r'([^|<]+)(<.*?>)?(\\|.*)?', ent)\n        if match is None:\n            return None\n        name, valid, default = match.groups()\n        default = default[1:] if default is not None else default\n\n        if name in entities and valid is not None:\n            ent_val = str(entities[name])\n            if not re.match(valid[1:-1], ent_val):\n                if default is None:\n                    return None\n                entities[name] = default\n\n        ent_val = entities.get(name, default)\n        if ent_val is None:\n            return None\n        new_path = new_path.replace('{%s}' % ent, str(ent_val))\n\n    return new_path\n\n\ndef build_path(entities, path_patterns, strict=False):\n    \"\"\"\n    Constructs a path given a set of entities and a list of potential\n    filename patterns to use.\n\n    Args:\n        entities (dict): A dictionary mapping entity names to entity values.\n        path_patterns (str, list): One or more filename patterns to write\n            the file to. Entities should be represented by the name\n            surrounded by curly braces. Optional portions of the patterns\n            should be denoted by square brackets. Entities that require a\n            specific value for the pattern to match can pass them inside\n            carets. Default values can be assigned by specifying a string after\n            the pipe operator. E.g., (e.g., {type<image>|bold} would only match\n            the pattern if the entity 'type' was passed and its value is\n            \"image\", otherwise the default value \"bold\" will be used).\n                Example 1: 'sub-{subject}/[var-{name}/]{id}.csv'\n                Result 2: 'sub-01/var-SES/1045.csv'\n        strict (bool): If True, all passed entities must be matched inside a\n            pattern in order to be a valid match. If False, extra entities will\n            be ignored so long as all mandatory entities are found.\n\n    Returns:\n        A constructed path for this file based on the provided patterns.\n    \"\"\"\n    path_patterns = listify(path_patterns)\n\n    # Loop over available patherns, return first one that matches all\n    for pattern in path_patterns:\n        # If strict, all entities must be contained in the pattern\n        if strict:\n            defined = re.findall(r'\\{(.*?)(?:<[^>]+>)?\\}', pattern)\n            if set(entities.keys()) - set(defined):\n                continue\n        # Iterate through the provided path patterns\n        new_path = pattern\n        optional_patterns = re.findall(r'\\[(.*?)\\]', pattern)\n        # First build from optional patterns if possible\n        for optional_pattern in optional_patterns:\n            optional_chunk = replace_entities(entities, optional_pattern) or ''\n            new_path = new_path.replace('[%s]' % optional_pattern,\n                                        optional_chunk)\n        # Replace remaining entities\n        new_path = replace_entities(entities, new_path)\n\n        if new_path:\n            return new_path\n\n    return None\n\n\ndef write_contents_to_file(path, contents=None, link_to=None,\n                           content_mode='text', root=None, conflicts='fail'):\n    \"\"\"\n    Uses provided filename patterns to write contents to a new path, given\n    a corresponding entity map.\n\n    Args:\n        path (str): Destination path of the desired contents.\n        contents (str): Raw text or binary encoded string of contents to write\n            to the new path.\n        link_to (str): Optional path with which to create a symbolic link to.\n            Used as an alternative to and takes priority over the contents\n            argument.\n        content_mode (str): Either 'text' or 'binary' to indicate the writing\n            mode for the new file. Only relevant if contents is provided.\n        root (str): Optional root directory that all patterns are relative\n            to. Defaults to current working directory.\n        conflicts (str): One of 'fail', 'skip', 'overwrite', or 'append'\n            that defines the desired action when the output path already\n            exists. 'fail' raises an exception; 'skip' does nothing;\n            'overwrite' overwrites the existing file; 'append' adds  a suffix\n            to each file copy, starting with 1. Default is 'fail'.\n    \"\"\"\n\n    if root is None and not isabs(path):\n        root = os.getcwd()\n\n    if root:\n        path = join(root, path)\n\n    if exists(path) or islink(path):\n        if conflicts == 'fail':\n            msg = 'A file at path {} already exists.'\n            raise ValueError(msg.format(path))\n        elif conflicts == 'skip':\n            msg = 'A file at path {} already exists, skipping writing file.'\n            warnings.warn(msg.format(path))\n            return\n        elif conflicts == 'overwrite':\n            if isdir(path):\n                warnings.warn('New path is a directory, not going to '\n                             'overwrite it, skipping instead.')\n                return\n            os.remove(path)\n        elif conflicts == 'append':\n            i = 1\n            while i < sys.maxsize:\n                path_splits = splitext(path)\n                path_splits[0] = path_splits[0] + '_%d' % i\n                appended_filename = os.extsep.join(path_splits)\n                if not exists(appended_filename) and \\\n                   not islink(appended_filename):\n                    path = appended_filename\n                    break\n                i += 1\n        else:\n            raise ValueError('Did not provide a valid conflicts parameter')\n\n    if not exists(dirname(path)):\n        os.makedirs(dirname(path))\n\n    if link_to:\n        os.symlink(link_to, path)\n    elif contents:\n        mode = 'wb' if content_mode == 'binary' else 'w'\n        with open(path, mode) as f:\n            f.write(contents)\n    else:\n        raise ValueError('One of contents or link_to must be provided.')\n", "path": "bids/layout/writing.py"}], "after_files": [{"content": "'''\nContains helper functions that involve writing operations.\n'''\n\nimport warnings\nimport os\nimport re\nimport sys\nfrom ..utils import splitext, listify\nfrom os.path import join, dirname, exists, islink, isabs, isdir\n\n\n__all__ = ['replace_entities', 'build_path', 'write_contents_to_file']\n\n\ndef replace_entities(entities, pattern):\n    \"\"\"\n    Replaces all entity names in a given pattern with the corresponding\n    values provided by entities.\n\n    Args:\n        entities (dict): A dictionary mapping entity names to entity values.\n        pattern (str): A path pattern that contains entity names denoted\n            by curly braces. Optional portions denoted by square braces.\n            For example: 'sub-{subject}/[var-{name}/]{id}.csv'\n            Accepted entity values, using regex matching, denoted within angle\n            brackets.\n            For example: 'sub-{subject<01|02>}/{task}.csv'\n\n    Returns:\n        A new string with the entity values inserted where entity names\n        were denoted in the provided pattern.\n    \"\"\"\n    entities = entities.copy()  # make a local copy, since dicts are mutable\n    ents = re.findall(r'\\{(.*?)\\}', pattern)\n    new_path = pattern\n    for ent in ents:\n        match = re.search(r'([^|<]+)(<.*?>)?(\\|.*)?', ent)\n        if match is None:\n            return None\n        name, valid, default = match.groups()\n        default = default[1:] if default is not None else default\n\n        if name in entities and valid is not None:\n            ent_val = str(entities[name])\n            if not re.match(valid[1:-1], ent_val):\n                if default is None:\n                    return None\n                entities[name] = default\n\n        ent_val = entities.get(name, default)\n        if ent_val is None:\n            return None\n        new_path = new_path.replace('{%s}' % ent, str(ent_val))\n\n    return new_path\n\n\ndef build_path(entities, path_patterns, strict=False):\n    \"\"\"\n    Constructs a path given a set of entities and a list of potential\n    filename patterns to use.\n\n    Args:\n        entities (dict): A dictionary mapping entity names to entity values.\n        path_patterns (str, list): One or more filename patterns to write\n            the file to. Entities should be represented by the name\n            surrounded by curly braces. Optional portions of the patterns\n            should be denoted by square brackets. Entities that require a\n            specific value for the pattern to match can pass them inside\n            carets. Default values can be assigned by specifying a string after\n            the pipe operator. E.g., (e.g., {type<image>|bold} would only match\n            the pattern if the entity 'type' was passed and its value is\n            \"image\", otherwise the default value \"bold\" will be used).\n                Example 1: 'sub-{subject}/[var-{name}/]{id}.csv'\n                Result 2: 'sub-01/var-SES/1045.csv'\n        strict (bool): If True, all passed entities must be matched inside a\n            pattern in order to be a valid match. If False, extra entities will\n            be ignored so long as all mandatory entities are found.\n\n    Returns:\n        A constructed path for this file based on the provided patterns.\n    \"\"\"\n    path_patterns = listify(path_patterns)\n\n    # Loop over available patherns, return first one that matches all\n    for pattern in path_patterns:\n        # If strict, all entities must be contained in the pattern\n        if strict:\n            defined = re.findall(r'\\{(.*?)(?:<[^>]+>)?\\}', pattern)\n            if set(entities.keys()) - set(defined):\n                continue\n        # Iterate through the provided path patterns\n        new_path = pattern\n        optional_patterns = re.findall(r'\\[(.*?)\\]', pattern)\n        # First build from optional patterns if possible\n        for optional_pattern in optional_patterns:\n            optional_chunk = replace_entities(entities, optional_pattern) or ''\n            new_path = new_path.replace('[%s]' % optional_pattern,\n                                        optional_chunk)\n        # Replace remaining entities\n        new_path = replace_entities(entities, new_path)\n\n        if new_path:\n            return new_path\n\n    return None\n\n\ndef write_contents_to_file(path, contents=None, link_to=None,\n                           content_mode='text', root=None, conflicts='fail'):\n    \"\"\"\n    Uses provided filename patterns to write contents to a new path, given\n    a corresponding entity map.\n\n    Args:\n        path (str): Destination path of the desired contents.\n        contents (str): Raw text or binary encoded string of contents to write\n            to the new path.\n        link_to (str): Optional path with which to create a symbolic link to.\n            Used as an alternative to and takes priority over the contents\n            argument.\n        content_mode (str): Either 'text' or 'binary' to indicate the writing\n            mode for the new file. Only relevant if contents is provided.\n        root (str): Optional root directory that all patterns are relative\n            to. Defaults to current working directory.\n        conflicts (str): One of 'fail', 'skip', 'overwrite', or 'append'\n            that defines the desired action when the output path already\n            exists. 'fail' raises an exception; 'skip' does nothing;\n            'overwrite' overwrites the existing file; 'append' adds  a suffix\n            to each file copy, starting with 1. Default is 'fail'.\n    \"\"\"\n\n    if root is None and not isabs(path):\n        root = os.getcwd()\n\n    if root:\n        path = join(root, path)\n\n    if exists(path) or islink(path):\n        if conflicts == 'fail':\n            msg = 'A file at path {} already exists.'\n            raise ValueError(msg.format(path))\n        elif conflicts == 'skip':\n            msg = 'A file at path {} already exists, skipping writing file.'\n            warnings.warn(msg.format(path))\n            return\n        elif conflicts == 'overwrite':\n            if isdir(path):\n                warnings.warn('New path is a directory, not going to '\n                             'overwrite it, skipping instead.')\n                return\n            os.remove(path)\n        elif conflicts == 'append':\n            i = 1\n            while i < sys.maxsize:\n                path_splits = splitext(path)\n                path_splits[0] = path_splits[0] + '_%d' % i\n                appended_filename = os.extsep.join(path_splits)\n                if not exists(appended_filename) and \\\n                   not islink(appended_filename):\n                    path = appended_filename\n                    break\n                i += 1\n        else:\n            raise ValueError('Did not provide a valid conflicts parameter')\n\n    if not exists(dirname(path)):\n        os.makedirs(dirname(path))\n\n    if link_to:\n        os.symlink(link_to, path)\n    elif contents:\n        mode = 'wb' if content_mode == 'binary' else 'w'\n        with open(path, mode) as f:\n            f.write(contents)\n    else:\n        raise ValueError('One of contents or link_to must be provided.')\n", "path": "bids/layout/writing.py"}]}