Skip to content
Merged
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
49 changes: 35 additions & 14 deletions leafmap/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -3538,21 +3538,26 @@ def get_local_tile_url(
for key in client_args:
kwargs[key] = client_args[key]

# Make it compatible with binder and JupyterHub
if os.environ.get("JUPYTERHUB_SERVICE_PREFIX") is not None:
os.environ["LOCALTILESERVER_CLIENT_PREFIX"] = (
f"{os.environ['JUPYTERHUB_SERVICE_PREFIX'].lstrip('/')}/proxy/{{port}}"
)
# Only override LOCALTILESERVER_CLIENT_PREFIX if it's unset
if os.environ.get("LOCALTILESERVER_CLIENT_PREFIX") is None:
# Make it compatible with binder and JupyterHub
if os.environ.get("JUPYTERHUB_SERVICE_PREFIX") is not None:
os.environ["LOCALTILESERVER_CLIENT_PREFIX"] = (
f"{os.environ['JUPYTERHUB_SERVICE_PREFIX'].strip('/')}/proxy/{{port}}"
)

if is_studio_lab():
os.environ["LOCALTILESERVER_CLIENT_PREFIX"] = (
f"studiolab/default/jupyter/proxy/{{port}}"
)
elif is_on_aws():
os.environ["LOCALTILESERVER_CLIENT_PREFIX"] = "proxy/{port}"
elif "prefix" in kwargs:
os.environ["LOCALTILESERVER_CLIENT_PREFIX"] = kwargs["prefix"]
kwargs.pop("prefix")
if is_studio_lab():
os.environ["LOCALTILESERVER_CLIENT_PREFIX"] = (
"studiolab/default/jupyter/proxy/{port}"
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
elif is_on_aws():
os.environ["LOCALTILESERVER_CLIENT_PREFIX"] = "proxy/{port}"

# The prefix kwarg has final precedence
if "prefix" in kwargs:
prefix = kwargs.pop("prefix")
if prefix is not None:
os.environ["LOCALTILESERVER_CLIENT_PREFIX"] = str(prefix)

from localtileserver import TileClient

Expand Down Expand Up @@ -3583,6 +3588,22 @@ def get_local_tile_url(
colormap = colormap.lower()

client = TileClient(source, port=port, **client_args)

# Route tile URLs through the jupyter-loopback comm bridge so that rasters
# render in webview-based frontends (VS Code, Colab, Solara, marimo, etc.)
# and in containerized/remote Jupyter environments where the browser cannot
# reach the jupyter-server origin directly. The `get_leaflet_tile_layer` and
# `get_folium_tile_layer` helpers used by `get_local_tile_layer` do this
# automatically, but a bare `TileClient` does not. See
# https://github.com/opengeos/leafmap/issues/1331
if hasattr(client, "enable_jupyter_loopback"):
try:
client.enable_jupyter_loopback()
except Exception as e:
warnings.warn(
f"Failed to enable jupyter loopback for the local tile client: {e}"
)

url = client.get_tile_url(
indexes=indexes,
colormap=colormap,
Expand Down
49 changes: 49 additions & 0 deletions tests/test_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,55 @@ def test_set_proxy_successful_request(self, mock_get):
self.assertEqual(os.environ["HTTPS_PROXY"], "http://192.168.0.1:8080")
mock_get.assert_called_once_with("https://google.com")

def _mock_localtileserver(self):
"""Return a fake ``localtileserver`` module with a mocked TileClient."""
fake_client = MagicMock()
fake_client.get_tile_url.return_value = (
"http://127.0.0.1:0/api/tiles/{z}/{x}/{y}.png"
)
fake_module = MagicMock()
fake_module.TileClient.return_value = fake_client
return fake_module, fake_client

@patch.dict(os.environ, {}, clear=True)
def test_get_local_tile_url_enables_jupyter_loopback(self):
"""enable_jupyter_loopback is invoked when the client supports it."""
fake_module, fake_client = self._mock_localtileserver()
with patch.dict("sys.modules", {"localtileserver": fake_module}):
url = get_local_tile_url("test.tif")
fake_client.enable_jupyter_loopback.assert_called_once()
self.assertEqual(url, "http://127.0.0.1:0/api/tiles/{z}/{x}/{y}.png")

@patch.dict(os.environ, {}, clear=True)
def test_get_local_tile_url_without_loopback_support(self):
"""A client lacking enable_jupyter_loopback must not raise."""
fake_client = MagicMock(spec=["get_tile_url"])
fake_client.get_tile_url.return_value = "http://tile"
fake_module = MagicMock()
fake_module.TileClient.return_value = fake_client
with patch.dict("sys.modules", {"localtileserver": fake_module}):
url = get_local_tile_url("test.tif")
self.assertFalse(hasattr(fake_client, "enable_jupyter_loopback"))
self.assertEqual(url, "http://tile")

@patch.dict(os.environ, {}, clear=True)
def test_get_local_tile_url_prefix_precedence(self):
"""The prefix kwarg takes final precedence over the env var."""
fake_module, _ = self._mock_localtileserver()
with patch.dict("sys.modules", {"localtileserver": fake_module}):
get_local_tile_url("test.tif", prefix="proxy/{port}")
self.assertEqual(os.environ["LOCALTILESERVER_CLIENT_PREFIX"], "proxy/{port}")

Comment on lines +110 to +117

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a test for prefix overriding an already-set env var.

test_get_local_tile_url_prefix_precedence clears the environment first, so it only proves prefix is applied when nothing was previously set — it doesn't verify the "final precedence" claim (explicit prefix overriding an existing LOCALTILESERVER_CLIENT_PREFIX value), which is the core behavior this PR's commit message calls out.

`@patch.dict`(
    os.environ, {"LOCALTILESERVER_CLIENT_PREFIX": "old/{port}"}, clear=True
)
def test_get_local_tile_url_prefix_overrides_existing_env(self):
    """An explicit prefix kwarg overrides an already-set env var."""
    fake_module, _ = self._mock_localtileserver()
    with patch.dict("sys.modules", {"localtileserver": fake_module}):
        get_local_tile_url("test.tif", prefix="proxy/{port}")
    self.assertEqual(os.environ["LOCALTILESERVER_CLIENT_PREFIX"], "proxy/{port}")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_common.py` around lines 110 - 117, The current test only covers
the case where LOCALTILESERVER_CLIENT_PREFIX is unset, so it does not verify
that get_local_tile_url’s prefix argument overrides an existing environment
value. Add a test alongside test_get_local_tile_url_prefix_precedence that
starts with LOCALTILESERVER_CLIENT_PREFIX already set, then calls
get_local_tile_url with an explicit prefix and asserts the env var ends up as
the passed prefix; use _mock_localtileserver and the get_local_tile_url helper
to locate the behavior.

@patch.dict(
os.environ, {"LOCALTILESERVER_CLIENT_PREFIX": "keep/{port}"}, clear=True
)
def test_get_local_tile_url_prefix_none_ignored(self):
"""prefix=None is ignored and does not overwrite an existing env var."""
fake_module, _ = self._mock_localtileserver()
with patch.dict("sys.modules", {"localtileserver": fake_module}):
get_local_tile_url("test.tif", prefix=None)
self.assertEqual(os.environ["LOCALTILESERVER_CLIENT_PREFIX"], "keep/{port}")

# def test_pmtile_metadata_validates_pmtiles_suffix(self):
# with self.assertRaises(ValueError) as cm:
# pmtiles_metadata("/some/path/to/pmtiles.pmtiles")
Expand Down
Loading