Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/werkzeug/utils.py: 23%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

274 statements  

1from __future__ import annotations 

2 

3import io 

4import mimetypes 

5import os 

6import pkgutil 

7import re 

8import sys 

9import typing as t 

10import unicodedata 

11import wsgiref.types as wt 

12from datetime import datetime 

13from functools import update_wrapper 

14from time import time 

15from urllib.parse import quote 

16 

17from markupsafe import escape 

18 

19from ._internal import _missing 

20from .datastructures.cache_control import ResponseCacheControl 

21from .datastructures.etag import ETag 

22from .datastructures.headers import Headers 

23from .exceptions import HTTPException 

24from .exceptions import NotFound 

25from .security import _windows_device_files 

26from .security import safe_join 

27 

28if t.TYPE_CHECKING: 

29 from .wrappers.response import Response 

30 

31T = t.TypeVar("T") 

32 

33_entity_re = re.compile(r"&([^;]+);") 

34_filename_ascii_strip_re = re.compile(r"[^A-Za-z0-9_.-]") 

35 

36 

37class cached_property(property, t.Generic[T]): 

38 """A :func:`property` that is only evaluated once. Subsequent access 

39 returns the cached value. Setting the property sets the cached 

40 value. Deleting the property clears the cached value, accessing it 

41 again will evaluate it again. 

42 

43 .. code-block:: python 

44 

45 class Example: 

46 @cached_property 

47 def value(self): 

48 # calculate something important here 

49 return 42 

50 

51 e = Example() 

52 e.value # evaluates 

53 e.value # uses cache 

54 e.value = 16 # sets cache 

55 del e.value # clears cache 

56 

57 If the class defines ``__slots__``, it must add ``_cache_{name}`` as 

58 a slot. Alternatively, it can add ``__dict__``, but that's usually 

59 not desirable. 

60 

61 .. versionchanged:: 2.1 

62 Works with ``__slots__``. 

63 

64 .. versionchanged:: 2.0 

65 ``del obj.name`` clears the cached value. 

66 """ 

67 

68 def __init__( 

69 self, 

70 fget: t.Callable[[t.Any], T], 

71 name: str | None = None, 

72 doc: str | None = None, 

73 ) -> None: 

74 update_wrapper(self, fget) # type: ignore[arg-type] 

75 super().__init__(fget, doc=doc) 

76 

77 if name is not None: 

78 self.__name__ = name 

79 

80 self.slot_name = f"_cache_{self.__name__}" 

81 

82 @t.overload 

83 def __get__(self, obj: None, cls: type[t.Any], /) -> t.Self: ... 

84 @t.overload 

85 def __get__(self, obj: t.Any, cls: type[t.Any] | None = ..., /) -> T: ... 

86 def __get__( 

87 self, obj: t.Any | None = None, cls: type[t.Any] | None = None, / 

88 ) -> T | t.Self: 

89 if obj is None: 

90 return self 

91 

92 obj_dict = getattr(obj, "__dict__", None) 

93 

94 if obj_dict is not None: 

95 value: T = obj_dict.get(self.__name__, _missing) 

96 else: 

97 value = getattr(obj, self.slot_name, _missing) # type: ignore[arg-type] 

98 

99 if value is _missing: 

100 value = self.fget(obj) # type: ignore[misc] 

101 

102 if obj_dict is not None: 

103 obj.__dict__[self.__name__] = value 

104 else: 

105 setattr(obj, self.slot_name, value) 

106 

107 return value 

108 

109 def __set__(self, obj: object, value: T) -> None: 

110 if hasattr(obj, "__dict__"): 

111 obj.__dict__[self.__name__] = value 

112 else: 

113 setattr(obj, self.slot_name, value) 

114 

115 def __delete__(self, obj: t.Any) -> None: 

116 if hasattr(obj, "__dict__"): 

117 obj.__dict__.pop(self.__name__, None) 

118 else: 

119 setattr(obj, self.slot_name, _missing) 

120 

121 

