Repository: numba/numba

Issue:
Numba cache should handle lack of write permissions
Currently numba's cache will fail if the user running the code lacks write permission for the directory that code is installed in. This has come up a few times with users installing library code using `sudo`, but then trying to import it with user privileges.



Discussion:
At a minimum, we should only produce a warning if a cache file cannot be written.  (In addition, we should then mark the function as uncached, so we don't attempt to repeatedly write cache files on every single call.)

If possible, it would be nice to also define a cache location in the user's home directory that could be used.

Being one of those users who is affected I would like to say that the idea of the cache location - something like environment variable NUMBA_CACHE_DIR - would work perfectly in my case.
Then at my user's bashrc or in my lmod modules or in my batch scripts - I can define local cache and feel safe... 

Indeed we could define a fallback location for when the source code directories can't be written to.

Also should be clear about which location takes precedence when both are available.

This reminds me, can we have a message that prints (in NUMBA_DEBUG mode only) the full path when a cache file is loaded.  I don't recall if that is present already.

Fallback might be dangerous.
Imagine that sys-admin X with default group XG and default permissions [g+rw, o+r] installs some package which is using numba.
Another user Y with default group YG and default permissions [g+rw, others - not allowed] runs that package for the first time. Assuming that he also belongs to group XG - he creates the cache folder.
Then user Z who belongs to the group XG, but not to the group YG, **can** get the package data, but **can't** get to the cache folder's content...

I had similar situation when I managed python distribution in the system location for a few users...

> Then user Z who belongs to the group XG, but not to the group YG, can get the package data, but can't get to the cache folder's content...

I don't see how that's dangerous. User Z will then use the fallback location for cache files. What would be the problem?


Resolving patch:
diff --git a/numba/appdirs.py b/numba/appdirs.py
new file mode 100644
--- /dev/null
+++ b/numba/appdirs.py
@@ -0,0 +1,557 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# Copyright (c) 2005-2010 ActiveState Software Inc.
+# Copyright (c) 2013 Eddy Petrișor
+
+"""Utilities for determining application-specific dirs.
+
+See <http://github.com/ActiveState/appdirs> for details and usage.
+"""
+# Dev Notes:
+# - MSDN on where to store app data files:
+#   http://support.microsoft.com/default.aspx?scid=kb;en-us;310294#XSLTH3194121123120121120120
+# - Mac OS X: http://developer.apple.com/documentation/MacOSX/Conceptual/BPFileSystem/index.html
+# - XDG spec for Un*x: http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
+
+__version_info__ = (1, 4, 1)
+__version__ = '.'.join(map(str, __version_info__))
+
+
+import sys
+import os
+
+PY3 = sys.version_info[0] == 3
+
+if PY3:
+    unicode = str
+
+if sys.platform.startswith('java'):
+    import platform
+    os_name = platform.java_ver()[3][0]
+    if os_name.startswith('Windows'): # "Windows XP", "Windows 7", etc.
+        system = 'win32'
+    elif os_name.startswith('Mac'): # "Mac OS X", etc.
+        system = 'darwin'
+    else: # "Linux", "SunOS", "FreeBSD", etc.
+        # Setting this to "linux2" is not ideal, but only Windows or Mac
+        # are actually checked for and the rest of the module expects
+        # *sys.platform* style strings.
+        system = 'linux2'
+else:
+    system = sys.platform
+
+
+
+def user_data_dir(appname=None, appauthor=None, version=None, roaming=False):
+    r"""Return full path to the user-specific data dir for this application.
+
+        "appname" is the name of application.
+            If None, just the system directory is returned.
+        "appauthor" (only used on Windows) is the name of the
+            appauthor or distributing body for this application. Typically
+            it is the owning company name. This falls back to appname. You may
+            pass False to disable it.
+        "version" is an optional version path element to append to the
+            path. You might want to use this if you want multiple versions
+            of your app to be able to run independently. If used, this
+            would typically be "<major>.<minor>".
+            Only applied when appname is present.
+        "roaming" (boolean, default False) can be set True to use the Windows
+            roaming appdata directory. That means that for users on a Windows
+            network setup for roaming profiles, this user data will be
+            sync'd on login. See
+            <http://technet.microsoft.com/en-us/library/cc766489(WS.10).aspx>
+            for a discussion of issues.
+
+    Typical user data directories are:
+        Mac OS X:               ~/Library/Application Support/<AppName>
+        Unix:                   ~/.local/share/<AppName>    # or in $XDG_DATA_HOME, if defined
+        Win XP (not roaming):   C:\Documents and Settings\<username>\Application Data\<AppAuthor>\<AppName>
+        Win XP (roaming):       C:\Documents and Settings\<username>\Local Settings\Application Data\<AppAuthor>\<AppName>
+        Win 7  (not roaming):   C:\Users\<username>\AppData\Local\<AppAuthor>\<AppName>
+        Win 7  (roaming):       C:\Users\<username>\AppData\Roaming\<AppAuthor>\<AppName>
+
+    For Unix, we follow the XDG spec and support $XDG_DATA_HOME.
+    That means, by default "~/.local/share/<AppName>".
+    """
+    if system == "win32":
+        if appauthor is None:
+            appauthor = appname
+        const = roaming and "CSIDL_APPDATA" or "CSIDL_LOCAL_APPDATA"
+        path = os.path.normpath(_get_win_folder(const))
+        if appname:
+            if appauthor is not False:
+                path = os.path.join(path, appauthor, appname)
+            else:
+                path = os.path.join(path, appname)
+    elif system == 'darwin':
+        path = os.path.expanduser('~/Library/Application Support/')
+        if appname:
+            path = os.path.join(path, appname)
+    else:
+        path = os.getenv('XDG_DATA_HOME', os.path.expanduser("~/.local/share"))
+        if appname:
+            path = os.path.join(path, appname)
+    if appname and version:
+        path = os.path.join(path, version)
+    return path
+
+
+def site_data_dir(appname=None, appauthor=None, version=None, multipath=False):
+    """Return full path to the user-shared data dir for this application.
+
+        "appname" is the name of application.
+            If None, just the system directory is returned.
+        "appauthor" (only used on Windows) is the name of the
+            appauthor or distributing body for this application. Typically
+            it is the owning company name. This falls back to appname. You may
+            pass False to disable it.
+        "version" is an optional version path element to append to the
+            path. You might want to use this if you want multiple versions
+            of your app to be able to run independently. If used, this
+            would typically be "<major>.<minor>".
+            Only applied when appname is present.
+        "multipath" is an optional parameter only applicable to *nix
+            which indicates that the entire list of data dirs should be
+            returned. By default, the first item from XDG_DATA_DIRS is
+            returned, or '/usr/local/share/<AppName>',
+            if XDG_DATA_DIRS is not set
+
+    Typical user data directories are:
+        Mac OS X:   /Library/Application Support/<AppName>
+        Unix:       /usr/local/share/<AppName> or /usr/share/<AppName>
+        Win XP:     C:\Documents and Settings\All Users\Application Data\<AppAuthor>\<AppName>
+        Vista:      (Fail! "C:\ProgramData" is a hidden *system* directory on Vista.)
+        Win 7:      C:\ProgramData\<AppAuthor>\<AppName>   # Hidden, but writeable on Win 7.
+
+    For Unix, this is using the $XDG_DATA_DIRS[0] default.
+
+    WARNING: Do not use this on Windows. See the Vista-Fail note above for why.
+    """
+    if system == "win32":
+        if appauthor is None:
+            appauthor = appname
+        path = os.path.normpath(_get_win_folder("CSIDL_COMMON_APPDATA"))
+        if appname:
+            if appauthor is not False:
+                path = os.path.join(path, appauthor, appname)
+            else:
+                path = os.path.join(path, appname)
+    elif system == 'darwin':
+        path = os.path.expanduser('/Library/Application Support')
+        if appname:
+            path = os.path.join(path, appname)
+    else:
+        # XDG default for $XDG_DATA_DIRS
+        # only first, if multipath is False
+        path = os.getenv('XDG_DATA_DIRS',
+                         os.pathsep.join(['/usr/local/share', '/usr/share']))
+        pathlist = [os.path.expanduser(x.rstrip(os.sep)) for x in path.split(os.pathsep)]
+        if appname:
+            if version:
+                appname = os.path.join(appname, version)
+            pathlist = [os.sep.join([x, appname]) for x in pathlist]
+
+        if multipath:
+            path = os.pathsep.join(pathlist)
+        else:
+            path = pathlist[0]
+        return path
+
+    if appname and version:
+        path = os.path.join(path, version)
+    return path
+
+
+def user_config_dir(appname=None, appauthor=None, version=None, roaming=False):
+    r"""Return full path to the user-specific config dir for this application.
+
+        "appname" is the name of application.
+            If None, just the system directory is returned.
+        "appauthor" (only used on Windows) is the name of the
+            appauthor or distributing body for this application. Typically
+            it is the owning company name. This falls back to appname. You may
+            pass False to disable it.
+        "version" is an optional version path element to append to the
+            path. You might want to use this if you want multiple versions
+            of your app to be able to run independently. If used, this
+            would typically be "<major>.<minor>".
+            Only applied when appname is present.
+        "roaming" (boolean, default False) can be set True to use the Windows
+            roaming appdata directory. That means that for users on a Windows
+            network setup for roaming profiles, this user data will be
+            sync'd on login. See
+            <http://technet.microsoft.com/en-us/library/cc766489(WS.10).aspx>
+            for a discussion of issues.
+
+    Typical user data directories are:
+        Mac OS X:               same as user_data_dir
+        Unix:                   ~/.config/<AppName>     # or in $XDG_CONFIG_HOME, if defined
+        Win *:                  same as user_data_dir
+
+    For Unix, we follow the XDG spec and support $XDG_CONFIG_HOME.
+    That means, by deafult "~/.config/<AppName>".
+    """
+    if system in ["win32", "darwin"]:
+        path = user_data_dir(appname, appauthor, None, roaming)
+    else:
+        path = os.getenv('XDG_CONFIG_HOME', os.path.expanduser("~/.config"))
+        if appname:
+            path = os.path.join(path, appname)
+    if appname and version:
+        path = os.path.join(path, version)
+    return path
+
+
+def site_config_dir(appname=None, appauthor=None, version=None, multipath=False):
+    """Return full path to the user-shared data dir for this application.
+
+        "appname" is the name of application.
+            If None, just the system directory is returned.
+        "appauthor" (only used on Windows) is the name of the
+            appauthor or distributing body for this application. Typically
+            it is the owning company name. This falls back to appname. You may
+            pass False to disable it.
+        "version" is an optional version path element to append to the
+            path. You might want to use this if you want multiple versions
+            of your app to be able to run independently. If used, this
+            would typically be "<major>.<minor>".
+            Only applied when appname is present.
+        "multipath" is an optional parameter only applicable to *nix
+            which indicates that the entire list of config dirs should be
+            returned. By default, the first item from XDG_CONFIG_DIRS is
+            returned, or '/etc/xdg/<AppName>', if XDG_CONFIG_DIRS is not set
+
+    Typical user data directories are:
+        Mac OS X:   same as site_data_dir
+        Unix:       /etc/xdg/<AppName> or $XDG_CONFIG_DIRS[i]/<AppName> for each value in
+                    $XDG_CONFIG_DIRS
+        Win *:      same as site_data_dir
+        Vista:      (Fail! "C:\ProgramData" is a hidden *system* directory on Vista.)
+
+    For Unix, this is using the $XDG_CONFIG_DIRS[0] default, if multipath=False
+
+    WARNING: Do not use this on Windows. See the Vista-Fail note above for why.
+    """
+    if system in ["win32", "darwin"]:
+        path = site_data_dir(appname, appauthor)
+        if appname and version:
+            path = os.path.join(path, version)
+    else:
+        # XDG default for $XDG_CONFIG_DIRS
+        # only first, if multipath is False
+        path = os.getenv('XDG_CONFIG_DIRS', '/etc/xdg')
+        pathlist = [os.path.expanduser(x.rstrip(os.sep)) for x in path.split(os.pathsep)]
+        if appname:
+            if version:
+                appname = os.path.join(appname, version)
+            pathlist = [os.sep.join([x, appname]) for x in pathlist]
+
+        if multipath:
+            path = os.pathsep.join(pathlist)
+        else:
+            path = pathlist[0]
+    return path
+
+
+def user_cache_dir(appname=None, appauthor=None, version=None, opinion=True):
+    r"""Return full path to the user-specific cache dir for this application.
+
+        "appname" is the name of application.
+            If None, just the system directory is returned.
+        "appauthor" (only used on Windows) is the name of the
+            appauthor or distributing body for this application. Typically
+            it is the owning company name. This falls back to appname. You may
+            pass False to disable it.
+        "version" is an optional version path element to append to the
+            path. You might want to use this if you want multiple versions
+            of your app to be able to run independently. If used, this
+            would typically be "<major>.<minor>".
+            Only applied when appname is present.
+        "opinion" (boolean) can be False to disable the appending of
+            "Cache" to the base app data dir for Windows. See
+            discussion below.
+
+    Typical user cache directories are:
+        Mac OS X:   ~/Library/Caches/<AppName>
+        Unix:       ~/.cache/<AppName> (XDG default)
+        Win XP:     C:\Documents and Settings\<username>\Local Settings\Application Data\<AppAuthor>\<AppName>\Cache
+        Vista:      C:\Users\<username>\AppData\Local\<AppAuthor>\<AppName>\Cache
+
+    On Windows the only suggestion in the MSDN docs is that local settings go in
+    the `CSIDL_LOCAL_APPDATA` directory. This is identical to the non-roaming
+    app data dir (the default returned by `user_data_dir` above). Apps typically
+    put cache data somewhere *under* the given dir here. Some examples:
+        ...\Mozilla\Firefox\Profiles\<ProfileName>\Cache
+        ...\Acme\SuperApp\Cache\1.0
+    OPINION: This function appends "Cache" to the `CSIDL_LOCAL_APPDATA` value.
+    This can be disabled with the `opinion=False` option.
+    """
+    if system == "win32":
+        if appauthor is None:
+            appauthor = appname
+        path = os.path.normpath(_get_win_folder("CSIDL_LOCAL_APPDATA"))
+        if appname:
+            if appauthor is not False:
+                path = os.path.join(path, appauthor, appname)
+            else:
+                path = os.path.join(path, appname)
+            if opinion:
+                path = os.path.join(path, "Cache")
+    elif system == 'darwin':
+        path = os.path.expanduser('~/Library/Caches')
+        if appname:
+            path = os.path.join(path, appname)
+    else:
+        path = os.getenv('XDG_CACHE_HOME', os.path.expanduser('~/.cache'))
+        if appname:
+            path = os.path.join(path, appname)
+    if appname and version:
+        path = os.path.join(path, version)
+    return path
+
+
+def user_log_dir(appname=None, appauthor=None, version=None, opinion=True):
+    r"""Return full path to the user-specific log dir for this application.
+
+        "appname" is the name of application.
+            If None, just the system directory is returned.
+        "appauthor" (only used on Windows) is the name of the
+            appauthor or distributing body for this application. Typically
+            it is the owning company name. This falls back to appname. You may
+            pass False to disable it.
+        "version" is an optional version path element to append to the
+            path. You might want to use this if you want multiple versions
+            of your app to be able to run independently. If used, this
+            would typically be "<major>.<minor>".
+            Only applied when appname is present.
+        "opinion" (boolean) can be False to disable the appending of
+            "Logs" to the base app data dir for Windows, and "log" to the
+            base cache dir for Unix. See discussion below.
+
+    Typical user cache directories are:
+        Mac OS X:   ~/Library/Logs/<AppName>
+        Unix:       ~/.cache/<AppName>/log  # or under $XDG_CACHE_HOME if defined
+        Win XP:     C:\Documents and Settings\<username>\Local Settings\Application Data\<AppAuthor>\<AppName>\Logs
+        Vista:      C:\Users\<username>\AppData\Local\<AppAuthor>\<AppName>\Logs
+
+    On Windows the only suggestion in the MSDN docs is that local settings
+    go in the `CSIDL_LOCAL_APPDATA` directory. (Note: I'm interested in
+    examples of what some windows apps use for a logs dir.)
+
+    OPINION: This function appends "Logs" to the `CSIDL_LOCAL_APPDATA`
+    value for Windows and appends "log" to the user cache dir for Unix.
+    This can be disabled with the `opinion=False` option.
+    """
+    if system == "darwin":
+        path = os.path.join(
+            os.path.expanduser('~/Library/Logs'),
+            appname)
+    elif system == "win32":
+        path = user_data_dir(appname, appauthor, version)
+        version = False
+        if opinion:
+            path = os.path.join(path, "Logs")
+    else:
+        path = user_cache_dir(appname, appauthor, version)
+        version = False
+        if opinion:
+            path = os.path.join(path, "log")
+    if appname and version:
+        path = os.path.join(path, version)
+    return path
+
+
+class AppDirs(object):
+    """Convenience wrapper for getting application dirs."""
+    def __init__(self, appname, appauthor=None, version=None, roaming=False,
+                 multipath=False):
+        self.appname = appname
+        self.appauthor = appauthor
+        self.version = version
+        self.roaming = roaming
+        self.multipath = multipath
+
+    @property
+    def user_data_dir(self):
+        return user_data_dir(self.appname, self.appauthor,
+                             version=self.version, roaming=self.roaming)
+
+    @property
+    def site_data_dir(self):
+        return site_data_dir(self.appname, self.appauthor,
+                             version=self.version, multipath=self.multipath)
+
+    @property
+    def user_config_dir(self):
+        return user_config_dir(self.appname, self.appauthor,
+                               version=self.version, roaming=self.roaming)
+
+    @property
+    def site_config_dir(self):
+        return site_config_dir(self.appname, self.appauthor,
+                             version=self.version, multipath=self.multipath)
+
+    @property
+    def user_cache_dir(self):
+        return user_cache_dir(self.appname, self.appauthor,
+                              version=self.version)
+
+    @property
+    def user_log_dir(self):
+        return user_log_dir(self.appname, self.appauthor,
+                            version=self.version)
+
+
+#---- internal support stuff
+
+def _get_win_folder_from_registry(csidl_name):
+    """This is a fallback technique at best. I'm not sure if using the
+    registry for this guarantees us the correct answer for all CSIDL_*
+    names.
+    """
+    if PY3:
+      import winreg as _winreg
+    else:
+      import _winreg
+
+    shell_folder_name = {
+        "CSIDL_APPDATA": "AppData",
+        "CSIDL_COMMON_APPDATA": "Common AppData",
+        "CSIDL_LOCAL_APPDATA": "Local AppData",
+    }[csidl_name]
+
+    key = _winreg.OpenKey(
+        _winreg.HKEY_CURRENT_USER,
+        r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders"
+    )
+    dir, type = _winreg.QueryValueEx(key, shell_folder_name)
+    return dir
+
+
+def _get_win_folder_with_pywin32(csidl_name):
+    from win32com.shell import shellcon, shell
+    dir = shell.SHGetFolderPath(0, getattr(shellcon, csidl_name), 0, 0)
+    # Try to make this a unicode path because SHGetFolderPath does
+    # not return unicode strings when there is unicode data in the
+    # path.
+    try:
+        dir = unicode(dir)
+
+        # Downgrade to short path name if have highbit chars. See
+        # <http://bugs.activestate.com/show_bug.cgi?id=85099>.
+        has_high_char = False
+        for c in dir:
+            if ord(c) > 255:
+                has_high_char = True
+                break
+        if has_high_char:
+            try:
+                import win32api
+                dir = win32api.GetShortPathName(dir)
+            except ImportError:
+                pass
+    except UnicodeError:
+        pass
+    return dir
+
+
+def _get_win_folder_with_ctypes(csidl_name):
+    import ctypes
+
+    csidl_const = {
+        "CSIDL_APPDATA": 26,
+        "CSIDL_COMMON_APPDATA": 35,
+        "CSIDL_LOCAL_APPDATA": 28,
+    }[csidl_name]
+
+    buf = ctypes.create_unicode_buffer(1024)
+    ctypes.windll.shell32.SHGetFolderPathW(None, csidl_const, None, 0, buf)
+
+    # Downgrade to short path name if have highbit chars. See
+    # <http://bugs.activestate.com/show_bug.cgi?id=85099>.
+    has_high_char = False
+    for c in buf:
+        if ord(c) > 255:
+            has_high_char = True
+            break
+    if has_high_char:
+        buf2 = ctypes.create_unicode_buffer(1024)
+        if ctypes.windll.kernel32.GetShortPathNameW(buf.value, buf2, 1024):
+            buf = buf2
+
+    return buf.value
+
+def _get_win_folder_with_jna(csidl_name):
+    import array
+    from com.sun import jna
+    from com.sun.jna.platform import win32
+
+    buf_size = win32.WinDef.MAX_PATH * 2
+    buf = array.zeros('c', buf_size)
+    shell = win32.Shell32.INSTANCE
+    shell.SHGetFolderPath(None, getattr(win32.ShlObj, csidl_name), None, win32.ShlObj.SHGFP_TYPE_CURRENT, buf)
+    dir = jna.Native.toString(buf.tostring()).rstrip("\0")
+
+    # Downgrade to short path name if have highbit chars. See
+    # <http://bugs.activestate.com/show_bug.cgi?id=85099>.
+    has_high_char = False
+    for c in dir:
+        if ord(c) > 255:
+            has_high_char = True
+            break
+    if has_high_char:
+        buf = array.zeros('c', buf_size)
+        kernel = win32.Kernel32.INSTANCE
+        if kernel.GetShortPathName(dir, buf, buf_size):
+            dir = jna.Native.toString(buf.tostring()).rstrip("\0")
+
+    return dir
+
+if system == "win32":
+    try:
+        import win32com.shell
+        _get_win_folder = _get_win_folder_with_pywin32
+    except ImportError:
+        try:
+            from ctypes import windll
+            _get_win_folder = _get_win_folder_with_ctypes
+        except ImportError:
+            try:
+                import com.sun.jna
+                _get_win_folder = _get_win_folder_with_jna
+            except ImportError:
+                _get_win_folder = _get_win_folder_from_registry
+
+
+#---- self test code
+
+if __name__ == "__main__":
+    appname = "MyApp"
+    appauthor = "MyCompany"
+
+    props = ("user_data_dir", "site_data_dir",
+             "user_config_dir", "site_config_dir",
+             "user_cache_dir", "user_log_dir")
+
+    print("-- app dirs %s --" % __version__)
+
+    print("-- app dirs (with optional 'version')")
+    dirs = AppDirs(appname, appauthor, version="1.0")
+    for prop in props:
+        print("%s: %s" % (prop, getattr(dirs, prop)))
+
+    print("\n-- app dirs (without optional 'version')")
+    dirs = AppDirs(appname, appauthor)
+    for prop in props:
+        print("%s: %s" % (prop, getattr(dirs, prop)))
+
+    print("\n-- app dirs (without optional 'appauthor')")
+    dirs = AppDirs(appname)
+    for prop in props:
+        print("%s: %s" % (prop, getattr(dirs, prop)))
+
+    print("\n-- app dirs (with disabled 'appauthor')")
+    dirs = AppDirs(appname, appauthor=False)
+    for prop in props:
+        print("%s: %s" % (prop, getattr(dirs, prop)))
diff --git a/numba/config.py b/numba/config.py
--- a/numba/config.py
+++ b/numba/config.py
@@ -120,6 +120,9 @@ def _readenv(name, ctor, default):
         # Enable debugging of front-end operation (up to and including IR generation)
         DEBUG_FRONTEND = _readenv("NUMBA_DEBUG_FRONTEND", int, 0)
 
+        # Enable logging of cache operation
+        DEBUG_CACHE = _readenv("NUMBA_DEBUG_CACHE", int, DEBUG)
+
         # Enable tracing support
         TRACE = _readenv("NUMBA_TRACE", int, 0)
 
diff --git a/numba/dispatcher.py b/numba/dispatcher.py
--- a/numba/dispatcher.py
+++ b/numba/dispatcher.py
@@ -13,10 +13,13 @@
 from .six.moves import cPickle as pickle
 import struct
 import sys
+import tempfile
 import warnings
 
+from .appdirs import AppDirs
+
 import numba
-from numba import _dispatcher, compiler, utils, types
+from numba import _dispatcher, compiler, utils, types, config
 from numba.typeconv.rules import default_type_manager
 from numba import sigutils, serialize, typing
 from numba.typing.templates import fold_arguments
@@ -127,6 +130,15 @@ def _get_implementation(self, args, kws):
         return impl
 
 
+_CompileStats = collections.namedtuple(
+    '_CompileStats', ('cache_path', 'cache_hits', 'cache_misses'))
+
+def _cache_log(msg, *args):
+    if config.DEBUG_CACHE:
+        msg = msg % args
+        print(msg)
+
+
 class _DispatcherBase(_dispatcher.Dispatcher):
     """
     Common base class for dispatcher Implementations.
@@ -394,6 +406,8 @@ def __init__(self, py_func, locals={}, targetoptions={}, impl_kind='direct'):
         self._impl_kind = impl_kind
         self._compiler = compiler_class(py_func, self.targetdescr,
                                         targetoptions, locals)
+        self._cache_hits = collections.Counter()
+        self._cache_misses = collections.Counter()
 
         self.typingctx.insert_global(self, types.Dispatcher(self))
 
@@ -449,6 +463,7 @@ def compile(self, sig):
             # Try to load from disk cache
             cres = self._cache.load_overload(sig, self.targetctx)
             if cres is not None:
+                self._cache_hits[sig] += 1
                 # XXX fold this in add_overload()? (also see compiler.py)
                 if not cres.objectmode and not cres.interpmode:
                     self.targetctx.insert_user_function(cres.entry_point,
@@ -456,6 +471,7 @@ def compile(self, sig):
                 self.add_overload(cres)
                 return cres.entry_point
 
+            self._cache_misses[sig] += 1
             cres = self._compiler.compile(args, return_type)
             self.add_overload(cres)
             self._cache.save_overload(sig, cres)
@@ -478,6 +494,14 @@ def recompile(self):
         finally:
             self._can_compile = old_can_compile
 
+    @property
+    def stats(self):
+        return _CompileStats(
+            cache_path=self._cache.cache_path,
+            cache_hits=self._cache_hits,
+            cache_misses=self._cache_misses,
+            )
+
 
 class LiftedLoop(_DispatcherBase):
     """
@@ -539,6 +563,10 @@ def compile(self, sig):
 
 class NullCache(object):
 
+    @property
+    def cache_path(self):
+        return None
+
     def load_overload(self, sig, target_context):
         pass
 
@@ -557,6 +585,16 @@ def flush(self):
 
 class _CacheLocator(object):
 
+    def ensure_cache_path(self):
+        path = self.get_cache_path()
+        try:
+            os.makedirs(path)
+        except OSError as e:
+            if e.errno != errno.EEXIST:
+                raise
+        # Ensure the directory is writable by trying to write a temporary file
+        tempfile.TemporaryFile(dir=path).close()
+
     def get_cache_path(self):
         raise NotImplementedError
 
@@ -571,21 +609,12 @@ def from_function(cls, py_func, py_file):
         raise NotImplementedError
 
 
-class _SourceCacheLocator(_CacheLocator):
+class _SourceFileBackedLocatorMixin(object):
     """
-    A locator for functions backed by a regular Python module.
+    A cache locator mixin for functions which are backed by a well-known
+    Python source file.
     """
 
-    def __init__(self, py_func, py_file):
-        self._py_file = py_file
-        self._lineno = py_func.__code__.co_firstlineno
-
-    def get_cache_path(self):
-        # NOTE: this assumes the __pycache__ directory is writable, which
-        # is false for system installs, but true for conda environments
-        # and local work directories.
-        return os.path.join(os.path.dirname(self._py_file), '__pycache__')
-
     def get_source_stamp(self):
         st = os.stat(self._py_file)
         # We use both timestamp and size as some filesystems only have second
@@ -600,7 +629,52 @@ def from_function(cls, py_func, py_file):
         if not os.path.exists(py_file):
             # Perhaps a placeholder (e.g. "<ipython-XXX>")
             return
-        return cls(py_func, py_file)
+        self = cls(py_func, py_file)
+        try:
+            self.ensure_cache_path()
+        except OSError:
+            # Cannot ensure the cache directory exists or is writable
+            return
+        return self
+
+
+class _InTreeCacheLocator(_SourceFileBackedLocatorMixin, _CacheLocator):
+    """
+    A locator for functions backed by a regular Python module with a
+    writable __pycache__ directory.
+    """
+
+    def __init__(self, py_func, py_file):
+        self._py_file = py_file
+        self._lineno = py_func.__code__.co_firstlineno
+        self._cache_path = os.path.join(os.path.dirname(self._py_file), '__pycache__')
+
+    def get_cache_path(self):
+        return self._cache_path
+
+
+class _UserWideCacheLocator(_SourceFileBackedLocatorMixin, _CacheLocator):
+    """
+    A locator for functions backed by a regular Python module, cached
+    into a user-wide cache directory.
+    """
+
+    def __init__(self, py_func, py_file):
+        self._py_file = py_file
+        self._lineno = py_func.__code__.co_firstlineno
+        appdirs = AppDirs(appname="numba", appauthor=False)
+        cache_dir = appdirs.user_cache_dir
+        cache_subpath = os.path.dirname(py_file)
+        if os.name != "nt":
+            # On non-Windows, further disambiguate by appending the entire
+            # absolute source path to the cache dir, e.g.
+            # "$HOME/.cache/numba/usr/lib/.../mypkg/mysubpkg"
+            # On Windows, this is undesirable because of path length limitations
+            cache_subpath = os.path.abspath(cache_subpath).lstrip(os.path.sep)
+        self._cache_path = os.path.join(cache_dir, cache_subpath)
+
+    def get_cache_path(self):
+        return self._cache_path
 
 
 class _IPythonCacheLocator(_CacheLocator):
@@ -646,6 +720,11 @@ def get_disambiguator(self):
     def from_function(cls, py_func, py_file):
         if not py_file.startswith("<ipython-"):
             return
+        try:
+            self.ensure_cache_path()
+        except OSError:
+            # Cannot ensure the cache directory exists
+            return
         return cls(py_func, py_file)
 
 
@@ -668,7 +747,8 @@ class FunctionCache(object):
     """
 
     _source_stamp = None
-    _locator_classes = [_SourceCacheLocator, _IPythonCacheLocator]
+    _locator_classes = [_InTreeCacheLocator, _UserWideCacheLocator,
+                        _IPythonCacheLocator]
 
     def __init__(self, py_func):
         try:
@@ -712,6 +792,10 @@ def __init__(self, py_func):
     def __repr__(self):
         return "<%s fullname=%r>" % (self.__class__.__name__, self._fullname)
 
+    @property
+    def cache_path(self):
+        return self._cache_path
+
     def enable(self):
         self._enabled = True
         # This may be a bit strict but avoids us maintaining a magic number
@@ -750,11 +834,7 @@ def save_overload(self, sig, cres):
             return
         if not self._check_cachable(cres):
             return
-        try:
-            os.mkdir(self._cache_path)
-        except OSError as e:
-            if e.errno != errno.EEXIST:
-                raise
+        self._locator.ensure_cache_path()
         overloads = self._load_index()
         key = self._index_key(sig, cres.library.codegen)
         try:
@@ -842,6 +922,7 @@ def _load_index(self):
             # rest of the stream, as that may fail.
             return {}
         stamp, overloads = pickle.loads(data)
+        _cache_log("[cache] index loaded from %r", self._index_path)
         if stamp != self._source_stamp:
             # Cache is not fresh.  Stale data files will be eventually
             # overwritten, since they are numbered in incrementing order.
@@ -850,9 +931,11 @@ def _load_index(self):
             return overloads
 
     def _load_data(self, name, target_context):
-        with open(self._data_path(name), "rb") as f:
+        path = self._data_path(name)
+        with open(path, "rb") as f:
             data = f.read()
         tup = pickle.loads(data)
+        _cache_log("[cache] data loaded from %r", path)
         return compiler.CompileResult._rebuild(target_context, *tup)
 
     def _save_index(self, overloads):
@@ -861,12 +944,15 @@ def _save_index(self, overloads):
         with self._open_for_write(self._index_path) as f:
             pickle.dump(self._version, f, protocol=-1)
             f.write(data)
+        _cache_log("[cache] index saved to %r", self._index_path)
 
     def _save_data(self, name, cres):
         data = cres._reduce()
         data = self._dump(data)
-        with self._open_for_write(self._data_path(name)) as f:
+        path = self._data_path(name)
+        with self._open_for_write(path) as f:
             f.write(data)
+        _cache_log("[cache] data saved to %r", path)
 
     def _dump(self, obj):
         return pickle.dumps(obj, protocol=-1)


Test patch:
diff --git a/numba/tests/test_dispatcher.py b/numba/tests/test_dispatcher.py
--- a/numba/tests/test_dispatcher.py
+++ b/numba/tests/test_dispatcher.py
@@ -4,6 +4,7 @@
 import imp
 import os
 import shutil
+import stat
 import subprocess
 import sys
 import tempfile
@@ -13,7 +14,7 @@
 import numpy as np
 
 from numba import unittest_support as unittest
-from numba import utils, vectorize, jit, generated_jit, types
+from numba import utils, vectorize, jit, generated_jit, types, appdirs
 from numba.errors import NumbaWarning
 from .support import TestCase, tag, temp_directory, import_dynamic
 
@@ -539,7 +540,7 @@ def get_cache_mtimes(self):
         return dict((fn, os.path.getmtime(os.path.join(self.cache_dir, fn)))
                     for fn in sorted(self.cache_contents()))
 
-    def check_cache(self, n):
+    def check_pycache(self, n):
         c = self.cache_contents()
         self.assertEqual(len(c), n, c)
 
@@ -575,43 +576,53 @@ def run_in_separate_process(self):
                                  % (popen.returncode, err.decode()))
 
     def check_module(self, mod):
-        self.check_cache(0)
+        self.check_pycache(0)
         f = mod.add_usecase
         self.assertPreciseEqual(f(2, 3), 6)
-        self.check_cache(2)  # 1 index, 1 data
+        self.check_pycache(2)  # 1 index, 1 data
         self.assertPreciseEqual(f(2.5, 3), 6.5)
-        self.check_cache(3)  # 1 index, 2 data
+        self.check_pycache(3)  # 1 index, 2 data
 
         f = mod.add_objmode_usecase
         self.assertPreciseEqual(f(2, 3), 6)
-        self.check_cache(5)  # 2 index, 3 data
+        self.check_pycache(5)  # 2 index, 3 data
         self.assertPreciseEqual(f(2.5, 3), 6.5)
-        self.check_cache(6)  # 2 index, 4 data
+        self.check_pycache(6)  # 2 index, 4 data
+
+    def check_hits(self, func, hits, misses=None):
+        st = func.stats
+        self.assertEqual(sum(st.cache_hits.values()), hits, st.cache_hits)
+        if misses is not None:
+            self.assertEqual(sum(st.cache_misses.values()), misses,
+                             st.cache_misses)
 
     @tag('important')
     def test_caching(self):
-        self.check_cache(0)
+        self.check_pycache(0)
         mod = self.import_module()
-        self.check_cache(0)
+        self.check_pycache(0)
 
         f = mod.add_usecase
         self.assertPreciseEqual(f(2, 3), 6)
-        self.check_cache(2)  # 1 index, 1 data
+        self.check_pycache(2)  # 1 index, 1 data
         self.assertPreciseEqual(f(2.5, 3), 6.5)
-        self.check_cache(3)  # 1 index, 2 data
+        self.check_pycache(3)  # 1 index, 2 data
+        self.check_hits(f, 0, 2)
 
         f = mod.add_objmode_usecase
         self.assertPreciseEqual(f(2, 3), 6)
-        self.check_cache(5)  # 2 index, 3 data
+        self.check_pycache(5)  # 2 index, 3 data
         self.assertPreciseEqual(f(2.5, 3), 6.5)
-        self.check_cache(6)  # 2 index, 4 data
+        self.check_pycache(6)  # 2 index, 4 data
+        self.check_hits(f, 0, 2)
 
         f = mod.record_return
         rec = f(mod.aligned_arr, 1)
         self.assertPreciseEqual(tuple(rec), (2, 43.5))
         rec = f(mod.packed_arr, 1)
         self.assertPreciseEqual(tuple(rec), (2, 43.5))
-        self.check_cache(9)  # 3 index, 6 data
+        self.check_pycache(9)  # 3 index, 6 data
+        self.check_hits(f, 0, 2)
 
         f = mod.generated_usecase
         self.assertPreciseEqual(f(3, 2), 1)
@@ -624,42 +635,42 @@ def test_inner_then_outer(self):
         # Caching inner then outer function is ok
         mod = self.import_module()
         self.assertPreciseEqual(mod.inner(3, 2), 6)
-        self.check_cache(2)  # 1 index, 1 data
+        self.check_pycache(2)  # 1 index, 1 data
         # Uncached outer function shouldn't fail (issue #1603)
         f = mod.outer_uncached
         self.assertPreciseEqual(f(3, 2), 2)
-        self.check_cache(2)  # 1 index, 1 data
+        self.check_pycache(2)  # 1 index, 1 data
         mod = self.import_module()
         f = mod.outer_uncached
         self.assertPreciseEqual(f(3, 2), 2)
-        self.check_cache(2)  # 1 index, 1 data
+        self.check_pycache(2)  # 1 index, 1 data
         # Cached outer will create new cache entries
         f = mod.outer
         self.assertPreciseEqual(f(3, 2), 2)
-        self.check_cache(4)  # 2 index, 2 data
+        self.check_pycache(4)  # 2 index, 2 data
         self.assertPreciseEqual(f(3.5, 2), 2.5)
-        self.check_cache(6)  # 2 index, 4 data
+        self.check_pycache(6)  # 2 index, 4 data
 
     def test_outer_then_inner(self):
         # Caching outer then inner function is ok
         mod = self.import_module()
         self.assertPreciseEqual(mod.outer(3, 2), 2)
-        self.check_cache(4)  # 2 index, 2 data
+        self.check_pycache(4)  # 2 index, 2 data
         self.assertPreciseEqual(mod.outer_uncached(3, 2), 2)
-        self.check_cache(4)  # same
+        self.check_pycache(4)  # same
         mod = self.import_module()
         f = mod.inner
         self.assertPreciseEqual(f(3, 2), 6)
-        self.check_cache(4)  # same
+        self.check_pycache(4)  # same
         self.assertPreciseEqual(f(3.5, 2), 6.5)
-        self.check_cache(5)  # 2 index, 3 data
+        self.check_pycache(5)  # 2 index, 3 data
 
     def test_no_caching(self):
         mod = self.import_module()
 
         f = mod.add_nocache_usecase
         self.assertPreciseEqual(f(2, 3), 6)
-        self.check_cache(0)
+        self.check_pycache(0)
 
     def test_looplifted(self):
         # Loop-lifted functions can't be cached and raise a warning
@@ -670,7 +681,7 @@ def test_looplifted(self):
 
             f = mod.looplifted
             self.assertPreciseEqual(f(4), 6)
-            self.check_cache(0)
+            self.check_pycache(0)
 
         self.assertEqual(len(w), 1)
         self.assertEqual(str(w[0].message),
@@ -687,7 +698,7 @@ def test_ctypes(self):
 
             f = mod.use_c_sin
             self.assertPreciseEqual(f(0.0), 0.0)
-            self.check_cache(0)
+            self.check_pycache(0)
 
         self.assertEqual(len(w), 1)
         self.assertIn('Cannot cache compiled function "use_c_sin"',
@@ -703,7 +714,7 @@ def test_closure(self):
             self.assertPreciseEqual(f(3), 6)
             f = mod.closure2
             self.assertPreciseEqual(f(3), 8)
-            self.check_cache(0)
+            self.check_pycache(0)
 
         self.assertEqual(len(w), 2)
         for item in w:
@@ -713,6 +724,7 @@ def test_closure(self):
     def test_cache_reuse(self):
         mod = self.import_module()
         mod.add_usecase(2, 3)
+        mod.add_usecase(2.5, 3.5)
         mod.add_objmode_usecase(2, 3)
         mod.outer_uncached(2, 3)
         mod.outer(2, 3)
@@ -720,11 +732,19 @@ def test_cache_reuse(self):
         mod.record_return(mod.aligned_arr, 1)
         mod.generated_usecase(2, 3)
         mtimes = self.get_cache_mtimes()
+        # Two signatures compiled
+        self.check_hits(mod.add_usecase, 0, 2)
 
         mod2 = self.import_module()
         self.assertIsNot(mod, mod2)
-        mod.add_usecase(2, 3)
-        mod.add_objmode_usecase(2, 3)
+        f = mod2.add_usecase
+        f(2, 3)
+        self.check_hits(f, 1, 0)
+        f(2.5, 3.5)
+        self.check_hits(f, 2, 0)
+        f = mod2.add_objmode_usecase
+        f(2, 3)
+        self.check_hits(f, 1, 0)
 
         # The files haven't changed
         self.assertEqual(self.get_cache_mtimes(), mtimes)
@@ -773,6 +793,53 @@ def test_same_names(self):
         f = mod.renamed_function2
         self.assertPreciseEqual(f(2), 8)
 
+    def _test_pycache_fallback(self):
+        """
+        With a disabled __pycache__, test there is a working fallback
+        (e.g. on the user-wide cache dir)
+        """
+        mod = self.import_module()
+        f = mod.add_usecase
+        # Remove this function's cache files at the end, to avoid accumulation
+        # accross test calls.
+        self.addCleanup(shutil.rmtree, f.stats.cache_path, ignore_errors=True)
+
+        self.assertPreciseEqual(f(2, 3), 6)
+        # It's a cache miss since the file was copied to a new temp location
+        self.check_hits(f, 0, 1)
+
+        # Test re-use
+        mod2 = self.import_module()
+        f = mod2.add_usecase
+        self.assertPreciseEqual(f(2, 3), 6)
+        self.check_hits(f, 1, 0)
+
+        # The __pycache__ is empty (otherwise the test's preconditions
+        # wouldn't be met)
+        self.check_pycache(0)
+
+    @unittest.skipIf(os.name == "nt",
+                     "cannot easily make a directory read-only on Windows")
+    def test_non_creatable_pycache(self):
+        # Make it impossible to create the __pycache__ directory
+        old_perms = os.stat(self.tempdir).st_mode
+        os.chmod(self.tempdir, 0o500)
+        self.addCleanup(os.chmod, self.tempdir, old_perms)
+
+        self._test_pycache_fallback()
+
+    @unittest.skipIf(os.name == "nt",
+                     "cannot easily make a directory read-only on Windows")
+    def test_non_writable_pycache(self):
+        # Make it impossible to write to the __pycache__ directory
+        pycache = os.path.join(self.tempdir, '__pycache__')
+        os.mkdir(pycache)
+        old_perms = os.stat(pycache).st_mode
+        os.chmod(pycache, 0o500)
+        self.addCleanup(os.chmod, pycache, old_perms)
+
+        self._test_pycache_fallback()
+
 
 if __name__ == '__main__':
     unittest.main()