Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/flask/helpers.py: 31%

140 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-09 06:08 +0000

1from __future__ import annotations 

2 

3import importlib.util 

4import os 

5import socket 

6import sys 

7import typing as t 

8import warnings 

9from datetime import datetime 

10from functools import lru_cache 

11from functools import update_wrapper 

12from threading import RLock 

13 

14import werkzeug.utils 

15from werkzeug.exceptions import abort as _wz_abort 

16from werkzeug.utils import redirect as _wz_redirect 

17 

18from .globals import _cv_request 

19from .globals import current_app 

20from .globals import request 

21from .globals import request_ctx 

22from .globals import session 

23from .signals import message_flashed 

24 

25if t.TYPE_CHECKING: # pragma: no cover 

26 from werkzeug.wrappers import Response as BaseResponse 

27 from .wrappers import Response 

28 

29 

30def get_debug_flag() -> bool: 

31 """Get whether debug mode should be enabled for the app, indicated by the 

32 :envvar:`FLASK_DEBUG` environment variable. The default is ``False``. 

33 """ 

34 val = os.environ.get("FLASK_DEBUG") 

35 return bool(val and val.lower() not in {"0", "false", "no"}) 

36 

37 

38def get_load_dotenv(default: bool = True) -> bool: 

39 """Get whether the user has disabled loading default dotenv files by 

40 setting :envvar:`FLASK_SKIP_DOTENV`. The default is ``True``, load 

41 the files. 

42 

43 :param default: What to return if the env var isn't set. 

44 """ 

45 val = os.environ.get("FLASK_SKIP_DOTENV") 

46 

47 if not val: 

48 return default 

49 

50 return val.lower() in ("0", "false", "no") 

51 

52 

53def stream_with_context( 

54 generator_or_function: ( 

55 t.Iterator[t.AnyStr] | t.Callable[..., t.Iterator[t.AnyStr]] 

56 ) 

57) -> t.Iterator[t.AnyStr]: 

58 """Request contexts disappear when the response is started on the server. 

59 This is done for efficiency reasons and to make it less likely to encounter 

60 memory leaks with badly written WSGI middlewares. The downside is that if 

61 you are using streamed responses, the generator cannot access request bound 

62 information any more. 

63 

64 This function however can help you keep the context around for longer:: 

65 

66 from flask import stream_with_context, request, Response 

67 

68 @app.route('/stream') 

69 def streamed_response(): 

70 @stream_with_context 

71 def generate(): 

72 yield 'Hello ' 

73 yield request.args['name'] 

74 yield '!' 

75 return Response(generate()) 

76 

77 Alternatively it can also be used around a specific generator:: 

78 

79 from flask import stream_with_context, request, Response 

80 

81 @app.route('/stream') 

82 def streamed_response(): 

83 def generate(): 

84 yield 'Hello ' 

85 yield request.args['name'] 

86 yield '!' 

87 return Response(stream_with_context(generate())) 

88 

89 .. versionadded:: 0.9 

90 """ 

91 try: 

92 gen = iter(generator_or_function) # type: ignore 

93 except TypeError: 

94 

95 def decorator(*args: t.Any, **kwargs: t.Any) -> t.Any: 

96 gen = generator_or_function(*args, **kwargs) # type: ignore 

97 return stream_with_context(gen) 

98 

99 return update_wrapper(decorator, generator_or_function) # type: ignore 

100 

101 def generator() -> t.Generator: 

102 ctx = _cv_request.get(None) 

103 if ctx is None: 

104 raise RuntimeError( 

105 "'stream_with_context' can only be used when a request" 

106 " context is active, such as in a view function." 

107 ) 

108 with ctx: 

109 # Dummy sentinel. Has to be inside the context block or we're 

110 # not actually keeping the context around. 

111 yield None 

112 

113 # The try/finally is here so that if someone passes a WSGI level 

114 # iterator in we're still running the cleanup logic. Generators 

115 # don't need that because they are closed on their destruction 

116 # automatically. 

117 try: 

118 yield from gen 

119 finally: 

120 if hasattr(gen, "close"): 

121 gen.close() 

122 

123 # The trick is to start the generator. Then the code execution runs until 

124 # the first dummy None is yielded at which point the context was already 