122# https://cgit.freedesktop.org/xdg/shared-mime-info/tree/freedesktop.org.xml.in 

123# https://www.iana.org/assignments/media-types/media-types.xhtml 

124# Types listed in the XDG mime info that have a charset in the IANA registration. 

125_charset_mimetypes = { 

126 "application/ecmascript", 

127 "application/javascript", 

128 "application/sql", 

129 "application/xml", 

130 "application/xml-dtd", 

131 "application/xml-external-parsed-entity", 

132} 

133 

134 

135def _get_content_type(mimetype: str, charset: str) -> str: 

136 """Returns the full content type string with charset for a mimetype. 

137 

138 If the mimetype represents text, the charset parameter will be 

139 appended, otherwise the mimetype is returned unchanged. 

140 

141 :param mimetype: The mimetype to be used as content type. 

142 :param charset: The charset to be appended for text mimetypes. 

143 :return: The content type. 

144 

145 .. deprecated:: 3.2 

146 Will be removed in Werkzeug 4.0. Set ``Response.mimetype`` instead. 

147 

148 .. versionchanged:: 0.15 

149 Any type that ends with ``+xml`` gets a charset, not just those 

150 that start with ``application/``. Known text types such as 

151 ``application/javascript`` are also given charsets. 

152 """ 

153 # TODO remove charset param after deprecation 

154 if ( 

155 mimetype.startswith("text/") 

156 or mimetype in _charset_mimetypes 

157 or mimetype.endswith("+xml") 

158 ): 

159 mimetype += f"; charset={charset}" 

160 

161 return mimetype 

162 

163 

164def secure_filename(filename: str) -> str: 

165 """Validate and modify a filename so that it is safe to use on a regular 

166 filesystem and in :func:`os.path.join`. This may produce an empty string if 

167 nothing in the filename could be safely preserved. 

168 

169 Only the ASCII letters, digits, and ``._-`` characters are allowed, all 

170 others are removed or replaced. Unicode characters are decomposed and 

171 replaced with ASCII equivalents according to NFKD. ``/`` (and ``\\`` on 

172 Windows) and whitespace, are replaced with ``_``. Surrounding ``._`` 

173 characters are removed. 

174 

175 On Windows, special device names such as ``CON`` and ``NUL`` will be 

176 prefixed with ``_`` to avoid access. 

177 

178 This only considers the filename, it does not validate if the file does or 

179 does not exist, if it is a symlink, or other properties of the file. 

180 

181 :param filename: The filename to validate and modify. 

182 

183 .. versionadded:: 0.5 

184 """ 

185 # decompose combined chars, replace compat chars 

186 filename = unicodedata.normalize("NFKD", filename) 

187 # only keep ASCII chars 

188 filename = filename.encode("ascii", "ignore").decode("ascii") 

189 

190 for sep in os.sep, os.path.altsep: 

191 if sep: 

192 filename = filename.replace(sep, " ") 

193 

194 # replace any whitespace with _ 

195 filename = "_".join(filename.split()) 

196 # only allow ASCII letters, numbers, and _.- 

197 filename = _filename_ascii_strip_re.sub("", filename) 

198 # remove surrounding ._, left behind after replacing slash in ../ 

199 filename = filename.strip("._") 

200 

201 # prefix special device names on Windows 

202 if ( 

203 os.name == "nt" 

204 and filename.partition(".")[0].strip().upper() in _windows_device_files 

205 ): 

206 filename = f"_{filename}" 

207 

208 return filename 

209 

210 

211def redirect( 

212 location: str, code: int = 303, Response: type[Response] | None = None 

213) -> Response: 

