Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
fail-fast: false
matrix:
os: ['windows-latest', 'macOS-latest', 'ubuntu-latest']
python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13']
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14']
exclude:
- os: ubuntu-latest
python-version: '3.12'
Expand Down
2 changes: 1 addition & 1 deletion examples/dash_apps/01_minimal_global.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,4 @@ def plot_graph(n_clicks):

# --------------------------------- Running the app ---------------------------------
if __name__ == "__main__":
app.run_server(debug=True, port=9023)
app.run(debug=True, port=9023)
2 changes: 1 addition & 1 deletion examples/dash_apps/02_minimal_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,4 @@ def update_fig(relayoutdata: dict, fig: FigureResampler):

# --------------------------------- Running the app ---------------------------------
if __name__ == "__main__":
app.run_server(debug=True, port=9023)
app.run(debug=True, port=9023)
2 changes: 1 addition & 1 deletion examples/dash_apps/03_minimal_cache_dynamic.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,4 +115,4 @@ def update_fig(relayoutdata: dict, fig: FigureResampler):

# --------------------------------- Running the app ---------------------------------
if __name__ == "__main__":
app.run_server(debug=True, port=9023)
app.run(debug=True, port=9023)
2 changes: 1 addition & 1 deletion examples/dash_apps/04_minimal_cache_overview.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,4 +122,4 @@ def update_fig(relayoutdata: dict, fig: FigureResampler):

# --------------------------------- Running the app ---------------------------------
if __name__ == "__main__":
app.run_server(debug=True, port=9023, use_reloader=False)
app.run(debug=True, port=9023, use_reloader=False)
2 changes: 1 addition & 1 deletion examples/dash_apps/05_cache_overview_subplots.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,4 +157,4 @@ def update_fig(relayoutdata, fig: FigureResampler):

# --------------------------------- Running the app ---------------------------------
if __name__ == "__main__":
app.run_server(debug=True, port=9023, use_reloader=False)
app.run(debug=True, port=9023, use_reloader=False)
2 changes: 1 addition & 1 deletion examples/dash_apps/11_sine_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,4 +200,4 @@ def update_fig(relayoutdata: dict, fig: FigureResampler):

# --------------------------------- Running the app ---------------------------------
if __name__ == "__main__":
app.run_server(debug=True, port=9023, use_reloader=False)
app.run(debug=True, port=9023, use_reloader=False)
2 changes: 1 addition & 1 deletion examples/dash_apps/12_file_selector.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,4 +126,4 @@ def update_fig(relayoutdata: dict, fig: FigureResampler):