125 # pushed. This item is discarded. Then when the iteration continues the 

126 # real generator is executed. 

127 wrapped_g = generator() 

128 next(wrapped_g) 

129 return wrapped_g 

130 

131 

132def make_response(*args: t.Any) -> Response: 

133 """Sometimes it is necessary to set additional headers in a view. Because 

134 views do not have to return response objects but can return a value that 

135 is converted into a response object by Flask itself, it becomes tricky to 

136 add headers to it. This function can be called instead of using a return 

137 and you will get a response object which you can use to attach headers. 

138 

139 If view looked like this and you want to add a new header:: 

140 

141 def index(): 

142 return render_template('index.html', foo=42) 

143 

144 You can now do something like this:: 

145 

146 def index(): 

147 response = make_response(render_template('index.html', foo=42)) 

148 response.headers['X-Parachutes'] = 'parachutes are cool' 

149 return response 

150 

151 This function accepts the very same arguments you can return from a 

152 view function. This for example creates a response with a 404 error 

153 code:: 

154 

155 response = make_response(render_template('not_found.html'), 404) 

156 

157 The other use case of this function is to force the return value of a 

158 view function into a response which is helpful with view 

159 decorators:: 

160 

161 response = make_response(view_function()) 

162 response.headers['X-Parachutes'] = 'parachutes are cool' 

163 

164 Internally this function does the following things: 

165 

166 - if no arguments are passed, it creates a new response argument 

167 - if one argument is passed, :meth:`flask.Flask.make_response` 

168 is invoked with it. 

169 - if more than one argument is passed, the arguments are passed 

170 to the :meth:`flask.Flask.make_response` function as tuple. 

171 

172 .. versionadded:: 0.6 

173 """ 

174 if not args: 

175 return current_app.response_class() 

176 if len(args) == 1: 

177 args = args[0] 

178 return current_app.make_response(args) # type: ignore 

179 

180 

181def url_for( 

182 endpoint: str, 

183 *, 

184 _anchor: str | None = None, 

185 _method: str | None = None, 

186 _scheme: str | None = None, 

187 _external: bool | None = None, 

188 **values: t.Any, 

189) -> str: 

190 """Generate a URL to the given endpoint with the given values. 

191 

192 This requires an active request or application context, and calls 

193 :meth:`current_app.url_for() <flask.Flask.url_for>`. See that method 

194 for full documentation. 

195 

196 :param endpoint: The endpoint name associated with the URL to 

197 generate. If this starts with a ``.``, the current blueprint 

198 name (if any) will be used. 

199 :param _anchor: If given, append this as ``#anchor`` to the URL. 

200 :param _method: If given, generate the URL associated with this 

201 method for the endpoint. 

202 :param _scheme: If given, the URL will have this scheme if it is 

203 external. 

204 :param _external: If given, prefer the URL to be internal (False) or 

205 require it to be external (True). External URLs include the 

206 scheme and domain. When not in an active request, URLs are 

207 external by default. 

208 :param values: Values to use for the variable parts of the URL rule. 

209 Unknown keys are appended as query string arguments, like 

210 ``?a=b&c=d``. 

211 

212 .. versionchanged:: 2.2 

213 Calls ``current_app.url_for``, allowing an app to override the 

214 behavior. 

215 

216 .. versionchanged:: 0.10 

217 The ``_scheme`` parameter was added. 

218 

219 .. versionchanged:: 0.9 

220 The ``_anchor`` and ``_method`` parameters were added. 

221 

222 .. versionchanged:: 0.9 

223 Calls ``app.handle_url_build_error`` on build errors. 

224 """ 

225 return current_app.url_for( 

226 endpoint, 

227 _anchor=_anchor, 

228 _method=_method, 

229 _scheme=_scheme, 

230 _external=_external, 

231 **values, 

232 ) 

233 

234 

235def redirect( 

236 location: str, code: int = 302, Response: type[BaseResponse] | None = None 

237) -> BaseResponse: 

