Skip to content

Commit 2f6adf9

Browse files
Merge pull request #9 from analysiscenter/minor_fix
Minor fixes
2 parents f8c9e6e + 1e41add commit 2f6adf9

File tree

3 files changed

+55
-26
lines changed

3 files changed

+55
-26
lines changed

nbtools/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44
from .run_notebook import run_notebook
55
from .pylint_notebook import pylint_notebook
66

7-
__version__ = '0.9.11'
7+
__version__ = '0.9.12'

nbtools/pylint_notebook.py

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
""" Functions for code quality control of Jupyter Notebooks. """
22
#pylint: disable=import-outside-toplevel
33
import os
4+
from io import StringIO
5+
from contextlib import redirect_stderr, redirect_stdout
46

57
from .core import StringWithDisabledRepr, get_notebook_path, notebook_to_script
68

@@ -101,7 +103,13 @@ def pylint_notebook(path=None, options=(), disable=(), enable=(), printer=print,
101103
pylint_params : dict
102104
Additional parameter of linting. Each is converted to a separate valid entry in the `pylintrc` file.
103105
"""
104-
from pylint import epylint as lint
106+
try:
107+
from pylint.lint import Run
108+
from pylint.reporters.text import TextReporter
109+
except ImportError as exception:
110+
raise ImportError('Install pylint') from exception
111+
112+
105113
path = path or get_notebook_path()
106114
if path is None:
107115
raise ValueError('Provide path to Jupyter Notebook or run `pylint_notebook` inside of it!')
@@ -117,25 +125,32 @@ def pylint_notebook(path=None, options=(), disable=(), enable=(), printer=print,
117125
pylintrc = generate_pylintrc(path_pylintrc, disable=disable, enable=enable, **pylint_params)
118126

119127
# Run pylint on script with pylintrc configuration
120-
pylint_cmdline = ' '.join([path_pylintrc, f'--rcfile {path_pylintrc}', *options])
121-
pylint_stdout, pylint_stderr = lint.py_run(pylint_cmdline, return_std=True)
128+
pylint_cmdline = [path_pylintrc, f'--rcfile={path_pylintrc}', *options]
129+
130+
# Run pylint and catch messagies
131+
with redirect_stdout(StringIO()) as pylint_stdout, redirect_stderr(StringIO()) as pylint_stderr:
132+
reporter = TextReporter(pylint_stdout)
133+
Run(pylint_cmdline, reporter=reporter, exit=False)
122134

123-
errors = pylint_stderr.getvalue()
124-
report = pylint_stdout.getvalue()
135+
report = pylint_stdout.getvalue()
136+
errors = pylint_stderr.getvalue()
125137

126138
# Prepare custom report
127139
output = []
128140

129141
for line in report.split('\n'):
142+
# error line is a str in the format:
143+
# "filename.py:line_num:position_num: error_code: error_text (error_name)"
130144
if 'rated' in line:
131145
output.insert(0, line.strip(' '))
132146
output.insert(1, '–' * (len(line) - 1))
133147

134148
elif path_script in line or script_name in line:
135149
line = line.replace(path_script, '__').replace(script_name, '__')
150+
line_semicolon_split = line.split(':')
136151

137152
# Locate the cell and line inside the cell
138-
code_line_number = int(line.split(':')[1])
153+
code_line_number = int(line_semicolon_split[1])
139154
for cell_number, cell_ranges in cell_line_numbers.items():
140155
if code_line_number in cell_ranges:
141156
cell_line_number = code_line_number - cell_ranges[0]
@@ -144,12 +159,13 @@ def pylint_notebook(path=None, options=(), disable=(), enable=(), printer=print,
144159
cell_number, cell_line_number = -1, code_line_number
145160

146161
# Find error_code and error_name: for example, `C0123` and `invalid-name`
147-
position_left = line.find('(') + 1
148-
position_right = line.find(')') - 1
149-
error_message = line[position_left : position_right]
150-
error_human_message = line[position_right + 2:]
151-
error_code, error_name, *_ = error_message.split(',')
152-
error_name = error_name.strip()
162+
error_code = line_semicolon_split[3].strip()
163+
164+
error_message = line_semicolon_split[4].strip()
165+
error_name_start = error_message.rfind('(')
166+
167+
error_human_message = error_message[:error_name_start].strip()
168+
error_name = error_message[error_name_start+1:-1]
153169

154170
# Make new message
155171
message = f'Cell {cell_number}:{cell_line_number}, code={error_code}, name={error_name}'

nbtools/run_notebook.py

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,24 +19,29 @@ def run_in_process(func):
1919
""" Decorator to run the `func` in a separated process for terminating all relevant processes properly. """
2020
@wraps(func)
2121
def _wrapper(*args, **kwargs):
22-
# pylint: disable=bare-except
23-
returned_value = Queue()
24-
kwargs = {**kwargs, 'returned_value': returned_value}
22+
# pylint: disable=broad-exception-caught, broad-exception-raised
23+
_output_queue = Queue()
24+
kwargs = {**kwargs, '_output_queue': _output_queue}
2525

2626
json_path = None
2727

28+
output = {'failed': True, 'traceback': ''}
29+
2830
try:
2931
process = Process(target=func, args=args, kwargs=kwargs)
3032
process.start()
3133

3234
path = args[0] if args else kwargs['path']
3335
json_path = f'{TMP_DIR}/{process.pid}.json'
36+
3437
with open(json_path, 'w', encoding='utf-8') as file:
3538
json.dump({'path': path}, file)
3639

37-
output = returned_value.get()
40+
output = _output_queue.get()
3841
process.join()
39-
except:
42+
except Exception as e:
43+
output = {'failed': True, 'traceback': e}
44+
4045
# Terminate all relevant processes when something went wrong, e.g. Keyboard Interrupt
4146
for child in psutil.Process(process.pid).children():
4247
if psutil.pid_exists(child.pid):
@@ -48,6 +53,9 @@ def _wrapper(*args, **kwargs):
4853
if json_path is not None and os.path.exists(json_path):
4954
os.remove(json_path)
5055

56+
if kwargs.get('raise_exception', False) and output['failed']:
57+
raise Exception(output['traceback'])
58+
5159
return output
5260
return _wrapper
5361

@@ -130,7 +138,7 @@ def run_notebook(path, inputs=None, outputs=None, inputs_pos=1, replace_inputs_p
130138
working_dir = './', execute_kwargs=None,
131139
out_path_db=None, out_path_ipynb=None, out_path_html=None, remove_db='always', add_timestamp=True,
132140
hide_code_cells=False, mask_extra_code=False, display_links=True,
133-
raise_exception=False, return_notebook=False, returned_value=None):
141+
raise_exception=False, return_notebook=False, _output_queue=None):
134142
""" Execute a Jupyter Notebook programmatically.
135143
Heavily inspired by https://github.com/tritemio/nbrun.
136144
@@ -190,6 +198,9 @@ def run_notebook(path, inputs=None, outputs=None, inputs_pos=1, replace_inputs_p
190198
Note, that database exists only if inputs and/or outputs are provided.
191199
execute_kwargs : dict, optional
192200
Parameters of `:class:ExecutePreprocessor`.
201+
For example, you can provide timeout, kernel_name, resources (such as metadata)
202+
and other `nbclient.client.NotebookClient` arguments from :ref:`the NotebookClient doc page
203+
<https://nbclient.readthedocs.io/en/latest/reference/nbclient.html#nbclient.client.NotebookClient>`.
193204
add_timestamp : bool, optional
194205
Whether to add a cell with execution information at the beginning of the saved notebook.
195206
hide_code_cells : bool, optional
@@ -203,7 +214,7 @@ def run_notebook(path, inputs=None, outputs=None, inputs_pos=1, replace_inputs_p
203214
Whether to re-raise exceptions from the notebook.
204215
return_notebook : bool, optional
205216
Whether to return the notebook object from this function.
206-
returned_value : None
217+
_output_queue : None
207218
Placeholder for the :func:`~.run_in_process` decorator to return this function result.
208219
209220
Returns
@@ -224,7 +235,7 @@ def run_notebook(path, inputs=None, outputs=None, inputs_pos=1, replace_inputs_p
224235
Executed notebook object.
225236
Note that this output is provided only if `return_notebook` is True.
226237
"""
227-
# pylint: disable=bare-except, lost-exception
238+
# pylint: disable=bare-except, lost-exception, return-in-finally
228239
import nbformat
229240
from jupyter_client.manager import KernelManager
230241
from nbconvert.preprocessors import ExecutePreprocessor
@@ -310,9 +321,6 @@ def run_notebook(path, inputs=None, outputs=None, inputs_pos=1, replace_inputs_p
310321
if outputs is not None:
311322
executor.kc = kernel_manager.client() # For compatibility with 5.x.x version of `nbconvert`
312323
executor.preprocess_cell(output_cell, {'metadata': {'path': working_dir}}, -1)
313-
314-
if raise_exception:
315-
raise
316324
finally:
317325
# Shutdown kernel
318326
kernel_manager.cleanup_resources()
@@ -340,6 +348,11 @@ def run_notebook(path, inputs=None, outputs=None, inputs_pos=1, replace_inputs_p
340348
# Prepare execution results: execution state, notebook outputs and error info (if exists)
341349
if failed:
342350
exec_res = {'failed': failed, 'failed cell number': error_cell_num, 'traceback': traceback_message}
351+
352+
# Re-raise exception if needed
353+
if raise_exception:
354+
_output_queue.put(exec_res)
355+
return None
343356
else:
344357
exec_res = {'failed': failed, 'failed cell number': None, 'traceback': ''}
345358

@@ -372,8 +385,8 @@ def run_notebook(path, inputs=None, outputs=None, inputs_pos=1, replace_inputs_p
372385
if return_notebook:
373386
exec_res['notebook'] = notebook
374387

375-
returned_value.put(exec_res) # return for parent process
376-
return
388+
_output_queue.put(exec_res) # return for parent process
389+
return None
377390

378391
# Mask functions for database operations cells
379392
def mask_inputs_reading(notebook, pos):

0 commit comments

Comments
 (0)