Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support environments without a home dir or writable file system #1824

Merged
merged 17 commits into from Apr 16, 2013
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
8083f25
shutil.move: Guard against IOError and print warnings instead of cras…
mgiuca-google Mar 6, 2013
a697a26
matplotlib: _is_writable_dir uses os.access instead of TemporaryFile.
mgiuca-google Mar 6, 2013
4d65400
Matplotlib now works when the user has no home directory.
mgiuca-google Mar 6, 2013
21921a3
get_configdir returns None if tempfile.gettempdir() is not available.
mgiuca-google Mar 6, 2013
4987dcd
Deal with all cases where get_configdir might return None.
mgiuca-google Mar 6, 2013
64c797b
font_manager: Gracefully handle the case of there being no config dir.
mgiuca-google Mar 6, 2013
1dbd6de
texmanager: Gracefully handle the case of there being no config dir u…
mgiuca-google Mar 6, 2013
1adfc85
finance: Gracefully handle the case of there being no config dir.
mgiuca-google Mar 8, 2013
941efd4
Fix formatting and other misc code tweaks.
mgiuca-google Mar 17, 2013
ca6cd19
matplotlib.get_home: Removing catch-all except blocks.
mgiuca-google Mar 18, 2013
cc8cd1b
matplotlib, texmanager: Change WARNING prints into real warnings.
mgiuca-google Mar 18, 2013
f01ebe1
matplotlib, texmanager: Only print the rename message if it actually …
mgiuca-google Mar 18, 2013
018ce26
finance: Fixed caching when cachename is supplied.
mgiuca-google Mar 18, 2013
6a4f1e7
matplotlib: Use cbook.mkdirs instead of os.makedirs.
mgiuca-google Mar 18, 2013
4f55a27
matplotlib: Remove catch for OSError.
mgiuca-google Mar 18, 2013
81639a1
matplotlib: _is_writable_dir checks that it is a directory.
mgiuca-google Mar 18, 2013
8335773
matplotlib: _is_writable_dir tests with os.access and TemporaryFile.
mgiuca-google Mar 18, 2013
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
152 changes: 94 additions & 58 deletions lib/matplotlib/__init__.py
Expand Up @@ -213,16 +213,28 @@ def _is_writable_dir(p):
p is a string pointing to a putative writable dir -- return True p
is such a string, else False
"""
try: p + '' # test is string like
except TypeError: return False
try:
p + '' # test is string like
except TypeError:
return False

# Test whether the operating system thinks it's a writable directory.
# Note that this check is necessary on Google App Engine, because the
# subsequent check will succeed even though p may not be writable.
if not os.access(p, os.W_OK) or not os.path.isdir(p):
return False

# Also test that it is actually possible to write to a file here.
try:
t = tempfile.TemporaryFile(dir=p)
try:
t.write(ascii('1'))
finally:
t.close()
except OSError: return False
else: return True
except OSError:
return False

return True

class Verbose:
"""
Expand Down Expand Up @@ -475,38 +487,42 @@ def checkdep_usetex(s):

def _get_home():
"""Find user's home directory if possible.
Otherwise raise error.
Otherwise, returns None.

:see: http://mail.python.org/pipermail/python-list/2005-February/263921.html
:see: http://mail.python.org/pipermail/python-list/2005-February/325395.html
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

"""
path=''
try:
path=os.path.expanduser("~")
except:
path = os.path.expanduser("~")
except ImportError:
# This happens on Google App Engine (pwd module is not present).
pass
if not os.path.isdir(path):
for evar in ('HOME', 'USERPROFILE', 'TMP'):
try:
path = os.environ[evar]
if os.path.isdir(path):
break
except: pass
if path:
return path
else:
raise RuntimeError('please define environment variable $HOME')
if os.path.isdir(path):
return path
for evar in ('HOME', 'USERPROFILE', 'TMP'):
path = os.environ.get(evar)
if path is not None and os.path.isdir(path):
return path
return None


def _create_tmp_config_dir():
"""
If the config directory can not be created, create a temporary
directory.

Returns None if a writable temporary directory could not be created.
"""
import getpass
import tempfile