238 """Create a redirect response object. 

239 

240 If :data:`~flask.current_app` is available, it will use its 

241 :meth:`~flask.Flask.redirect` method, otherwise it will use 

242 :func:`werkzeug.utils.redirect`. 

243 

244 :param location: The URL to redirect to. 

245 :param code: The status code for the redirect. 

246 :param Response: The response class to use. Not used when 

247 ``current_app`` is active, which uses ``app.response_class``. 

248 

249 .. versionadded:: 2.2 

250 Calls ``current_app.redirect`` if available instead of always 

251 using Werkzeug's default ``redirect``. 

252 """ 

253 if current_app: 

254 return current_app.redirect(location, code=code) 

255 

256 return _wz_redirect(location, code=code, Response=Response) 

257 

258 

259def abort(code: int | BaseResponse, *args: t.Any, **kwargs: t.Any) -> t.NoReturn: 

260 """Raise an :exc:`~werkzeug.exceptions.HTTPException` for the given 

261 status code. 

262 

263 If :data:`~flask.current_app` is available, it will call its 

264 :attr:`~flask.Flask.aborter` object, otherwise it will use 

265 :func:`werkzeug.exceptions.abort`. 

266 

267 :param code: The status code for the exception, which must be 

268 registered in ``app.aborter``. 

269 :param args: Passed to the exception. 

270 :param kwargs: Passed to the exception. 

271 

272 .. versionadded:: 2.2 

273 Calls ``current_app.aborter`` if available instead of always 

274 using Werkzeug's default ``abort``. 

275 """ 

276 if current_app: 

277 current_app.aborter(code, *args, **kwargs) 

278 

279 _wz_abort(code, *args, **kwargs) 

280 

281 

282def get_template_attribute(template_name: str, attribute: str) -> t.Any: 

283 """Loads a macro (or variable) a template exports. This can be used to 

284 invoke a macro from within Python code. If you for example have a 

285 template named :file:`_cider.html` with the following contents: 

286 

287 .. sourcecode:: html+jinja 

288 

289 {% macro hello(name) %}Hello {{ name }}!{% endmacro %} 

290 

291 You can access this from Python code like this:: 

292 

293 hello = get_template_attribute('_cider.html', 'hello') 

294 return hello('World') 

295 

296 .. versionadded:: 0.2 

297 

298 :param template_name: the name of the template 

299 :param attribute: the name of the variable of macro to access 

300 """ 

301 return getattr(current_app.jinja_env.get_template(template_name).module, attribute) 

302 

303 

304def flash(message: str, category: str = "message") -> None: 

305 """Flashes a message to the next request. In order to remove the 

306 flashed message from the session and to display it to the user, 

307 the template has to call :func:`get_flashed_messages`. 

308 

309 .. versionchanged:: 0.3 

310 `category` parameter added. 

311 

312 :param message: the message to be flashed. 

313 :param category: the category for the message. The following values 

314 are recommended: ``'message'`` for any kind of message, 

315 ``'error'`` for errors, ``'info'`` for information 

316 messages and ``'warning'`` for warnings. However any 

317 kind of string can be used as category. 

318 """ 

319 # Original implementation: 

320 # 

321 # session.setdefault('_flashes', []).append((category, message)) 

322 # 

323 # This assumed that changes made to mutable structures in the session are 

324 # always in sync with the session object, which is not true for session 

325 # implementations that use external storage for keeping their keys/values. 

326 flashes = session.get("_flashes", []) 

327 flashes.append((category, message)) 

328 session["_flashes"] = flashes 

329 app = current_app._get_current_object() # type: ignore 

330 message_flashed.send( 

331 app, 

332 _async_wrapper=app.ensure_sync, 

333 message=message, 

334 category=category, 

335 ) 

336 

337 

338def get_flashed_messages( 

339 with_categories: bool = False, category_filter: t.Iterable[str] = () 

340) -> list[str] | list[tuple[str, str]]: 