214 """Create a response that redirects the client to the target location. 

215 

216 The default ``303 See Other`` status code instructs the client to make a 

217 ``GET`` request to the target, regardless of what method the current request 

218 is. This produces the correct result for the common use cases: page redirects 

219 and form success. The status codes you're likely to use are: 

220 

221 - ``303 See Other`` always uses a ``GET`` request. 

222 - ``307 Temporary Redirect`` preserves the current method. 

223 - ``308 Permanent Redirect`` preserves the current method, and instructs 

224 the client to permanently apply the result. This is hard to undo once 

225 you've sent it, so be sure the permanence is what you want. 

226 

227 Two older codes, ``302 Found`` and ``301 Moved Permanently``, are 

228 superseded by ``307`` and ``308`` respectively. They were not consistently 

229 implemented by clients, which tend to switch ``POST`` to ``GET`` but 

230 preserve other methods. Prefer using ``303``, ``307``, and ``308`` to get 

231 the exact behavior you intend. Other ``3xx`` codes are either not defined or 

232 have specific use cases. 

233 

234 :param location: The URL to redirect to. The client will interpret a 

235 relative URL (without the host) as relative to the host it's accessing. 

236 :param code: The redirect status code. This affects how the client issues 

237 the next request. Defaults to ``303``. 

238 :param Response: The response class. Defaults to 

239 :class:`werkzeug.wrappers.Response`. 

240 

241 .. versionchanged:: 3.2 

242 ``code`` defaults to 303 instead of 302. 

243 

244 .. versionchanged:: 0.10 

245 Added the ``response`` parameter. 

246 

247 .. versionchanged:: 0.6 

248 ``location`` can contain Unicode characters. 

249 """ 

250 if Response is None: 

251 from .wrappers import Response 

252 

253 html_location = escape(location) 

254 response = Response( # type: ignore[misc] 

255 "<!doctype html>\n" 

256 "<html lang=en>\n" 

257 "<title>Redirecting...</title>\n" 

258 "<h1>Redirecting...</h1>\n" 

259 "<p>You should be redirected automatically to the target URL: " 

260 f'<a href="{html_location}">{html_location}</a>. If not, click the link.\n', 

261 code, 

262 mimetype="text/html", 

263 ) 

264 response.location = location 

265 return response 

266 

267 

268def _append_slash_redirect(environ: wt.WSGIEnvironment, code: int = 308) -> Response: 

269 """Redirect to the current URL with a slash appended. 

270 

271 If the current URL is ``/user/42``, the redirect URL will be 

272 ``42/``. When joined to the current URL during response 

273 processing or by the browser, this will produce ``/user/42/``. 

274 

275 The behavior is undefined if the path ends with a slash already. If 

276 called unconditionally on a URL, it may produce a redirect loop. 

277 

278 :param environ: Use the path and query from this WSGI environment 

279 to produce the redirect URL. 

280 :param code: the status code for the redirect. 

281 

282 .. deprecated:: 3.2 

283 Will be removed in Werkzeug 4.0. Use ``routing.Map`` with 

284 ``strict_slashes`` instead. 

285 

286 .. versionchanged:: 2.1 

287 Produce a relative URL that only modifies the last segment. 

288 Relevant when the current path has multiple segments. 

289 

290 .. versionchanged:: 2.1 

291 The default status code is 308 instead of 301. This preserves 

292 the request method and body. 

293 """ 

294 tail = environ["PATH_INFO"].rpartition("/")[2] 

295 

296 if not tail: 

297 new_path = "./" 

298 else: 

299 new_path = f"{tail}/" 

300 

301 query_string = environ.get("QUERY_STRING") 

302 

303 if query_string: 

304 new_path = f"{new_path}?{query_string}" 

305 

306 return redirect(new_path, code) 

307 

308 

309def send_file( 

310 path_or_file: os.PathLike[str] | str | t.IO[bytes], 

311 environ: wt.WSGIEnvironment, 

312 mimetype: str | None = None, 

313 as_attachment: bool = False, 

314 download_name: str | None = None, 

315 conditional: bool = True, 

316 etag: bool | ETag | str = True, 

317 last_modified: datetime | int | float | None = None, 

318 max_age: int 

319 | t.Callable[[os.PathLike[str] | str | None], int | None] 

320 | None = None, 

321 use_x_sendfile: bool = False, 

322 response_class: type[Response] | None = None, 

323 _root_path: os.PathLike[str] | str | None = None, 

324) -> Response: 

