Coverage for python/lsst/resources/file.py: 89%

255 statements  

« prev     ^ index     » next       coverage.py v7.16.1, created at 2026-09-22 09:18 +0000

1# This file is part of lsst-resources. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (https://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# Use of this source code is governed by a 3-clause BSD-style 

10# license that can be found in the LICENSE file. 

11 

12from __future__ import annotations 

13 

14__all__ = ("FileResourcePath",) 

15 

16import contextlib 

17import copy 

18import datetime 

19import logging 

20import os 

21import os.path 

22import posixpath 

23import re 

24import shutil 

25import stat 

26import urllib.parse 

27from collections.abc import Generator, Iterator 

28from pathlib import Path 

29from typing import TYPE_CHECKING 

30 

31from ._resourceHandles._baseResourceHandle import ResourceHandleProtocol 

32from ._resourceHandles._fileResourceHandle import FileResourceHandle 

33from ._resourcePath import ResourceInfo, ResourcePath 

34from .utils import NoTransaction, ensure_directory_is_writeable, os2posix, posix2os 

35 

36try: 

37 import fsspec 

38 from fsspec.spec import AbstractFileSystem 

39except ImportError: 

40 # Hidden from type checkers so that the names above keep the types they 

41 # have when fsspec is installed. 

42 if not TYPE_CHECKING: 

43 fsspec = None 

44 AbstractFileSystem = type 

45 

46if TYPE_CHECKING: 

47 from importlib.resources.abc import Traversable 

48 

49 from .utils import TransactionProtocol 

50 

51 

52log = logging.getLogger(__name__) 

53 

54 

55def _path_to_info(uri: str, path: str | Path | Traversable) -> ResourceInfo | None: 

56 """Given a path to a local file, return a `ResourceInfo`.""" 

57 if isinstance(path, Path): 

58 stat_result = path.stat() 

59 elif isinstance(path, str): 59 ↛ 61line 59 didn't jump to line 61 because the condition on line 59 was always true

60 stat_result = os.stat(path) 

61 elif (stat_method := getattr(path, "stat", None)) and callable(stat_method): 

62 # Edge case triggered by importlib.resources. 

63 stat_result = stat_method() 

64 if not isinstance(stat_result, os.stat_result): 

65 raise RuntimeError(f"Unexpected stat result from {path}.stat()") 

66 else: 

67 return None 

68 

69 return ResourceInfo( 

70 uri=uri, 

71 is_file=not stat.S_ISDIR(stat_result.st_mode), 

72 size=0 if stat.S_ISDIR(stat_result.st_mode) else stat_result.st_size, 

73 last_modified=datetime.datetime.fromtimestamp(stat_result.st_mtime, tz=datetime.UTC), 

74 checksums={}, 

75 ) 

76 

77 

78class FileResourcePath(ResourcePath): 

79 """Path for explicit ``file`` URI scheme.""" 

80 

81 transferModes = ("copy", "link", "symlink", "hardlink", "relsymlink", "auto", "move") 

82 transferDefault: str = "link" 

83 

84 # By definition refers to a local file 

85 isLocal = True 

86 

87 # A warm existence check or removal here costs a few microseconds, almost 

88 # all of it holding the GIL, so a batch has to be this large before 

89 # spreading it over threads beats a plain loop in the calling thread. 

90 # Measurements above this size are flat, so it also matches the 1000 keys 

91 # an S3 bulk delete takes. 

92 _chunk_size = 1000 

93 

94 # A transfer costs orders of magnitude more than an existence check and 

95 # scales with the file size, so batches stay small enough to balance. 

96 _transfer_chunk_size = 25 

97 

98 @property 

99 def ospath(self) -> str: 

100 """Path component of the URI localized to current OS. 

101 

102 Will unquote URI path since a formal URI must include the quoting. 

103 """ 

104 return urllib.parse.unquote(posix2os(self._uri.path)) 

105 

106 def exists(self) -> bool: 

107 """Indicate that the file exists.""" 

108 # Uses os.path.exists so if there is a soft link that points 

109 # to a file that no longer exists this will return False 

110 return os.path.exists(self.ospath) 

111 

112 def size(self) -> int: 

113 """Return the size of the file in bytes.""" 

114 if not os.path.isdir(self.ospath): 

115 stat = os.stat(self.ospath) 

116 sz = stat.st_size 

117 else: 

118 sz = 0 

119 return sz 

120 

121 def get_info(self) -> ResourceInfo: 

