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

254 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-31 01:33 -0700

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 @property 

95 def ospath(self) -> str: 

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

97 

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

99 """ 

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

101 

102 def exists(self) -> bool: 

103 """Indicate that the file exists.""" 

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

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

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

107 

108 def size(self) -> int: 

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

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

111 stat = os.stat(self.ospath) 

112 sz = stat.st_size 

113 else: 

114 sz = 0 

115 return sz 

116 

117 def get_info(self) -> ResourceInfo: 

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

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

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

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

122 return info 

123 

124 def remove(self) -> None: 

125 """Remove the resource.""" 

126 os.remove(self.ospath) 

127 

128 @contextlib.contextmanager 

129 def _as_local( 

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

131 ) -> Generator[ResourcePath]: 

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

133 

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

135 

136 Parameters 

137 ---------- 

138 multithreaded : `bool`, optional 

139 Unused. 

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

141 Unused. 

142 

143 Returns 

144 ------- 

145 local_uri : `ResourcePath` 

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

147 """ 

148 yield self 

149 

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

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

152 return fh.read(size) 

153 

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

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

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

157 _create_directories(dir) 

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

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

160 f.write(data) 

161 

162 def mkdir(self) -> None: 

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

164 

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

166 looks like a file. 

167 

168 Raises 

169 ------ 

170 NotADirectoryError: 

171 Raised if a non-directory already exists. 

172 """ 

173 try: 

174 _create_directories(self.ospath) 

175 except FileExistsError: 

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

177 

178 def isdir(self) -> bool: 

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

180 

181 Returns 

182 ------- 

183 isdir : `bool` 

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

185 else `False`. 

186 """ 

187 if self.dirLike is None: 

188 # Cache state for next time. 

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

190 return self.dirLike 

191 

192 def transfer_from( 

193 self, 

194 src: ResourcePath, 

195 transfer: str, 

196 overwrite: bool = False, 

197 transaction: TransactionProtocol | None = None, 

198 multithreaded: bool = True, 

199 ) -> None: 

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

201 

202 Parameters 

203 ---------- 

204 src : `ResourcePath` 

205 Source URI. 

206 transfer : `str` 

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

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

209 overwrite : `bool`, optional 

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

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

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

213 multithreaded : `bool`, optional 

214 Whether threads are allowed to be used or not. 

215 """ 

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

217 if transfer not in self.transferModes: 

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

219 

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

221 # will be issued. 

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

223 log.debug( 

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

225 src, 

226 src.exists(), 

227 self, 

228 self.exists(), 

229 transfer, 

230 ) 

231 

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

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

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

235 if self == src: 

236 log.debug( 

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

238 " No further action required.", 

239 self, 

240 ) 

241 return 

242 

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

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

245 # the full answer later. 

246 dest_stat: os.stat_result | None 

247 try: 

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

249 dest_stat = os.lstat(self.ospath) 

250 except FileNotFoundError: 

251 dest_stat = None 

252 

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

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

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

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

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

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

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

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

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

262 local_src_stat = os.lstat(src.ospath) 

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

264 log.debug( 

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

266 " No further action required.", 

267 self, 

268 src, 

269 ) 

270 return 

271 

272 if not overwrite and dest_stat: 

273 raise FileExistsError( 

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

275 ) 

276 

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

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

279 # file existed already as a soft link) 

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

281 outputDir = os.path.dirname(newFullPath) 

282 

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

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

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

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

287 # but then end up with the download failing. 

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

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

290 is_temporary = local_uri.isTemporary 

291 local_src = local_uri.ospath 

292 

293 # Short circuit if the URIs are identical immediately. 

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

295 log.debug( 

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

297 " No further action required.", 

298 self, 

299 ) 

300 return 

301 

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

303 # file or not. 

304 if transfer == "auto": 

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

306 

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

308 if is_temporary: 

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

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

311 else: 

312 # This will not happen in normal scenarios. 

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

314 else: 

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

316 raise FileNotFoundError(msg) 

317 

318 # Follow soft links 

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

320 

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

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

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

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

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

326 # download. 

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

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

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

330 raise RuntimeError( 

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

332 ) 

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

334 log.debug( 

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

336 ) 

337 

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

339 requested_transfer = transfer 

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

341 transfer = "move" 

342 

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

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

345 # since another transfer running concurrently may 

346 # be relying on this existing. 

347 _create_directories(outputDir) 

348 

349 if transaction is None: 

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

351 transaction = NoTransaction() 

352 

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

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

355 # below 

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

357 with contextlib.suppress(Exception): 

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

359 # that will manifest immediately below with a more relevant 

360 # error message 

361 self.remove() 

362 

363 if transfer == "move": 

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

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

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

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

368 try: 

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

370 os.rename(local_src, newFullPath) 

371 except OSError: 

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

373 shutil.copy(local_src, temp_copy.ospath) 

374 with transaction.undoWith( 

375 f"move from {local_src}", 

376 shutil.move, 

377 newFullPath, 

378 local_src, 

379 copy_function=shutil.copy, 

380 ): 

381 os.rename(temp_copy.ospath, newFullPath) 

382 os.remove(local_src) 

383 elif transfer == "copy": 

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

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

386 # if multiple processes are writing to the same file 

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

388 if overwrite: 

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

390 shutil.copy(local_src, temp_copy.ospath) 

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

392 os.rename(temp_copy.ospath, newFullPath) 

393 else: 

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

395 # trying to write. 

396 temp_path = newFullPath + ".transfer-tmp" 

397 try: 

398 with open(temp_path, "x"): 

399 pass 

400 except FileExistsError: 

401 raise FileExistsError( 

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

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

404 ) 

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

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

407 st = os.stat(temp_path) 

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

409 shutil.copy(local_src, temp_path) 

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

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

412 try: 

413 os.link(temp_path, newFullPath) 

414 except FileExistsError: 

415 raise FileExistsError( 

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

417 ) 

418 finally: 

419 os.remove(temp_path) 

420 elif transfer == "link": 

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

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

423 try: 

424 os.link(local_src, newFullPath) 

425 except OSError: 

426 # Read through existing symlinks 

427 os.symlink(local_src, newFullPath) 

428 elif transfer == "hardlink": 

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

430 os.link(local_src, newFullPath) 

431 elif transfer == "symlink": 

432 # Read through existing symlinks 

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

434 os.symlink(local_src, newFullPath) 

435 elif transfer == "relsymlink": 

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

437 # Need the directory name to give to relative root 

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

439 newFullPathRoot = os.path.dirname(newFullPath) 

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

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

442 os.symlink(relPath, newFullPath) 

443 else: 

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

445 

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

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

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

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

450 # Transactions do not work here 

451 src.remove() 

452 

453 def walk( 

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

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

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

457 

458 Parameters 

459 ---------- 

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

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

462 

463 Yields 

464 ------ 

465 dirpath : `ResourcePath` 

466 Current directory being examined. 

467 dirnames : `list` of `str` 

468 Names of subdirectories within dirpath. 

469 filenames : `list` of `str` 

470 Names of all the files within dirpath. 

471 """ 

