Coverage for src/file_tree/file_tree.py: 87%

463 statements  

« prev     ^ index     » next       coverage.py v7.6.9, created at 2024-12-17 13:27 +0000

1"""Defines the main FileTree object, which will be the main point of interaction.""" 

2import os 

3import string 

4import warnings 

5from collections import defaultdict 

6from difflib import get_close_matches 

7from functools import cmp_to_key 

8from pathlib import Path 

9from shutil import copyfile 

10from typing import Any, Collection, Dict, Generator, Optional, Sequence, Set, Union 

11from warnings import warn 

12 

13import numpy as np 

14import rich 

15import xarray 

16 

17from .template import Placeholders, Template, is_singular, DuplicateTemplate 

18 

19 

20class FileTree: 

21 """Represents a structured directory. 

22 

23 The many methods can be split into 4 categories 

24 

25 1. The template interface. Each path (file or directory) is represented by a :class:`Template <file_tree.template.Template>`, 

26 which defines the filename with any unknown parts (e.g., subject ID) marked by placeholders. 

27 Templates are accessed based on their key. 

28 

29 - :meth:`get_template`: used to access a template based on its key. 

30 - :meth:`template_keys`: used to list all the template keys. 

31 - :meth:`add_template`: used to add a new template or overwrite an existing one. 

32 - :meth:`add_subtree`: can be used to add all the templates from a different tree to this one. 

33 - :meth:`override`: overrides some of the templates in this FileTree with that of another FileTree. 

34 - :meth:`filter_templates`: reduce the filetree to a user-provided list of templates and its parents 

35 

36 2. The placeholder interface. Placeholders represent values to be filled into the placeholders. 

37 Each placeholder can be either undefined, have a singular value, or have a sequence of possible values. 

38 

39 - You can access the :class:`placeholders dictionary-like object <file_tree.template.Placeholders>` directly through `FileTree.placeholders` 

40 - :meth:`update`: returns a new FileTree with updated placeholders or updates the placeholders in the current one. 

41 - :meth:`update_glob`: sets the placeholder values based on which files/directories exist on disk. 

42 - :meth:`iter_vars`: iterate over all possible values for the selected placeholders. 

43 - :meth:`iter`: iterate over all possible values for the placeholders that are part of a given template. 

44 

45 3. Getting the actual filenames based on filling the placeholder values into the templates. 

46 

47 - :meth:`get`: Returns a valid path by filling in all the placeholders in a template. 

48 For this to work all placeholder values should be defined and singular. 

49 - :meth:`get_mult`: Returns array of all possible valid paths by filling in the placeholders in a template. 

50 Placeholder values can be singular or a sequence of possible values. 

51 - :meth:`get_mult_glob`: Returns array with existing paths on disk. 

52 Placeholder values can be singular, a sequence of possible values, or undefined. 

53 In the latter case possible values for that placeholder are determined by checking the disk. 

54 - :meth:`fill`: Returns new FileTree with any singular values filled into the templates and removed from the placeholder dict. 

55 

56 4. Input/output 

57 

58 - :meth:`report`: create a pretty overview of the filetree 

59 - :meth:`run_app`: opens a terminal-based App to explore the filetree interactively 

60 - :meth:`empty`: creates empty FileTree with no templates or placeholder values. 

61 - :meth:`read`: reads a new FileTree from a file. 

62 - :meth:`from_string`: reads a new FileTree from a string. 

63 - :meth:`write`: writes a FileTree to a file. 

64 - :meth:`to_string`: writes a FileTree to a string. 

65 """ 

66 

67 def __init__( 

68 self, 

69 templates: Dict[str, Template], 

70 placeholders: Union[Dict[str, Any], Placeholders], 

71 return_path=False, 

72 glob=True, 

73 ): 

74 """Create a new FileTree with provided templates/placeholders.""" 

75 self._templates = templates 

76 self.placeholders = Placeholders(placeholders) 

77 self.return_path = return_path 

78 self.glob = glob 

79 if not ( 

80 glob in (False, True, "default", "first", "last") or 

81 callable(glob) 

82 ): 

83 raise ValueError(f"Invalid value for `glob` ({glob}). Please note that from v1.6 `glob` is now a setting in `FileTree` and can no longer be used as a placeholder name.") 

84 

85 # create new FileTree objects 

86 @classmethod 

87 def empty( 

88 cls, top_level: Union[str, Template] = ".", return_path=False, glob=True 

89 ) -> "FileTree": 

90 """Create a new empty FileTree containing only a top-level directory. 

91 

92 Args: 

93 top_level: Top-level directory that other templates will use as a reference. Defaults to current directory. 

94 return_path: if True, returns filenames as Path objects rather than strings. 

95 glob: determines whether to allow filename pattern matching in the templates. Globbing is only applied if there are `*` or `?` characters in the template. Possible values are: 

96 - `False`: do not do any pattern matching (identical to <= v1.5 behaviour). 

97 - `True`/"default": return filename if there is a single match. Raise an error otherwise. This is the default behaviour in v1.6 or later. 

98 - "first"/"last": return the first or last match (based on alphabetical ordering). An error is raised if there are no matches. 

99 - callable: return the match returned by the callable. The input to the callable is a list of all the matching filenames (possibly of zero length). 

100 

101 Returns: 

102 empty FileTree 

103 """ 

104 if not isinstance(top_level, Template): 

105 top_level = Template(None, top_level) 

106 return cls({"": top_level}, {}, return_path=return_path, glob=glob) 

107 

108 @classmethod 

109 def read( 

110 cls, 

111 name: str, 

112 top_level: Union[str, Template] = ".", 

113 return_path=False, 

114 glob=True, 

115 **placeholders, 

116 ) -> "FileTree": 

117 """Read a filetree based on the given name. 

118 

119 # noqa DAR101 

120 

121 Args: 

122 name: name of the filetree. Interpreted as: 

123 

124 - a filename containing the tree definition if "name" or "name.tree" exist on disk 

125 - one of the trees in `tree_directories` if one of those contains "name" or "name.tree" 

126 - one of the tree in the plugin FileTree modules 

127 

128 top_level: top-level directory name. Defaults to current directory. Set to parent template for sub-trees. 

129 return_path: if True, returns filenames as Path objects rather than strings. 

130 glob: determines whether to allow filename pattern matching in the templates. Globbing is only applied if there are `*` or `?` characters in the template. Possible values are: 

131 - `False`: do not do any pattern matching (identical to <= v1.5 behaviour). 

132 - `True`/"default": return filename if there is a single match. Raise an error otherwise. This is the default behaviour in v1.6 or later. 

133 - "first"/"last": return the first or last match (based on alphabetical ordering). An error is raised if there are no matches. 

134 - callable: return the match returned by the callable. The input to the callable is a list of all the matching filenames (possibly of zero length). 

135 placeholders: maps placeholder names to their values 

136 

137 Raises: 

138 ValueError: if FileTree is not found. 

139 

140 Returns: 

141 FileTree: tree matching the definition in the file 

142 """ 

