view mercurial/utils/resourceutil.py @ 51808:c87c56ad6913

utils: avoid using internal _imp.is_frozen() imp has been deprecated for a long time, and were removed in Python 3.12 . As a workaround, we started using the internal _imp. That is ugly and risky. It seems less risky to get the functionality in some other way. Here, we just inspect if 'origin' of the '__main__' module is set and 'frozen'. That seems to work and do the same, and might be better than using the internal _imp directly. This way of inspecting module attributes seems to work in some test cases, but it is a risky change. This level of importlib doesn't have much documentation, a complicated implementation, and we are dealing with some odd use cases.
author Mads Kiilerich <mads@kiilerich.com>
date Tue, 27 Jun 2023 13:05:03 +0200
parents 16d63d7799fa
children f4733654f144
line wrap: on
line source

# resourceutil.py - utility for looking up resources
#
#  Copyright 2005 K. Thananchayan <thananck@yahoo.com>
#  Copyright 2005-2007 Olivia Mackall <olivia@selenic.com>
#  Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2 or any later version.


import os
import sys
import typing

from .. import pycompat


if typing.TYPE_CHECKING:
    from typing import (
        BinaryIO,
        Iterator,
    )


def mainfrozen():
    """return True if we are a frozen executable.

    The code supports py2exe (most common, Windows only) and tools/freeze
    (portable, not much used).
    """
    return (
        hasattr(sys, "frozen")  # new py2exe
        or hasattr(sys, "importers")  # old py2exe
        or getattr(
            getattr(sys.modules.get('__main__'), '__spec__', None),
            'origin',
            None,
        )
        == 'frozen'  # tools/freeze
    )


# the location of data files matching the source code
if mainfrozen() and getattr(sys, "frozen", None) != "macosx_app":
    # executable version (py2exe) doesn't support __file__
    datapath = os.path.dirname(pycompat.sysexecutable)
    _rootpath = datapath

    # The installers store the files outside of library.zip, like
    # C:\Program Files\Mercurial\defaultrc\*.rc.  This strips the
    # leading "mercurial." off of the package name, so that these
    # pseudo resources are found in their directory next to the
    # executable.
    def _package_path(package: bytes) -> bytes:
        dirs = package.split(b".")
        assert dirs[0] == b"mercurial"
        return os.path.join(_rootpath, *dirs[1:])

else:
    datapath = os.path.dirname(os.path.dirname(pycompat.fsencode(__file__)))
    _rootpath = os.path.dirname(datapath)

    def _package_path(package: bytes) -> bytes:
        return os.path.join(_rootpath, *package.split(b"."))


try:
    # importlib.resources exists from Python 3.7; see fallback in except clause
    # further down
    from importlib import resources  # pytype: disable=import-error

    # Force loading of the resources module
    if hasattr(resources, 'files'):  # Introduced in Python 3.9
        resources.files  # pytype: disable=module-attr
    else:
        resources.open_binary  # pytype: disable=module-attr

    # py2exe raises an AssertionError if uses importlib.resources
    if getattr(sys, "frozen", None) in ("console_exe", "windows_exe"):
        raise ImportError

except (ImportError, AttributeError):
    # importlib.resources was not found (almost definitely because we're on a
    # Python version before 3.7)

    def open_resource(package: bytes, name: bytes) -> "BinaryIO":
        path = os.path.join(_package_path(package), name)
        return open(path, "rb")

    def is_resource(package: bytes, name: bytes) -> bool:
        path = os.path.join(_package_path(package), name)

        try:
            return os.path.isfile(pycompat.fsdecode(path))
        except (IOError, OSError):
            return False

    def contents(package: bytes) -> "Iterator[bytes]":
        path = pycompat.fsdecode(_package_path(package))

        for p in os.listdir(path):
            yield pycompat.fsencode(p)

else:
    from .. import encoding

    def open_resource(package: bytes, name: bytes) -> "BinaryIO":
        if hasattr(resources, 'files'):
            return (
                resources.files(  # pytype: disable=module-attr
                    pycompat.sysstr(package)
                )
                .joinpath(pycompat.sysstr(name))
                .open('rb')
            )
        else:
            return resources.open_binary(  # pytype: disable=module-attr
                pycompat.sysstr(package), pycompat.sysstr(name)
            )

    def is_resource(package: bytes, name: bytes) -> bool:
        if hasattr(resources, 'files'):  # Introduced in Python 3.9
            return (
                resources.files(pycompat.sysstr(package))
                .joinpath(encoding.strfromlocal(name))
                .is_file()
            )
        else:
            return resources.is_resource(  # pytype: disable=module-attr
                pycompat.sysstr(package), encoding.strfromlocal(name)
            )

    def contents(package: bytes) -> "Iterator[bytes]":
        if hasattr(resources, 'files'):  # Introduced in Python 3.9
            for path in resources.files(pycompat.sysstr(package)).iterdir():
                if path.is_file():
                    yield encoding.strtolocal(path.name)
        else:
            # pytype: disable=module-attr
            for r in resources.contents(pycompat.sysstr(package)):
                # pytype: enable=module-attr
                yield encoding.strtolocal(r)