122 """Return lightweight metadata about this file.""" 

123 info = _path_to_info(str(self), self.ospath) 

124 if info is None: 124 ↛ 125line 124 didn't jump to line 125 because the condition on line 124 was never true

125 raise RuntimeError(f"Unexpected internal failure obtaining file info for {self}") 

126 return info 

127 

128 def remove(self) -> None: 

129 """Remove the resource.""" 

130 os.remove(self.ospath) 

131 

132 @contextlib.contextmanager 

133 def _as_local( 

134 self, multithreaded: bool = True, tmpdir: ResourcePath | None = None 

135 ) -> Generator[ResourcePath]: 

136 """Return the local path of the file. 

137 

138 This is an internal helper for ``as_local()``. 

139 

140 Parameters 

141 ---------- 

142 multithreaded : `bool`, optional 

143 Unused. 

144 tmpdir : `ResourcePath` or `None`, optional 

145 Unused. 

146 

147 Returns 

148 ------- 

149 local_uri : `ResourcePath` 

150 A local URI. In this case it will be itself. 

151 """ 

152 yield self 

153 

154 def read(self, size: int = -1) -> bytes: 

155 with open(self.ospath, "rb") as fh: 

156 return fh.read(size) 

157 

158 def write(self, data: bytes, overwrite: bool = True) -> None: 

159 dir = os.path.dirname(self.ospath) 

160 if dir and not os.path.exists(dir): 

161 _create_directories(dir) 

162 mode = "wb" if overwrite else "xb" 

163 with open(self.ospath, mode) as f: 

164 f.write(data) 

165 

166 def mkdir(self) -> None: 

167 """Make the directory associated with this URI. 

168 

169 An attempt will be made to create the directory even if the URI 

170 looks like a file. 

171 

172 Raises 

173 ------ 

174 NotADirectoryError: 

175 Raised if a non-directory already exists. 

176 """ 

177 try: 

178 _create_directories(self.ospath) 

179 except FileExistsError: 

180 raise NotADirectoryError(f"{self.ospath} exists but is not a directory.") from None 

181 

182 def isdir(self) -> bool: 

183 """Return whether this URI is a directory. 

184 

185 Returns 

186 ------- 

187 isdir : `bool` 

188 `True` if this URI is a directory or looks like a directory, 

189 else `False`. 

190 """ 

191 if self.dirLike is None: 

192 # Cache state for next time. 

193 self.dirLike = os.path.isdir(self.ospath) 

194 return self.dirLike 

195 

196 def transfer_from( 

197 self, 

198 src: ResourcePath, 

199 transfer: str, 

200 overwrite: bool = False, 

201 transaction: TransactionProtocol | None = None, 

202 multithreaded: bool = True, 

203 ) -> None: 

204 """Transfer the current resource to a local file. 

205 

206 Parameters 

207 ---------- 

208 src : `ResourcePath` 

209 Source URI. 

210 transfer : `str` 

211 Mode to use for transferring the resource. Supports the following 

212 options: copy, link, symlink, hardlink, relsymlink. 

213 overwrite : `bool`, optional 

214 Allow an existing file to be overwritten. Defaults to `False`. 

215 transaction : `~lsst.resources.utils.TransactionProtocol`, optional 

216 If a transaction is provided, undo actions will be registered. 

217 multithreaded : `bool`, optional 

218 Whether threads are allowed to be used or not. 

219 """ 

220 # Fail early to prevent delays if remote resources are requested 

221 if transfer not in self.transferModes: 

222 raise ValueError(f"Transfer mode '{transfer}' not supported by URI scheme {self.scheme}") 

223 

224 # Existence checks can take time so only try if the log message 

225 # will be issued. 

226 if log.isEnabledFor(logging.DEBUG): 226 ↛ 239line 226 didn't jump to line 239 because the condition on line 226 was always true

227 log.debug( 

228 "Transferring %s [exists: %s] -> %s [exists: %s] (transfer=%s)", 

229 src, 

230 src.exists(), 

231 self, 

232 self.exists(), 

233 transfer, 

234 ) 

235 

236 # Short circuit if the URIs are identical. The inode comparison below 

237 # only runs for a local source, so a non-local source that happens to 

238 # name this same resource would otherwise be reported as a clash. 

239 if self == src: 

240 log.debug( 

241 "Target and destination URIs are identical: %s, returning immediately." 

242 " No further action required.", 

243 self, 

244 ) 