143 from . import parse_tree 

144 

145 if "directory" in placeholders: 

146 warnings.warn( 

147 f"Setting the 'directory' placeholder to {placeholders['directory']}. " 

148 + "This differs from the behaviour of the old filetree in fslpy, which used the `directory` keyword to set the top-level directory. " 

149 + "If you want to do that, please use the new `top_level` keyword instead of `directory`." 

150 ) 

151 found_tree = parse_tree.search_tree(name) 

152 if isinstance(found_tree, Path): 

153 with open(found_tree, "r") as f: 

154 text = f.read() 

155 with parse_tree.extra_tree_dirs([found_tree.parent]): 

156 return cls.from_string( 

157 text, top_level, return_path=return_path, glob=glob, **placeholders 

158 ) 

159 elif isinstance(found_tree, str): 

160 return cls.from_string( 

161 found_tree, top_level, return_path=return_path, glob=glob, **placeholders 

162 ) 

163 elif isinstance(found_tree, FileTree): 

164 new_tree = cls.empty(top_level, return_path, glob=glob) 

165 new_tree.add_subtree(found_tree, fill=False) 

166 return new_tree.update(**placeholders) 

167 raise ValueError( 

168 f"Type of object ({type(found_tree)}) returned when searching for FileTree named '{name}' was not recognised" 

169 ) 

170 

171 @classmethod 

172 def from_string( 

173 cls, 

174 definition: str, 

175 top_level: Union[str, Template] = ".", 

176 return_path=False, 

177 glob=True, 

178 **placeholders, 

179 ) -> "FileTree": 

180 """Create a FileTree based on the given definition. 

181 

182 Args: 

183 definition: A FileTree definition describing a structured directory 

184 top_level: top-level directory name. Defaults to current directory. Set to parent template for sub-trees. 

185 return_path: if True, returns filenames as Path objects rather than strings. 

186 glob: determines whether to allow filename pattern matching in the templates. Globbing is only applied if there are `*` or `?` characters in the string. Possible values are: 

187 - `False`: do not do any pattern matching (identical to <= v1.5 behaviour). 

188 - `True`/"default": return filename if there is a single match. Raise an error otherwise. This is the default behaviour in v1.6 or later. 

189 - "first"/"last": return the first or last match (based on alphabetical ordering). An error is raised if there are no matches. 

190 - callable: return the match returned by the callable. The input to the callable is a list of all the matching filenames (possibly of zero length). 

191 placeholders: key->value pairs setting initial value for any placeholders. 

192 

193 Returns: 

194 FileTree: tree matching the definition in the file 

195 """ 

196 from . import parse_tree 

197 

198 res = parse_tree.read_file_tree_text(definition.splitlines(), top_level).update( 

199 inplace=True, **placeholders 

200 ) 

201 res.return_path = return_path 

202 res.glob = glob 

203 return res 

204 

205 def copy( 

206 self, 

207 ) -> "FileTree": 

208 """Create a copy of the tree. 

209 

210 The dictionaries (templates, placeholders) are copied, but the values within them are not. 

211 

212 Returns: 

213 FileTree: new tree object with identical templates, sub-trees and placeholders 

214 """ 

215 new_tree = type(self)( 

216 dict(self._templates), Placeholders(self.placeholders), self.return_path, self.glob 

217 ) 

218 return new_tree 

219 

220 # template interface 

221 def get_template(self, key: str, error_duplicate=True) -> Template: 

222 """Return the template corresponding to `key`. 

223 

224 Raises: 

225 KeyError: if no template with that identifier is available # noqa DAR402 

226 ValueError: if multiple templates with that identifier are available (suppress using `error_duplicate=False`) 

227 

228 Args: 

229 key (str): key identifying the template. 

230 error_duplicate (bool): set to False to return a `DuplicateTemplate` object rather than raising an error 

231 

232 Returns: 

233 Template: description of pathname with placeholders not filled in 

234 """ 

235 try: 

236 value = self._templates[key] 

237 if error_duplicate and isinstance(value, DuplicateTemplate): 

238 templates = ", ".join([str(t.as_path) for t in value.templates]) 

239 raise ValueError(f"There are multiple templates matching key '{key}': {templates}") 

240 return value 

241 except KeyError: 

242 pass 

243 matches = get_close_matches(key, self.template_keys()) 

244 if len(matches) == 0: 

245 raise KeyError(f"Template key '{key}' not found in FileTree.") 

246 else: 

247 raise KeyError( 

248 f"Template key '{key}' not found in FileTree; did you mean {' or '.join(sorted(matches))}?" 

249 ) 

250 

251 @property 

252 def top_level( 

253 self, 

254 ): 

255 """Top-level directory. 

256 

257 Within the template dictionary this top-level directory is represented with an empty string 

258 """ 

259 as_string = self.get_template("").unique_part 

260 if self.return_path: 

261 return Path(as_string) 

262 return str(as_string) 

263 

264 @top_level.setter 

265 def top_level(self, value: str): 

266 self.get_template("").unique_part = str(value) 

267 

268 def add_template( 

269 self, 

270 template_path: str, 

271 key: Optional[Union[str, Sequence[str]]] = None, 

272 parent: Optional[str] = "", 

273 overwrite=False, 

274 ) -> Template: 

275 """Update the FileTree with the new template. 

276 

277 Args: 

278 template_path: path name with respect to the parent (or top-level if no parent provided) 

279 key: key(s) to access this template in the future. Defaults to result from :meth:`Template.guess_key <file_tree.template.Template.guess_key>` 

280 (i.e., the path basename without the extension). 

281 parent: if defined, `template_path` will be interpreted as relative to this template. 

282 By default the top-level template is used as reference. 

283 To create a template unaffiliated with the rest of the tree, set `parent` to None. 

284 Such a template should be an absolute path or relative to the current directory and can be used as parent for other templates 

285 overwrite: if True, overwrites any existing template rather than raising a ValueError. Defaults to False. 

286 

287 Returns: 

288 Template: the newly added template object 

289 """ 

