From eaa120774ad2c56f58c7507c1c30bd540aa98aa5 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Sun, 12 Nov 2017 15:53:50 -0800 Subject: [PATCH] Some more fixes for the verbose -> logging switch. Technically, this PR changes the behavior of get_home(), get_cachedir(), etc. making them insensitive to changes in the relevant environment variables *after* matplotlib is imported. In practice I strongly doubt that we supported that use case to begin with (because the values are probably cached in other places); I am also happy with saying "don't do this" until someone brings up a good reason to change the cache dir in the middle of the program... --- doc/devel/contributing.rst | 21 +++++----- lib/matplotlib/__init__.py | 68 +++++++++++++------------------ lib/matplotlib/style/core.py | 2 +- lib/matplotlib/testing/compare.py | 5 +-- 4 files changed, 41 insertions(+), 55 deletions(-) diff --git a/doc/devel/contributing.rst b/doc/devel/contributing.rst index 3cecacb9330b..614c9503017b 100644 --- a/doc/devel/contributing.rst +++ b/doc/devel/contributing.rst @@ -472,19 +472,18 @@ Then they will receive messages like:: Which logging level to use? ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -There are five levels at which you can emit messages. -`logging.critical` and `logging.error` -are really only there for errors that will end the use of the library but -not kill the interpreter. `logging.warning` overlaps with the -``warnings`` library. The -`logging tutorial `_ -suggests that the difference -between `logging.warning` and `warnings.warn` is that -`warnings.warn` be used for things the user must change to stop -the warning, whereas `logging.warning` can be more persistent. +There are five levels at which you can emit messages. `logging.critical` and +`logging.error` are really only there for errors that will end the use of the +library but not kill the interpreter. `logging.warning` overlaps with the +`warnings` library. The `logging tutorial`_ suggests that the difference +between `logging.warning` and `warnings.warn` is that `warnings.warn` be used +for things the user must change to stop the warning, whereas `logging.warning` +can be more persistent. + +.. _logging tutorial: https://docs.python.org/3/howto/logging.html#logging-basic-tutorial By default, `logging` displays all log messages at levels higher than -`logging.WARNING` to `sys.stderr`. +``logging.WARNING`` to `sys.stderr`. Calls to `logging.info` are not displayed by default. They are for information that the user may want to know if the program behaves oddly. diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index a0685dd76e24..c0e54cb6c99f 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -396,31 +396,6 @@ def ge(self, level): return self.vald[self.level] >= self.vald[level] -def _wrap(fmt, func, level='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): - lvl = logging.getLevelName(level.upper()) - _log.log(lvl, fmt % ret) - spoke = True - if not wrapper._spoke: - wrapper._spoke = spoke - return ret - wrapper._spoke = False - wrapper.__doc__ = func.__doc__ - return wrapper - - def checkdep_dvipng(): try: s = subprocess.Popen([str('dvipng'), '-version'], @@ -589,7 +564,27 @@ def checkdep_usetex(s): return flag -def _get_home(): +def _log_and_cache_result(fmt, level="DEBUG", func=None): + """Decorator that logs & caches the result of a function with no arguments. + + The first time the decorated *func* is called, its result is logged at + level *level* using log format *fmt*, and cached. Later calls immediately + return the cached result without logging. + """ + if func is None: + return functools.partial(_log_and_cache_result, fmt, level) + + @functools.lru_cache(1) # Calls after the 1st return the cached result. + @functools.wraps(func) + def wrapper(): + retval = func() + _log.log(logging.getLevelName(level.upper()), fmt, retval) + return retval + return wrapper + + +@_log_and_cache_result("HOME=%s") +def get_home(): """Find user's home directory if possible. Otherwise, returns None. @@ -620,9 +615,6 @@ 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 @@ -684,7 +676,8 @@ def _get_config_or_cache_dir(xdg_base): return _create_tmp_config_dir() -def _get_configdir(): +@_log_and_cache_result("CONFIGDIR=%s") +def get_configdir(): """ Return the string representing the configuration directory. @@ -705,10 +698,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(): +@_log_and_cache_result("CACHEDIR=%s") +def get_cachedir(): """ Return the location of the cache directory. @@ -717,8 +709,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): @@ -770,14 +760,12 @@ def _get_data_path(): raise RuntimeError('Could not find the matplotlib data files') -def _get_data_path_cached(): +@_log_and_cache_result('rcParams["datapath"]=%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(): datapath = get_data_path() @@ -835,7 +823,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 593dd9dcb1cd..7cc498a000d2 100644 --- a/lib/matplotlib/style/core.py +++ b/lib/matplotlib/style/core.py @@ -28,7 +28,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 218ba33297fa..bfb0998a1323 100644 --- a/lib/matplotlib/testing/compare.py +++ b/lib/matplotlib/testing/compare.py @@ -23,7 +23,6 @@ from matplotlib.compat import subprocess from matplotlib.testing.exceptions import ImageComparisonFailure from matplotlib import _png -from matplotlib import _get_cachedir from matplotlib import cbook __all__ = ['compare_float', 'compare_images', 'comparable_formats'] @@ -82,7 +81,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') @@ -272,7 +271,7 @@ def convert(filename, cache): created file. If *cache* is True, the result of the conversion is cached in - `matplotlib._get_cachedir() + '/test_cache/'`. The caching is based + `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.