325 """Send the contents of a file to the client. 

326 

327 The first argument can be a file path or a file-like object in ``rb`` mode. 

328 Paths are preferred in most cases because Werkzeug can manage the file. 

329 Passing a file-like object is mostly useful when building a file in memory 

330 with :class:`io.BytesIO`. If the file is seekable, its position will be set 

331 to the beginning. 

332 

333 Never pass a path provided by a user. The path is assumed to be trusted, so 

334 a user could craft a path to access a file you didn't intend. Use 

335 :func:`send_from_directory` to safely serve user-provided paths. 

336 

337 If the WSGI server provides ``environ["wsgi.file_wrapper"]``, it is 

338 used. Alternatively, if the HTTP server supports ``X-Sendfile``, 

339 ``use_x_sendfile=True`` will tell the server to send the given path, which 

340 is much more efficient than reading it in Python. 

341 

342 :param path_or_file: The path to the file to send, relative to the current 

343 directory if a relative path is given. Alternatively, a file-like object 

344 in ``rb`` mode. The path will be ``file.name`` if available. 

345 :param environ: The WSGI environ for the current request. 

346 :param mimetype: The MIME type to send for the file. If not provided, it is 

347 detected from the file, and falls back to ``application/octet-stream``. 

348 :param as_attachment: Indicate to a browser that it should offer to save the 

349 file instead of displaying it. 

350 :param download_name: The default name browsers will use when saving the 

351 file. Defaults to the passed file name. 

352 :param conditional: Enable conditional and range responses based on request 

353 headers. 

354 :param etag: Generate a strong ETag based on the file's last modified time 

355 and size if available. Or an already generated ETag, assumed strong if a 

356 ``str`` is given. 

357 :param last_modified: The last modified time as a ``datetime`` or seconds 

358 seconds. If not provided, it is detected from the file. Typically only 

359 needs to be provided for ``BytesIO``. 

360 :param max_age: How long the client should cache the file, in seconds. If 

361 set, ``Cache-Control`` will be ``public``, otherwise it will be 

362 ``no-cache`` to prefer conditional caching. 

363 :param use_x_sendfile: Set the ``X-Sendfile`` header to let the server 

364 efficiently send the file. Requires support from the HTTP server.. 

365 :param response_class: Build the response using this class. Defaults to 

366 :class:`.Response`. 

367 :param _root_path: Do not use. For internal use only. Use 

368 :func:`send_from_directory` to safely send files under a path. 

369 

370 .. versionchanged:: 3.2 

371 Improve handling of conditional and range requests. 

372 

373 Path, size, and modification time are detected from file-like objects. 

374 The position is set to the beginning. 

375 

376 If ``mimetype`` nor ``download_name`` is given, default to 

377 ``application/octet-stream``. 

378 

379 .. versionchanged:: 2.0.2 

380 ``send_file`` only sets a detected ``Content-Encoding`` if 

381 ``as_attachment`` is disabled. 

382 

383 .. versionadded:: 2.0 

384 Adapted from Flask's implementation. 

385 

386 ``download_name`` replaces Flask's ``attachment_filename`` parameter. If 

387 ``as_attachment=False``, it is passed with 

388 ``Content-Disposition: inline`` instead. 

389 

390 ``max_age`` replaces Flask's ``cache_timeout`` parameter. 

391 ``conditional`` is enabled and ``max_age`` is not set by default. 

392 

393 ``etag`` replaces Flask's ``add_etags`` parameter. It can be a string to 

394 use instead of generating one. 

395 

396 If an encoding is returned when guessing ``mimetype``, set the 

397 ``Content-Encoding`` header. 

398 """ 

399 if response_class is None: 

400 from .wrappers import Response 

401 

402 response_class = Response 

403 

404 path: os.PathLike[str] | str | None = None 

405 file: t.IO[bytes] 

406 size: int | None = None 

407 headers = Headers() 