290 if parent is None: 

291 parent_template = None 

292 elif isinstance(parent, Template): 

293 parent_template = parent 

294 else: 

295 parent_template = self.get_template(parent) 

296 new_template = Template(parent_template, template_path) 

297 if ( 

298 parent_template is not None 

299 and new_template.as_path == parent_template.as_path 

300 ): 

301 new_template = parent_template 

302 return self._add_actual_template(new_template, key, overwrite=overwrite) 

303 

304 def _add_actual_template( 

305 self, 

306 template: Template, 

307 keys: Optional[Union[str, Sequence[str]]] = None, 

308 overwrite=False, 

309 ): 

310 if keys is None: 

311 keys = template.guess_key() 

312 if isinstance(keys, Path): 

313 keys = str(keys) 

314 if isinstance(keys, str): 

315 keys = [keys] 

316 for key in keys: 

317 if key in self._templates: 

318 old_template = self.get_template(key, error_duplicate=overwrite) 

319 if not overwrite: 

320 if isinstance(old_template, DuplicateTemplate): 

321 old_template.add_template(template) 

322 else: 

323 self._templates[key] = DuplicateTemplate(old_template, template) 

324 continue 

325 

326 for potential_child in self._templates.values(): 

327 if potential_child.parent is old_template: 

328 potential_child.parent = template 

329 self._templates[key] = template 

330 return template 

331 

332 def override(self, new_filetree: "FileTree", required: Collection[str]=[], optional: Collection[str]=[]): 

333 """Overide some templates and all placeholders in this filetree with templates from `new_filetree`. 

334  

335 A new `FileTree` is returned with all the template keys in `required` replaced or added. 

336 Template keys in `optional` will also be replaced or added if they are present in `new_filetree`. 

337 

338 Any placeholders defined in `new_filetree` will be transfered as well. 

339 

340 Without supplying any keys to `required` or `optional` the new `FileTree` will be identical to this one. 

341 """ 

342 if isinstance(required, str): 

343 required = [required] 

344 if isinstance(optional, str): 

345 optional = [optional] 

346 all_keys = set(required).union(optional) 

347 

348 old_duplicate_keys = self.template_keys(skip_duplicates=False).difference(self.template_keys(skip_duplicates=True)) 

349 duplicates = [key for key in all_keys if key in old_duplicate_keys] 

350 if len(duplicates) > 0: 

351 raise ValueError("Some of the keys to be replaced in the original FileTree are duplicates: %s", ", ".join(duplicates)) 

352 

353 new_available_keys = new_filetree.template_keys(skip_duplicates=False) 

354 undefined = [key for key in required if key not in new_available_keys] 

355 if len(undefined) > 0: 

356 raise ValueError("Some required keys are missing from the input FileTree: %s", ", ".join(undefined)) 

357 

358 new_duplicate_keys = new_available_keys.difference(new_filetree.template_keys(skip_duplicates=True)) 

359 duplicates = [key for key in all_keys if key in new_duplicate_keys] 

360 if len(duplicates) > 0: 

361 raise ValueError("Some of the keys to be used in the input FileTree are duplicates: %s", ", ".join(duplicates)) 

362 

363 res_filetree = self.copy() 

364 res_filetree.placeholders.update(new_filetree.placeholders) 

365 for key in all_keys: 

366 if key in new_available_keys: 

367 res_filetree._add_actual_template(new_filetree.get_template(key), key, overwrite=True) 

368 return res_filetree 

369 

370 @property 

371 def _iter_templates(self, ) -> Dict[Template, Set[str]]: 

372 result = defaultdict(set) 

373 def add_parent(t:Template): 

374 if t.parent is None or t.parent in result: 

375 return 

376 result[t.parent] 

377 add_parent(t.parent) 

378 

379 for (key, possible) in self._templates.items(): 

380 if isinstance(possible, DuplicateTemplate): 

381 for template in possible.templates: 

382 result[template].add(key) 

383 add_parent(template) 

384 else: 

385 result[possible].add(key) 

386 add_parent(possible) 

387 return dict(result) 

388 

389 def template_keys(self, only_leaves=False, skip_duplicates=True): 

390 """Return the keys of all the templates in the FileTree. 

391 

392 Each key will be returned for templates with multiple keys. 

393 

394 Args 

395 only_leaves (bool, optional): set to True to only return templates that do not have any children. 

396 skip_duplicates (bool, optional): set to False to include keys that point to multiple templates. 

397 """ 

398 if skip_duplicates: 

399 keys = {k for (k, v) in self._templates.items() if isinstance(v, Template)} 

400 else: 

401 keys = set(self._templates.keys()) 

402 if not only_leaves: 

403 return keys 

404 elif not skip_duplicates: 

405 raise ValueError("Cannot select only leaves when not skipping duplicates.") 

406 

407 parents = {t.parent for t in self._iter_templates.keys() if t.parent is not None} 

408 return { 

409 key for key in keys if self.get_template(key) not in parents 

410 } 

411 

412 def add_subtree( 

413 self, 

414 sub_tree: "FileTree", 

415 precursor: Union[Optional[str], Sequence[Optional[str]]] = (None,), 

416 parent: Optional[Union[str, Template]] = "", 

417 fill=None, 

418 ) -> None: 

419 """Update the templates and the placeholders in place with those in sub_tree. 

420 

421 The top-level directory of the sub-tree will be replaced by the `parent` (unless set to None). 

422 The sub-tree templates will be available with the key "<precursor>/<original_key>", 

423 unless the precursor is None in which case they will be unchanged (which can easily lead to errors due to naming conflicts). 

424 

425 What happens with the placeholder values of the sub-tree depends on whether the precursor is None or not: 

426 

427 - if the precursor is None, any singular values are directly filled into the sub-tree templates. 

428 Any placeholders with multiple values will be added to the top-level variable list (error is raised in case of conflicts). 

429 - if the precursor is a string, the templates are updated to look for "<precursor>/<original_placeholder>" and 

430 all sub-tree placeholder values are also prepended with this precursor. 

431 Any template values with "<precursor>/<key>" will first look for that full key, but if that is undefined 

432 they will fall back to "<key>" (see :class:`Placeholders <file_tree.template.Placeholders>`). 

433 

434 The net effect of either of these procedures is that the sub-tree placeholder values will be used in that sub-tree, 

435 but will not affect templates defined elsewhere in the parent tree. 

436 If a placeholder is undefined in a sub-tree, it will be taken from the parent placeholder values (if available). 

437 

438 Args: 

439 sub_tree: tree to be added to the current one 

440 precursor: name(s) of the sub-tree. Defaults to just adding the sub-tree to the main tree without precursor 

441 parent: key of the template used as top-level directory for the sub tree. 

442 Defaults to top-level directory of the main tree. 

443 Can be set to None for an independent tree. 

444 fill: whether any defined placeholders should be filled in before adding the sub-tree. By default this is True if there is no precursor and false otherwise 

445 

446 Raises: 

447 ValueError: if there is a conflict in the template names. 

448 """ 

