codekingpro/portable-devtools
114k
1from __future__ import annotations2 3import importlib.util4import os5import pathlib6import sys7import typing as t8from collections import defaultdict9from functools import update_wrapper10 11from jinja2 import BaseLoader12from jinja2 import FileSystemLoader13from werkzeug.exceptions import default_exceptions14from werkzeug.exceptions import HTTPException15from werkzeug.utils import cached_property16 17from .. import typing as ft18from ..helpers import get_root_path19from ..templating import _default_template_ctx_processor20 21if t.TYPE_CHECKING: # pragma: no cover22 from click import Group23 24# a singleton sentinel value for parameter defaults25_sentinel = object()26 27F = t.TypeVar("F", bound=t.Callable[..., t.Any])28T_after_request = t.TypeVar("T_after_request", bound=ft.AfterRequestCallable[t.Any])29T_before_request = t.TypeVar("T_before_request", bound=ft.BeforeRequestCallable)30T_error_handler = t.TypeVar("T_error_handler", bound=ft.ErrorHandlerCallable)31T_teardown = t.TypeVar("T_teardown", bound=ft.TeardownCallable)32T_template_context_processor = t.TypeVar(33 "T_template_context_processor", bound=ft.TemplateContextProcessorCallable34)35T_url_defaults = t.TypeVar("T_url_defaults", bound=ft.URLDefaultCallable)36T_url_value_preprocessor = t.TypeVar(37 "T_url_value_preprocessor", bound=ft.URLValuePreprocessorCallable38)39T_route = t.TypeVar("T_route", bound=ft.RouteCallable)40 41 42def setupmethod(f: F) -> F:43 f_name = f.__name__44 45 def wrapper_func(self: Scaffold, *args: t.Any, **kwargs: t.Any) -> t.Any:46 self._check_setup_finished(f_name)47 return f(self, *args, **kwargs)48 49 return t.cast(F, update_wrapper(wrapper_func, f))50 51 52class Scaffold:53 """Common behavior shared between :class:`~flask.Flask` and54 :class:`~flask.blueprints.Blueprint`.55 56 :param import_name: The import name of the module where this object57 is defined. Usually :attr:`__name__` should be used.58 :param static_folder: Path to a folder of static files to serve.59 If this is set, a static route will be added.60 :param static_url_path: URL prefix for the static route.61 :param template_folder: Path to a folder containing template files.62 for rendering. If this is set, a Jinja loader will be added.63 :param root_path: The path that static, template, and resource files64 are relative to. Typically not set, it is discovered based on65 the ``import_name``.66 67 .. versionadded:: 2.068 """69 70 cli: Group71 name: str72 _static_folder: str | None = None73 _static_url_path: str | None = None74 75 def __init__(76 self,77 import_name: str,78 static_folder: str | os.PathLike[str] | None = None,79 static_url_path: str | None = None,80 template_folder: str | os.PathLike[str] | None = None,81 root_path: str | None = None,82 ):83 #: The name of the package or module that this object belongs84 #: to. Do not change this once it is set by the constructor.85 self.import_name = import_name86 87 self.static_folder = static_folder88 self.static_url_path = static_url_path89 90 #: The path to the templates folder, relative to91 #: :attr:`root_path`, to add to the template loader. ``None`` if92 #: templates should not be added.93 self.template_folder = template_folder94 95 if root_path is None:96 root_path = get_root_path(self.import_name)97 98 #: Absolute path to the package on the filesystem. Used to look99 #: up resources contained in the package.100 self.root_path = root_path101 102 #: A dictionary mapping endpoint names to view functions.103 #:104 #: To register a view function, use the :meth:`route` decorator.105 #:106 #: This data structure is internal. It should not be modified107 #: directly and its format may change at any time.108 self.view_functions: dict[str, ft.RouteCallable] = {}109 110 #: A data structure of registered error handlers, in the format111 #: ``{scope: {code: {class: handler}}}``. The ``scope`` key is112 #: the name of a blueprint the handlers are active for, or113 #: ``None`` for all requests. The ``code`` key is the HTTP114 #: status code for ``HTTPException``, or ``None`` for115 #: other exceptions. The innermost dictionary maps exception116 #: classes to handler functions.117 #:118 #: To register an error handler, use the :meth:`errorhandler`119 #: decorator.120 #:121 #: This data structure is internal. It should not be modified122 #: directly and its format may change at any time.123 self.error_handler_spec: dict[124 ft.AppOrBlueprintKey,125 dict[int | None, dict[type[Exception], ft.ErrorHandlerCallable]],126 ] = defaultdict(lambda: defaultdict(dict))127 128 #: A data structure of functions to call at the beginning of129 #: each request, in the format ``{scope: [functions]}``. The130 #: ``scope`` key is the name of a blueprint the functions are131 #: active for, or ``None`` for all requests.132 #:133 #: To register a function, use the :meth:`before_request`134 #: decorator.135 #:136 #: This data structure is internal. It should not be modified137 #: directly and its format may change at any time.138 self.before_request_funcs: dict[139 ft.AppOrBlueprintKey, list[ft.BeforeRequestCallable]140 ] = defaultdict(list)141 142 #: A data structure of functions to call at the end of each143 #: request, in the format ``{scope: [functions]}``. The144 #: ``scope`` key is the name of a blueprint the functions are145 #: active for, or ``None`` for all requests.146 #:147 #: To register a function, use the :meth:`after_request`148 #: decorator.149 #:150 #: This data structure is internal. It should not be modified151 #: directly and its format may change at any time.152 self.after_request_funcs: dict[153 ft.AppOrBlueprintKey, list[ft.AfterRequestCallable[t.Any]]154 ] = defaultdict(list)155 156 #: A data structure of functions to call at the end of each157 #: request even if an exception is raised, in the format158 #: ``{scope: [functions]}``. The ``scope`` key is the name of a159 #: blueprint the functions are active for, or ``None`` for all160 #: requests.161 #:162 #: To register a function, use the :meth:`teardown_request`163 #: decorator.164 #:165 #: This data structure is internal. It should not be modified166 #: directly and its format may change at any time.167 self.teardown_request_funcs: dict[168 ft.AppOrBlueprintKey, list[ft.TeardownCallable]169 ] = defaultdict(list)170 171 #: A data structure of functions to call to pass extra context172 #: values when rendering templates, in the format173 #: ``{scope: [functions]}``. The ``scope`` key is the name of a174 #: blueprint the functions are active for, or ``None`` for all175 #: requests.176 #:177 #: To register a function, use the :meth:`context_processor`178 #: decorator.179 #:180 #: This data structure is internal. It should not be modified181 #: directly and its format may change at any time.182 self.template_context_processors: dict[183 ft.AppOrBlueprintKey, list[ft.TemplateContextProcessorCallable]184 ] = defaultdict(list, {None: [_default_template_ctx_processor]})185 186 #: A data structure of functions to call to modify the keyword187 #: arguments passed to the view function, in the format188 #: ``{scope: [functions]}``. The ``scope`` key is the name of a189 #: blueprint the functions are active for, or ``None`` for all190 #: requests.191 #:192 #: To register a function, use the193 #: :meth:`url_value_preprocessor` decorator.194 #:195 #: This data structure is internal. It should not be modified196 #: directly and its format may change at any time.197 self.url_value_preprocessors: dict[198 ft.AppOrBlueprintKey,199 list[ft.URLValuePreprocessorCallable],200 ] = defaultdict(list)201 202 #: A data structure of functions to call to modify the keyword203 #: arguments when generating URLs, in the format204 #: ``{scope: [functions]}``. The ``scope`` key is the name of a205 #: blueprint the functions are active for, or ``None`` for all206 #: requests.207 #:208 #: To register a function, use the :meth:`url_defaults`209 #: decorator.210 #:211 #: This data structure is internal. It should not be modified212 #: directly and its format may change at any time.213 self.url_default_functions: dict[214 ft.AppOrBlueprintKey, list[ft.URLDefaultCallable]215 ] = defaultdict(list)216 217 def __repr__(self) -> str:218 return f"<{type(self).__name__} {self.name!r}>"219 220 def _check_setup_finished(self, f_name: str) -> None:221 raise NotImplementedError222 223 @property224 def static_folder(self) -> str | None:225 """The absolute path to the configured static folder. ``None``226 if no static folder is set.227 """228 if self._static_folder is not None:229 return os.path.join(self.root_path, self._static_folder)230 else:231 return None232 233 @static_folder.setter234 def static_folder(self, value: str | os.PathLike[str] | None) -> None:235 if value is not None:236 value = os.fspath(value).rstrip(r"\/")237 238 self._static_folder = value239 240 @property241 def has_static_folder(self) -> bool:242 """``True`` if :attr:`static_folder` is set.243 244 .. versionadded:: 0.5245 """246 return self.static_folder is not None247 248 @property249 def static_url_path(self) -> str | None:250 """The URL prefix that the static route will be accessible from.251 252 If it was not configured during init, it is derived from253 :attr:`static_folder`.254 """255 if self._static_url_path is not None:256 return self._static_url_path257 258 if self.static_folder is not None:259 basename = os.path.basename(self.static_folder)260 return f"/{basename}".rstrip("/")261 262 return None263 264 @static_url_path.setter265 def static_url_path(self, value: str | None) -> None:266 if value is not None:267 value = value.rstrip("/")268 269 self._static_url_path = value270 271 @cached_property272 def jinja_loader(self) -> BaseLoader | None:273 """The Jinja loader for this object's templates. By default this274 is a class :class:`jinja2.loaders.FileSystemLoader` to275 :attr:`template_folder` if it is set.276 277 .. versionadded:: 0.5278 """279 if self.template_folder is not None:280 return FileSystemLoader(os.path.join(self.root_path, self.template_folder))281 else:282 return None283 284 def _method_route(285 self,286 method: str,287 rule: str,288 options: dict[str, t.Any],289 ) -> t.Callable[[T_route], T_route]:290 if "methods" in options:291 raise TypeError("Use the 'route' decorator to use the 'methods' argument.")292 293 return self.route(rule, methods=[method], **options)294 295 @setupmethod296 def get(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]:297 """Shortcut for :meth:`route` with ``methods=["GET"]``.298 299 .. versionadded:: 2.0300 """301 return self._method_route("GET", rule, options)302 303 @setupmethod304 def post(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]:305 """Shortcut for :meth:`route` with ``methods=["POST"]``.306 307 .. versionadded:: 2.0308 """309 return self._method_route("POST", rule, options)310 311 @setupmethod312 def put(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]:313 """Shortcut for :meth:`route` with ``methods=["PUT"]``.314 315 .. versionadded:: 2.0316 """317 return self._method_route("PUT", rule, options)318 319 @setupmethod320 def delete(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]:321 """Shortcut for :meth:`route` with ``methods=["DELETE"]``.322 323 .. versionadded:: 2.0324 """325 return self._method_route("DELETE", rule, options)326 327 @setupmethod328 def patch(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]:329 """Shortcut for :meth:`route` with ``methods=["PATCH"]``.330 331 .. versionadded:: 2.0332 """333 return self._method_route("PATCH", rule, options)334 335 @setupmethod336 def route(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]:337 """Decorate a view function to register it with the given URL338 rule and options. Calls :meth:`add_url_rule`, which has more339 details about the implementation.340 341 .. code-block:: python342 343 @app.route("/")344 def index():345 return "Hello, World!"346 347 See :ref:`url-route-registrations`.348 349 The endpoint name for the route defaults to the name of the view350 function if the ``endpoint`` parameter isn't passed.351 352 The ``methods`` parameter defaults to ``["GET"]``. ``HEAD`` and353 ``OPTIONS`` are added automatically.354 355 :param rule: The URL rule string.356 :param options: Extra options passed to the357 :class:`~werkzeug.routing.Rule` object.358 """359 360 def decorator(f: T_route) -> T_route:361 endpoint = options.pop("endpoint", None)362 self.add_url_rule(rule, endpoint, f, **options)363 return f364 365 return decorator366 367 @setupmethod368 def add_url_rule(369 self,370 rule: str,371 endpoint: str | None = None,372 view_func: ft.RouteCallable | None = None,373 provide_automatic_options: bool | None = None,374 **options: t.Any,375 ) -> None:376 """Register a rule for routing incoming requests and building377 URLs. The :meth:`route` decorator is a shortcut to call this378 with the ``view_func`` argument. These are equivalent:379 380 .. code-block:: python381 382 @app.route("/")383 def index():384 ...385 386 .. code-block:: python387 388 def index():389 ...390 391 app.add_url_rule("/", view_func=index)392 393 See :ref:`url-route-registrations`.394 395 The endpoint name for the route defaults to the name of the view396 function if the ``endpoint`` parameter isn't passed. An error397 will be raised if a function has already been registered for the398 endpoint.399 400 The ``methods`` parameter defaults to ``["GET"]``. ``HEAD`` is401 always added automatically, and ``OPTIONS`` is added402 automatically by default.403 404 ``view_func`` does not necessarily need to be passed, but if the405 rule should participate in routing an endpoint name must be406 associated with a view function at some point with the407 :meth:`endpoint` decorator.408 409 .. code-block:: python410 411 app.add_url_rule("/", endpoint="index")412 413 @app.endpoint("index")414 def index():415 ...416 417 If ``view_func`` has a ``required_methods`` attribute, those418 methods are added to the passed and automatic methods. If it419 has a ``provide_automatic_methods`` attribute, it is used as the420 default if the parameter is not passed.421 422 :param rule: The URL rule string.423 :param endpoint: The endpoint name to associate with the rule424 and view function. Used when routing and building URLs.425 Defaults to ``view_func.__name__``.426 :param view_func: The view function to associate with the427 endpoint name.428 :param provide_automatic_options: Add the ``OPTIONS`` method and429 respond to ``OPTIONS`` requests automatically.430 :param options: Extra options passed to the431 :class:`~werkzeug.routing.Rule` object.432 """433 raise NotImplementedError434 435 @setupmethod436 def endpoint(self, endpoint: str) -> t.Callable[[F], F]:437 """Decorate a view function to register it for the given438 endpoint. Used if a rule is added without a ``view_func`` with439 :meth:`add_url_rule`.440 441 .. code-block:: python442 443 app.add_url_rule("/ex", endpoint="example")444 445 @app.endpoint("example")446 def example():447 ...448 449 :param endpoint: The endpoint name to associate with the view450 function.451 """452 453 def decorator(f: F) -> F:454 self.view_functions[endpoint] = f455 return f456 457 return decorator458 459 @setupmethod460 def before_request(self, f: T_before_request) -> T_before_request:461 """Register a function to run before each request.462 463 For example, this can be used to open a database connection, or464 to load the logged in user from the session.465 466 .. code-block:: python467 468 @app.before_request469 def load_user():470 if "user_id" in session:471 g.user = db.session.get(session["user_id"])472 473 The function will be called without any arguments. If it returns474 a non-``None`` value, the value is handled as if it was the475 return value from the view, and further request handling is476 stopped.477 478 This is available on both app and blueprint objects. When used on an app, this479 executes before every request. When used on a blueprint, this executes before480 every request that the blueprint handles. To register with a blueprint and481 execute before every request, use :meth:`.Blueprint.before_app_request`.482 """483 self.before_request_funcs.setdefault(None, []).append(f)484 return f485 486 @setupmethod487 def after_request(self, f: T_after_request) -> T_after_request:488 """Register a function to run after each request to this object.489 490 The function is called with the response object, and must return491 a response object. This allows the functions to modify or492 replace the response before it is sent.493 494 If a function raises an exception, any remaining495 ``after_request`` functions will not be called. Therefore, this496 should not be used for actions that must execute, such as to497 close resources. Use :meth:`teardown_request` for that.498 499 This is available on both app and blueprint objects. When used on an app, this500 executes after every request. When used on a blueprint, this executes after501 every request that the blueprint handles. To register with a blueprint and502 execute after every request, use :meth:`.Blueprint.after_app_request`.503 """504 self.after_request_funcs.setdefault(None, []).append(f)505 return f506 507 @setupmethod508 def teardown_request(self, f: T_teardown) -> T_teardown:509 """Register a function to be called when the request context is510 popped. Typically this happens at the end of each request, but511 contexts may be pushed manually as well during testing.512 513 .. code-block:: python514 515 with app.test_request_context():516 ...517 518 When the ``with`` block exits (or ``ctx.pop()`` is called), the519 teardown functions are called just before the request context is520 made inactive.521 522 When a teardown function was called because of an unhandled523 exception it will be passed an error object. If an524 :meth:`errorhandler` is registered, it will handle the exception525 and the teardown will not receive it.526 527 Teardown functions must avoid raising exceptions. If they528 execute code that might fail they must surround that code with a529 ``try``/``except`` block and log any errors.530 531 The return values of teardown functions are ignored.532 533 This is available on both app and blueprint objects. When used on an app, this534 executes after every request. When used on a blueprint, this executes after535 every request that the blueprint handles. To register with a blueprint and536 execute after every request, use :meth:`.Blueprint.teardown_app_request`.537 """538 self.teardown_request_funcs.setdefault(None, []).append(f)539 return f540 541 @setupmethod542 def context_processor(543 self,544 f: T_template_context_processor,545 ) -> T_template_context_processor:546 """Registers a template context processor function. These functions run before547 rendering a template. The keys of the returned dict are added as variables548 available in the template.549 550 This is available on both app and blueprint objects. When used on an app, this551 is called for every rendered template. When used on a blueprint, this is called552 for templates rendered from the blueprint's views. To register with a blueprint553 and affect every template, use :meth:`.Blueprint.app_context_processor`.554 """555 self.template_context_processors[None].append(f)556 return f557 558 @setupmethod559 def url_value_preprocessor(560 self,561 f: T_url_value_preprocessor,562 ) -> T_url_value_preprocessor:563 """Register a URL value preprocessor function for all view564 functions in the application. These functions will be called before the565 :meth:`before_request` functions.566 567 The function can modify the values captured from the matched url before568 they are passed to the view. For example, this can be used to pop a569 common language code value and place it in ``g`` rather than pass it to570 every view.571 572 The function is passed the endpoint name and values dict. The return573 value is ignored.574 575 This is available on both app and blueprint objects. When used on an app, this576 is called for every request. When used on a blueprint, this is called for577 requests that the blueprint handles. To register with a blueprint and affect578 every request, use :meth:`.Blueprint.app_url_value_preprocessor`.579 """580 self.url_value_preprocessors[None].append(f)581 return f582 583 @setupmethod584 def url_defaults(self, f: T_url_defaults) -> T_url_defaults:585 """Callback function for URL defaults for all view functions of the586 application. It's called with the endpoint and values and should587 update the values passed in place.588 589 This is available on both app and blueprint objects. When used on an app, this590 is called for every request. When used on a blueprint, this is called for591 requests that the blueprint handles. To register with a blueprint and affect592 every request, use :meth:`.Blueprint.app_url_defaults`.593 """594 self.url_default_functions[None].append(f)595 return f596 597 @setupmethod598 def errorhandler(599 self, code_or_exception: type[Exception] | int600 ) -> t.Callable[[T_error_handler], T_error_handler]:601 """Register a function to handle errors by code or exception class.602 603 A decorator that is used to register a function given an604 error code. Example::605 606 @app.errorhandler(404)607 def page_not_found(error):608 return 'This page does not exist', 404609 610 You can also register handlers for arbitrary exceptions::611 612 @app.errorhandler(DatabaseError)613 def special_exception_handler(error):614 return 'Database connection failed', 500615 616 This is available on both app and blueprint objects. When used on an app, this617 can handle errors from every request. When used on a blueprint, this can handle618 errors from requests that the blueprint handles. To register with a blueprint619 and affect every request, use :meth:`.Blueprint.app_errorhandler`.620 621 .. versionadded:: 0.7622 Use :meth:`register_error_handler` instead of modifying623 :attr:`error_handler_spec` directly, for application wide error624 handlers.625 626 .. versionadded:: 0.7627 One can now additionally also register custom exception types628 that do not necessarily have to be a subclass of the629 :class:`~werkzeug.exceptions.HTTPException` class.630 631 :param code_or_exception: the code as integer for the handler, or632 an arbitrary exception633 """634 635 def decorator(f: T_error_handler) -> T_error_handler:636 self.register_error_handler(code_or_exception, f)637 return f638 639 return decorator640 641 @setupmethod642 def register_error_handler(643 self,644 code_or_exception: type[Exception] | int,645 f: ft.ErrorHandlerCallable,646 ) -> None:647 """Alternative error attach function to the :meth:`errorhandler`648 decorator that is more straightforward to use for non decorator649 usage.650 651 .. versionadded:: 0.7652 """653 exc_class, code = self._get_exc_class_and_code(code_or_exception)654 self.error_handler_spec[None][code][exc_class] = f655 656 @staticmethod657 def _get_exc_class_and_code(658 exc_class_or_code: type[Exception] | int,659 ) -> tuple[type[Exception], int | None]:660 """Get the exception class being handled. For HTTP status codes661 or ``HTTPException`` subclasses, return both the exception and662 status code.663 664 :param exc_class_or_code: Any exception class, or an HTTP status665 code as an integer.666 """667 exc_class: type[Exception]668 669 if isinstance(exc_class_or_code, int):670 try:671 exc_class = default_exceptions[exc_class_or_code]672 except KeyError:673 raise ValueError(674 f"'{exc_class_or_code}' is not a recognized HTTP"675 " error code. Use a subclass of HTTPException with"676 " that code instead."677 ) from None678 else:679 exc_class = exc_class_or_code680 681 if isinstance(exc_class, Exception):682 raise TypeError(683 f"{exc_class!r} is an instance, not a class. Handlers"684 " can only be registered for Exception classes or HTTP"685 " error codes."686 )687 688 if not issubclass(exc_class, Exception):689 raise ValueError(690 f"'{exc_class.__name__}' is not a subclass of Exception."691 " Handlers can only be registered for Exception classes"692 " or HTTP error codes."693 )694 695 if issubclass(exc_class, HTTPException):696 return exc_class, exc_class.code697 else:698 return exc_class, None699 700 701def _endpoint_from_view_func(view_func: ft.RouteCallable) -> str:702 """Internal helper that returns the default endpoint for a given703 function. This always is the function name.704 """705 assert view_func is not None, "expected view func if endpoint is not provided."706 return view_func.__name__707 708 709def _find_package_path(import_name: str) -> str:710 """Find the path that contains the package or module."""711 root_mod_name, _, _ = import_name.partition(".")712 713 try:714 root_spec = importlib.util.find_spec(root_mod_name)715 716 if root_spec is None:717 raise ValueError("not found")718 except (ImportError, ValueError):719 # ImportError: the machinery told us it does not exist720 # ValueError:721 # - the module name was invalid722 # - the module name is __main__723 # - we raised `ValueError` due to `root_spec` being `None`724 return os.getcwd()725 726 if root_spec.submodule_search_locations:727 if root_spec.origin is None or root_spec.origin == "namespace":728 # namespace package729 package_spec = importlib.util.find_spec(import_name)730 731 if package_spec is not None and package_spec.submodule_search_locations:732 # Pick the path in the namespace that contains the submodule.733 package_path = pathlib.Path(734 os.path.commonpath(package_spec.submodule_search_locations)735 )736 search_location = next(737 location738 for location in root_spec.submodule_search_locations739 if package_path.is_relative_to(location)740 )741 else:742 # Pick the first path.743 search_location = root_spec.submodule_search_locations[0]744 745 return os.path.dirname(search_location)746 else:747 # package with __init__.py748 return os.path.dirname(os.path.dirname(root_spec.origin))749 else:750 # module751 return os.path.dirname(root_spec.origin) # type: ignore[type-var, return-value]752 753 754def find_package(import_name: str) -> tuple[str | None, str]:755 """Find the prefix that a package is installed under, and the path756 that it would be imported from.757 758 The prefix is the directory containing the standard directory759 hierarchy (lib, bin, etc.). If the package is not installed to the760 system (:attr:`sys.prefix`) or a virtualenv (``site-packages``),761 ``None`` is returned.762 763 The path is the entry in :attr:`sys.path` that contains the package764 for import. If the package is not installed, it's assumed that the765 package was imported from the current working directory.766 """767 package_path = _find_package_path(import_name)768 py_prefix = os.path.abspath(sys.prefix)769 770 # installed to the system771 if pathlib.PurePath(package_path).is_relative_to(py_prefix):772 return py_prefix, package_path773 774 site_parent, site_folder = os.path.split(package_path)775 776 # installed to a virtualenv777 if site_folder.lower() == "site-packages":778 parent, folder = os.path.split(site_parent)779 780 # Windows (prefix/lib/site-packages)781 if folder.lower() == "lib":782 return parent, package_path783 784 # Unix (prefix/lib/pythonX.Y/site-packages)785 if os.path.basename(parent).lower() == "lib":786 return os.path.dirname(parent), package_path787 788 # something else (prefix/site-packages)789 return site_parent, package_path790 791 # not installed792 return None, package_path793 