408 

409 if isinstance(path_or_file, (os.PathLike, str)): 

410 path = path_or_file 

411 

412 # Flask will pass app.root_path, allowing its send_file wrapper 

413 # to not have to deal with paths. 

414 if _root_path is not None: 

415 path = os.path.join(_root_path, path) 

416 else: 

417 path = os.path.abspath(path) 

418 

419 stat = os.stat(path) 

420 size = stat.st_size 

421 

422 if last_modified is None: 

423 last_modified = stat.st_mtime 

424 

425 file = open(path, "rb") 

426 else: 

427 file = path_or_file 

428 

429 if isinstance(file, io.TextIOBase): 

430 raise ValueError("File must be in binary mode, or 'BytesIO'.") 

431 

432 if hasattr(file, "name"): 

433 path = file.name 

434 

435 if file.seekable(): 

436 size = file.seek(0, os.SEEK_END) 

437 file.seek(0) 

438 

439 if size is None or last_modified is None: 

440 try: 

441 fileno = file.fileno() 

442 except OSError: 

443 pass 

444 else: 

445 stat = os.stat(fileno) 

446 

447 if size is None: 

448 size = stat.st_size 

449 

450 if last_modified is None: 

451 last_modified = stat.st_mtime 

452 

453 if download_name is None and path is not None: 

454 download_name = os.path.basename(path) 

455 

456 if mimetype is None: 

457 if download_name is None: 

458 mimetype = "application/octet-stream" 

459 else: 

460 mimetype, encoding = mimetypes.guess_type(download_name) 

461 

462 if mimetype is None: 

463 mimetype = "application/octet-stream" 

464 

465 # Don't send encoding for attachments, it causes browsers to 

466 # decompress tar.gz files when saving. 

467 if encoding is not None and not as_attachment: 

468 headers["Content-Encoding"] = encoding 

469 

470 if download_name is not None: 

471 if download_name.isascii(): 

472 names = {"filename": download_name} 

473 else: 

474 simple = unicodedata.normalize("NFKD", download_name) 

475 simple = simple.encode("ascii", "ignore").decode("ascii") 

476 # safe = RFC 5987 attr-char 

477 quoted = quote(download_name, safe="!#$&+-.^_`|~") 

478 names = {"filename": simple, "filename*": f"UTF-8''{quoted}"} 

479 

480 value = "attachment" if as_attachment else "inline" 

481 headers.set("Content-Disposition", value, **names) 

482 elif as_attachment: 

483 raise TypeError( 

484 "No name provided for attachment. Either pass 'download_name', or" 

485 " pass a path instead of a file." 

486 ) 

487 

488 from .wrappers.request import Request 

489 

490 request = Request(environ) 

491 rv = response_class( 

492 request.wrap_file(file), 

493 mimetype=mimetype, 

494 headers=headers, 

495 direct_passthrough=True, 

496 ) 

497 

498 if use_x_sendfile and path is not None: 

499 file.close() 

500 rv.headers["X-Sendfile"] = path 

501 rv.response = [] 

502 

503 if size is not None: 

504 rv.content_length = size 

505 

506 if last_modified is not None: 

507 rv.last_modified = last_modified 

508 

509 cache_control = ResponseCacheControl() 

510 cache_control.no_cache = True 

511 

512 # Flask will pass app.get_send_file_max_age, allowing its send_file 

513 # wrapper to not have to deal with paths. 

514 if callable(max_age): 

515 max_age = max_age(path) 

516 

517 if max_age is not None: 

518 if max_age > 0: 

519 cache_control.no_cache = None 

520 cache_control.public = True 

521 

522 cache_control.max_age = max_age 

523 rv.expires = int(time() + max_age) 

524 

525 rv.cache_control = cache_control 

526 

527 if isinstance(etag, str): 

528 rv.etag = etag 

529 elif etag: 

530 parts = [] 

531 

532 if last_modified is not None: 

533 if isinstance(last_modified, datetime): 