449 if isinstance(precursor, str) or precursor is None: 

450 precursor = [precursor] 

451 for name in precursor: 

452 sub_tree_fill = sub_tree 

453 if name is None: 

454 add_string = "" 

455 if (fill is None) or fill: 

456 sub_tree_fill = sub_tree.fill() 

457 else: 

458 add_string = name + "/" 

459 if fill: 

460 sub_tree_fill = sub_tree.fill() 

461 

462 to_assign = dict(sub_tree_fill._iter_templates) 

463 sub_top_level = [k for (k, v) in to_assign.items() if "" in v][0] 

464 

465 if parent is None: 

466 new_top_level = Template(None, sub_top_level.unique_part) 

467 elif isinstance(parent, Template): 

468 new_top_level = parent 

469 else: 

470 new_top_level = self.get_template(parent) 

471 if name is None: 

472 if parent is None: 

473 for letter in string.ascii_letters: 

474 label = f"tree_top_{letter}" 

475 if ( 

476 label not in sub_tree_fill.template_keys() 

477 and label not in self.template_keys() 

478 ): 

479 self._add_actual_template(new_top_level, label) 

480 break 

481 else: 

482 self._add_actual_template(new_top_level, add_string) 

483 

484 been_assigned = {sub_top_level: new_top_level} 

485 del to_assign[sub_top_level] 

486 while len(to_assign) > 0: 

487 for old_template, keys in list(to_assign.items()): 

488 if old_template.parent is None: 

489 parent_template = None 

490 elif old_template.parent in been_assigned: 

491 parent_template = been_assigned[old_template.parent] 

492 else: 

493 continue 

494 new_template = Template( 

495 parent_template, old_template.unique_part 

496 ).add_precursor(add_string) 

497 for key in keys: 

498 self._add_actual_template(new_template, add_string + key) 

499 been_assigned[old_template] = new_template 

500 del to_assign[old_template] 

501 

502 if name is None: 

503 conflict = { 

504 key 

505 for key in sub_tree_fill.placeholders.keys() 

506 if key in self.placeholders 

507 } 

508 if len(conflict) > 0: 

509 raise ValueError( 

510 f"Sub-tree placeholder values for {conflict} conflict with those set in the parent tree." 

511 ) 

512 for old_key, old_value in sub_tree_fill.placeholders.items(): 

513 if isinstance(old_key, str): 

514 self.placeholders[add_string + old_key] = old_value 

515 else: 

516 self.placeholders[frozenset(add_string + k for k in old_key)] = { 

517 add_string + k: v for k, v in old_value.items() 

518 } 

519 

520 def filter_templates( 

521 self, template_names: Collection[str], check=True 

522 ) -> "FileTree": 

523 """Create new FileTree containing just the templates in `template_names` and their parents. 

524 

525 Args: 

526 template_names: names of the templates to keep. 

527 check: if True, check whether all template names are actually part of the FileTree 

528 

529 Raises: 

530 KeyError: if any of the template names are not in the FileTree (unless `check` is set to False). 

531 

532 Returns: 

533 FileTree containing requested subset of templates. 

534 """ 

535 all_keys = self.template_keys() 

536 if check: 

537 undefined = {name for name in template_names if name not in all_keys} 

538 if len(undefined) > 0: 

539 raise KeyError("Undefined template names found in filter: ", undefined) 

540 

541 new_filetree = FileTree( 

542 {}, self.placeholders.copy(), return_path=self.return_path, glob=self.glob 

543 ) 

544 

545 already_added = set() 

546 

547 def add_template(template: Template): 

548 if template in already_added: 

549 return 

550 if template.parent is not None: 

551 add_template(template.parent) 

552 new_filetree._add_actual_template(template, self._iter_templates[template]) 

553 already_added.add(template) 

554 

555 for name in template_names: 

556 if name not in all_keys: 

557 continue 

558 add_template(self.get_template(name)) 

559 

560 return new_filetree 

561 

562 # placeholders interface 

563 def update(self, inplace=False, **placeholders) -> "FileTree": 

564 """Update the placeholder values to be filled into the templates. 

565 

566 Args: 

567 inplace (bool): if True change the placeholders in-place (and return the FileTree itself); 

568 by default a new FileTree is returned with the updated values without altering this one. 

569 **placeholders (Dict[str, Any]): maps placeholder names to their new values (None to mark placeholder as undefined) 

570 

571 Returns: 

572 FileTree: Tree with updated placeholders (same tree as the current one if inplace is True) 

573 """ 

574 new_tree = self if inplace else self.copy() 

575 new_tree.placeholders.update(placeholders) 

576 return new_tree 

577 

578 def update_glob( 

579 self, template_key: Union[str, Sequence[str]], inplace=False, 

580 link: Union[None, Sequence[str], Sequence[Sequence[str]]]=None 

581 ) -> "FileTree": 

582 """Update any undefined placeholders based on which files exist on disk for template. 

583 

584 Args: 

585 template_key (str or sequence of str): key(s) of the template(s) to use 

586 inplace (bool): if True change the placeholders in-place (and return the FileTree itself); 

587 by default a new FileTree is returned with the updated values without altering this one. 

588 link (sequences of str): template keys that should be linked together in the output. 

589 

590 Returns: 

591 FileTree: Tree with updated placeholders (same tree as the current one if inplace is True) 

592 """ 

593 if link is None: 

594 link = [] 

595 elif len(link) > 0 and isinstance(link[0], str): 

596 link = [link] 

597 link_as_frozenset = [frozenset(l) for l in link] 

598 

599 if isinstance(template_key, str): 

600 template_key = [template_key] 

601 new_placeholders: Dict[str, Set[str]] = defaultdict(set) 

602 new_links = [set() for _ in range(len(link))] 

603 for key in template_key: 

604 template = self.get_template(key) 

