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

253 statements  

« prev     ^ index     » next       coverage.py v7.16.0, created at 2026-09-19 01:59 -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 @property 

88 def ospath(self) -> str: 

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

90 

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

92 """ 

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

94 

95 def exists(self) -> bool: 

96 """Indicate that the file exists.""" 

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

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

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

100 

101 def size(self) -> int: 

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

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

104 stat = os.stat(self.ospath) 

105 sz = stat.st_size 

106 else: 

107 sz = 0 

108 return sz 

109 

110 def get_info(self) -> ResourceInfo: 

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

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

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

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

115 return info 

116 

117 def remove(self) -> None: 

118 """Remove the resource.""" 

119 os.remove(self.ospath) 

120 

121 @contextlib.contextmanager 

122 def _as_local( 

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

124 ) -> Generator[ResourcePath]: 

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

126 

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

128 

129 Parameters 

130 ---------- 

131 multithreaded : `bool`, optional 

132 Unused. 

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

134 Unused. 

135 

136 Returns 

137 ------- 

138 local_uri : `ResourcePath` 

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

140 """ 

141 yield self 

142 

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

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

145 return fh.read(size) 

146 

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

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

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

150 _create_directories(dir) 

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

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

153 f.write(data) 

154 

155 def mkdir(self) -> None: 

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

157 

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

159 looks like a file. 

160 

161 Raises 

162 ------ 

163 NotADirectoryError: 

164 Raised if a non-directory already exists. 

165 """ 

166 try: 

167 _create_directories(self.ospath) 

168 except FileExistsError: 

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

170 

171 def isdir(self) -> bool: 

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

173 

174 Returns 

175 ------- 

176 isdir : `bool` 

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

178 else `False`. 

179 """ 

180 if self.dirLike is None: 

181 # Cache state for next time. 

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

183 return self.dirLike 

184 

185 def transfer_from( 

186 self, 

187 src: ResourcePath, 

188 transfer: str, 

189 overwrite: bool = False, 

190 transaction: TransactionProtocol | None = None, 

191 multithreaded: bool = True, 

192 ) -> None: 

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

194 

195 Parameters 

196 ---------- 

197 src : `ResourcePath` 

198 Source URI. 

199 transfer : `str` 

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

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

202 overwrite : `bool`, optional 

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

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

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

206 multithreaded : `bool`, optional 

207 Whether threads are allowed to be used or not. 

208 """ 

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

210 if transfer not in self.transferModes: 

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

212 

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

214 # will be issued. 

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

216 log.debug( 

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

218 src, 

219 src.exists(), 

220 self, 

221 self.exists(), 

222 transfer, 

223 ) 

224 

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

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

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

228 if self == src: 

229 log.debug( 

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

231 " No further action required.", 

232 self, 

233 ) 

234 return 

235 

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

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

238 # the full answer later. 

239 dest_stat: os.stat_result | None 

240 try: 

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

242 dest_stat = os.lstat(self.ospath) 

243 except FileNotFoundError: 

244 dest_stat = None 

245 

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

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

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

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

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

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

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

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

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

255 local_src_stat = os.lstat(src.ospath) 

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

257 log.debug( 

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

259 " No further action required.", 

260 self, 

261 src, 

262 ) 

263 return 

264 

265 if not overwrite and dest_stat: 

266 raise FileExistsError( 

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

268 ) 

269 

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

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

272 # file existed already as a soft link) 

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

274 outputDir = os.path.dirname(newFullPath) 

275 

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

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

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

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

280 # but then end up with the download failing. 

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

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

283 is_temporary = local_uri.isTemporary 

284 local_src = local_uri.ospath 

285 

286 # Short circuit if the URIs are identical immediately. 

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

288 log.debug( 

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

290 " No further action required.", 

291 self, 

292 ) 

293 return 

294 

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

296 # file or not. 

297 if transfer == "auto": 

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

299 

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

301 if is_temporary: 

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

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

304 else: 

305 # This will not happen in normal scenarios. 

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

307 else: 

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

309 raise FileNotFoundError(msg) 

310 

311 # Follow soft links 

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

313 

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

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

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

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

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

319 # download. 

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

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

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

323 raise RuntimeError( 

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

325 ) 

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

327 log.debug( 

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

329 ) 

330 

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

332 requested_transfer = transfer 

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

334 transfer = "move" 

335 

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

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

338 # since another transfer running concurrently may 

339 # be relying on this existing. 

340 _create_directories(outputDir) 

341 

342 if transaction is None: 342 ↛ 349line 342 didn't jump to line 349 because the condition on line 342 was always true

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

344 transaction = NoTransaction() 

345 

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

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

348 # below 

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

350 with contextlib.suppress(Exception): 

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

352 # that will manifest immediately below with a more relevant 

353 # error message 

354 self.remove() 

355 

356 if transfer == "move": 

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

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

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

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

361 try: 

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

363 os.rename(local_src, newFullPath) 

364 except OSError: 

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

366 shutil.copy(local_src, temp_copy.ospath) 

367 with transaction.undoWith( 

368 f"move from {local_src}", 

369 shutil.move, 

370 newFullPath, 

371 local_src, 

372 copy_function=shutil.copy, 

373 ): 

374 os.rename(temp_copy.ospath, newFullPath) 

375 os.remove(local_src) 

376 elif transfer == "copy": 

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

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

379 # if multiple processes are writing to the same file 

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

381 if overwrite: 

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

383 shutil.copy(local_src, temp_copy.ospath) 

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

385 os.rename(temp_copy.ospath, newFullPath) 

386 else: 

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

388 # trying to write. 

389 temp_path = newFullPath + ".transfer-tmp" 

390 try: 

391 with open(temp_path, "x"): 

392 pass 

393 except FileExistsError: 

394 raise FileExistsError( 

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

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

397 ) 

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

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

400 st = os.stat(temp_path) 

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

402 shutil.copy(local_src, temp_path) 

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

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

405 try: 

406 os.link(temp_path, newFullPath) 

407 except FileExistsError: 

408 raise FileExistsError( 

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

410 ) 

411 finally: 

412 os.remove(temp_path) 

413 elif transfer == "link": 

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

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

416 try: 

417 os.link(local_src, newFullPath) 

418 except OSError: 

419 # Read through existing symlinks 

420 os.symlink(local_src, newFullPath) 

421 elif transfer == "hardlink": 

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

423 os.link(local_src, newFullPath) 

424 elif transfer == "symlink": 

425 # Read through existing symlinks 

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

427 os.symlink(local_src, newFullPath) 

428 elif transfer == "relsymlink": 

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

430 # Need the directory name to give to relative root 

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

432 newFullPathRoot = os.path.dirname(newFullPath) 

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

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

435 os.symlink(relPath, newFullPath) 

436 else: 

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

438 

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

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

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

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

443 # Transactions do not work here 

444 src.remove() 

445 

446 def walk( 

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

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

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

450 

451 Parameters 

452 ---------- 

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

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

455 

456 Yields 

457 ------ 

458 dirpath : `ResourcePath` 

459 Current directory being examined. 

460 dirnames : `list` of `str` 

461 Names of subdirectories within dirpath. 

462 filenames : `list` of `str` 

463 Names of all the files within dirpath. 

464 """ 