341 """Pulls all flashed messages from the session and returns them. 

342 Further calls in the same request to the function will return 

343 the same messages. By default just the messages are returned, 

344 but when `with_categories` is set to ``True``, the return value will 

345 be a list of tuples in the form ``(category, message)`` instead. 

346 

347 Filter the flashed messages to one or more categories by providing those 

348 categories in `category_filter`. This allows rendering categories in 

349 separate html blocks. The `with_categories` and `category_filter` 

350 arguments are distinct: 

351 

352 * `with_categories` controls whether categories are returned with message 

353 text (``True`` gives a tuple, where ``False`` gives just the message text). 

354 * `category_filter` filters the messages down to only those matching the 

355 provided categories. 

356 

357 See :doc:`/patterns/flashing` for examples. 

358 

359 .. versionchanged:: 0.3 

360 `with_categories` parameter added. 

361 

362 .. versionchanged:: 0.9 

363 `category_filter` parameter added. 

364 

365 :param with_categories: set to ``True`` to also receive categories. 

366 :param category_filter: filter of categories to limit return values. Only 

367 categories in the list will be returned. 

368 """ 

369 flashes = request_ctx.flashes 

370 if flashes is None: 

371 flashes = session.pop("_flashes") if "_flashes" in session else [] 

372 request_ctx.flashes = flashes 

373 if category_filter: 

374 flashes = list(filter(lambda f: f[0] in category_filter, flashes)) 

375 if not with_categories: 

376 return [x[1] for x in flashes] 

377 return flashes 

378 

379 

380def _prepare_send_file_kwargs(**kwargs: t.Any) -> dict[str, t.Any]: 

381 if kwargs.get("max_age") is None: 

382 kwargs["max_age"] = current_app.get_send_file_max_age 

383 

384 kwargs.update( 

385 environ=request.environ, 

386 use_x_sendfile=current_app.config["USE_X_SENDFILE"], 

387 response_class=current_app.response_class, 

388 _root_path=current_app.root_path, # type: ignore 

389 ) 

390 return kwargs 

391 

392 

393def send_file( 

394 path_or_file: os.PathLike | str | t.BinaryIO, 

395 mimetype: str | None = None, 

396 as_attachment: bool = False, 

397 download_name: str | None = None, 

398 conditional: bool = True, 

399 etag: bool | str = True, 

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

401 max_age: None | (int | t.Callable[[str | None], int | None]) = None, 

402) -> Response: 

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

404 

405 The first argument can be a file path or a file-like object. Paths 

406 are preferred in most cases because Werkzeug can manage the file and 

407 get extra information from the path. Passing a file-like object 

408 requires that the file is opened in binary mode, and is mostly 

409 useful when building a file in memory with :class:`io.BytesIO`. 

410 

411 Never pass file paths provided by a user. The path is assumed to be 

412 trusted, so a user could craft a path to access a file you didn't 

413 intend. Use :func:`send_from_directory` to safely serve 

414 user-requested paths from within a directory. 

415 

416 If the WSGI server sets a ``file_wrapper`` in ``environ``, it is 

417 used, otherwise Werkzeug's built-in wrapper is used. Alternatively, 

418 if the HTTP server supports ``X-Sendfile``, configuring Flask with 

419 ``USE_X_SENDFILE = True`` will tell the server to send the given 

420 path, which is much more efficient than reading it in Python. 

421 

422 :param path_or_file: The path to the file to send, relative to the 

423 current working directory if a relative path is given. 

424 Alternatively, a file-like object opened in binary mode. Make 

425 sure the file pointer is seeked to the start of the data. 

426 :param mimetype: The MIME type to send for the file. If not 

427 provided, it will try to detect it from the file name. 

428 :param as_attachment: Indicate to a browser that it should offer to 

429 save the file instead of displaying it. 

430 :param download_name: The default name browsers will use when saving 

431 the file. Defaults to the passed file name. 

432 :param conditional: Enable conditional and range responses based on 

433 request headers. Requires passing a file path and ``environ``. 

434 :param etag: Calculate an ETag for the file, which requires passing 

435 a file path. Can also be a string to use instead. 

436 :param last_modified: The last modified time to send for the file, 

437 in seconds. If not provided, it will try to detect it from the 

438 file path. 

439 :param max_age: How long the client should cache the file, in 

440 seconds. If set, ``Cache-Control`` will be ``public``, otherwise 

441 it will be ``no-cache`` to prefer conditional caching. 

442 

443 .. versionchanged:: 2.0 

444 ``download_name`` replaces the ``attachment_filename`` 

445 parameter. If ``as_attachment=False``, it is passed with 

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

447 

448 .. versionchanged:: 2.0 