605 from_template = template.get_all_placeholders(self.placeholders, link=link) 

606 for name, values in from_template.items(): 

607 if isinstance(name, frozenset): 

608 index = link_as_frozenset.index(name) 

609 values_as_tuples = zip(*[values[key] for key in link[index]]) 

610 new_links[index].update(values_as_tuples) 

611 else: 

612 new_placeholders[name] = new_placeholders[name].union(values) 

613 

614 def cmp(item1, item2): 

615 if item1 is None: 

616 return -1 

617 if item2 is None: 

618 return 1 

619 if item1 < item2: 

620 return -1 

621 if item1 > item2: 

622 return 1 

623 return 0 

624 

625 new_tree = self if inplace else self.copy() 

626 new_tree.placeholders.update( 

627 {k: sorted(v, key=cmp_to_key(cmp)) for k, v in new_placeholders.items()}, 

628 ) 

629 for key, value in zip(link, new_links): 

630 new_tree.placeholders[tuple(key)] = list(zip(*sorted(value))) 

631 return new_tree 

632 

633 # Extract paths 

634 def get(self, key: str, make_dir=False, glob=None) -> Union[str, Path]: 

635 """Return template with placeholder values filled in. 

636 

637 Args: 

638 key (str): identifier for the template 

639 make_dir (bool, optional): If set to True, create the parent directory of the returned path. 

640 glob: determines whether to allow filename pattern matching in the templates. By default the value set when creating the file-tree is used (see `FileTree.glob`). Globbing is only applied if there are `*` or `?` in the template. Possible values are: 

641 - `False`: do not do any pattern matching (identical to <= v1.5 behaviour). Use this to get the raw string including any `*` or `?` characters 

642 - `True`/"default": return filename if there is a single match. Raise an error otherwise. 

643 - "first"/"last": return the first or last match (based on alphabetical ordering). An error is raised if there are no matches. 

644 - callable: return the match returned by the callable. The input to the callable is a list of all the matching filenames (possibly of zero length). 

645 

646 Returns: 

647 Path: Filled in template as Path object. 

648 Returned as a `pathlib.Path` object if `FileTree.return_path` is True. 

649 Otherwise a string is returned. 

650 """ 

651 path = self.get_template(key).format_single(self.placeholders, glob=self.glob if glob is None else glob) 

652 if make_dir: 

653 Path(path).parent.mkdir(parents=True, exist_ok=True) 

654 if self.return_path: 

655 return Path(path) 

656 return path 

657 

658 def get_mult( 

659 self, key: Union[str, Sequence[str]], filter=False, make_dir=False, glob=None 

660 ) -> Union[xarray.DataArray, xarray.Dataset]: 

661 """Return array of paths with all possible values filled in for the placeholders. 

662 

663 Singular placeholder values are filled into the template directly. 

664 For each placeholder with multiple values a dimension is added to the output array. 

665 This dimension will have the name of the placeholder and labels corresponding to the possible values (see http://xarray.pydata.org/en/stable/). 

666 The precense of required, undefined placeholders will lead to an error 

667 (see :meth:`get_mult_glob` or :meth:`update_glob` to set these placeholders based on which files exist on disk). 

668 

669 Args: 

670 key (str, Sequence[str]): identifier(s) for the template. 

671 filter (bool, optional): If Set to True, will filter out any non-existent files. 

672 If the return type is strings, non-existent entries will be empty strings. 

673 If the return type is Path objects, non-existent entries will be None. 

674 Note that the default behaviour is opposite from :meth:`get_mult_glob`. 

675 make_dir (bool, optional): If set to True, create the parent directory for each returned path. 

676 glob: determines whether to allow filename pattern matching in the templates. By default the value set when creating the file-tree is used (see `FileTree.glob`). Globbing is only applied if there are `*` or `?` in the template. Possible values are: 

677 - `False`: do not do any pattern matching (identical to <= v1.5 behaviour). Use this to get the raw string including any `*` or `?` characters 

678 - `True`/"default": return filename if there is a single match. Raise an error otherwise. 

679 - "first"/"last": return the first or last match (based on alphabetical ordering). An error is raised if there are no matches. 

680 - callable: return the match returned by the callable. The input to the callable is a list of all the matching filenames (possibly of zero length). 

681 

682 Returns: 

683 xarray.DataArray, xarray.Dataset: For a single key returns all possible paths in an xarray DataArray. 

684 For multiple keys it returns the combination of them in an xarray Dataset. 

685 Each element of in the xarray is a `pathlib.Path` object if `FileTree.return_path` is True. 

686 Otherwise the xarray will contain the paths as strings. 

687 """ 

688 if isinstance(key, str): 

689 paths = self.get_template(key).format_mult(self.placeholders, filter=filter, glob=self.glob if glob is None else glob) 

690 paths.name = key 

691 if make_dir: 

692 for path in paths.data.flat: 

693 if path is not None: 

694 Path(path).parent.mkdir(parents=True, exist_ok=True) 

695 if self.return_path: 

696 return xarray.apply_ufunc( 

697 lambda p: None if p == "" else Path(p), paths, vectorize=True 

698 ) 

699 return paths 

700 else: 

701 return xarray.merge( 

702 [self.get_mult(k, filter=filter, make_dir=make_dir) for k in key], 

703 join="exact", 

704 ) 

705 

706 def get_mult_glob( 

707 self, key: Union[str, Sequence[str]], glob=None 

708 ) -> Union[xarray.DataArray, xarray.Dataset]: 

709 """Return array of paths with all possible values filled in for the placeholders. 

710 

711 Singular placeholder values are filled into the template directly. 

712 For each placeholder with multiple values a dimension is added to the output array. 

713 This dimension will have the name of the placeholder and labels corresponding to the possible values (see http://xarray.pydata.org/en/stable/). 

714 The possible values for undefined placeholders will be determined by which files actually exist on disk. 

715 

716 The same result can be obtained by calling `self.update_glob(key).get_mult(key, filter=True)`. 

717 However calling this method is more efficient, because it only has to check the disk for which files exist once. 

718 

719 Args: 

720 key (str, Sequence[str]): identifier(s) for the template. 

721 glob: determines whether to allow filename pattern matching in the templates. By default the value set when creating the file-tree is used (see `FileTree.glob`). Globbing is only applied if there are `*` or `?` in the template. Possible values are: 

722 - `False`: do not do any pattern matching (identical to <= v1.5 behaviour). Use this to get the raw string including any `*` or `?` characters 

723 - `True`/"default": return filename if there is a single match. Raise an error otherwise. 

724 - "first"/"last": return the first or last match (based on alphabetical ordering). An error is raised if there are no matches. 

725 - callable: return the match returned by the callable. The input to the callable is a list of all the matching filenames (possibly of zero length). 

726 

727 Returns: 

728 xarray.DataArray, xarray.Dataset: For a single key returns all possible paths in an xarray DataArray. 

729 For multiple keys it returns the combination of them in an xarray Dataset. 

730 Each element of in the xarray is a `pathlib.Path` object if `FileTree.return_path` is True. 

731 Otherwise the xarray will contain the paths as strings. 

732 """ 