534 parts.append(str(last_modified.timestamp())) 

535 else: 

536 parts.append(str(last_modified)) 

537 

538 if size is not None: 

539 parts.append(str(size)) 

540 

541 rv.etag = "-".join(parts) 

542 

543 if conditional: 

544 try: 

545 rv.apply_conditions(request) 

546 rv.apply_range(request) 

547 except HTTPException: 

548 rv.close() 

549 raise 

550 

551 if rv.status_code == 304: 

552 rv.headers.pop("X-Sendfile", None) 

553 

554 return rv 

555 

556 

557def send_from_directory( 

558 directory: os.PathLike[str] | str, 

559 path: os.PathLike[str] | str, 

560 environ: wt.WSGIEnvironment, 

561 **kwargs: t.Any, 

562) -> Response: 

563 """Send a file from within a directory using :func:`send_file`. 

564 

565 This is a secure way to serve files from a folder, such as static 

566 files or uploads. Uses :func:`~werkzeug.security.safe_join` to 

567 ensure the path coming from the client is not maliciously crafted to 

568 point outside the specified directory. 

569 

570 If the final path does not point to an existing regular file, 

571 returns a 404 :exc:`~werkzeug.exceptions.NotFound` error. 

572 

573 :param directory: The directory that ``path`` must be located under. This *must not* 

574 be a value provided by the client, otherwise it becomes insecure. 

575 :param path: The path to the file to send, relative to ``directory``. This is the 

576 part of the path provided by the client, which is checked for security. 

577 :param environ: The WSGI environ for the current request. 

578 :param kwargs: Arguments to pass to :func:`send_file`. 

579 

580 .. versionadded:: 2.0 

581 Adapted from Flask's implementation. 

582 """ 

583 path_str = safe_join(os.fspath(directory), os.fspath(path)) 

584 

585 if path_str is None: 

586 raise NotFound() 

587 

588 # Flask will pass app.root_path, allowing its send_from_directory 

589 # wrapper to not have to deal with paths. 

590 if "_root_path" in kwargs: 

591 path_str = os.path.join(kwargs["_root_path"], path_str) 

592 

593 if not os.path.isfile(path_str): 

594 raise NotFound() 

595 

596 return send_file(path_str, environ, **kwargs) 

597 

598 

599def import_string(import_name: str, silent: bool = False) -> t.Any: 

600 """Imports an object based on a string. This is useful if you want to 

601 use import paths as endpoints or something similar. An import path can 

602 be specified either in dotted notation (``xml.sax.saxutils.escape``) 

603 or with a colon as object delimiter (``xml.sax.saxutils:escape``). 

604 

605 If `silent` is True the return value will be `None` if the import fails. 

606 

607 :param import_name: the dotted name for the object to import. 

608 :param silent: if set to `True` import errors are ignored and 

609 `None` is returned instead. 

610 :return: imported object 

611 """ 

612 import_name = import_name.replace(":", ".") 

613 try: 

614 try: 

615 __import__(import_name) 

616 except ImportError: 

617 if "." not in import_name: 

618 raise 

619 else: 

620 return sys.modules[import_name] 

621 

622 module_name, obj_name = import_name.rsplit(".", 1) 

623 module = __import__(module_name, globals(), locals(), [obj_name]) 

624 try: 

625 return getattr(module, obj_name) 

626 except AttributeError as e: 

627 raise ImportError(e) from None 

628 

629 except ImportError as e: 

630 if not silent: 

631 raise ImportStringError(import_name, e).with_traceback( 

632 sys.exc_info()[2] 

633 ) from None 

634 

635 return None 

636 

637 

638def find_modules( 

639 import_path: str, include_packages: bool = False, recursive: bool = False 

640) -> t.Iterator[str]: 