449 ``max_age`` replaces the ``cache_timeout`` parameter. 

450 ``conditional`` is enabled and ``max_age`` is not set by 

451 default. 

452 

453 .. versionchanged:: 2.0 

454 ``etag`` replaces the ``add_etags`` parameter. It can be a 

455 string to use instead of generating one. 

456 

457 .. versionchanged:: 2.0 

458 Passing a file-like object that inherits from 

459 :class:`~io.TextIOBase` will raise a :exc:`ValueError` rather 

460 than sending an empty file. 

461 

462 .. versionadded:: 2.0 

463 Moved the implementation to Werkzeug. This is now a wrapper to 

464 pass some Flask-specific arguments. 

465 

466 .. versionchanged:: 1.1 

467 ``filename`` may be a :class:`~os.PathLike` object. 

468 

469 .. versionchanged:: 1.1 

470 Passing a :class:`~io.BytesIO` object supports range requests. 

471 

472 .. versionchanged:: 1.0.3 

473 Filenames are encoded with ASCII instead of Latin-1 for broader 

474 compatibility with WSGI servers. 

475 

476 .. versionchanged:: 1.0 

477 UTF-8 filenames as specified in :rfc:`2231` are supported. 

478 

479 .. versionchanged:: 0.12 

480 The filename is no longer automatically inferred from file 

481 objects. If you want to use automatic MIME and etag support, 

482 pass a filename via ``filename_or_fp`` or 

483 ``attachment_filename``. 

484 

485 .. versionchanged:: 0.12 

486 ``attachment_filename`` is preferred over ``filename`` for MIME 

487 detection. 

488 

489 .. versionchanged:: 0.9 

490 ``cache_timeout`` defaults to 

491 :meth:`Flask.get_send_file_max_age`. 

492 

493 .. versionchanged:: 0.7 

494 MIME guessing and etag support for file-like objects was 

495 deprecated because it was unreliable. Pass a filename if you are 

496 able to, otherwise attach an etag yourself. 

497 

498 .. versionchanged:: 0.5 

499 The ``add_etags``, ``cache_timeout`` and ``conditional`` 

500 parameters were added. The default behavior is to add etags. 

501 

502 .. versionadded:: 0.2 

503 """ 

504 return werkzeug.utils.send_file( # type: ignore[return-value] 

505 **_prepare_send_file_kwargs( 

506 path_or_file=path_or_file, 

507 environ=request.environ, 

508 mimetype=mimetype, 

509 as_attachment=as_attachment, 

510 download_name=download_name, 

511 conditional=conditional, 

512 etag=etag, 

513 last_modified=last_modified, 

514 max_age=max_age, 

515 ) 

516 ) 

517 

518 

519def send_from_directory( 

520 directory: os.PathLike | str, 

521 path: os.PathLike | str, 

522 **kwargs: t.Any, 

523) -> Response: 

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

525 

526 .. code-block:: python 

527 

528 @app.route("/uploads/<path:name>") 

529 def download_file(name): 

530 return send_from_directory( 

531 app.config['UPLOAD_FOLDER'], name, as_attachment=True 

532 ) 

533 

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

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

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

537 point outside the specified directory. 

538 

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

540 raises a 404 :exc:`~werkzeug.exceptions.NotFound` error. 

541 

542 :param directory: The directory that ``path`` must be located under, 

543 relative to the current application's root path. 

544 :param path: The path to the file to send, relative to 

545 ``directory``. 

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

547 

548 .. versionchanged:: 2.0 

549 ``path`` replaces the ``filename`` parameter. 

550 

551 .. versionadded:: 2.0 

552 Moved the implementation to Werkzeug. This is now a wrapper to 

553 pass some Flask-specific arguments. 

554 

555 .. versionadded:: 0.5 

556 """ 

557 return werkzeug.utils.send_from_directory( # type: ignore[return-value] 

558 directory, path, **_prepare_send_file_kwargs(**kwargs) 

559 ) 

560 

561 

562def get_root_path(import_name: str) -> str: 

563 """Find the root path of a package, or the path that contains a 

564 module. If it cannot be found, returns the current working 

565 directory. 

566 

567 Not to be confused with the value returned by :func:`find_package`. 

568 

569 :meta private: 

570 """ 