245 return 

246 

247 # The output location should not exist unless overwrite=True. 

248 # Rather than use `exists()`, use os.stat since we might need 

249 # the full answer later. 

250 dest_stat: os.stat_result | None 

251 try: 

252 # Do not read through links of the file itself. 

253 dest_stat = os.lstat(self.ospath) 

254 except FileNotFoundError: 

255 dest_stat = None 

256 

257 # It is possible that the source URI and target URI refer 

258 # to the same file. This can happen for a number of reasons 

259 # (such as soft links in the path, or they really are the same). 

260 # In that case log a message and return as if the transfer 

261 # completed (it technically did). A temporary file download 

262 # can't be the same so the test can be skipped. 

263 if dest_stat and src.isLocal and not src.isTemporary: 

264 # Be consistent and use lstat here (even though realpath 

265 # has been called). It does not harm. 

266 local_src_stat = os.lstat(src.ospath) 

267 if dest_stat.st_ino == local_src_stat.st_ino and dest_stat.st_dev == local_src_stat.st_dev: 

268 log.debug( 

269 "Destination URI %s is the same file as source URI %s, returning immediately." 

270 " No further action required.", 

271 self, 

272 src, 

273 ) 

274 return 

275 

276 if not overwrite and dest_stat: 

277 raise FileExistsError( 

278 f"Destination path '{self}' already exists. Transfer from {src} cannot be completed." 

279 ) 

280 

281 # Make the destination path absolute (but don't follow links since 

282 # that would possibly cause us to end up in the wrong place if the 

283 # file existed already as a soft link) 

284 newFullPath = os.path.abspath(self.ospath) 

285 outputDir = os.path.dirname(newFullPath) 

286 

287 # We do not have to special case FileResourcePath here because 

288 # as_local handles that. If remote download, download it to the 

289 # destination directory to allow an atomic rename but only if that 

290 # directory exists because we do not want to create a directory 

291 # but then end up with the download failing. 

292 tmpdir = outputDir if os.path.exists(outputDir) else None 

293 with src.as_local(multithreaded=multithreaded, tmpdir=tmpdir) as local_uri: 

294 is_temporary = local_uri.isTemporary 

295 local_src = local_uri.ospath 

296 

297 # Short circuit if the URIs are identical immediately. 

298 if self == local_uri: 298 ↛ 299line 298 didn't jump to line 299 because the condition on line 298 was never true

299 log.debug( 

300 "Target and destination URIs are identical: %s, returning immediately." 

301 " No further action required.", 

302 self, 

303 ) 

304 return 

305 

306 # Default transfer mode depends on whether we have a temporary 

307 # file or not. 

308 if transfer == "auto": 

309 transfer = self.transferDefault if not is_temporary else "copy" 

310 

311 if not os.path.exists(local_src): 

312 if is_temporary: 

313 if src == local_uri: 313 ↛ 317line 313 didn't jump to line 317 because the condition on line 313 was always true

314 msg = f"Local temporary file {src} has gone missing." 

315 else: 

316 # This will not happen in normal scenarios. 

317 msg = f"Local file {local_uri} downloaded from {src} has gone missing" 

318 else: 

319 msg = f"Source URI {src} does not exist" 

320 raise FileNotFoundError(msg) 

321 

322 # Follow soft links 

323 local_src = os.path.realpath(os.path.normpath(local_src)) 

324 

325 # Creating a symlink to a local copy of a remote resource 

326 # should never work. Creating a hardlink will work but should 

327 # not be allowed since it is highly unlikely that this is ever 

328 # an intended option and depends on the local target being 

329 # on the same file system as was used for the temporary file 

330 # download. 

331 # If a symlink is being requested for a local temporary file 

332 # that is likely undesirable but should not be refused. 

333 if is_temporary and src != local_uri and "link" in transfer: 

334 raise RuntimeError( 

335 f"Can not use local file system transfer mode {transfer} for remote resource ({src})" 

336 ) 

337 elif is_temporary and src == local_uri and "symlink" in transfer: 

338 log.debug( 

339 "Using a symlink for a temporary resource may lead to unexpected downstream failures." 

340 ) 

341 

342 # For temporary files we can own them if we created it. 

343 requested_transfer = transfer 

344 if src != local_uri and is_temporary and transfer == "copy": 

345 transfer = "move" 

346 

347 if not os.path.isdir(outputDir): 

348 # Must create the directory -- this can not be rolled back 

