From 4be0b728880cd76663885f03ae4e099c27338172 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Thu, 7 Jun 2018 17:51:04 +0200 Subject: [PATCH 1/3] Simplify _get_config_or_cache_dir logic. 1. In get_home, we shouldn't check for $TMP, but instead let this case fall through to _create_config_or_cache_dir. 2. When checking for $XDG_{CONFIG,CACHE}_HOME and $MPLCONFIGDIR, consider empty strings as equivalent to unset (which is standard behavior, e.g. one could write `XDG_CONFIG_HOME= python ...` and expect things to behave as if `XDG_CONFIG_HOME` was indeed unset). 3. The logic in _get_config_or_cache_dir can greatly simplified: try to create a candidate, generating it on the way if necessary; if it cannot be created or is not writable, fallback to a temporary directory. --- lib/matplotlib/__init__.py | 99 +++++++++++++------------------------- 1 file changed, 34 insertions(+), 65 deletions(-) diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index 2e4ee4edb393..c5289ae3312c 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -216,13 +216,6 @@ def compare_versions(a, b): sys.argv = ['modpython'] -def _is_writable_dir(p): - """ - p is a string pointing to a putative writable dir -- return True p - is such a string, else False - """ - return os.access(p, os.W_OK) and os.path.isdir(p) - _verbose_msg = """\ matplotlib.verbose is deprecated; Command line argument --verbose-LEVEL is deprecated. @@ -552,26 +545,22 @@ def checkdep_usetex(s): def _get_home(): - """Find user's home directory if possible. - Otherwise, returns None. + """ + Return the user's home directory. - :see: - http://mail.python.org/pipermail/python-list/2005-February/325395.html + If the user's home directory cannot be found, return None. """ - path = os.path.expanduser("~") - 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 + try: + return str(Path.home()) + except Exception: + return None + +get_home = _wrap('$HOME=%s', _get_home, always=False) def _create_tmp_config_dir(): """ - If the config directory can not be created, create a temporary - directory. + If the config directory can not be created, create a temporary directory. """ configdir = os.environ['MPLCONFIGDIR'] = ( tempfile.mkdtemp(prefix='matplotlib-')) @@ -579,21 +568,16 @@ def _create_tmp_config_dir(): return configdir -get_home = _wrap('$HOME=%s', _get_home, always=False) - - def _get_xdg_config_dir(): """ Returns the XDG configuration directory, according to the `XDG base directory spec `_. """ - path = os.environ.get('XDG_CONFIG_HOME') - if path is None: - path = get_home() - if path is not None: - path = os.path.join(path, '.config') - return path + return (os.environ.get('XDG_CONFIG_HOME') + or (Path(get_home(), ".config") + if get_home() + else None)) def _get_xdg_cache_dir(): @@ -602,43 +586,31 @@ def _get_xdg_cache_dir(): base directory spec `_. """ - path = os.environ.get('XDG_CACHE_HOME') - if path is None: - path = get_home() - if path is not None: - path = os.path.join(path, '.cache') - return path + return (os.environ.get('XDG_CACHE_HOME') + or (Path(get_home(), ".cache") + if get_home() + else None)) def _get_config_or_cache_dir(xdg_base): configdir = os.environ.get('MPLCONFIGDIR') - if configdir is not None: - configdir = os.path.abspath(configdir) - Path(configdir).mkdir(parents=True, exist_ok=True) - if not _is_writable_dir(configdir): - return _create_tmp_config_dir() - return configdir - - p = None - h = get_home() - if h is not None: - p = os.path.join(h, '.matplotlib') - if sys.platform.startswith(('linux', 'freebsd')): - p = None - if xdg_base is not None: - p = os.path.join(xdg_base, 'matplotlib') - - if p is not None: - if os.path.exists(p): - if _is_writable_dir(p): - return p + configdir = ( + Path(configdir).resolve() + if configdir + else Path(xdg_base, "matplotlib") + if sys.platform.startswith(('linux', 'freebsd')) and xdg_base + else Path(get_home(), ".matplotlib") + if get_home() + else None) + + if configdir: + try: + configdir.mkdir(parents=True, exist_ok=True) + except OSError: + pass else: - try: - Path(p).mkdir(parents=True, exist_ok=True) - except OSError: - pass - else: - return p + if os.access(str(configdir), os.W_OK) and configdir.is_dir(): + return str(configdir) return _create_tmp_config_dir() @@ -650,12 +622,9 @@ def _get_configdir(): The directory is chosen as follows: 1. If the MPLCONFIGDIR environment variable is supplied, choose that. - 2a. On Linux, follow the XDG specification and look first in `$XDG_CONFIG_HOME`, if defined, or `$HOME/.config`. - 2b. On other platforms, choose `$HOME/.matplotlib`. - 3. If the chosen directory exists and is writable, use that as the configuration directory. 4. If possible, create a temporary directory, and use it as the From 5840fee323ab3d2c8dfd1cf64c5c4ecca79e8fc7 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Thu, 7 Jun 2018 18:12:10 +0200 Subject: [PATCH 2/3] Simplify caching of directories retrieval. --- lib/matplotlib/__init__.py | 58 +++++++++++++------------------ lib/matplotlib/style/core.py | 2 +- lib/matplotlib/testing/compare.py | 16 ++++----- 3 files changed, 31 insertions(+), 45 deletions(-) diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index c5289ae3312c..6ea313990433 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -386,27 +386,22 @@ def ge(self, level): verbose = Verbose() -def _wrap(fmt, func, level=logging.DEBUG, always=True): - """ - return a callable function that wraps func and reports its - output through logger - - if always is True, the report will occur on every function - call; otherwise only on the first time the function is called - """ - assert callable(func) - - def wrapper(*args, **kwargs): - ret = func(*args, **kwargs) - - if (always or not wrapper._spoke): - _log.log(level, fmt % ret) - spoke = True - if not wrapper._spoke: - wrapper._spoke = spoke +def _logged_cached(fmt, func=None): + if func is None: + return functools.partial(_logged_cached, fmt) + + called = False + ret = None + + @functools.wraps(func) + def wrapper(): + nonlocal called, ret + if not called: + ret = func() + called = True + _log.debug(fmt, ret) return ret - wrapper._spoke = False - wrapper.__doc__ = func.__doc__ + return wrapper @@ -544,7 +539,8 @@ def checkdep_usetex(s): return flag -def _get_home(): +@_logged_cached('$HOME=%s') +def get_home(): """ Return the user's home directory. @@ -555,8 +551,6 @@ def _get_home(): except Exception: return None -get_home = _wrap('$HOME=%s', _get_home, always=False) - def _create_tmp_config_dir(): """ @@ -615,7 +609,8 @@ def _get_config_or_cache_dir(xdg_base): return _create_tmp_config_dir() -def _get_configdir(): +@_logged_cached('CONFIGDIR=%s') +def get_configdir(): """ Return the string representing the configuration directory. @@ -633,10 +628,9 @@ def _get_configdir(): """ return _get_config_or_cache_dir(_get_xdg_config_dir()) -get_configdir = _wrap('CONFIGDIR=%s', _get_configdir, always=False) - -def _get_cachedir(): +@_logged_cached('CACHEDIR=%s') +def get_cachedir(): """ Return the location of the cache directory. @@ -645,8 +639,6 @@ def _get_cachedir(): """ return _get_config_or_cache_dir(_get_xdg_cache_dir()) -get_cachedir = _wrap('CACHEDIR=%s', _get_cachedir, always=False) - def _decode_filesystem_path(path): if not isinstance(path, str): @@ -698,14 +690,12 @@ def _get_data_path(): raise RuntimeError('Could not find the matplotlib data files') -def _get_data_path_cached(): +@_logged_cached('matplotlib data path: %s') +def get_data_path(): if defaultParams['datapath'][0] is None: defaultParams['datapath'][0] = _get_data_path() return defaultParams['datapath'][0] -get_data_path = _wrap('matplotlib data path %s', _get_data_path_cached, - always=False) - def get_py2exe_datafiles(): data_path = Path(get_data_path()) @@ -756,7 +746,7 @@ def gen_candidates(): else: yield matplotlibrc yield os.path.join(matplotlibrc, 'matplotlibrc') - yield os.path.join(_get_configdir(), 'matplotlibrc') + yield os.path.join(get_configdir(), 'matplotlibrc') yield os.path.join(get_data_path(), 'matplotlibrc') for fname in gen_candidates(): diff --git a/lib/matplotlib/style/core.py b/lib/matplotlib/style/core.py index 27cc8339c242..d7c59f844563 100644 --- a/lib/matplotlib/style/core.py +++ b/lib/matplotlib/style/core.py @@ -25,7 +25,7 @@ BASE_LIBRARY_PATH = os.path.join(mpl.get_data_path(), 'stylelib') # Users may want multiple library paths, so store a list of paths. -USER_LIBRARY_PATHS = [os.path.join(mpl._get_configdir(), 'stylelib')] +USER_LIBRARY_PATHS = [os.path.join(mpl.get_configdir(), 'stylelib')] STYLE_EXTENSION = 'mplstyle' STYLE_FILE_PATTERN = re.compile(r'([\S]+).%s$' % STYLE_EXTENSION) diff --git a/lib/matplotlib/testing/compare.py b/lib/matplotlib/testing/compare.py index 0f5e149a567b..6413916916c2 100644 --- a/lib/matplotlib/testing/compare.py +++ b/lib/matplotlib/testing/compare.py @@ -18,8 +18,7 @@ import matplotlib from matplotlib.testing.exceptions import ImageComparisonFailure -from matplotlib import _png -from matplotlib import _get_cachedir +from matplotlib import _png, cbook from matplotlib import cbook __all__ = ['compare_float', 'compare_images', 'comparable_formats'] @@ -79,7 +78,7 @@ def compare_float(expected, actual, relTol=None, absTol=None): def get_cache_dir(): - cachedir = _get_cachedir() + cachedir = matplotlib.get_cachedir() if cachedir is None: raise RuntimeError('Could not find a suitable configuration directory') cache_dir = os.path.join(cachedir, 'test_cache') @@ -293,15 +292,12 @@ def comparable_formats(): def convert(filename, cache): """ - Convert the named file into a png file. Returns the name of the - created file. + Convert the named file to png; return the name of the created file. If *cache* is True, the result of the conversion is cached in - `matplotlib._get_cachedir() + '/test_cache/'`. The caching is based - on a hash of the exact contents of the input file. The is no limit - on the size of the cache, so it may need to be manually cleared - periodically. - + `matplotlib.get_cachedir() + '/test_cache/'`. The caching is based on a + hash of the exact contents of the input file. There is no limit on the + size of the cache, so it may need to be manually cleared periodically. """ base, extension = filename.rsplit('.', 1) if extension not in converter: From 8be4eaedee52043aa1eef4890e73fb3b357ca1e4 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Mon, 25 Jun 2018 00:21:31 +0200 Subject: [PATCH 3/3] Fixes following PR review. --- lib/matplotlib/__init__.py | 34 +++++++++++++++++++++---------- lib/matplotlib/cbook/__init__.py | 2 +- lib/matplotlib/testing/compare.py | 1 - 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index 6ea313990433..07beae316f5a 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -387,7 +387,19 @@ def ge(self, level): def _logged_cached(fmt, func=None): - if func is None: + """ + Decorator that logs a function's return value, and memoizes that value. + + After :: + + @_logged_cached(fmt) + def func(): ... + + the first call to *func* will log its return value at the DEBUG level using + %-format string *fmt*, and memoize it; later calls to *func* will directly + return that value. + """ + if func is None: # Return the actual decorator. return functools.partial(_logged_cached, fmt) called = False @@ -569,7 +581,7 @@ def _get_xdg_config_dir(): `_. """ return (os.environ.get('XDG_CONFIG_HOME') - or (Path(get_home(), ".config") + or (str(Path(get_home(), ".config")) if get_home() else None)) @@ -581,21 +593,21 @@ def _get_xdg_cache_dir(): `_. """ return (os.environ.get('XDG_CACHE_HOME') - or (Path(get_home(), ".cache") + or (str(Path(get_home(), ".cache")) if get_home() else None)) def _get_config_or_cache_dir(xdg_base): configdir = os.environ.get('MPLCONFIGDIR') - configdir = ( - Path(configdir).resolve() - if configdir - else Path(xdg_base, "matplotlib") - if sys.platform.startswith(('linux', 'freebsd')) and xdg_base - else Path(get_home(), ".matplotlib") - if get_home() - else None) + if configdir: + configdir = Path(configdir).resolve() + elif sys.platform.startswith(('linux', 'freebsd')) and xdg_base: + configdir = Path(xdg_base, "matplotlib") + elif get_home(): + configdir = Path(get_home(), ".matplotlib") + else: + configdir = None if configdir: try: diff --git a/lib/matplotlib/cbook/__init__.py b/lib/matplotlib/cbook/__init__.py index 7abcac3e66d8..02f8c1ff6c63 100644 --- a/lib/matplotlib/cbook/__init__.py +++ b/lib/matplotlib/cbook/__init__.py @@ -1984,7 +1984,7 @@ class so far, an alias named ``get_alias`` will be defined; the same will can be used by `~.normalize_kwargs` (which assumes that higher priority aliases come last). """ - if cls is None: + if cls is None: # Return the actual class decorator. return functools.partial(_define_aliases, alias_d) def make_alias(name): # Enforce a closure over *name*. diff --git a/lib/matplotlib/testing/compare.py b/lib/matplotlib/testing/compare.py index 6413916916c2..d9528e3a4e2b 100644 --- a/lib/matplotlib/testing/compare.py +++ b/lib/matplotlib/testing/compare.py @@ -19,7 +19,6 @@ import matplotlib from matplotlib.testing.exceptions import ImageComparisonFailure from matplotlib import _png, cbook -from matplotlib import cbook __all__ = ['compare_float', 'compare_images', 'comparable_formats']