diff --git a/sphinx_gallery/docs_resolv.py b/sphinx_gallery/docs_resolv.py index 0f9943b68..cc31a56d6 100644 --- a/sphinx_gallery/docs_resolv.py +++ b/sphinx_gallery/docs_resolv.py @@ -13,8 +13,6 @@ import shelve import sys -from sphinx.util.console import fuchsia - # Try Python 2 first, otherwise load from Python 3 try: import cPickle as pickle @@ -29,6 +27,11 @@ from io import StringIO +from . import sphinx_compatibility + + +logger = sphinx_compatibility.getLogger('sphinx-gallery') + def _get_data(url): """Helper function to get data over http or from a local file""" @@ -348,15 +351,14 @@ def _embed_code_links(app, gallery_conf, gallery_dir): src_gallery_dir) except HTTPError as e: - print("The following HTTP Error has occurred:\n") - print(e.code) + logger.warning("The following HTTP Error has occurred: %d", e.code) except URLError as e: - print("\n...\n" - "Warning: Embedding the documentation hyperlinks requires " - "Internet access.\nPlease check your network connection.\n" - "Unable to continue embedding `{0}` links due to a URL " - "Error:\n".format(this_module)) - print(e.args) + logger.warning( + "Embedding the documentation hyperlinks requires Internet " + "access.\nPlease check your network connection.\nUnable to " + "continue embedding `%s` links due to a URL Error:\n%s", + this_module, + str(e.args)) html_gallery_dir = os.path.abspath(os.path.join(app.builder.outdir, gallery_dir)) @@ -370,8 +372,8 @@ def _embed_code_links(app, gallery_conf, gallery_dir): flat = [[dirpath, filename] for dirpath, _, filenames in os.walk(html_gallery_dir) for filename in filenames] - iterator = app.status_iterator( - flat, os.path.basename(html_gallery_dir), colorfunc=fuchsia, + iterator = sphinx_compatibility.status_iterator( + flat, gallery_dir, color='fuchsia', length=len(flat), stringify_func=lambda x: os.path.basename(x[1])) for dirpath, fname in iterator: full_fname = os.path.join(html_gallery_dir, dirpath, fname) @@ -400,8 +402,8 @@ def _embed_code_links(app, gallery_conf, gallery_dir): extra = e.code else: extra = e.reason - print("\n\t\tError resolving %s.%s: %r (%s)" - % (cobj['module'], cobj['name'], e, extra)) + logger.warning("Error resolving %s.%s: %r (%s)", + cobj['module'], cobj['name'], e, extra) continue if link is not None: @@ -451,7 +453,8 @@ def embed_code_links(app, exception): if app.builder.name not in ['html', 'readthedocs']: return - print('Embedding documentation hyperlinks in examples..') + logger.info('Embedding documentation hyperlinks in examples ...', + color='white') gallery_conf = app.config.sphinx_gallery_conf diff --git a/sphinx_gallery/gen_gallery.py b/sphinx_gallery/gen_gallery.py index 3cfb02857..f19cdb98a 100644 --- a/sphinx_gallery/gen_gallery.py +++ b/sphinx_gallery/gen_gallery.py @@ -16,6 +16,7 @@ import os from . import glr_path_static +from . import sphinx_compatibility from .gen_rst import generate_dir_rst, SPHX_GLR_SIG from .docs_resolv import embed_code_links from .downloads import generate_zipfiles @@ -41,6 +42,8 @@ 'expected_failing_examples': set(), } +logger = sphinx_compatibility.getLogger('sphinx-gallery') + def clean_gallery_out(build_dir): """Deletes images under the sphx_glr namespace in the build directory""" @@ -99,24 +102,28 @@ def parse_config(app): https://sphinx-gallery.readthedocs.io/en/latest/advanced_configuration.html#references-to-examples""" gallery_conf['backreferences_dir'] = gallery_conf['mod_example_dir'] - app.warn("Old configuration for backreferences detected \n" - "using the configuration variable `mod_example_dir`\n" - + backreferences_warning - + update_msg, prefix="DeprecationWarning: ") + logger.warning( + "Old configuration for backreferences detected \n" + "using the configuration variable `mod_example_dir`\n" + "%s%s", + backreferences_warning, + update_msg, + type=DeprecationWarning) elif gallery_conf['backreferences_dir'] is None: no_care_msg = """ If you don't care about this features set in your conf.py 'backreferences_dir': False\n""" - app.warn(backreferences_warning + no_care_msg) + logger.warning(backreferences_warning + no_care_msg) gallery_conf['backreferences_dir'] = os.path.join( 'modules', 'generated') - app.warn("using old default 'backreferences_dir':'{}'.\n" - " This will be disabled in future releases\n".format( - gallery_conf['backreferences_dir']), - prefix="DeprecationWarning: ") + logger.warning( + "Using old default 'backreferences_dir':'%s'.\n" + "This will be disabled in future releases\n", + gallery_conf['backreferences_dir'], + type=DeprecationWarning) # this assures I can call the config in other places app.config.sphinx_gallery_conf = gallery_conf @@ -150,7 +157,7 @@ def generate_gallery_rst(app): Start the sphinx-gallery configuration and recursively scan the examples directories in order to populate the examples gallery """ - print('Generating gallery') + logger.info('Generating gallery...', color='white') gallery_conf = parse_config(app) clean_gallery_out(app.builder.outdir) @@ -201,12 +208,12 @@ def generate_gallery_rst(app): fhindex.flush() if gallery_conf['plot_gallery']: - print("Computation time summary:") + logger.info("Computation time summary:", color='white') for time_elapsed, fname in sorted(computation_times)[::-1]: if time_elapsed is not None: - print("\t- %s : %.2g sec" % (fname, time_elapsed)) + logger.info("\t- %s : %.2g sec", fname, time_elapsed) else: - print("\t- %s : not run" % fname) + logger.info("\t- %s : not run", fname) def touch_empty_backreferences(app, what, name, obj, options, lines): @@ -248,13 +255,11 @@ def sumarize_failing_examples(app, exception): examples_expected_to_fail = failing_examples.intersection( expected_failing_examples) - expected_fail_msg = [] if examples_expected_to_fail: - expected_fail_msg.append("\n\nExamples failing as expected:") + logger.info("Examples failing as expected:", color='brown') for fail_example in examples_expected_to_fail: - expected_fail_msg.append(fail_example + ' failed leaving traceback:\n' + - gallery_conf['failing_examples'][fail_example] + '\n') - print("\n".join(expected_fail_msg)) + logger.info('%s failed leaving traceback:', fail_example) + logger.info(gallery_conf['failing_examples'][fail_example]) examples_not_expected_to_fail = failing_examples.difference( expected_failing_examples) @@ -288,6 +293,8 @@ def default_getter(conf): def setup(app): """Setup sphinx-gallery sphinx extension""" + sphinx_compatibility._app = app + app.add_config_value('sphinx_gallery_conf', DEFAULT_GALLERY_CONF, 'html') for key in ['plot_gallery', 'abort_on_example_error']: app.add_config_value(key, get_default_config_value(key), 'html') diff --git a/sphinx_gallery/gen_rst.py b/sphinx_gallery/gen_rst.py index c2a0b9554..b60c691c2 100644 --- a/sphinx_gallery/gen_rst.py +++ b/sphinx_gallery/gen_rst.py @@ -23,7 +23,6 @@ import subprocess import sys import traceback -import warnings # Try Python 2 first, otherwise load from Python 3 @@ -72,6 +71,7 @@ def prefixed_lines(): import matplotlib.pyplot as plt from . import glr_path_static +from . import sphinx_compatibility from .backreferences import write_backreferences, _thumbnail_div from .downloads import CODE_DOWNLOAD from .py_source_parser import (get_docstring_and_rest, @@ -85,28 +85,10 @@ def prefixed_lines(): basestring = str unicode = str +logger = sphinx_compatibility.getLogger('sphinx-gallery') -############################################################################### - - -class Tee(object): - """A tee object to redirect streams to multiple outputs""" - def __init__(self, file1, file2): - self.file1 = file1 - self.file2 = file2 - - def write(self, data): - self.file1.write(data) - self.file2.write(data) - - def flush(self): - self.file1.flush() - self.file2.flush() - - # When called from a local terminal seaborn needs it in Python3 - def isatty(self): - self.file1.isatty() +############################################################################### class MixedEncodingStringIO(StringIO): @@ -355,8 +337,8 @@ def scale_image(in_fname, out_fname, max_width, max_height): try: subprocess.call(["optipng", "-quiet", "-o", "9", out_fname]) except Exception: - warnings.warn('Install optipng to reduce the size of the \ - generated images') + logger.warning( + 'Install optipng to reduce the size of the generated images') def save_thumbnail(image_path_template, src_file, gallery_conf): @@ -392,11 +374,8 @@ def save_thumbnail(image_path_template, src_file, gallery_conf): def generate_dir_rst(src_dir, target_dir, gallery_conf, seen_backrefs): """Generate the gallery reStructuredText for an example directory""" if not os.path.exists(os.path.join(src_dir, 'README.txt')): - print(80 * '_') - print('Example directory %s does not have a README.txt file' % - src_dir) - print('Skipping this directory') - print(80 * '_') + logger.warning('Skipping example directory without a README.txt file', + location=src_dir) return "", [] # because string is an expected return type with open(os.path.join(src_dir, 'README.txt')) as fid: @@ -411,9 +390,13 @@ def generate_dir_rst(src_dir, target_dir, gallery_conf, seen_backrefs): entries_text = [] computation_times = [] build_target_dir = os.path.relpath(target_dir, gallery_conf['src_dir']) - for fname in sorted_listdir: - amount_of_code, time_elapsed = \ - generate_file_rst(fname, target_dir, src_dir, gallery_conf) + iterator = sphinx_compatibility.status_iterator( + sorted_listdir, + 'Generating gallery for %s ' % build_target_dir, + length=len(sorted_listdir)) + for fname in iterator: + amount_of_code, time_elapsed = generate_file_rst(fname, target_dir, + src_dir, gallery_conf) computation_times.append((time_elapsed, fname)) new_fname = os.path.join(src_dir, fname) intro = extract_intro(new_fname) @@ -442,7 +425,7 @@ def generate_dir_rst(src_dir, target_dir, gallery_conf, seen_backrefs): return fhindex, computation_times -def execute_code_block(code_block, example_globals, +def execute_code_block(src_file, code_block, example_globals, block_vars, gallery_conf): """Executes the code block of the example file""" time_elapsed = 0 @@ -462,9 +445,7 @@ def execute_code_block(code_block, example_globals, # First cd in the original example dir, so that any file # created by the example get created in this directory os.chdir(os.path.dirname(src_file)) - my_buffer = MixedEncodingStringIO() - my_stdout = Tee(sys.stdout, my_buffer) - sys.stdout = my_stdout + sys.stdout = my_stdout = MixedEncodingStringIO() t_start = time() # don't use unicode_literals at the top of this file or you get @@ -474,10 +455,11 @@ def execute_code_block(code_block, example_globals, sys.stdout = orig_stdout - my_stdout = my_buffer.getvalue().strip().expandtabs() - # raise RuntimeError + my_stdout = my_stdout.getvalue().strip().expandtabs() if my_stdout: stdout = CODE_OUTPUT.format(indent(my_stdout, u' ' * 4)) + logger.verbose('Output from %s', src_file, color='brown') + logger.verbose(my_stdout) os.chdir(cwd) images_rst, fig_num = save_figures(block_vars['image_path'], block_vars['fig_count'], gallery_conf) @@ -485,10 +467,8 @@ def execute_code_block(code_block, example_globals, except Exception: formatted_exception = traceback.format_exc() - fail_example_warning = 80 * '_' + '\n' + \ - '%s failed to execute correctly:' % src_file + \ - formatted_exception + 80 * '_' + '\n' - warnings.warn(fail_example_warning) + logger.warning('%s failed to execute correctly:%s', src_file, + formatted_exception) fig_num = 0 images_rst = codestr2rst(formatted_exception, lang='pytb') @@ -588,14 +568,11 @@ def generate_file_rst(fname, target_dir, src_dir, gallery_conf): time_elapsed = 0 block_vars = {'execute_script': execute_script, 'fig_count': 0, 'image_path': image_path_template, 'src_file': src_file} - if block_vars['execute_script']: - print('Executing file %s' % src_file) for blabel, bcontent in script_blocks: if blabel == 'code': - code_output, rtime = execute_code_block(bcontent, + code_output, rtime = execute_code_block(src_file, bcontent, example_globals, - block_vars, - gallery_conf) + block_vars, gallery_conf) time_elapsed += rtime @@ -636,6 +613,6 @@ def generate_file_rst(fname, target_dir, src_dir, gallery_conf): f.write(example_rst) if block_vars['execute_script']: - print("{0} ran in : {1:.2g} seconds\n".format(src_file, time_elapsed)) + logger.debug("%s ran in : %.2g seconds", src_file, time_elapsed) return amount_of_code, time_elapsed diff --git a/sphinx_gallery/sphinx_compatibility.py b/sphinx_gallery/sphinx_compatibility.py new file mode 100644 index 000000000..53b8306c8 --- /dev/null +++ b/sphinx_gallery/sphinx_compatibility.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +""" +Backwards-compatility shims for Sphinx +====================================== + +""" +from __future__ import division, absolute_import, print_function + +from distutils.version import LooseVersion + +import sphinx +import sphinx.util + + +# This gets set when the extension is initialized. +_app = None + + +def _app_get_logger(name): + class SphinxLoggerAdapter: + def _color_to_func(self, kwargs, default=''): + return getattr(sphinx.util.console, + kwargs.pop('color', default), + None) + + def error(self, msg, *args, **kwargs): + msg = msg % args + colorfunc = self._color_to_func(kwargs, default='red') + return _app.warn(colorfunc(msg), **kwargs) + + def critical(self, msg, *args, **kwargs): + msg = msg % args + colorfunc = self._color_to_func(kwargs, default='red') + return _app.warn(colorfunc(msg), **kwargs) + + def warning(self, msg, *args, **kwargs): + msg = msg % args + colorfunc = self._color_to_func(kwargs) + if colorfunc: + # colorfunc is a valid kwarg in 1.5, but not older, so we just + # apply it ourselves. + msg = colorfunc(msg) + return _app.warn(msg, **kwargs) + + def info(self, msg='', *args, **kwargs): + msg = msg % args + colorfunc = self._color_to_func(kwargs) + if colorfunc: + msg = colorfunc(msg) + return _app.info(msg, **kwargs) + + def verbose(self, msg, *args, **kwargs): + return _app.verbose(msg, *args, **kwargs) + + def debug(self, msg, *args, **kwargs): + return _app.debug(msg, *args, **kwargs) + + return SphinxLoggerAdapter() + + +def _app_status_iterator(iterable, summary, **kwargs): + global _app + + color = kwargs.pop('color', None) + if color is not None: + kwargs['colorfunc'] = getattr(sphinx.util.console, color) + + for item in _app.status_iterator(iterable, summary, **kwargs): + yield item + + +if LooseVersion(sphinx.__version__) >= '1.6': + getLogger = sphinx.util.logging.getLogger + status_iterator = sphinx.util.status_iterator +else: + getLogger = _app_get_logger + status_iterator = _app_status_iterator diff --git a/sphinx_gallery/tests/conftest.py b/sphinx_gallery/tests/conftest.py new file mode 100644 index 000000000..52ead110c --- /dev/null +++ b/sphinx_gallery/tests/conftest.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- +""" +Pytest fixtures +""" +from __future__ import division, absolute_import, print_function + +import collections +import logging + +import pytest + +import sphinx_gallery.docs_resolv +import sphinx_gallery.gen_gallery +import sphinx_gallery.gen_rst +from sphinx_gallery import sphinx_compatibility + + +Params = collections.namedtuple('Params', 'args kwargs') + + +class FakeSphinxApp: + def __init__(self): + self.calls = collections.defaultdict(list) + + def status_iterator(self, *args, **kwargs): + self.calls['status_iterator'].append(Params(args, kwargs)) + yield + + def warning(self, *args, **kwargs): + self.calls['warning'].append(Params(args, kwargs)) + + def warn(self, *args, **kwargs): + self.calls['warn'].append(Params(args, kwargs)) + + def info(self, *args, **kwargs): + self.calls['info'].append(Params(args, kwargs)) + + def verbose(self, *args, **kwargs): + self.calls['verbose'].append(Params(args, kwargs)) + + def debug(self, *args, **kwargs): + self.calls['debug'].append(Params(args, kwargs)) + + +@pytest.fixture +def fakesphinxapp(): + orig_app = sphinx_gallery.sphinx_compatibility._app + sphinx_gallery.sphinx_compatibility._app = app = FakeSphinxApp() + try: + yield app + finally: + sphinx_gallery.sphinx_compatibility._app = orig_app + + +@pytest.fixture +def log_collector(): + orig_dr_logger = sphinx_gallery.docs_resolv.logger + orig_gg_logger = sphinx_gallery.gen_gallery.logger + orig_gr_logger = sphinx_gallery.gen_rst.logger + app = FakeSphinxApp() + sphinx_gallery.docs_resolv.logger = app + sphinx_gallery.gen_gallery.logger = app + sphinx_gallery.gen_rst.logger = app + try: + yield app + finally: + sphinx_gallery.docs_resolv.logger = orig_dr_logger + sphinx_gallery.gen_gallery.logger = orig_gg_logger + sphinx_gallery.gen_rst.logger = orig_gr_logger diff --git a/sphinx_gallery/tests/test_gen_rst.py b/sphinx_gallery/tests/test_gen_rst.py index e9d176a28..18f2e9bfb 100644 --- a/sphinx_gallery/tests/test_gen_rst.py +++ b/sphinx_gallery/tests/test_gen_rst.py @@ -149,7 +149,7 @@ def build_test_configuration(**kwargs): return gallery_conf -def test_fail_example(): +def test_fail_example(log_collector): """Test that failing examples are only executed until failing block""" gallery_conf = build_test_configuration(filename_pattern='raise.py') @@ -161,11 +161,10 @@ def test_fail_example(): mode='w', encoding='utf-8') as f: f.write('\n'.join(failing_code)) - with warnings.catch_warnings(record=True) as w: - sg.generate_file_rst('raise.py', gallery_conf['gallery_dir'], - gallery_conf['examples_dir'], gallery_conf) - assert len(w) == 1 - assert 'not defined' in str(w[0].message) + sg.generate_file_rst('raise.py', gallery_conf['gallery_dir'], + gallery_conf['examples_dir'], gallery_conf) + assert len(log_collector.calls['warning']) == 1 + assert 'not defined' in log_collector.calls['warning'][0].args[2] # read rst file and check if it contains traceback output @@ -178,7 +177,7 @@ def test_fail_example(): raise ValueError('Did not stop executing script after error') -def test_pattern_matching(): +def test_pattern_matching(log_collector): """Test if only examples matching pattern are executed""" gallery_conf = build_test_configuration( diff --git a/sphinx_gallery/tests/test_sphinx_compatibility.py b/sphinx_gallery/tests/test_sphinx_compatibility.py new file mode 100644 index 000000000..9dba26c6e --- /dev/null +++ b/sphinx_gallery/tests/test_sphinx_compatibility.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +""" +Testing the Sphinx compatibility shims +""" +from __future__ import division, absolute_import, print_function + +import sphinx.util.console + +from sphinx_gallery import sphinx_compatibility + + +def test_status_iterator(fakesphinxapp): + for _ in sphinx_compatibility._app_status_iterator([1, 2, 3], + 'summary', + length=3): + pass + + assert len(fakesphinxapp.calls['status_iterator']) == 1 + call = fakesphinxapp.calls['status_iterator'][0] + assert call.args == ([1, 2, 3], 'summary') + assert 'color' not in call.kwargs + assert 'colorfunc' not in call.kwargs + assert call.kwargs['length'] == 3 + + +def test_status_iterator_color(fakesphinxapp): + for _ in sphinx_compatibility._app_status_iterator([1, 2, 3], + 'summary', + color='green', + length=3): + pass + + assert len(fakesphinxapp.calls['status_iterator']) == 1 + call = fakesphinxapp.calls['status_iterator'][0] + assert call.args == ([1, 2, 3], 'summary') + assert 'color' not in call.kwargs + assert call.kwargs['colorfunc'] == sphinx.util.console.green + assert call.kwargs['length'] == 3 + + +def test_get_logger(fakesphinxapp): + logger = sphinx_compatibility._app_get_logger('sphinx-gallery-tests') + logger.error('error') + logger.critical('critical') + logger.warning('warning 1') + logger.warning('warning 2', color='green') + logger.info('info 1') + logger.info('info 2', color='green') + logger.verbose('verbose') + logger.debug('debug') + + # Error + critical both go through warning: + assert len(fakesphinxapp.calls['warn']) == 4 + error, critical, warning1, warning2 = fakesphinxapp.calls['warn'] + assert error.args == (sphinx.util.console.red('error'), ) + assert error.kwargs == {} + assert critical.args == (sphinx.util.console.red('critical'), ) + assert critical.kwargs == {} + assert warning1.args == ('warning 1', ) + assert warning1.kwargs == {} + assert warning2.args == (sphinx.util.console.green('warning 2'), ) + assert warning2.kwargs == {} + + assert len(fakesphinxapp.calls['info']) == 2 + info1, info2 = fakesphinxapp.calls['info'] + assert info1.args == ('info 1', ) + assert info1.kwargs == {} + assert info2.args == (sphinx.util.console.green('info 2'), ) + assert info2.kwargs == {} + + assert len(fakesphinxapp.calls['verbose']) == 1 + assert fakesphinxapp.calls['verbose'][0].args == ('verbose', ) + assert fakesphinxapp.calls['verbose'][0].kwargs == {} + + assert len(fakesphinxapp.calls['debug']) == 1 + assert fakesphinxapp.calls['debug'][0].args == ('debug', ) + assert fakesphinxapp.calls['debug'][0].kwargs == {}