349 # since another transfer running concurrently may 

350 # be relying on this existing. 

351 _create_directories(outputDir) 

352 

353 if transaction is None: 

354 # Use a no-op transaction to reduce code duplication 

355 transaction = NoTransaction() 

356 

357 # For links the OS doesn't let us overwrite so if something does 

358 # exist we have to remove it before we do the actual "transfer" 

359 # below 

360 if "link" in transfer and overwrite and dest_stat: 

361 with contextlib.suppress(Exception): 

362 # If this fails we ignore it since it's a problem 

363 # that will manifest immediately below with a more relevant 

364 # error message 

365 self.remove() 

366 

367 if transfer == "move": 

368 # If a rename works we try that since that is guaranteed to 

369 # be atomic. If that fails we copy and rename. We do this 

370 # in case other processes are trying to move to the same 

371 # file and we want the "winner" to not be corrupted. 

372 try: 

373 with transaction.undoWith(f"move from {local_src}", os.rename, newFullPath, local_src): 

374 os.rename(local_src, newFullPath) 

375 except OSError: 

376 with self.temporary_uri(prefix=self.parent(), suffix=self.getExtension()) as temp_copy: 

377 shutil.copy(local_src, temp_copy.ospath) 

378 with transaction.undoWith( 

379 f"move from {local_src}", 

380 shutil.move, 

381 newFullPath, 

382 local_src, 

383 copy_function=shutil.copy, 

384 ): 

385 os.rename(temp_copy.ospath, newFullPath) 

386 os.remove(local_src) 

387 elif transfer == "copy": 

388 # We want atomic copies so first copy to a temp location in 

389 # the same output directory. This at least guarantees that 

390 # if multiple processes are writing to the same file 

391 # simultaneously the file we end up with will not be corrupt. 

392 if overwrite: 

393 with self.temporary_uri(prefix=self.parent(), suffix=self.getExtension()) as temp_copy: 

394 shutil.copy(local_src, temp_copy.ospath) 

395 with transaction.undoWith(f"copy from {local_src}", os.remove, newFullPath): 

396 os.rename(temp_copy.ospath, newFullPath) 

397 else: 

398 # Create the file exclusively to ensure that no others are 

399 # trying to write. 

400 temp_path = newFullPath + ".transfer-tmp" 

401 try: 

402 with open(temp_path, "x"): 

403 pass 

404 except FileExistsError: 

405 raise FileExistsError( 

406 f"Another process is writing to '{self}'." 

407 f" Transfer from {src} cannot be completed." 

408 ) 

409 with transaction.undoWith(f"copy from {local_src}", os.remove, temp_path): 

410 # Make sure file is writable, no matter the umask. 

411 st = os.stat(temp_path) 

412 os.chmod(temp_path, st.st_mode | stat.S_IWUSR) 

413 shutil.copy(local_src, temp_path) 

414 # Use link/remove to atomically and exclusively move the 

415 # file into place (only one concurrent linker can win). 

416 try: 

417 os.link(temp_path, newFullPath) 

418 except FileExistsError: 

419 raise FileExistsError( 

420 f"Another process wrote to '{self}'. Transfer from {src} cannot be completed." 

421 ) 

422 finally: 

423 os.remove(temp_path) 

424 elif transfer == "link": 

425 # Try hard link and if that fails use a symlink 

426 with transaction.undoWith(f"link to {local_src}", os.remove, newFullPath): 

427 try: 

428 os.link(local_src, newFullPath) 

429 except OSError: 

430 # Read through existing symlinks 

431 os.symlink(local_src, newFullPath) 

432 elif transfer == "hardlink": 

433 with transaction.undoWith(f"hardlink to {local_src}", os.remove, newFullPath): 

434 os.link(local_src, newFullPath) 

435 elif transfer == "symlink": 

436 # Read through existing symlinks 

437 with transaction.undoWith(f"symlink to {local_src}", os.remove, newFullPath): 

438 os.symlink(local_src, newFullPath) 

439 elif transfer == "relsymlink": 

440 # This is a standard symlink but using a relative path 

441 # Need the directory name to give to relative root 

442 # A full file path confuses it into an extra ../ 

443 newFullPathRoot = os.path.dirname(newFullPath) 

444 relPath = os.path.relpath(local_src, newFullPathRoot) 

445 with transaction.undoWith(f"relsymlink to {local_src}", os.remove, newFullPath): 

446 os.symlink(relPath, newFullPath) 

447 else: 