641 """Finds all the modules below a package. This can be useful to 

642 automatically import all views / controllers so that their metaclasses / 

643 function decorators have a chance to register themselves on the 

644 application. 

645 

646 Packages are not returned unless `include_packages` is `True`. This can 

647 also recursively list modules but in that case it will import all the 

648 packages to get the correct load path of that module. 

649 

650 :param import_path: the dotted name for the package to find child modules. 

651 :param include_packages: set to `True` if packages should be returned, too. 

652 :param recursive: set to `True` if recursion should happen. 

653 :return: generator 

654 """ 

655 module = import_string(import_path) 

656 path = getattr(module, "__path__", None) 

657 if path is None: 

658 raise ValueError(f"{import_path!r} is not a package") 

659 basename = f"{module.__name__}." 

660 for _importer, modname, ispkg in pkgutil.iter_modules(path): 

661 modname = basename + modname 

662 if ispkg: 

663 if include_packages: 

664 yield modname 

665 if recursive: 

666 yield from find_modules(modname, include_packages, True) 

667 else: 

668 yield modname 

669 

670 

671class ImportStringError(ImportError): 

672 """Provides information about a failed :func:`import_string` attempt.""" 

673 

674 #: String in dotted notation that failed to be imported. 

675 import_name: str 

676 #: Wrapped exception. 

677 exception: BaseException 

678 

679 def __init__(self, import_name: str, exception: BaseException) -> None: 

680 self.import_name = import_name 

681 self.exception = exception 

682 msg = import_name 

683 name = "" 

684 tracked = [] 

685 for part in import_name.replace(":", ".").split("."): 

686 name = f"{name}.{part}" if name else part 

687 imported = import_string(name, silent=True) 

688 if imported: 

689 tracked.append((name, getattr(imported, "__file__", None))) 

690 else: 

691 track = [f"- {n!r} found in {i!r}." for n, i in tracked] 

692 track.append(f"- {name!r} not found.") 

693 track_str = "\n".join(track) 

694 msg = ( 

695 f"import_string() failed for {import_name!r}. Possible reasons" 

696 f" are:\n\n" 

697 "- missing __init__.py in a package;\n" 

698 "- package or module path not included in sys.path;\n" 

699 "- duplicated package or module name taking precedence in" 

700 " sys.path;\n" 

701 "- missing module, class, function or variable;\n\n" 

702 f"Debugged import:\n\n{track_str}\n\n" 

703 f"Original exception:\n\n{type(exception).__name__}: {exception}" 

704 ) 

705 break 

706 

707 super().__init__(msg) 

708 

709 def __repr__(self) -> str: 

710 return f"<{type(self).__name__}({self.import_name!r}, {self.exception!r})>" 

711 

712 

713if not t.TYPE_CHECKING: 

714 

715 def __getattr__(name: str) -> t.Any: 

716 import warnings 

717 

718 if name == "environ_property": 

719 from ._header_property import environ_property 

720 

721 warnings.warn( 

722 "'environ_property' is deprecated and will be removed in" 

723 " Werkzeug 4.0. Access 'environ' directly instead.", 

724 DeprecationWarning, 

725 stacklevel=2, 

726 ) 

727 return environ_property 

728 

729 if name == "header_property": 

730 from ._header_property import header_property 

731 

732 warnings.warn( 

733 "'header_property' is deprecated and will be removed in" 

734 " Werkzeug 4.0. Access 'headers' directly instead.", 

735 DeprecationWarning, 

736 stacklevel=2, 

737 ) 

738 return header_property 

739 

740 if name == "get_content_type": 

741 warnings.warn( 

742 "'get_content_type' is deprecated and will be removed in" 

743 " Werkzeug 4.0. Set 'Response.mimetype' instead.", 

744 DeprecationWarning, 

745 stacklevel=2, 

746 ) 

747 return _get_content_type 

748 

749 if name == "append_slash_redirect": 

750 warnings.warn( 

751 "'append_slash_redirect' is deprecated and will be removed in Werkzeug" 

752 " 4.0. Use 'routing.Map' with 'strict_slashes' instead.", 

753 DeprecationWarning, 

754 stacklevel=2, 

755 ) 

756 return _append_slash_redirect 

757 

758 raise AttributeError(name)