tempdir = os.path.join(
tempfile.gettempdir(), 'matplotlib-%s' % getpass.getuser())
try:
tempdir = tempfile.gettempdir()
except NotImplementedError:
# Some restricted platforms (such as Google App Engine) do not provide
# gettempdir.
return None
tempdir = os.path.join(tempdir, 'matplotlib-%s' % getpass.getuser())
os.environ['MPLCONFIGDIR'] = tempdir

return tempdir
Expand All @@ -518,35 +534,42 @@ def _get_configdir():
"""
Return the string representing the configuration directory.

Default is HOME/.matplotlib. You can override this with the
MPLCONFIGDIR environment variable. If the default is not
writable, and MPLCONFIGDIR is not set, then
tempfile.gettempdir() is used to provide a directory in
which a matplotlib subdirectory is created as the configuration
directory.
The directory is chosen as follows:

1. If the MPLCONFIGDIR environment variable is supplied, choose that. Else,
choose the '.matplotlib' subdirectory of the user's home directory (and
create it if necessary).
2. If the chosen directory exists and is writable, use that as the
configuration directory.
3. If possible, create a temporary directory, and use it as the
configuration directory.
4. A writable directory could not be found or created; return None.
"""

configdir = os.environ.get('MPLCONFIGDIR')
if configdir is not None:
if not os.path.exists(configdir):
os.makedirs(configdir)
mkdirs(configdir)
if not _is_writable_dir(configdir):
return _create_tmp_config_dir()
return configdir

h = get_home()
p = os.path.join(get_home(), '.matplotlib')
if h is not None:
p = os.path.join(h, '.matplotlib')

if os.path.exists(p):
if not _is_writable_dir(p):
return _create_tmp_config_dir()
else:
if not _is_writable_dir(h):
return _create_tmp_config_dir()
from matplotlib.cbook import mkdirs
mkdirs(p)
if os.path.exists(p):
if not _is_writable_dir(p):
return _create_tmp_config_dir()
else:
if not _is_writable_dir(h):
return _create_tmp_config_dir()
from matplotlib.cbook import mkdirs
mkdirs(p)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There must be a subtlety that I'm missing here - I can't see the difference between cbook.mkdirs and os.makedirs. Any ideas?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The difference is that cbook.mkdirs doesn't complain if the directory already exists.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cbook.mkdirs() is even safer than os.makedirs(). Imagine two processes, one needing to make A/B, and the other needing to make A/C. There is a race condition in os.makedirs() where one of the two processes will fail to make their directory because the exception was thrown while dealing with the parent directory.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So why does it use os.makedirs above? (It's a little bit above the context shown in this patch, but the first block of get_configdir calls os.makedirs, whereas the second block calls cbook.mkdirs.) Can we change it so it consistently calls cbook.mkdirs?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes -- let's change this to cbook.mkdirs in both cases.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.


return p
return p

return _create_tmp_config_dir()
get_configdir = verbose.wrap('CONFIGDIR=%s', _get_configdir, always=False)


Expand Down Expand Up @@ -636,27 +659,39 @@ def matplotlib_fname():