733 if isinstance(key, str): 

734 template = self.get_template(key) 

735 matches = template.all_matches(self.placeholders) 

736 

737 new_placeholders = Placeholders(self.placeholders) 

738 updates, matches = template.get_all_placeholders(self.placeholders, return_matches=True) 

739 new_placeholders.update(updates) 

740 

741 paths = template.format_mult(new_placeholders, filter=True, matches=matches, glob=self.glob if glob is None else glob) 

742 paths.name = key 

743 if self.return_path: 

744 return paths 

745 res = xarray.apply_ufunc( 

746 lambda p: "" if p is None else str(p), paths, vectorize=True 

747 ) 

748 return res 

749 else: 

750 return xarray.merge( 

751 [self.get_mult_glob(k, glob) for k in key], 

752 join="outer", 

753 fill_value=None if self.return_path else "", 

754 ) 

755 

756 def fill(self, keep_optionals=True) -> "FileTree": 

757 """Fill in singular placeholder values. 

758 

759 Args: 

760 keep_optionals: if True keep optional parameters that have not been set 

761 

762 Returns: 

763 FileTree: new tree with singular placeholder values filled into the templates and removed from the placeholder dict 

764 """ 

765 new_tree = FileTree({}, self.placeholders.split()[1], self.return_path, glob=self.glob) 

766 to_assign = dict(self._iter_templates) 

767 template_mappings = {None: None} 

768 while len(to_assign) > 0: 

769 for old_template, keys in list(to_assign.items()): 

770 if old_template.parent in template_mappings: 

771 new_parent = template_mappings[old_template.parent] 

772 else: 

773 continue 

774 new_template = Template( 

775 new_parent, 

776 str( 

777 Template(None, old_template.unique_part).format_single( 

778 self.placeholders, 

779 check=False, 

780 keep_optionals=keep_optionals, 

781 glob=False 

782 ) 

783 ), 

784 ) 

785 template_mappings[old_template] = new_template 

786 new_tree._add_actual_template(new_template, keys) 

787 del to_assign[old_template] 

788 return new_tree 

789 

790 # iteration 

791 def iter_vars( 

792 self, placeholders: Sequence[str] 

793 ) -> Generator["FileTree", None, None]: 

794 """Iterate over the user-provided placeholders. 

795 

796 A single file-tree is yielded for each possible value of the placeholders. 

797 

798 Args: 

799 placeholders (Sequence[str]): sequence of placeholder names to iterate over 

800 

801 Yields: 

802 FileTrees, where each placeholder only has a single possible value 

803 """ 

804 for sub_placeholders in self.placeholders.iter_over(placeholders): 

805 yield FileTree(self._templates, sub_placeholders, self.return_path, glob=self.glob) 

806 

807 def iter( 

808 self, template: str, check_exists: bool = False 

809 ) -> Generator["FileTree", None, None]: 

810 """Iterate over trees containng all possible values for template. 

811 

812 Args: 

813 template (str): short name identifier of the template 

814 check_exists (bool): set to True to only return trees for which the template actually exists 

815 

816 Yields: 

817 FileTrees, where each placeholder in given template only has a single possible value 

818 """ 

819 placeholders = self.get_template(template).placeholders( 

820 valid=self.placeholders.keys() 

821 ) 

822 for tree in self.iter_vars(placeholders): 

823 if not check_exists or Path(tree.get(template)).exists: 

824 yield tree 

825 

826 # convert to string 

827 def to_string(self, indentation=4) -> str: 

828 """Convert FileTree into a valid filetree definition. 

829 

830 An identical FileTree can be created by running :meth:`from_string` on the resulting string. 

831 

832 Args: 

833 indentation (int, optional): Number of spaces to use for indendation. Defaults to 4. 

834 

835 Returns: 

836 String representation of FileTree. 

837 """ 

838 lines = [self.placeholders.to_string()] 

839 

840 top_level = sorted( 

841 [ 

842 template 

843 for template in self._iter_templates.keys() 

844 if template.parent is None 

845 ], 

846 key=lambda k: ",".join(self._iter_templates[k]), 

847 ) 

848 already_done = set() 

849 for t in top_level: 

850 if t not in already_done: 

851 lines.append(t.as_multi_line(self._iter_templates, indentation=indentation)) 

852 already_done.add(t) 

853 return "\n\n".join(lines) 

854 

855 def write(self, filename, indentation=4): 

856 """Write the FileTree to a disk as a text file. 

857 

858 The first few lines will contain the placeholders. 

859 The remaining lines will contain the actual FileTree with all the templates (including sub-trees). 

860 The top-level directory is not stored in the file and hence will need to be provided when reading the tree from the file. 

861 

862 Args: 

863 filename (str or Path): where to store the file (directory should exist already) 

864 indentation (int, optional): Number of spaces to use in indendation. Defaults to 4. 

865 """ 

866 with open(filename, "w") as f: 

867 f.write(self.to_string(indentation=indentation)) 

868 

869 def report(self, fill=True, pager=False): 

870 """Print a formatted report of the filetree to the console. 

871 

872 Prints a report of the file-tree to the terminal with: 

873 - table with placeholders and their values 

874 - tree of templates with template keys marked in cyan 

875 

876 Args: 

877 fill (bool, optional): by default any fixed placeholders are filled in before printing the tree (using :meth:`fill`). Set to False to disable this. 

878 pager (bool, optional): if set to True, the report will be filed into a pager (recommended if the output is very large) 

879 """ 

880 if fill: 

881 self = self.fill() 

882 

883 if pager: 

884 from rich.console import Console 

885 

886 printer = Console() 

887 with printer.pager(): 

888 for part in self._generate_rich_report(): 

889 printer.print(part) 

890 else: 

891 for part in self._generate_rich_report(): 

892 rich.print(part) 

893 