472 if not self.isdir(): 

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

474 

475 if isinstance(file_filter, str): 

476 file_filter = re.compile(file_filter) 

477 

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

479 # Filter by the regex 

480 if file_filter is not None: 

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

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

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

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

485 path = os2posix(root) 

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

487 path = urllib.parse.quote(path) 

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

489 

490 @classmethod 

491 def _fixupPathUri( 

492 cls, 

493 parsed: urllib.parse.ParseResult, 

494 root: ResourcePath | None = None, 

495 forceAbsolute: bool = False, 

496 forceDirectory: bool | None = None, 

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

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

499 

500 Parameters 

501 ---------- 

502 parsed : `~urllib.parse.ParseResult` 

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

504 root : `ResourcePath`, optional 

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

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

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

508 forceAbsolute : `bool`, ignored 

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

510 absolute. 

511 forceDirectory : `bool`, optional 

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

513 URI is interpreted as is. 

514 

515 Returns 

516 ------- 

517 modified : `~urllib.parse.ParseResult` 

518 Update result if a URI is being handled. 

519 dirLike : `bool` or `None` 

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

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

522 value of ``forceDirectory``. 

523 

524 Notes 

525 ----- 

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

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

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

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

530 """ 

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

532 dirLike = forceDirectory 

533 

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

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

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

537 # to not do tilde expansion. 

538 sep = posixpath.sep 

539 

540 # Consistency check. 

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

542 raise ValueError( 

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

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

545 ) 

546 

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

548 # to force the directory separator 

549 if posixpath.isabs(parsed.path): 

550 if forceDirectory: 

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

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

553 dirLike = True 

554 return copy.copy(parsed), dirLike 

555 

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

557 

558 # Replacement values for the URI 

559 replacements = {} 

560 

561 if root is None: 

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

563 else: 

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

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

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

567 

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

569 

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

571 # Acknowledge that trailing separator exists. 

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

573 replacements["path"] += sep 

574 dirLike = True 

575 

576 # ParseResult is a NamedTuple so _replace is standard API 

577 parsed = parsed._replace(**replacements) 

578 

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

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

581 

582 return parsed, dirLike 

583 

584 @contextlib.contextmanager 

585 def _openImpl( 

586 self, 

587 mode: str = "r", 

588 *, 

589 encoding: str | None = None, 

590 ) -> Generator[ResourceHandleProtocol]: 

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

592 yield buffer 

593 

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

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

596 

597 Returns 

598 ------- 

599 fs : `fsspec.spec.AbstractFileSystem` 

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

601 path : `str` 

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

603 """ 

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

605 raise ImportError("fsspec is not available") 

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

607 # path instead. 

608 return fsspec.url_to_fs(self.ospath) 

609 

610 

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

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

613 exist. 

614 

615 Parameters 

616 ---------- 

617 name : `str` or `bytes` 

618 Path to the directory to be created 

619 

620 Notes 

621 ----- 

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

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

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

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

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

627 """ 

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

629 # constant here. 

630 mode = 0o777 

631 exist_ok = True 

632 

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

634 if not tail: 

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

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

637 try: 

638 _create_directories(head) 

639 except FileExistsError: 

640 # Defeats race condition when another thread created the path 

641 pass 

642 cdir: str | bytes = os.curdir 

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

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

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

646 return 

647 try: 

648 os.mkdir(name, mode) 

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

650 # version of the function. 

651 ensure_directory_is_writeable(name) 

652 # end modified portion 

653 except OSError: 

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

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

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

657 raise