# --------------------------------- Running the app ---------------------------------
if __name__ == "__main__":
app.run_server(debug=True, port=9023, use_reloader=False)
app.run(debug=True, port=9023, use_reloader=False)
2 changes: 1 addition & 1 deletion examples/dash_apps/13_coarse_fine.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,4 +183,4 @@ def update_dynamic_fig(

# --------------------------------- Running the app ---------------------------------
if __name__ == "__main__":
app.run_server(debug=True, port=9023, use_reloader=False)
app.run(debug=True, port=9023, use_reloader=False)
5,366 changes: 3,272 additions & 2,094 deletions poetry.lock

Large diffs are not rendered by default.

20 changes: 13 additions & 7 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ classifiers = [
"License :: OSI Approved :: MIT License",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
Expand All @@ -34,7 +33,7 @@ classifiers = [
]

[tool.poetry.dependencies]
python = "^3.8"
python = "^3.9"
plotly = ">=5.5.0,<7.0.0"
dash = ">=2.11.0" # from dash 2.11, jupyter support is included
pandas = [
Expand All @@ -46,11 +45,11 @@ numpy = [
{ version = ">=1.24", python = ">=3.11,<3.13" },
{ version = ">=2.0", python = ">=3.13" }
]
orjson = "^3.10.0" # Faster json serialization (from 3.10 onwards f16 is supported)
orjson = ">=3.10.0" # Faster json serialization (from 3.10 onwards f16 is supported)
# Optional dependencies
Flask-Cors = { version = "^4.0.2", optional = true }
# Lock kaleido dependency until https://github.com/plotly/Kaleido/issues/156 is resolved
kaleido = {version = "0.2.1", optional = true}
kaleido = {version = ">=1.0.0", optional = true}
tsdownsample = ">=0.1.3"

[tool.poetry.extras]
Expand All @@ -60,7 +59,7 @@ inline_persistent = ["kaleido", "Flask-Cors", "ipython"]
[tool.poetry.group.dev.dependencies]
pytest = "^7.2.0"
pytest-cov = "^3.0.0"
selenium = "4.2.0"
selenium = "^4.2.0"
pytest-selenium = "^2.0.1"
blinker= "1.7.0" # we need version 1.7.0 (otherwise we get a blinker._saferef module not found error
selenium-wire = "^5.0"
Expand All @@ -70,7 +69,7 @@ pyarrow = [
]
ipywidgets = "^7.7.1" # needs to be v7 in order to support serialization
memory-profiler = "^0.60.0"
line-profiler = "^4.0"
line-profiler = ">=4.2.1"
ruff = ">=0.9.6"
black = "^24.3.0"
pytest-lazy-fixture = "^0.6.3"
Expand All @@ -84,10 +83,17 @@ mike = "^1.1.2"
mkdocs-material = "^9.6.18"
mkdocs-literate-nav = "^0.6.0"
mkdocs-section-index = "^0.3.5"
cffi = ">=1.16"
cffi = ">=1.17"
anywidget = "^0.9.13"

# Linting
py = "^1.11.0"
pytest-variables = ">=1.5.0,<2.0.0"
webdriver-manager = "^4.0.2"

[tool.pytest.ini_options]
testpaths = ["tests"]

[tool.ruff]
line-length = 88

Expand Down
65 changes: 49 additions & 16 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@
_nb_samples = 10_000
data_dir = "examples/data/"
headless = True
TESTING_LOCAL = False # SET THIS TO TRUE IF YOU ARE TESTING LOCALLY
# Automatically detect if running in CI (GitHub Actions, etc.)
TESTING_LOCAL = (
os.environ.get("CI") not in ("true", "1", "True")
and os.environ.get("GITHUB_ACTIONS") != "true"
)


@pytest.fixture
Expand All @@ -46,38 +50,67 @@ def pickle_figure():

@pytest.fixture
def driver():
import os
import shutil
import time

from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
from selenium.webdriver.chrome.service import Service
from seleniumwire import webdriver
from webdriver_manager.chrome import ChromeDriverManager

time.sleep(3)

options = Options()
d = DesiredCapabilities.CHROME
d["goog:loggingPrefs"] = {"browser": "ALL"}
# Set logging preferences via options (replaces DesiredCapabilities in Selenium 4.x)
options.set_capability("goog:loggingPrefs", {"browser": "ALL"})

if not TESTING_LOCAL:
# CI environment (GitHub Actions)
if headless:
options.add_argument("--headless")
options.add_argument("--no-sandbox")
options.add_argument("--disable-dev-shm-usage")
options.add_argument("--disable-gpu")
# options.add_argument("--no=sandbox")

driver = webdriver.Chrome(
options=options,
desired_capabilities=d,
)
# In CI, try ChromeDriver from PATH first (set by setup-chromedriver action)
# The setup-chromedriver action adds chromedriver to PATH
# Strategy: Try PATH first, then CHROMEDRIVER_PATH env var, then webdriver-manager
# Check if chromedriver is available in PATH
chromedriver_in_path = shutil.which("chromedriver")
if chromedriver_in_path:
# Use chromedriver from PATH (installed by setup-chromedriver action)
service = Service(chromedriver_in_path)
else:
# Try CHROMEDRIVER_PATH environment variable
chromedriver_path = os.environ.get("CHROMEDRIVER_PATH")
if chromedriver_path and os.path.exists(chromedriver_path):
service = Service(chromedriver_path)
else:
# Fall back to webdriver-manager to auto-download correct version
service = Service(ChromeDriverManager().install())
else:
# Local development environment
options.add_argument("--remote-debugging-port=9222")
driver = webdriver.Chrome(
options=options,
# executable_path="/home/jeroen/chromedriver",
# executable_path="/home/jonas/Documents/chromedriver-linux64/chromedriver",
desired_capabilities=d,
)
# driver = webdriver.Firefox(executable_path='/home/jonas/git/gIDLaB/plotly-dynamic-resampling/geckodriver')

# Try hardcoded path first (for backward compatibility)
hardcoded_path = "/home/jonas/Documents/chromedriver-linux64/chromedriver"
if os.path.exists(hardcoded_path):
try:
# Try the hardcoded path first
service = Service(executable_path=hardcoded_path)
# Test if it works by attempting to create driver (will raise if version mismatch)
test_driver = webdriver.Chrome(options=options, service=service)
test_driver.quit()
# If we get here, the hardcoded path works
except Exception:
# Version mismatch or other error, use webdriver-manager
service = Service(ChromeDriverManager().install())
else:
# Use webdriver-manager to auto-download correct version
service = Service(ChromeDriverManager().install())

driver = webdriver.Chrome(options=options, service=service)
return driver


Expand Down
86 changes: 75 additions & 11 deletions tests/test_figure_resampler_selenium.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import json
import multiprocessing
import platform
import time
Expand Down Expand Up @@ -75,7 +76,13 @@ def test_multiple_tz(driver, multiple_tz_figure):
time.sleep(1)
autoscale_requests = RequestParser.filter_callback_requests(fr.get_requests())
assert len(autoscale_requests) == 1
assert autoscale_requests[0].response.status_code == 204
try:
assert autoscale_requests[0].response.status_code == 204
except AssertionError:
# In a more recent dash version, this returns a 200 status with no data
assert autoscale_requests[0].response.status_code == 200
response_data = json.loads(autoscale_requests[0].response.body)
assert response_data.get("response") == {}

if len(driver.get_log("browser")) > 0: # Check no errors in the browser
for entry in driver.get_log("browser"):
Expand Down Expand Up @@ -159,7 +166,13 @@ def test_basic_example_gui(driver, example_figure):
time.sleep(1)
vertical_requests = RequestParser.filter_callback_requests(fr.get_requests())
assert len(vertical_requests) == 1
assert vertical_requests[0].response.status_code == 204
try:
assert vertical_requests[0].response.status_code == 204
except AssertionError:
# In a more recent dash version, this returns a 200 status with no data
assert vertical_requests[0].response.status_code == 200
response_data = json.loads(vertical_requests[0].response.body)
assert response_data.get("response") == {}

# we autoscale to the current front-end view, no updated dat will be sent from
# the server to the front-end, however, a callback will still be made, but
Expand All @@ -170,7 +183,12 @@ def test_basic_example_gui(driver, example_figure):
time.sleep(1)
autoscale_requests = RequestParser.filter_callback_requests(fr.get_requests())
assert len(autoscale_requests) == 1
assert autoscale_requests[0].response.status_code == 204
try:
assert autoscale_requests[0].response.status_code == 204
except AssertionError:
assert autoscale_requests[0].response.status_code == 200
response_data = json.loads(autoscale_requests[0].response.body)
assert response_data.get("response") == {}

# The reset axes autoscales AND resets tot he global data view -> all data
# will be updated.
Expand Down Expand Up @@ -273,7 +291,13 @@ def test_basic_example_gui_existing(driver, example_figure_fig):
time.sleep(1)
vertical_requests = RequestParser.filter_callback_requests(fr.get_requests())
assert len(vertical_requests) == 1
assert vertical_requests[0].response.status_code == 204
try:
assert vertical_requests[0].response.status_code == 204
except AssertionError:
# In a more recent dash version, this returns a 200 status with no data
assert vertical_requests[0].response.status_code == 200
response_data = json.loads(vertical_requests[0].response.body)
assert response_data.get("response") == {}

# we autoscale to the current front-end view, no updated dat will be sent from
# the server to the front-end, however, a callback will still be made, but
Expand All @@ -284,7 +308,12 @@ def test_basic_example_gui_existing(driver, example_figure_fig):
time.sleep(1)
autoscale_requests = RequestParser.filter_callback_requests(fr.get_requests())
assert len(autoscale_requests) == 1
assert autoscale_requests[0].response.status_code == 204
try:
assert autoscale_requests[0].response.status_code == 204
except AssertionError:
assert autoscale_requests[0].response.status_code == 200
response_data = json.loads(autoscale_requests[0].response.body)
assert response_data.get("response") == {}

# The reset axes autoscales AND resets tot he global data view -> all data
# will be updated.
Expand Down Expand Up @@ -395,7 +424,13 @@ def test_gsr_gui(driver, gsr_figure):
time.sleep(1)
vertical_requests = RequestParser.filter_callback_requests(fr.get_requests())
assert len(vertical_requests) == 1
assert vertical_requests[0].response.status_code == 204
try:
assert vertical_requests[0].response.status_code == 204
except AssertionError:
# In a more recent dash version, this returns a 200 status with no data
assert vertical_requests[0].response.status_code == 200
response_data = json.loads(vertical_requests[0].response.body)
assert response_data.get("response") == {}

# autoscale
# we autoscale to the current front-end view, no updated dat will be sent from
Expand All @@ -407,7 +442,12 @@ def test_gsr_gui(driver, gsr_figure):
time.sleep(1)
autoscale_requests = RequestParser.filter_callback_requests(fr.get_requests())
assert len(autoscale_requests) == 1
assert autoscale_requests[0].response.status_code == 204
try:
assert autoscale_requests[0].response.status_code == 204
except AssertionError:
assert autoscale_requests[0].response.status_code == 200
response_data = json.loads(autoscale_requests[0].response.body)
assert response_data.get("response") == {}

fr.reset_axes()
time.sleep(0.2)
Expand Down Expand Up @@ -460,14 +500,26 @@ def test_cat_gui(driver, cat_series_box_hist_figure):
time.sleep(1)
vertical_requests = RequestParser.filter_callback_requests(fr.get_requests())
assert len(vertical_requests) == 1
assert vertical_requests[0].response.status_code == 204
try:
assert vertical_requests[0].response.status_code == 204
except AssertionError:
# In a more recent dash version, this returns a 200 status with no data
assert vertical_requests[0].response.status_code == 200
response_data = json.loads(vertical_requests[0].response.body)
assert response_data.get("response") == {}

fr.clear_requests(sleep_time_s=1)
fr.autoscale()
time.sleep(1)
autoscale_requests = RequestParser.filter_callback_requests(fr.get_requests())
assert len(autoscale_requests) == 1
assert autoscale_requests[0].response.status_code == 204
try:
assert autoscale_requests[0].response.status_code == 204
except AssertionError:
# In a more recent dash version, this returns a 200 status with no data
assert autoscale_requests[0].response.status_code == 200
response_data = json.loads(autoscale_requests[0].response.body)
assert response_data.get("response") == {}

# Note: as there is only 1 hf-scatter-trace, the reset axes command will only
# update a single trace
Expand Down Expand Up @@ -573,7 +625,13 @@ def test_shared_hover_gui(driver, shared_hover_figure):
time.sleep(1)
autoscale_requests = RequestParser.filter_callback_requests(fr.get_requests())
assert len(autoscale_requests) == 1
assert autoscale_requests[0].response.status_code == 204
try:
assert autoscale_requests[0].response.status_code == 204
except AssertionError:
# In a more recent dash version, this returns a 200 status with no data
assert autoscale_requests[0].response.status_code == 200
response_data = json.loads(autoscale_requests[0].response.body)
assert response_data.get("response") == {}

if len(driver.get_log("browser")) > 0: # Check no errors in the browser
for entry in driver.get_log("browser"):
Expand Down Expand Up @@ -652,7 +710,13 @@ def test_multi_trace_go_figure(driver, multi_trace_go_figure):
time.sleep(3)
autoscale_requests = RequestParser.filter_callback_requests(fr.get_requests())
assert len(autoscale_requests) == 1
assert autoscale_requests[0].response.status_code == 204
try:
assert autoscale_requests[0].response.status_code == 204
except AssertionError:
# In a more recent dash version, this returns a 200 status with no data
assert autoscale_requests[0].response.status_code == 200
response_data = json.loads(autoscale_requests[0].response.body)
assert response_data.get("response") == {}

if len(driver.get_log("browser")) > 0: # Check no errors in the browser
for entry in driver.get_log("browser"):
Expand Down
Loading