894 def _generate_rich_report(self): 

895 """Generate a sequence of Rich renderables to produce report.""" 

896 from rich.table import Table 

897 from rich.tree import Tree 

898 

899 single_vars = {} 

900 multi_vars = {} 

901 linked_vars = [] 

902 for key, value in self.placeholders.items(): 

903 if value is None: 

904 continue 

905 if isinstance(key, frozenset): 

906 linked_vars.append(sorted(key)) 

907 for linked_key, linked_value in value.items(): 

908 multi_vars[linked_key] = linked_value 

909 elif np.array(value).ndim == 1: 

910 multi_vars[key] = value 

911 else: 

912 single_vars[key] = value 

913 if len(single_vars) > 0: 

914 single_var_table = Table("name", "value", title="Defined placeholders") 

915 for key in sorted(single_vars.keys()): 

916 single_var_table.add_row(key, single_vars[key]) 

917 yield single_var_table 

918 if len(multi_vars) > 0: 

919 multi_var_table = Table( 

920 "name", "value", title="Placeholders with multiple options" 

921 ) 

922 for key in sorted(multi_vars.keys()): 

923 multi_var_table.add_row(key, ", ".join(str(v) for v in multi_vars[key])) 

924 yield multi_var_table 

925 

926 if len(linked_vars) > 0: 

927 yield "Linked variables:\n" + ( 

928 "\n".join([", ".join(v) for v in linked_vars]) 

929 ) 

930 

931 

932 def add_children(t: Template, tree: Optional[Tree]): 

933 for child in sorted(t.children(self._iter_templates.keys()), key=lambda t: t.as_string): 

934 child_tree = tree.add(child.rich_line(self._iter_templates)) 

935 add_children(child, child_tree) 

936 

937 top_level = sorted( 

938 [ 

939 template 

940 for template in self._iter_templates.keys() 

941 if template.parent is None 

942 ], 

943 key=lambda t: ",".join(self._iter_templates[t]), 

944 ) 

945 already_done = set() 

946 for t in top_level: 

947 if t not in already_done: 

948 base_tree = Tree(t.rich_line(self._iter_templates)) 

949 add_children(t, base_tree) 

950 yield base_tree 

951 

952 def run_app( 

953 self, 

954 ): 

955 """ 

956 Open a terminal-based App to explore the filetree interactively. 

957 

958 The resulting app runs directly in the terminal, 

959 so it should work when ssh'ing to some remote cluster. 

960 

961 There will be two panels: 

962 

963 - The left panel will show all the templates in a tree format. 

964 Template keys are shown in cyan. 

965 For each template the number of files that exist on disc out of the total number is shown 

966 colour coded based on completeness (red: no files; yellow: some files; blue: all files). 

967 Templates can be selected by hovering over them. 

968 Clicking on directories with hide/show their content. 

969 - The right panel will show for the selected template the complete template string 

970 and a table showing for which combination of placeholders the file is present/absent 

971 (rows for absent files are colour-coded red). 

972 """ 

973 from . import app 

974 app.FileTreeViewer(self).run() 

975 

976 

977def convert( 

978 src_tree: FileTree, 

979 target_tree: Optional[FileTree] = None, 

980 keys=None, 

981 symlink=False, 

982 overwrite=False, 

983 glob_placeholders=None, 

984): 

985 """ 

986 Copy or link files defined in `keys` from the `src_tree` to the `target_tree`. 

987 

988 Given two example trees 

989 

990 - source:: 

991 

992 subject = A,B 

993 

994 sub-{subject} 

995 data 

996 T1w.nii.gz 

997 FLAIR.nii.gz 

998 

999 - target:: 

1000 

1001 subject = A,B 

1002 

1003 data 

1004 sub-{subject} 

1005 {subject}-T1w.nii.gz (T1w) 

1006 {subject}-T2w.nii.gz (T2w) 

1007 

1008 And given pre-existing data matching the source tree:: 

1009 

1010 . 

1011 ├── sub-A 

1012 │ └── data 

1013 │ ├── FLAIR.nii.gz 

1014 │ └── T1w.nii.gz 

1015 └── sub-B 

1016 └── data 

1017 ├── FLAIR.nii.gz 

1018 └── T1w.nii.gz 

1019 

1020 We can do the following conversions: 

1021 

1022 - `convert(source, target)`: 

1023 copies all matching keys from `source` to `target`. 

1024 This will only copy the "T1w.nii.gz" files, because they are the only match in the template keys. 

1025 Note that the `data` template key also matches between the two trees, but this template is not a leaf, so is ignored. 

1026 - `convert(source, target, keys=['T1w', ('FLAIR', 'T2w')])`: 

1027 copies the "T1w.nii.gz" files from `source` to `target` and 

1028 copies the "FLAIR.nii.gz" in `source` to "T2w..nii.gz" in `target`. 

1029 - `convert(source.update(subject='B'), source.update(subject='C'))`: 

1030 creates a new "data/sub-C" directory and 

1031 copies all the data from "data/sub-B" into that directory. 

1032 - `convert(source, keys=[('FLAIR', 'T1w')], overwrite=True)`: 

1033 copies the "FLAIR.nii.gz" into the "T1w.nii.gz" files overwriting the originals. 

1034 

1035 Warnings are raised in two cases: 

1036 

1037 - if a source file is missing 

1038 - if a target file already exists and `overwrite` is False 

1039 

1040 Args: 

1041 src_tree: prepopulated filetree with the source files 

1042 target_tree: filetree that will be populated. Defaults to same as `src_tree`. 

1043 keys: collection of template keys to transfer from `src_tree` to `target_tree`. Defaults to all templates keys shared between `src_tree` and `target_tree`. 

1044 symlink: if set to true links the files rather than copying them 

1045 overwrite: if set to True overwrite any existing files 

1046 glob_placeholders: Placeholders that should be treated as wildcards. This is meant for placeholders that have different values for each filename. 

1047 

1048 Raises: 

1049 ValueError: if the conversion can not be carried out. If raised no data will be copied/linked. 

1050 """ 

1051 if target_tree is None and keys is None: 

1052 raise ValueError("Conversion requires either `target_tree` or `keys` to be set") 

1053 src_tree = src_tree.copy() 

1054 if target_tree is None: 

1055 target_tree = src_tree 

1056 target_tree = target_tree.copy() 

1057 if keys is None: 

1058 keys = set(src_tree.template_keys(only_leaves=True)).intersection( 

1059 target_tree.template_keys(only_leaves=True) 

1060 ) 