571 # Module already imported and has a file attribute. Use that first. 

572 mod = sys.modules.get(import_name) 

573 

574 if mod is not None and hasattr(mod, "__file__") and mod.__file__ is not None: 

575 return os.path.dirname(os.path.abspath(mod.__file__)) 

576 

577 # Next attempt: check the loader. 

578 spec = importlib.util.find_spec(import_name) 

579 loader = spec.loader if spec is not None else None 

580 

581 # Loader does not exist or we're referring to an unloaded main 

582 # module or a main module without path (interactive sessions), go 

583 # with the current working directory. 

584 if loader is None or import_name == "__main__": 

585 return os.getcwd() 

586 

587 if hasattr(loader, "get_filename"): 

588 filepath = loader.get_filename(import_name) 

589 else: 

590 # Fall back to imports. 

591 __import__(import_name) 

592 mod = sys.modules[import_name] 

593 filepath = getattr(mod, "__file__", None) 

594 

595 # If we don't have a file path it might be because it is a 

596 # namespace package. In this case pick the root path from the 

597 # first module that is contained in the package. 

598 if filepath is None: 

599 raise RuntimeError( 

600 "No root path can be found for the provided module" 

601 f" {import_name!r}. This can happen because the module" 

602 " came from an import hook that does not provide file" 

603 " name information or because it's a namespace package." 

604 " In this case the root path needs to be explicitly" 

605 " provided." 

606 ) 

607 

608 # filepath is import_name.py for a module, or __init__.py for a package. 

609 return os.path.dirname(os.path.abspath(filepath)) 

610 

611 

612class locked_cached_property(werkzeug.utils.cached_property): 

613 """A :func:`property` that is only evaluated once. Like 

614 :class:`werkzeug.utils.cached_property` except access uses a lock 

615 for thread safety. 

616 

617 .. deprecated:: 2.3 

618 Will be removed in Flask 2.4. Use a lock inside the decorated function if 

619 locking is needed. 

620 

621 .. versionchanged:: 2.0 

622 Inherits from Werkzeug's ``cached_property`` (and ``property``). 

623 """ 

624 

625 def __init__( 

626 self, 

627 fget: t.Callable[[t.Any], t.Any], 

628 name: str | None = None, 

629 doc: str | None = None, 

630 ) -> None: 

631 import warnings 

632 

633 warnings.warn( 

634 "'locked_cached_property' is deprecated and will be removed in Flask 2.4." 

635 " Use a lock inside the decorated function if locking is needed.", 

636 DeprecationWarning, 

637 stacklevel=2, 

638 ) 

639 super().__init__(fget, name=name, doc=doc) 

640 self.lock = RLock() 

641 

642 def __get__(self, obj: object, type: type = None) -> t.Any: # type: ignore 

643 if obj is None: 

644 return self 

645 

646 with self.lock: 

647 return super().__get__(obj, type=type) 

648 

649 def __set__(self, obj: object, value: t.Any) -> None: 

650 with self.lock: 

651 super().__set__(obj, value) 

652 

653 def __delete__(self, obj: object) -> None: 

654 with self.lock: 

655 super().__delete__(obj) 

656 

657 

658def is_ip(value: str) -> bool: 

659 """Determine if the given string is an IP address. 

660 

661 :param value: value to check 

662 :type value: str 

663 

664 :return: True if string is an IP address 

665 :rtype: bool 

666 

667 .. deprecated:: 2.3 

668 Will be removed in Flask 2.4. 

669 """ 

670 warnings.warn( 

671 "The 'is_ip' function is deprecated and will be removed in Flask 2.4.", 

672 DeprecationWarning, 

673 stacklevel=2, 

674 ) 

675 

676 for family in (socket.AF_INET, socket.AF_INET6): 

677 try: 

678 socket.inet_pton(family, value) 

679 except OSError: 

680 pass 

681 else: 

682 return True 

683 

684 return False 

685 

686 

687@lru_cache(maxsize=None) 

688def _split_blueprint_path(name: str) -> list[str]: 

689 out: list[str] = [name] 

690 

691 if "." in name: 

692 out.extend(_split_blueprint_path(name.rpartition(".")[0])) 

693 

694 return out