"""

oldname = os.path.join( os.getcwd(), '.matplotlibrc')
oldname = os.path.join(os.getcwd(), '.matplotlibrc')
if os.path.exists(oldname):
print("""\
WARNING: Old rc filename ".matplotlibrc" found in working dir
and and renamed to new default rc file name "matplotlibrc"
(no leading"dot"). """, file=sys.stderr)
shutil.move('.matplotlibrc', 'matplotlibrc')
try:
shutil.move('.matplotlibrc', 'matplotlibrc')
except IOError as e:
warnings.warn('File could not be renamed: %s' % e)
else:
warnings.warn("""\
Old rc filename ".matplotlibrc" found in working dir and and renamed to new
default rc file name "matplotlibrc" (no leading ".").""")

home = get_home()
oldname = os.path.join( home, '.matplotlibrc')
if os.path.exists(oldname):
configdir = get_configdir()
newname = os.path.join(configdir, 'matplotlibrc')
print("""\
WARNING: Old rc filename "%s" found and renamed to
new default rc file name "%s"."""%(oldname, newname), file=sys.stderr)

shutil.move(oldname, newname)

configdir = get_configdir()
if home:
oldname = os.path.join(home, '.matplotlibrc')
if os.path.exists(oldname):
if configdir is not None:
newname = os.path.join(configdir, 'matplotlibrc')

try:
shutil.move(oldname, newname)
except IOError as e:
warnings.warn('File could not be renamed: %s' % e)
else:
warnings.warn("""\
Old rc filename "%s" found and renamed to new default rc file name "%s"."""
% (oldname, newname))
else:
warnings.warn("""\
Could not rename old rc file "%s": a suitable configuration directory could not
be found.""" % oldname)

fname = os.path.join( os.getcwd(), 'matplotlibrc')
fname = os.path.join(os.getcwd(), 'matplotlibrc')
if os.path.exists(fname): return fname

if 'MATPLOTLIBRC' in os.environ:
Expand All @@ -666,9 +701,10 @@ def matplotlib_fname():
if os.path.exists(fname):
return fname

fname = os.path.join(get_configdir(), 'matplotlibrc')
if os.path.exists(fname): return fname

if configdir is not None:
fname = os.path.join(configdir, 'matplotlibrc')
if os.path.exists(fname):
return fname

path = get_data_path() # guaranteed to exist or raise
fname = os.path.join(path, 'matplotlibrc')
Expand Down
37 changes: 24 additions & 13 deletions lib/matplotlib/finance.py
Expand Up @@ -28,7 +28,13 @@


configdir = get_configdir()
cachedir = os.path.join(configdir, 'finance.cache')
# cachedir will be None if there is no writable directory.
if configdir is not None:
cachedir = os.path.join(configdir, 'finance.cache')
else:
# Should only happen in a restricted environment (such as Google App
# Engine). Deal with this gracefully by not caching finance data.
cachedir = None


stock_dt = np.dtype([('date', object),
Expand Down Expand Up @@ -178,20 +184,25 @@ def fetch_historical_yahoo(ticker, date1, date2, cachename=None,dividends=False)
d2[0], d2[1], d2[2], ticker, g)


if cachename is None:
# Cache the finance data if cachename is supplied, or there is a writable
# cache directory.
if cachename is None and cachedir is not None:
cachename = os.path.join(cachedir, md5(url).hexdigest())
if os.path.exists(cachename):
fh = open(cachename)
verbose.report('Using cachefile %s for %s'%(cachename, ticker))
if cachename is not None:
if os.path.exists(cachename):
fh = open(cachename)
verbose.report('Using cachefile %s for %s'%(cachename, ticker))
else:
mkdirs(os.path.abspath(os.path.dirname(cachename)))
with contextlib.closing(urlopen(url)) as urlfh:
with open(cachename, 'wb') as fh:
fh.write(urlfh.read())
verbose.report('Saved %s data to cache file %s'%(ticker, cachename))
fh = open(cachename, 'r')

return fh
else:
mkdirs(os.path.abspath(os.path.dirname(cachename)))
with contextlib.closing(urlopen(url)) as urlfh:
with open(cachename, 'wb') as fh:
fh.write(urlfh.read())
verbose.report('Saved %s data to cache file %s'%(ticker, cachename))
fh = open(cachename, 'r')

return fh
return urlopen(url)


def quotes_historical_yahoo(ticker, date1, date2, asobject=False,
Expand Down
34 changes: 22 additions & 12 deletions lib/matplotlib/font_manager.py
Expand Up @@ -1311,28 +1311,38 @@ def findfont(prop, fontext='ttf'):
return result

else:
if sys.version_info[0] >= 3:
_fmcache = os.path.join(get_configdir(), 'fontList.py3k.cache')
configdir = get_configdir()
if configdir is not None:
if sys.version_info[0] >= 3:
_fmcache = os.path.join(configdir, 'fontList.py3k.cache')
else:
_fmcache = os.path.join(configdir, 'fontList.cache')
else:
_fmcache = os.path.join(get_configdir(), 'fontList.cache')
# Should only happen in a restricted environment (such as Google App
# Engine). Deal with this gracefully by not caching fonts.
_fmcache = None

fontManager = None

def _rebuild():
global fontManager
fontManager = FontManager()
pickle_dump(fontManager, _fmcache)
if _fmcache:
pickle_dump(fontManager, _fmcache)
verbose.report("generated new fontManager")

try:
fontManager = pickle_load(_fmcache)
if (not hasattr(fontManager, '_version') or
fontManager._version != FontManager.__version__):
if _fmcache:
try:
fontManager = pickle_load(_fmcache)
if (not hasattr(fontManager, '_version') or
fontManager._version != FontManager.__version__):
_rebuild()
else:
fontManager.default_size = None
verbose.report("Using fontManager instance from %s" % _fmcache)
except:
_rebuild()
else:
fontManager.default_size = None
verbose.report("Using fontManager instance from %s" % _fmcache)
except:
else:
_rebuild()

def findfont(prop, **kw):
Expand Down
5 changes: 4 additions & 1 deletion lib/matplotlib/testing/compare.py
Expand Up @@ -99,7 +99,10 @@ def compare_float( expected, actual, relTol = None, absTol = None ):
# parameters old and new to a list that can be passed to Popen to
# convert files with that extension to png format.
def get_cache_dir():
cache_dir = os.path.join(_get_configdir(), 'test_cache')
configdir = _get_configdir()
if configdir is None:
raise RuntimeError('Could not find a suitable configuration directory')
cache_dir = os.path.join(configdir, 'test_cache')
if not os.path.exists(cache_dir):
try:
os.makedirs(cache_dir)
Expand Down
36 changes: 28 additions & 8 deletions lib/matplotlib/texmanager.py
Expand Up @@ -41,6 +41,7 @@
import os
import shutil
import sys
import warnings

from hashlib import md5

Expand Down Expand Up @@ -94,16 +95,30 @@ class TexManager:
oldcache = os.path.join(oldpath, '.tex.cache')

configdir = mpl.get_configdir()
texcache = os.path.join(configdir, 'tex.cache')
if configdir is not None:
texcache = os.path.join(configdir, 'tex.cache')
else:
# Should only happen in a restricted environment (such as Google App
# Engine). Deal with this gracefully by not creating a cache directory.
texcache = None

if os.path.exists(oldcache):
# FIXME raise proper warning
print("""\
WARNING: found a TeX cache dir in the deprecated location "%s".
Moving it to the new default location "%s".""" % (oldcache, texcache),
file=sys.stderr)
shutil.move(oldcache, texcache)
mkdirs(texcache)
if texcache is not None:
try:
shutil.move(oldcache, texcache)
except IOError as e:
warnings.warn('File could not be renamed: %s' % e)
else:
warnings.warn("""\
Found a TeX cache dir in the deprecated location "%s".
Moving it to the new default location "%s".""" % (oldcache, texcache))
else:
warnings.warn("""\
Could not rename old TeX cache dir "%s": a suitable configuration
directory could not be found.""" % oldcache)

if texcache is not None:
mkdirs(texcache)

_dvipng_hack_alpha = None
#_dvipng_hack_alpha = dvipng_hack_alpha()
Expand Down Expand Up @@ -145,6 +160,11 @@ class TexManager:

def __init__(self):

if self.texcache is None:
raise RuntimeError(
('Cannot create TexManager, as there is no cache directory '
'available'))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This probably doesn't affect Google AppEngine (which I suspect doesn't have a TeX installation anyway), but it would be nice to continue to be able to get TeX snippets without them being cached in this case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From my reading of the code, this whole module doesn't work if there is no writable directory. The so-called "texcache" is not really a cache at all, it's also a working directory, where intermediate files are generated and processed with dvipng. So even if we assume a system that does have a TeX installation and the ability to launch external commands, but does not have any writable file system, we won't be able to run these commands. (This isn't a simple matter of disabling caching.)

Correct me if I'm wrong about that.


mkdirs(self.texcache)
ff = rcParams['font.family'].lower()
if ff in self.font_families:
Expand Down