mirror of
https://github.com/hanzoai/jupyterlab-sos.git
synced 2026-10-30 18:33:08 +00:00
936 lines
33 KiB
Python
936 lines
33 KiB
Python
#!/usr/bin/env python3
|
|
#
|
|
# Copyright (c) Bo Peng and the University of Texas MD Anderson Cancer Center
|
|
# Distributed under the terms of the 3-clause BSD License.
|
|
|
|
#
|
|
# NOTE: for some namespace reason, this test can only be tested using
|
|
# nose.
|
|
|
|
import atexit
|
|
import os
|
|
import re
|
|
import time
|
|
#
|
|
#
|
|
from contextlib import contextmanager
|
|
from queue import Empty
|
|
from sys import platform
|
|
from textwrap import dedent
|
|
|
|
import pytest
|
|
from ipykernel.tests import utils as test_utils
|
|
from selenium.common.exceptions import NoSuchElementException
|
|
from selenium.webdriver import ActionChains
|
|
from selenium.webdriver.common.by import By
|
|
from selenium.webdriver.common.keys import Keys
|
|
from selenium.webdriver.remote.webelement import WebElement
|
|
from selenium.webdriver.support import expected_conditions as EC
|
|
from selenium.webdriver.support.ui import Select, WebDriverWait
|
|
|
|
pjoin = os.path.join
|
|
|
|
test_utils.TIMEOUT = 60
|
|
|
|
KM = None
|
|
KC = None
|
|
|
|
|
|
@contextmanager
|
|
def sos_kernel():
|
|
"""Context manager for the global kernel instance
|
|
Should be used for most kernel tests
|
|
Returns
|
|
-------
|
|
kernel_client: connected KernelClient instance
|
|
"""
|
|
yield start_sos_kernel()
|
|
|
|
|
|
def flush_channels(kc=None):
|
|
"""flush any messages waiting on the queue"""
|
|
|
|
if kc is None:
|
|
kc = KC
|
|
for channel in (kc.shell_channel, kc.iopub_channel):
|
|
while True:
|
|
try:
|
|
channel.get_msg(block=True, timeout=0.1)
|
|
except Empty:
|
|
break
|
|
# do not validate message because SoS has special sos_comm
|
|
# else:
|
|
# validate_message(msg)
|
|
|
|
|
|
def start_sos_kernel():
|
|
"""start the global kernel (if it isn't running) and return its client"""
|
|
global KM, KC
|
|
if KM is None:
|
|
KM, KC = test_utils.start_new_kernel(kernel_name='sos')
|
|
atexit.register(stop_sos_kernel)
|
|
else:
|
|
flush_channels(KC)
|
|
return KC
|
|
|
|
|
|
def stop_sos_kernel():
|
|
"""Stop the global shared kernel instance, if it exists"""
|
|
global KM, KC
|
|
KC.stop_channels()
|
|
KC = None
|
|
if KM is None:
|
|
return
|
|
KM.shutdown_kernel(now=False)
|
|
KM = None
|
|
|
|
|
|
def get_result(iopub):
|
|
"""retrieve result from an execution"""
|
|
result = None
|
|
while True:
|
|
msg = iopub.get_msg(block=True, timeout=1)
|
|
msg_type = msg['msg_type']
|
|
content = msg['content']
|
|
if msg_type == 'status' and content['execution_state'] == 'idle':
|
|
# idle message signals end of output
|
|
break
|
|
elif msg['msg_type'] == 'execute_result':
|
|
result = content['data']
|
|
elif msg['msg_type'] == 'display_data':
|
|
result = content['data']
|
|
else:
|
|
# other output, ignored
|
|
pass
|
|
# text/plain can have fronzen dict, this is ok,
|
|
from numpy import array, matrix, uint8
|
|
|
|
# suppress pyflakes warning
|
|
array
|
|
matrix
|
|
uint8
|
|
|
|
# it can also have dict_keys, we will have to redefine it
|
|
|
|
def dict_keys(args):
|
|
return args
|
|
|
|
if result is None:
|
|
return None
|
|
else:
|
|
return eval(result['text/plain'])
|
|
|
|
|
|
def get_display_data(iopub, data_type='text/plain'):
|
|
"""retrieve display_data from an execution from subkernel
|
|
because subkernel (for example irkernel) does not return
|
|
execution_result
|
|
"""
|
|
result = None
|
|
while True:
|
|
msg = iopub.get_msg(block=True, timeout=1)
|
|
msg_type = msg['msg_type']
|
|
content = msg['content']
|
|
if msg_type == 'status' and content['execution_state'] == 'idle':
|
|
# idle message signals end of output
|
|
break
|
|
elif msg['msg_type'] == 'display_data':
|
|
if isinstance(data_type, str):
|
|
if data_type in content['data']:
|
|
result = content['data'][data_type]
|
|
else:
|
|
for dt in data_type:
|
|
if dt in content['data']:
|
|
result = content['data'][dt]
|
|
# some early version of IRKernel still passes execute_result
|
|
elif msg['msg_type'] == 'execute_result':
|
|
result = content['data']['text/plain']
|
|
return result
|
|
|
|
|
|
def clear_channels(iopub):
|
|
"""assemble stdout/err from an execution"""
|
|
while True:
|
|
msg = iopub.get_msg(block=True, timeout=1)
|
|
msg_type = msg['msg_type']
|
|
content = msg['content']
|
|
if msg_type == 'status' and content['execution_state'] == 'idle':
|
|
# idle message signals end of output
|
|
break
|
|
|
|
|
|
def get_std_output(iopub):
|
|
'''Obtain stderr and remove some unnecessary warning from
|
|
https://github.com/jupyter/jupyter_client/pull/201#issuecomment-314269710'''
|
|
stdout, stderr = test_utils.assemble_output(iopub)
|
|
return stdout, '\n'.join([
|
|
x for x in stderr.splitlines() if 'sticky' not in x and
|
|
'RuntimeWarning' not in x and 'communicator' not in x
|
|
])
|
|
|
|
|
|
def wait_for_selector(browser,
|
|
selector,
|
|
timeout=60,
|
|
visible=False,
|
|
single=False):
|
|
wait = WebDriverWait(browser, timeout)
|
|
if single:
|
|
if visible:
|
|
conditional = EC.visibility_of_element_located
|
|
else:
|
|
conditional = EC.presence_of_element_located
|
|
else:
|
|
if visible:
|
|
conditional = EC.visibility_of_all_elements_located
|
|
else:
|
|
conditional = EC.presence_of_all_elements_located
|
|
return wait.until(conditional((By.CSS_SELECTOR, selector)))
|
|
|
|
|
|
def wait_for_tag(driver,
|
|
tag,
|
|
timeout=10,
|
|
visible=False,
|
|
single=False,
|
|
wait_for_n=1):
|
|
if wait_for_n > 1:
|
|
return _wait_for_multiple(driver, By.TAG_NAME, tag, timeout, wait_for_n,
|
|
visible)
|
|
return _wait_for(driver, By.TAG_NAME, tag, timeout, visible, single)
|
|
|
|
|
|
def _wait_for(driver,
|
|
locator_type,
|
|
locator,
|
|
timeout=10,
|
|
visible=False,
|
|
single=False):
|
|
"""Waits `timeout` seconds for the specified condition to be met. Condition is
|
|
met if any matching element is found. Returns located element(s) when found.
|
|
Args:
|
|
driver: Selenium web driver instance
|
|
locator_type: type of locator (e.g. By.CSS_SELECTOR or By.TAG_NAME)
|
|
locator: name of tag, class, etc. to wait for
|
|
timeout: how long to wait for presence/visibility of element
|
|
visible: if True, require that element is not only present, but visible
|
|
single: if True, return a single element, otherwise return a list of matching
|
|
elements
|
|
"""
|
|
wait = WebDriverWait(driver, timeout)
|
|
if single:
|
|
if visible:
|
|
conditional = EC.visibility_of_element_located
|
|
else:
|
|
conditional = EC.presence_of_element_located
|
|
else:
|
|
if visible:
|
|
conditional = EC.visibility_of_all_elements_located
|
|
else:
|
|
conditional = EC.presence_of_all_elements_located
|
|
return wait.until(conditional((locator_type, locator)))
|
|
|
|
|
|
def _wait_for_multiple(driver,
|
|
locator_type,
|
|
locator,
|
|
timeout,
|
|
wait_for_n,
|
|
visible=False):
|
|
"""Waits until `wait_for_n` matching elements to be present (or visible).
|
|
Returns located elements when found.
|
|
Args:
|
|
driver: Selenium web driver instance
|
|
locator_type: type of locator (e.g. By.CSS_SELECTOR or By.TAG_NAME)
|
|
locator: name of tag, class, etc. to wait for
|
|
timeout: how long to wait for presence/visibility of element
|
|
wait_for_n: wait until this number of matching elements are present/visible
|
|
visible: if True, require that elements are not only present, but visible
|
|
"""
|
|
wait = WebDriverWait(driver, timeout)
|
|
|
|
def multiple_found(driver):
|
|
elements = driver.find_elements(locator_type, locator)
|
|
if visible:
|
|
elements = [e for e in elements if e.is_displayed()]
|
|
if len(elements) < wait_for_n:
|
|
return False
|
|
return elements
|
|
|
|
return wait.until(multiple_found)
|
|
|
|
|
|
class CellTypeError(ValueError):
|
|
|
|
def __init__(self, message=""):
|
|
self.message = message
|
|
|
|
|
|
promise_js = """
|
|
var done = arguments[arguments.length - 1];
|
|
%s.then(
|
|
data => { done(["success", data]); },
|
|
error => { done(["error", error]); }
|
|
);
|
|
"""
|
|
|
|
|
|
def execute_promise(js, browser):
|
|
state, data = browser.execute_async_script(promise_js % js)
|
|
if state == 'success':
|
|
return data
|
|
raise Exception(data)
|
|
|
|
|
|
class Notebook:
|
|
|
|
def __init__(self, browser):
|
|
self.browser = browser
|
|
self._disable_autosave_and_onbeforeunload()
|
|
# with open('./test/jquery-3.4.1.min.js', 'r') as jquery_js:
|
|
# jquery=jquery_js.read()
|
|
# self.browser.execute_script(jquery)
|
|
wait_for_selector(
|
|
browser,
|
|
"div.jp-Notebook-cell",
|
|
timeout=10,
|
|
visible=False,
|
|
single=True)
|
|
# self.prompt_cell = list(
|
|
# self.browser.find_elements_by_xpath(
|
|
# "//*[@id='panel-wrapper']/div"))[-1]
|
|
|
|
def __len__(self):
|
|
return len(self.cells)
|
|
|
|
def __getitem__(self, key):
|
|
return self.cells[key]
|
|
|
|
def __setitem__(self, key, item):
|
|
if isinstance(key, int):
|
|
self.edit_cell(index=key, content=item, render=False)
|
|
# TODO: re-add slicing support, handle general python slicing behaviour
|
|
# includes: overwriting the entire self.cells object if you do
|
|
# self[:] = []
|
|
# elif isinstance(key, slice):
|
|
# indices = (self.index(cell) for cell in self[key])
|
|
# for k, v in zip(indices, item):
|
|
# self.edit_cell(index=k, content=v, render=False)
|
|
|
|
def __iter__(self):
|
|
return (cell for cell in self.cells)
|
|
|
|
@property
|
|
def body(self):
|
|
# return self.browser.find_element_by_tag_name("body")
|
|
return self.browser.find_element_by_xpath(".//*[@id='main']")
|
|
|
|
@property
|
|
def cells(self):
|
|
"""Gets all cells once they are visible.
|
|
"""
|
|
# For SOS note book, there are 2 extra cells, one is the selection box for kernel, the other is the preview panel
|
|
|
|
return list(
|
|
self.browser.find_elements_by_xpath(
|
|
".//div[contains(@class,'jp-Notebook-cell')]"))
|
|
# ".//div[contains(@class,'jp-InputArea-editor')]"))
|
|
|
|
@property
|
|
def panel_cells(self):
|
|
return list(
|
|
self.browser.find_elements_by_css_selector("div .jp-Console-cell"))
|
|
|
|
@property
|
|
def current_index(self):
|
|
return self.index(self.current_cell)
|
|
|
|
@property
|
|
def menu_buttons(self):
|
|
return self.browser.find_elements_by_css_selector("div .p-MenuBar li")
|
|
|
|
def index(self, cell):
|
|
return self.cells.index(cell)
|
|
|
|
def save(self, name=''):
|
|
if name:
|
|
self.browser.execute_script(
|
|
f"Jupyter.notebook.set_notebook_name(arguments[0])", name)
|
|
time.sleep(5)
|
|
return execute_promise('Jupyter.notebook.save_notebook()', self.browser)
|
|
|
|
#
|
|
# operation
|
|
#
|
|
|
|
def append_cell(self, *values, cell_type="code"):
|
|
for i, value in enumerate(values):
|
|
if isinstance(value, str):
|
|
self.add_cell(cell_type=cell_type, content=value)
|
|
else:
|
|
raise TypeError("Don't know how to add cell from %r" % value)
|
|
|
|
def add_cell(self, index=-1, cell_type="code", content=""):
|
|
self._focus_cell(index)
|
|
# self._to_command_mode()
|
|
|
|
addButton = self.browser.find_element_by_xpath(
|
|
'.//div[contains(@class,"jp-NotebookPanel-toolbar")]//button[contains(@title,"Insert a cell below")]'
|
|
)
|
|
ActionChains(self.browser).move_to_element(addButton).click().perform()
|
|
|
|
new_index = index + 1 if index >= 0 else index
|
|
|
|
# self.current_cell=self.cells[new_index]
|
|
self._focus_cell(new_index)
|
|
if content:
|
|
self.edit_cell(index=new_index, content=content)
|
|
if cell_type != 'code':
|
|
self._convert_cell_type(index=new_index, cell_type=cell_type)
|
|
|
|
def select_kernel(self, index=0, kernel_name="SoS", by_click=True):
|
|
self._focus_cell(index)
|
|
kernel_selector = 'option[value={}]'.format(kernel_name)
|
|
kernelList = self.current_cell.find_element_by_tag_name("select")
|
|
kernel = wait_for_selector(kernelList, kernel_selector, single=True)
|
|
if by_click:
|
|
kernel.click()
|
|
else:
|
|
self.edit_cell(
|
|
index=0, content="%use {}".format(kernel_name), render=True)
|
|
|
|
def edit_cell(self, cell=None, index=0, content="", render=False):
|
|
"""Set the contents of a cell to *content*, by cell object or by index
|
|
"""
|
|
if cell is not None:
|
|
index = self.index(cell)
|
|
# print("begin edit cell",index,dedent(content))
|
|
# Select & delete anything already in the cell
|
|
# ActionChains(self.browser).move_to_element(self.current_cell).send_keys(Keys.ENTER).perform()
|
|
ActionChains(self.browser).send_keys_to_element(self.current_cell,
|
|
Keys.DELETE).perform()
|
|
|
|
ActionChains(self.browser).move_to_element(self.current_cell).send_keys(
|
|
dedent(content)).perform()
|
|
# print("change content",index)
|
|
|
|
if render:
|
|
self.execute_cell(self.current_index)
|
|
|
|
#
|
|
# Get info
|
|
#
|
|
def get_kernel_list(self):
|
|
# kernelMenu = self.browser.find_element_by_id(
|
|
# "menu-change-kernel-submenu")
|
|
wait_for_selector(self.browser,
|
|
'div[title="Kernel Idle"]')
|
|
kernelMenu = self.cells[0].find_element_by_class_name(
|
|
"jp-CelllanguageDropDown")
|
|
kernelEntries = kernelMenu.find_elements_by_tag_name("option")
|
|
kernels = []
|
|
for kernelEntry in kernelEntries:
|
|
kernels.append(kernelEntry.text)
|
|
return kernels
|
|
|
|
def get_input_backgroundColor(self, index=0, in_console=False):
|
|
if in_console:
|
|
rgba = self.current_cell.find_element_by_class_name(
|
|
"jp-InputPrompt").value_of_css_property("background-color")
|
|
else:
|
|
self._focus_cell(index)
|
|
rgba = self.current_cell.find_element_by_class_name(
|
|
"jp-InputPrompt").value_of_css_property("background-color")
|
|
|
|
r, g, b, a = map(
|
|
int,
|
|
re.search(r'rgba\((\d+),\s*(\d+),\s*(\d+),\s*(\d+)', rgba).groups())
|
|
return [r, g, b]
|
|
|
|
def get_output_backgroundColor(self, index=0):
|
|
|
|
rgba = self.current_cell.find_element_by_class_name(
|
|
"jp-OutputPrompt").value_of_css_property("background-color")
|
|
r, g, b, a = map(
|
|
int,
|
|
re.search(r'rgba\((\d+),\s*(\d+),\s*(\d+),\s*(\d+)', rgba).groups())
|
|
return [r, g, b]
|
|
|
|
#
|
|
# Execution of cells
|
|
#
|
|
|
|
def click_on_run_menu(self, index, selector):
|
|
|
|
for button in self.menu_buttons:
|
|
if button.text == "Run":
|
|
if index != -1:
|
|
self._focus_cell(index)
|
|
ActionChains(
|
|
self.browser).move_to_element(button).click().perform()
|
|
# time.sleep(5)
|
|
runButton = self.browser.find_element_by_css_selector(selector)
|
|
ActionChains(
|
|
self.browser).move_to_element(runButton).click().perform()
|
|
|
|
def execute_cell(self,
|
|
cell_or_index=None,
|
|
expect_error=False,
|
|
in_console=False):
|
|
if not in_console:
|
|
if isinstance(cell_or_index, int):
|
|
index = cell_or_index
|
|
elif isinstance(cell_or_index, WebElement):
|
|
index = self.index(cell_or_index)
|
|
else:
|
|
raise TypeError(
|
|
"execute_cell only accepts a WebElement or an int")
|
|
self._focus_cell(index)
|
|
runButton = self.browser.find_element_by_xpath(
|
|
'.//div[contains(@class,"jp-NotebookPanel-toolbar")]//button[contains(@title,"Run the selected cells and advance")]'
|
|
)
|
|
ActionChains(
|
|
self.browser).move_to_element(runButton).click().perform()
|
|
# self.current_cell.send_keys(Keys.CONTROL, Keys.ENTER)
|
|
# ActionChains(self.browser).move_to_element(self.current_cell).click().send_keys(Keys.CONTROL, Keys.ENTER).perform()
|
|
self._wait_for_done(index, expect_error)
|
|
else:
|
|
print("run in console")
|
|
self.click_on_run_menu(
|
|
cell_or_index,
|
|
'div.p-MenuBar-menu [data-command="notebook:run-in-console"]')
|
|
|
|
def call(self, content="", kernel="SoS", expect_error=False):
|
|
'''
|
|
Append a codecell to the end of the notebook, with specified `content` and
|
|
`kernel`, execute it, waits for the completion of execution, and raise an
|
|
exception if there is any error (stderr message), unless `expect_error` is
|
|
set to `True`. This function returns the index of the cell, which can be
|
|
used to retrieve output. Note that the `content` will be automatically
|
|
dedented.
|
|
'''
|
|
# there will be at least a new cell from the new notebook.
|
|
index = len(self.cells)
|
|
self.add_cell(
|
|
index=index - 1, cell_type="code", content=dedent(content))
|
|
self.select_kernel(index=index, kernel_name=kernel, by_click=True)
|
|
self.execute_cell(cell_or_index=index, expect_error=expect_error)
|
|
return index
|
|
|
|
def check_output(self,
|
|
content='',
|
|
kernel="SoS",
|
|
expect_error=False,
|
|
selector=None,
|
|
attribute='src'):
|
|
'''
|
|
This function calls call and gets its output with get_cell_output.
|
|
'''
|
|
return self.get_cell_output(
|
|
self.call(content, kernel, expect_error),
|
|
selector=selector,
|
|
attribute=attribute)
|
|
|
|
#
|
|
# check output
|
|
#
|
|
|
|
def get_cell_output(self,
|
|
index=0,
|
|
in_console=False,
|
|
selector=None,
|
|
attribute='src'):
|
|
outputs = ""
|
|
if in_console:
|
|
wait_for_selector(self.browser, "div .jp-Console-cell")
|
|
outputs = self.browser.find_elements_by_css_selector(
|
|
"div .jp-Console-cell .jp-Cell-outputWrapper"
|
|
)[index].find_elements_by_css_selector("div .jp-OutputArea-output")
|
|
else:
|
|
outputs = self.cells[index].find_elements_by_css_selector(
|
|
".jp-OutputArea-output")
|
|
output_text = ""
|
|
has_error = False
|
|
for output in outputs:
|
|
if selector:
|
|
try:
|
|
# some div might not have img
|
|
elem = output.find_element_by_css_selector(selector)
|
|
output_text += elem.get_attribute(attribute) + '\n'
|
|
except NoSuchElementException:
|
|
pass
|
|
|
|
output_text += output.text + "\n"
|
|
# if "Out" in output_text:
|
|
# output_text = "".join(output_text.split(":")[1:])
|
|
|
|
return output_text.strip()
|
|
|
|
#
|
|
# For console panel
|
|
#
|
|
def is_console_panel_open(self):
|
|
try:
|
|
self.browser.find_element_by_css_selector("div .jp-ConsolePanel")
|
|
return True
|
|
except NoSuchElementException:
|
|
return False
|
|
|
|
def open_console(self):
|
|
if not self.is_console_panel_open():
|
|
ActionChains(self.browser).move_to_element(
|
|
self.body).context_click().send_keys(Keys.DOWN).send_keys(
|
|
Keys.DOWN).send_keys(Keys.DOWN).send_keys(
|
|
Keys.DOWN).send_keys(Keys.DOWN).send_keys(
|
|
Keys.DOWN).send_keys(Keys.DOWN).click().perform()
|
|
|
|
def toggle_console_panel(self):
|
|
if self.is_console_panel_open():
|
|
panelButton = self.browser.find_elements_by_css_selector(
|
|
"div .p-TabBar-tabCloseIcon")[-1]
|
|
ActionChains(
|
|
self.browser).move_to_element(panelButton).click().perform()
|
|
else:
|
|
ActionChains(self.browser).move_to_element(
|
|
self.body).context_click().send_keys(Keys.DOWN).send_keys(
|
|
Keys.DOWN).send_keys(Keys.DOWN).send_keys(
|
|
Keys.DOWN).send_keys(Keys.DOWN).send_keys(
|
|
Keys.DOWN).send_keys(Keys.DOWN).click().perform()
|
|
wait_for_selector(self.browser, "div .jp-ConsolePanel", timeout=120)
|
|
|
|
def edit_prompt_cell(self,
|
|
content,
|
|
kernel='SoS',
|
|
execute=False,
|
|
expect_error=False):
|
|
# # print("panel", self.prompt_cell.get_attribute("innerHTML"))
|
|
# self.browser.execute_script("window.my_panel.cell.set_text(" +
|
|
# repr(dedent(content)) + ")")
|
|
|
|
# # the div is not clickable so I use send_key to get around it
|
|
# self.prompt_cell.send_keys('\n')
|
|
wait_for_selector(self.browser, "div.jp-CodeConsole-input")
|
|
self.prompt_cell = self.browser.find_element_by_css_selector(
|
|
"div.jp-CodeConsole-input")
|
|
ActionChains(self.browser).move_to_element(
|
|
self.prompt_cell).click().send_keys(content).perform()
|
|
|
|
self.select_console_kernel(kernel)
|
|
# self.prompt_cell.find_element_by_css_selector('.CodeMirror').click()
|
|
|
|
if execute:
|
|
ActionChains(self.browser).move_to_element(
|
|
self.prompt_cell).click().perform()
|
|
self.click_on_run_menu(
|
|
-1, 'div.p-MenuBar-menu [data-command="runmenu:run"]')
|
|
self._wait_for_done(-1, expect_error=expect_error)
|
|
|
|
def send_keys_on_prompt_cell(self, *argv):
|
|
ActionChains(self.browser).move_to_element(
|
|
self.prompt_cell).click().send_keys(argv).perform()
|
|
|
|
def get_prompt_content(self):
|
|
# JS = 'return window.my_panel.cell.get_text();'
|
|
# return self.browser.execute_script(JS)
|
|
promptText = self.prompt_cell.find_element_by_class_name(
|
|
"jp-InputArea-editor").text
|
|
print(promptText)
|
|
return promptText
|
|
|
|
def select_console_kernel(self, kernel_name="SoS"):
|
|
kernel_selector = 'option[value={}]'.format(kernel_name)
|
|
kernelList = self.prompt_cell.find_element_by_tag_name("select")
|
|
kernel = wait_for_selector(kernelList, kernel_selector, single=True)
|
|
kernel.click()
|
|
|
|
@classmethod
|
|
def new_notebook(cls, browser, kernel_name='kernel-sos'):
|
|
with new_window(browser, selector=".cell"):
|
|
select_kernel(browser, kernel_name=kernel_name)
|
|
return cls(browser)
|
|
|
|
#
|
|
# PRIVATE FUNCTIONS
|
|
#
|
|
|
|
def _disable_autosave_and_onbeforeunload(self):
|
|
"""Disable request to save before closing window and autosave.
|
|
|
|
This is most easily done by using js directly.
|
|
"""
|
|
self.browser.execute_script("window.onbeforeunload = null;")
|
|
# self.browser.execute_script("Jupyter.notebook.set_autosave_interval(0)")
|
|
|
|
def _to_command_mode(self):
|
|
"""Changes us into command mode on currently focused cell
|
|
|
|
"""
|
|
# self.body.send_keys(Keys.ESCAPE)
|
|
ActionChains(self.browser).move_to_element(self.body).send_keys(
|
|
Keys.ESCAPE).perform()
|
|
|
|
# self.browser.execute_script(
|
|
# "return Jupyter.notebook.handle_command_mode("
|
|
# "Jupyter.notebook.get_cell("
|
|
# "Jupyter.notebook.get_edit_index()))")
|
|
|
|
def _focus_cell(self, index=0):
|
|
cell = self.cells[index]
|
|
ActionChains(self.browser).move_to_element(cell).click().perform()
|
|
self.current_cell = cell
|
|
# print("focus cell",index)
|
|
|
|
# print(self.current_cell)
|
|
|
|
def _convert_cell_type(self, index=0, cell_type="code"):
|
|
# TODO add check to see if it is already present
|
|
self._focus_cell(index)
|
|
cell = self.cells[index]
|
|
if cell_type == "markdown":
|
|
self.current_cell.send_keys("m")
|
|
elif cell_type == "raw":
|
|
self.current_cell.send_keys("r")
|
|
elif cell_type == "code":
|
|
self.current_cell.send_keys("y")
|
|
else:
|
|
raise CellTypeError(
|
|
("{} is not a valid cell type,"
|
|
"use 'code', 'markdown', or 'raw'").format(cell_type))
|
|
|
|
# self.wait_for_stale_cell(cell)
|
|
self._focus_cell(index)
|
|
return self.current_cell
|
|
|
|
def _wait_for_done(self, index, expect_error=False):
|
|
#
|
|
# index < 0 means console panel
|
|
while True:
|
|
# main notebook
|
|
if index >= 0:
|
|
prompt = self.cells[index].find_element_by_css_selector(
|
|
'.jp-InputArea-prompt').text
|
|
else:
|
|
if len(self.panel_cells) > 0:
|
|
prompt = self.panel_cells[-1].find_element_by_css_selector(
|
|
'.jp-InputArea-prompt').text
|
|
else:
|
|
prompt = ""
|
|
if '*' not in prompt:
|
|
break
|
|
else:
|
|
time.sleep(0.1)
|
|
# check if there is output
|
|
try:
|
|
# no output? OK.
|
|
# outputs = self.cells[index].find_elements_by_xpath(".//div[contains(@class,'jp-OutputArea-output')]")
|
|
outputs = self.cells[index].find_elements_by_css_selector(
|
|
".jp-OutputArea-output")
|
|
except NoSuchElementException:
|
|
return
|
|
#
|
|
has_error = False
|
|
for output in outputs:
|
|
try:
|
|
# errors = output.find_element_by_css_selector('.output_stderr')
|
|
mime = output.get_attribute('data-mime-type')
|
|
if mime == "application/vnd.jupyter.stderr":
|
|
if expect_error:
|
|
has_error = True
|
|
else:
|
|
raise ValueError(
|
|
f'Cell produces error message: {output.text}. Use expect_error=True to suppress this error if needed.'
|
|
)
|
|
except NoSuchElementException:
|
|
# if no error, ok
|
|
pass
|
|
#
|
|
if expect_error and not has_error:
|
|
raise ValueError(
|
|
'Expect an error message from cell output, none found.')
|
|
|
|
# def wait_for_output(self, index=0):
|
|
# time.sleep(10)
|
|
# return self.get_cell_output(index)
|
|
|
|
# def set_cell_metadata(self, index, key, value):
|
|
# JS = 'Jupyter.notebook.get_cell({}).metadata.{} = {}'.format(
|
|
# index, key, value)
|
|
# return self.browser.execute_script(JS)
|
|
|
|
# def get_cell_type(self, index=0):
|
|
# JS = 'return Jupyter.notebook.get_cell({}).cell_type'.format(index)
|
|
# return self.browser.execute_script(JS)
|
|
|
|
# def set_cell_input_prompt(self, index, prmpt_val):
|
|
# JS = 'Jupyter.notebook.get_cell({}).set_input_prompt({})'.format(
|
|
# index, prmpt_val)
|
|
# self.browser.execute_script(JS)
|
|
|
|
# def delete_cell(self, index):
|
|
# self._focus_cell(index)
|
|
# self._to_command_mode()
|
|
# self.current_cell.send_keys('dd')
|
|
|
|
# def add_markdown_cell(self, index=-1, content="", render=True):
|
|
# self.add_cell(index, cell_type="markdown")
|
|
# self.edit_cell(index=index, content=content, render=render)
|
|
|
|
# def extend(self, values):
|
|
# self.append_cell(*values)
|
|
|
|
# def run_all(self):
|
|
# for cell in self:
|
|
# self.execute_cell(cell)
|
|
|
|
# def trigger_keydown(self, keys):
|
|
# trigger_keystrokes(self.body, keys)
|
|
|
|
# def add_and_execute_cell(self, index=-1, cell_type="code", content=""):
|
|
# self.add_cell(index=index, cell_type=cell_type, content=content)
|
|
# self.execute_cell(index)
|
|
|
|
# def add_and_execute_cell_in_kernel(self, index=-1, cell_type="code", content="", kernel="SoS"):
|
|
# self.add_cell(index=index, cell_type=cell_type, content=content)
|
|
# self.select_kernel(index=index+1, kernel_name=kernel, by_click=True)
|
|
# self.execute_cell(cell_or_index=index+1)
|
|
|
|
# def select_cell_range(self, initial_index=0, final_index=0):
|
|
# self._focus_cell(initial_index)
|
|
# self._to_command_mode()
|
|
# for i in range(final_index - initial_index):
|
|
# shift(self.browser, 'j')
|
|
|
|
# def find_and_replace(self, index=0, find_txt='', replace_txt=''):
|
|
# self._focus_cell(index)
|
|
# self._to_command_mode()
|
|
# self.body.send_keys('f')
|
|
# wait_for_selector(self.browser, "#find-and-replace", single=True)
|
|
# self.browser.find_element_by_id("findreplace_allcells_btn").click()
|
|
# self.browser.find_element_by_id(
|
|
# "findreplace_find_inp").send_keys(find_txt)
|
|
# self.browser.find_element_by_id(
|
|
# "findreplace_replace_inp").send_keys(replace_txt)
|
|
# self.browser.find_element_by_id("findreplace_replaceall_btn").click()
|
|
|
|
# def wait_for_stale_cell(self, cell):
|
|
# """ This is needed to switch a cell's mode and refocus it, or to render it.
|
|
|
|
# Warning: there is currently no way to do this when changing between
|
|
# markdown and raw cells.
|
|
# """
|
|
# wait = WebDriverWait(self.browser, 10)
|
|
# element = wait.until(EC.staleness_of(cell))
|
|
|
|
# def get_cells_contents(self):
|
|
# JS = 'return Jupyter.notebook.get_cells().map(function(c) {return c.get_text();})'
|
|
# return self.browser.execute_script(JS)
|
|
|
|
# def get_cell_contents(self, index=0, selector='div .CodeMirror-code'):
|
|
# return self.cells[index].find_element_by_css_selector(selector).text
|
|
|
|
|
|
# def select_kernel(browser, kernel_name='kernel-sos'):
|
|
# """Clicks the "new" button and selects a kernel from the options.
|
|
# """
|
|
# wait = WebDriverWait(browser, 10)
|
|
# new_button = wait.until(
|
|
# EC.element_to_be_clickable((By.ID, "new-dropdown-button")))
|
|
# new_button.click()
|
|
# kernel_selector = '#{} a'.format(kernel_name)
|
|
# kernel = wait_for_selector(browser, kernel_selector, single=True)
|
|
# kernel.click()
|
|
|
|
|
|
def select_kernel(browser, kernel_name='kernel-sos'):
|
|
"""Clicks the "new" button and selects a kernel from the options.
|
|
"""
|
|
wait = WebDriverWait(browser, 10)
|
|
new_button = wait.until(
|
|
# EC.element_to_be_clickable((By.XPATH, "//img[@ src='http://localhost:8888/kernelspecs/sos/logo-64x64.png']/..")))
|
|
EC.element_to_be_clickable((
|
|
By.XPATH,
|
|
".//*[@data-category='Notebook']//div[contains(string(),'SoS')]/..")
|
|
))
|
|
# new_button.click()
|
|
browser.execute_script("arguments[0].click();", new_button)
|
|
|
|
# kernel_selector = '#{} a'.format(kernel_name)
|
|
# kernel = wait_for_selector(browser, kernel_selector, single=True)
|
|
# kernel.click()
|
|
|
|
|
|
@contextmanager
|
|
def new_window(browser, selector=None):
|
|
"""Contextmanager for switching to & waiting for a window created.
|
|
|
|
This context manager gives you the ability to create a new window inside
|
|
the created context and it will switch you to that new window.
|
|
|
|
If you know a CSS selector that can be expected to appear on the window,
|
|
then this utility can wait on that selector appearing on the page before
|
|
releasing the context.
|
|
|
|
Usage example:
|
|
|
|
from notebook.tests.selenium.utils import new_window, Notebook
|
|
|
|
⋮ # something that creates a browser object
|
|
|
|
with new_window(browser, selector=".cell"):
|
|
select_kernel(browser, kernel_name=kernel_name)
|
|
nb = Notebook(browser)
|
|
|
|
"""
|
|
initial_window_handles = browser.window_handles
|
|
try:
|
|
yield
|
|
new_window_handle = next(window for window in browser.window_handles
|
|
if window not in initial_window_handles)
|
|
except StopIteration:
|
|
return
|
|
browser.switch_to.window(new_window_handle)
|
|
if selector is not None:
|
|
wait_for_selector(browser, selector)
|
|
|
|
|
|
def shift(browser, k):
|
|
"""Send key combination Shift+(k)"""
|
|
trigger_keystrokes(browser, "shift-%s" % k)
|
|
|
|
|
|
def ctrl(browser, k):
|
|
"""Send key combination Ctrl+(k)"""
|
|
trigger_keystrokes(browser, "control-%s" % k)
|
|
|
|
|
|
def command(browser, k):
|
|
trigger_keystrokes(browser, "command-%s" % k)
|
|
|
|
|
|
def trigger_keystrokes(browser, *keys):
|
|
""" Send the keys in sequence to the browser.
|
|
Handles following key combinations
|
|
1. with modifiers eg. 'control-alt-a', 'shift-c'
|
|
2. just modifiers eg. 'alt', 'esc'
|
|
3. non-modifiers eg. 'abc'
|
|
Modifiers : http://seleniumhq.github.io/selenium/docs/api/py/webdriver/selenium.webdriver.common.keys.html
|
|
"""
|
|
for each_key_combination in keys:
|
|
keys = each_key_combination.split('-')
|
|
if len(keys) > 1: # key has modifiers eg. control, alt, shift
|
|
modifiers_keys = [getattr(Keys, x.upper()) for x in keys[:-1]]
|
|
ac = ActionChains(browser)
|
|
for i in modifiers_keys:
|
|
ac = ac.key_down(i)
|
|
ac.send_keys(keys[-1])
|
|
for i in modifiers_keys[::-1]:
|
|
ac = ac.key_up(i)
|
|
ac.perform()
|
|
else: # single key stroke. Check if modifier eg. "up"
|
|
browser.send_keys(getattr(Keys, keys[0].upper(), keys[0]))
|
|
|
|
|
|
@pytest.mark.usefixtures("notebook")
|
|
class NotebookTest:
|
|
pass
|