448 raise NotImplementedError(f"Transfer type '{transfer}' not supported.") 

449 

450 # This was an explicit move requested from a remote resource 

451 # try to remove that remote resource. We check is_temporary because 

452 # the local file would have been moved by shutil.move already. 

453 if requested_transfer == "move" and is_temporary and src != local_uri: 

454 # Transactions do not work here 

455 src.remove() 

456 

457 def walk( 

458 self, file_filter: str | re.Pattern | None = None 

459 ) -> Iterator[list | tuple[ResourcePath, list[str], list[str]]]: 

460 """Walk the directory tree returning matching files and directories. 

461 

462 Parameters 

463 ---------- 

464 file_filter : `str` or `re.Pattern`, optional 

465 Regex to filter out files from the list before it is returned. 

466 

467 Yields 

468 ------ 

469 dirpath : `ResourcePath` 

470 Current directory being examined. 

471 dirnames : `list` of `str` 

472 Names of subdirectories within dirpath. 

473 filenames : `list` of `str` 

474 Names of all the files within dirpath. 

475 """ 

476 if not self.isdir(): 

477 raise ValueError("Can not walk a non-directory URI") 

478 

479 if isinstance(file_filter, str): 

480 file_filter = re.compile(file_filter) 

481 

482 for root, dirs, files in os.walk(self.ospath, followlinks=True): 

483 # Filter by the regex 

484 if file_filter is not None: 

485 files = [f for f in files if file_filter.search(f)] 

486 # Rebuild from the parsed URI rather than from the OS path, so 

487 # that a subclass keeps its own scheme and netloc. Constructing 

488 # from a plain path always resolves to a file URI. 

489 path = os2posix(root) 

490 if self.quotePaths: 490 ↛ 492line 490 didn't jump to line 492 because the condition on line 490 was always true

491 path = urllib.parse.quote(path) 

492 yield self.replace(path=path, forceDirectory=True), dirs, files 

493 

494 @classmethod 

495 def _fixupPathUri( 

496 cls, 

497 parsed: urllib.parse.ParseResult, 

498 root: ResourcePath | None = None, 

499 forceAbsolute: bool = False, 

500 forceDirectory: bool | None = None, 

501 ) -> tuple[urllib.parse.ParseResult, bool | None]: 

502 """Fix up relative paths in URI instances. 

503 

504 Parameters 

505 ---------- 

506 parsed : `~urllib.parse.ParseResult` 

507 The result from parsing a URI using `urllib.parse`. 

508 root : `ResourcePath`, optional 

509 Path to use as root when converting relative to absolute. 

510 If `None`, it will be the current working directory. It is only 

511 used if a file-scheme is used incorrectly with a relative path. 

512 forceAbsolute : `bool`, ignored 

513 Has no effect for this subclass. ``file`` URIs are always 

514 absolute. 

515 forceDirectory : `bool`, optional 

516 If `True` forces the URI to end with a separator, otherwise given 

517 URI is interpreted as is. 

518 

519 Returns 

520 ------- 

521 modified : `~urllib.parse.ParseResult` 

522 Update result if a URI is being handled. 

523 dirLike : `bool` or `None` 

524 `True` if given parsed URI has a trailing separator or 

525 ``forceDirectory`` is `True`. Otherwise can return the given 

526 value of ``forceDirectory``. 

527 

528 Notes 

529 ----- 

530 Relative paths are explicitly not supported by RFC8089 but `urllib` 

531 does accept URIs of the form ``file:relative/path.ext``. They need 

532 to be turned into absolute paths before they can be used. This is 

533 always done regardless of the ``forceAbsolute`` parameter. 

534 """ 

535 # assume we are not dealing with a directory like URI 

536 dirLike = forceDirectory 

537 

538 # file URI implies POSIX path separators so split as POSIX, 

539 # then join as os, and convert to abspath. Do not handle 

540 # home directories since "file" scheme is explicitly documented 

541 # to not do tilde expansion. 

542 sep = posixpath.sep 

543 

544 # Consistency check. 

545 if forceDirectory is False and parsed.path.endswith(sep): 

546 raise ValueError( 

547 f"URI {parsed.geturl()} ends with {sep} but " 

548 "forceDirectory parameter declares it to be a file." 

549 ) 

550 

551 # For an absolute path all we need to do is check if we need 

552 # to force the directory separator 

553 if posixpath.isabs(parsed.path): 

554 if forceDirectory: 