1061 if glob_placeholders is None: 

1062 glob_placeholders = set() 

1063 for p in glob_placeholders: 

1064 if p in src_tree.placeholders: 

1065 raise ValueError( 

1066 f"Placeholder {p} has been selected for globbing, however values were set for it in source tree" 

1067 ) 

1068 if p in target_tree.placeholders: 

1069 raise ValueError( 

1070 f"Placeholder {p} has been selected for globbing, however values were set for it in target tree" 

1071 ) 

1072 

1073 full_keys = { 

1074 (key_definition, key_definition) 

1075 if isinstance(key_definition, str) 

1076 else key_definition 

1077 for key_definition in keys 

1078 } 

1079 for src_key, target_key in full_keys: 

1080 # ensure template placeholders are consistent between source and target tree 

1081 for placeholder in ( 

1082 src_tree.get_template(src_key).placeholders() 

1083 + target_tree.get_template(target_key).placeholders() 

1084 ): 

1085 if placeholder in glob_placeholders: 

1086 continue 

1087 if placeholder not in src_tree.placeholders: 

1088 if placeholder not in target_tree.placeholders: 

1089 raise ValueError( 

1090 f"Can not convert template {src_key}, because no values have been set for {placeholder}" 

1091 ) 

1092 src_tree.placeholders[placeholder] = target_tree.placeholders[ 

1093 placeholder 

1094 ] 

1095 elif placeholder not in target_tree.placeholders: 

1096 target_tree.placeholders[placeholder] = src_tree.placeholders[ 

1097 placeholder 

1098 ] 

1099 nsrc = ( 

1100 -1 

1101 if is_singular(src_tree.placeholders[placeholder]) 

1102 else len(src_tree.placeholders[placeholder]) 

1103 ) 

1104 ntarget = ( 

1105 -1 

1106 if is_singular(target_tree.placeholders[placeholder]) 

1107 else len(target_tree.placeholders[placeholder]) 

1108 ) 

1109 if nsrc != ntarget: 

1110 raise ValueError( 

1111 f"Number of possible values for {placeholder} do not match between source and target tree" 

1112 ) 

1113 

1114 # ensure non-singular placeholders match 

1115 src_non_singular = { 

1116 p 

1117 for p in src_tree.get_template(src_key).placeholders() 

1118 if p not in glob_placeholders and not is_singular(src_tree.placeholders[p]) 

1119 } 

1120 target_non_singular = { 

1121 p 

1122 for p in target_tree.get_template(target_key).placeholders() 

1123 if p not in glob_placeholders 

1124 and not is_singular(target_tree.placeholders[p]) 

1125 } 

1126 

1127 diff = src_non_singular.difference(target_non_singular).difference( 

1128 glob_placeholders 

1129 ) 

1130 if len(diff) > 0: 

1131 raise ValueError( 

1132 f"Placeholders {diff} in source template {src_key} has no equivalent in target template {target_key}" 

1133 ) 

1134 diff = target_non_singular.difference(src_non_singular) 

1135 if len(diff) > 0: 

1136 raise ValueError( 

1137 f"Placeholders {diff} in target template {target_key} has no equivalent in source template {src_key}" 

1138 ) 

1139 

1140 # all checks have passed; let's get to work 

1141 to_warn_about = ([], []) 

1142 

1143 transfer_filenames = [] 

1144 

1145 for src_key, target_key in sorted(full_keys): 

1146 iter_placeholders = sorted( 

1147 { 

1148 p 

1149 for p in src_tree.get_template(src_key).placeholders() 

1150 if p not in glob_placeholders 

1151 and not is_singular(src_tree.placeholders[p]) 

1152 } 

1153 ) 

1154 for single_src_tree, single_target_tree in zip( 

1155 src_tree.iter_vars(iter_placeholders), 

1156 target_tree.iter_vars(iter_placeholders), 

1157 ): 

1158 if len(glob_placeholders) == 0: 

1159 src_fn = single_src_tree.get(src_key) 

1160 target_fn = single_target_tree.get(target_key) 

1161 else: 

1162 try: 

1163 src_trees = list(single_src_tree.update_glob(src_key).iter(src_key)) 

1164 except ValueError: 

1165 to_warn_about[0].append( 

1166 single_src_tree.get_template(src_key).format_single( 

1167 single_src_tree.placeholders, check=False 

1168 ) 

1169 ) 

1170 continue 

1171 if len(src_trees) > 1: 

1172 raise ValueError( 

1173 f"Multiple matching filenames were found when globbing {src_key} ({single_src_tree.get(src_key)})" 

1174 ) 

1175 

1176 keys = { 

1177 key: src_trees[0].placeholders[key] 

1178 for key in glob_placeholders 

1179 if key in src_trees[0].placeholders 

1180 } 

1181 src_fn = single_src_tree.update(**keys).get(src_key) 

1182 target_fn = single_target_tree.update(**keys).get(src_key) 

1183 

1184 transfer_filenames.append((Path(src_fn), Path(target_fn))) 

1185 if len(to_warn_about[0]) > 0: 

1186 warn( 

1187 f"Following source files were not found during FileTree conversion: {to_warn_about[0]}" 

1188 ) 

1189 if len(to_warn_about[1]) > 0: 

1190 warn( 

1191 f"Following target files already existed during FileTree conversion: {to_warn_about[1]}" 

1192 ) 

1193 for fn1, fn2 in transfer_filenames: 

1194 _convert_file( 

1195 fn1, 

1196 fn2, 

1197 to_warn_about, 

1198 symlink=symlink, 

1199 overwrite=overwrite, 

1200 ) 

1201 

1202 

1203def _convert_file( 

1204 source: Path, target: Path, to_warn_about, symlink=False, overwrite=False 

1205): 

1206 """ 

1207 Copy or link `source` file to `target` file. 

1208 

1209 Helper function for :func:`convert` 

1210 """ 

1211 if not source.exists(): 

1212 to_warn_about[0].append(str(source)) 

1213 return 

1214 if target.exists(): 

1215 if not overwrite: 

1216 to_warn_about[1].append(str(target)) 

1217 return 

1218 os.remove(target) 

1219 target.parent.mkdir(parents=True, exist_ok=True) 

1220 if not symlink: 

1221 copyfile(source, target, follow_symlinks=False) 

1222 elif source.is_absolute(): 

1223 target.symlink_to(source) 

1224 else: 

1225 target.symlink_to(os.path.relpath(source, target.parent))