465 if not self.isdir(): 

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

467 

468 if isinstance(file_filter, str): 

469 file_filter = re.compile(file_filter) 

470 

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

472 # Filter by the regex 

473 if file_filter is not None: 

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

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

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

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

478 path = os2posix(root) 

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

480 path = urllib.parse.quote(path) 

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

482 

483 @classmethod 

484 def _fixupPathUri( 

485 cls, 

486 parsed: urllib.parse.ParseResult, 

487 root: ResourcePath | None = None, 

488 forceAbsolute: bool = False, 

489 forceDirectory: bool | None = None, 

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

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

492 

493 Parameters 

494 ---------- 

495 parsed : `~urllib.parse.ParseResult` 

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

497 root : `ResourcePath`, optional 

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

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

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

501 forceAbsolute : `bool`, ignored 

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

503 absolute. 

504 forceDirectory : `bool`, optional 

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

506 URI is interpreted as is. 

507 

508 Returns 

509 ------- 

510 modified : `~urllib.parse.ParseResult` 

511 Update result if a URI is being handled. 

512 dirLike : `bool` or `None` 

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

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

515 value of ``forceDirectory``. 

516 

517 Notes 

518 ----- 

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

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

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

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

523 """ 

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

525 dirLike = forceDirectory 

526 

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

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

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

530 # to not do tilde expansion. 

531 sep = posixpath.sep 

532 

533 # Consistency check. 

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

535 raise ValueError( 

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

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

538 ) 

539 

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

541 # to force the directory separator 

542 if posixpath.isabs(parsed.path): 

543 if forceDirectory: 

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

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

546 dirLike = True 

547 return copy.copy(parsed), dirLike 

548 

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

550 

551 # Replacement values for the URI 

552 replacements = {} 

553 

554 if root is None: 

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

556 else: 

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

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

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

560 

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

562 

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

564 # Acknowledge that trailing separator exists. 

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

566 replacements["path"] += sep 

567 dirLike = True 

568 

569 # ParseResult is a NamedTuple so _replace is standard API 

570 parsed = parsed._replace(**replacements) 

571 

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

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

574 

575 return parsed, dirLike 

576 

577 @contextlib.contextmanager 

578 def _openImpl( 

579 self, 

580 mode: str = "r", 

581 *, 

582 encoding: str | None = None, 

583 ) -> Generator[ResourceHandleProtocol]: 

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

585 yield buffer 

586 

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

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

589 

590 Returns 

591 ------- 

592 fs : `fsspec.spec.AbstractFileSystem` 

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

594 path : `str` 

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

596 """ 

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

598 raise ImportError("fsspec is not available") 

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

600 # path instead. 

601 return fsspec.url_to_fs(self.ospath) 

602 

603 

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

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

606 exist. 

607 

608 Parameters 

609 ---------- 

610 name : `str` or `bytes` 

611 Path to the directory to be created 

612 

613 Notes 

614 ----- 

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

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

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

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

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

620 """ 

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

622 # constant here. 

623 mode = 0o777 

624 exist_ok = True 

625 

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

627 if not tail: 

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

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

630 try: 

631 _create_directories(head) 

632 except FileExistsError: 

633 # Defeats race condition when another thread created the path 

634 pass 

635 cdir: str | bytes = os.curdir 

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

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

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

639 return 

640 try: 

641 os.mkdir(name, mode) 

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

643 # version of the function. 

644 ensure_directory_is_writeable(name) 

645 # end modified portion 

646 except OSError: 

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

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

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

650 raise