555 if not parsed.path.endswith(sep): 

556 parsed = parsed._replace(path=parsed.path + sep) 

557 dirLike = True 

558 return copy.copy(parsed), dirLike 

559 

560 # Relative path so must fix it to be compliant with the standard 

561 

562 # Replacement values for the URI 

563 replacements = {} 

564 

565 if root is None: 

566 root_str = os.path.abspath(os.path.curdir) 

567 else: 

568 if root.scheme and root.scheme != "file": 568 ↛ 569line 568 didn't jump to line 569 because the condition on line 568 was never true

569 raise RuntimeError(f"The override root must be a file URI not {root.scheme}") 

570 root_str = os.path.abspath(root.ospath) 

571 

572 replacements["path"] = posixpath.normpath(posixpath.join(os2posix(root_str), parsed.path)) 

573 

574 # normpath strips trailing "/" so put it back if necessary 

575 # Acknowledge that trailing separator exists. 

576 if forceDirectory or (parsed.path.endswith(sep) and not replacements["path"].endswith(sep)): 

577 replacements["path"] += sep 

578 dirLike = True 

579 

580 # ParseResult is a NamedTuple so _replace is standard API 

581 parsed = parsed._replace(**replacements) 

582 

583 if parsed.params or parsed.query: 583 ↛ 584line 583 didn't jump to line 584 because the condition on line 583 was never true

584 log.warning("Additional items unexpectedly encountered in file URI: %s", parsed.geturl()) 

585 

586 return parsed, dirLike 

587 

588 @contextlib.contextmanager 

589 def _openImpl( 

590 self, 

591 mode: str = "r", 

592 *, 

593 encoding: str | None = None, 

594 ) -> Generator[ResourceHandleProtocol]: 

595 with FileResourceHandle(mode=mode, log=log, uri=self, encoding=encoding) as buffer: 

596 yield buffer 

597 

598 def to_fsspec(self) -> tuple[AbstractFileSystem, str]: 

599 """Return an abstract file system and path that can be used by fsspec. 

600 

601 Returns 

602 ------- 

603 fs : `fsspec.spec.AbstractFileSystem` 

604 A file system object suitable for use with the returned path. 

605 path : `str` 

606 A path that can be opened by the file system object. 

607 """ 

608 if fsspec is None: 608 ↛ 609line 608 didn't jump to line 609 because the condition on line 608 was never true

609 raise ImportError("fsspec is not available") 

610 # fsspec does not like URL encodings in file URIs so pass it the os 

611 # path instead. 

612 return fsspec.url_to_fs(self.ospath) 

613 

614 

615def _create_directories(name: str | bytes) -> None: 

616 """Create a directory and all of its parent directories that don't yet 

617 exist. 

618 

619 Parameters 

620 ---------- 

621 name : `str` or `bytes` 

622 Path to the directory to be created 

623 

624 Notes 

625 ----- 

626 The code in this function is duplicated from the Python standard library 

627 function os.makedirs with one change: if the user has set a process umask 

628 that prevents us from creating/accessing files in the newly created 

629 directories, the permissions of the directories are altered to allow 

630 owner-write and owner-traverse so that they can be used. 

631 """ 

632 # These are optional parameters in the original function, but they can be 

633 # constant here. 

634 mode = 0o777 

635 exist_ok = True 

636 

637 head, tail = os.path.split(name) 

638 if not tail: 

639 head, tail = os.path.split(head) 

640 if head and tail and not os.path.exists(head): 

641 try: 

642 _create_directories(head) 

643 except FileExistsError: 

644 # Defeats race condition when another thread created the path 

645 pass 

646 cdir: str | bytes = os.curdir 

647 if isinstance(tail, bytes): 647 ↛ 648line 647 didn't jump to line 648 because the condition on line 647 was never true

648 cdir = bytes(os.curdir, "ASCII") 

649 if tail == cdir: # xxx/newdir/. exists if xxx/newdir exists 649 ↛ 650line 649 didn't jump to line 650 because the condition on line 649 was never true

650 return 

651 try: 

652 os.mkdir(name, mode) 

653 # This is the portion that is modified relative to the standard library 

654 # version of the function. 

655 ensure_directory_is_writeable(name) 

656 # end modified portion 

657 except OSError: 

658 # Cannot rely on checking for EEXIST, since the operating system 

659 # could give priority to other errors like EACCES or EROFS 

660 if not exist_ok or not os.path.isdir(name): 

661 raise