Skip to content

Instantly share code, notes, and snippets.

@jmchilton
Created December 19, 2025 16:35
Show Gist options
  • Select an option

  • Save jmchilton/7a1af593c45091d51ccc9b390d562079 to your computer and use it in GitHub Desktop.

Select an option

Save jmchilton/7a1af593c45091d51ccc9b390d562079 to your computer and use it in GitHub Desktop.
Add tool_shed_url config for explicit hostname setting
commit fa276e6532ba34939ce7d796370f679526e986e9
Author: John Chilton <jmchilton@gmail.com>
Date: Fri Dec 19 11:16:09 2025 -0500
Add tool_shed_url config for explicit hostname setting
Allows admins to set a fixed base URL for clone URLs and repository
hostnames, useful when behind proxies or needing a specific public URL.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
diff --git a/lib/galaxy/config/schemas/tool_shed_config_schema.yml b/lib/galaxy/config/schemas/tool_shed_config_schema.yml
index 0434319fd3..0725cd52c1 100644
--- a/lib/galaxy/config/schemas/tool_shed_config_schema.yml
+++ b/lib/galaxy/config/schemas/tool_shed_config_schema.yml
@@ -46,6 +46,16 @@ mapping:
Allow pushing directly to mercurial repositories directly
and without authentication.
+ tool_shed_url:
+ type: str
+ required: false
+ desc: |
+ The base URL of the Tool Shed server used for generating clone URLs
+ and repository hostnames. If set, this value will be used instead of
+ deriving the URL from the incoming request.
+
+ Example: https://toolshed.g2.bx.psu.edu
+
file_path:
type: str
default: database/community_files
diff --git a/lib/tool_shed/context.py b/lib/tool_shed/context.py
index b936a7b7bf..051e7929f7 100644
--- a/lib/tool_shed/context.py
+++ b/lib/tool_shed/context.py
@@ -148,6 +148,11 @@ class SessionRequestContextImpl(SessionRequestContext):
@property
def repositories_hostname(self) -> str:
+ # Use configured tool_shed_url if available
+ tool_shed_url = self.app.config.tool_shed_url
+ if tool_shed_url:
+ return tool_shed_url.rstrip("/")
+ # Fall back to request-based URL
return str(self.request.base).rstrip("/")
@property
diff --git a/lib/tool_shed/webapp/buildapp.py b/lib/tool_shed/webapp/buildapp.py
index 16b47bc450..752f9701f5 100644
--- a/lib/tool_shed/webapp/buildapp.py
+++ b/lib/tool_shed/webapp/buildapp.py
@@ -34,6 +34,11 @@ log = logging.getLogger(__name__)
class ToolShedGalaxyWebTransaction(GalaxyWebTransaction):
@property
def repositories_hostname(self) -> str:
+ # Use configured tool_shed_url if available
+ tool_shed_url = self.app.config.tool_shed_url
+ if tool_shed_url:
+ return tool_shed_url.rstrip("/")
+ # Fall back to request-based URL
return url_for("/", qualified=True).rstrip("/")
def get_or_create_default_history(self):

Add tool_shed_url Config for Explicit Hostname Setting

Overview

Add a new config variable tool_shed_url to allow administrators to explicitly set the Tool Shed's base URL. Modify repositories_hostname to use this config variable when set, falling back to request-based URL when not configured.

Problem

Currently, repositories_hostname uses the incoming request to dynamically generate the hostname. This can cause issues with clone URL generation when the Tool Shed is behind a proxy or needs a specific public URL that differs from the request URL.

Solution

  1. Add optional tool_shed_url config variable to Tool Shed schema
  2. Modify repositories_hostname property in both SessionRequestContextImpl and ToolShedGalaxyWebTransaction to prefer configured URL
  3. Maintain backward compatibility - falls back to request-based URL when not set

Files Changed

Implementation (commit fa276e6532)

  • lib/galaxy/config/schemas/tool_shed_config_schema.yml - New config option
  • lib/tool_shed/context.py - Updated SessionRequestContextImpl.repositories_hostname
  • lib/tool_shed/webapp/buildapp.py - Updated ToolShedGalaxyWebTransaction.repositories_hostname

Tests (new file)

  • test/unit/tool_shed/test_context.py - 13 unit tests covering both context implementations

Test Summary

TestSessionRequestContextImplRepositoriesHostname (6 tests):

  • Config URL used when set
  • Trailing slash stripped from config
  • Falls back to request.base when not configured
  • Trailing slash stripped from request.base
  • Empty string config falls back to request
  • Config URL with port works

TestToolShedGalaxyWebTransactionRepositoriesHostname (7 tests):

  • Config URL used when set
  • Trailing slash stripped from config
  • Falls back to url_for when not configured (mocked)
  • Trailing slash stripped from url_for
  • Empty string config falls back to url_for
  • Config URL with port works
  • url_for not called when config is set

Usage

# tool_shed.yml
tool_shed:
  tool_shed_url: https://toolshed.g2.bx.psu.edu
"""Tests for tool_shed_url config and repositories_hostname property."""
from unittest import mock
import pytest
from tool_shed.context import SessionRequestContextImpl
from tool_shed.webapp.buildapp import ToolShedGalaxyWebTransaction
from ._util import TestToolShedApp
class ToolShedConfigWithUrl:
"""Extended config that supports tool_shed_url."""
def __init__(self, base_config, tool_shed_url=None):
self._base = base_config
self._tool_shed_url = tool_shed_url
@property
def tool_shed_url(self):
return self._tool_shed_url
def __getattr__(self, name):
return getattr(self._base, name)
class MockRequest:
"""Mock request with configurable base URL."""
def __init__(self, base="http://request-based.example.com/"):
self._base = base
@property
def base(self):
return self._base
@property
def host(self):
return "request-based.example.com"
class MockResponse:
"""Minimal mock response."""
headers = {}
@pytest.fixture
def shed_app_with_url():
"""Factory fixture that creates app with optional tool_shed_url config."""
def _create(tool_shed_url=None):
app = TestToolShedApp()
app.config = ToolShedConfigWithUrl(app.config, tool_shed_url)
return app
return _create
class TestSessionRequestContextImplRepositoriesHostname:
"""Tests for SessionRequestContextImpl.repositories_hostname."""
def test_returns_config_url_when_set(self, shed_app_with_url):
"""When tool_shed_url is configured, use it."""
app = shed_app_with_url("https://toolshed.example.com")
request = MockRequest()
response = MockResponse()
ctx = SessionRequestContextImpl(app, request, response)
assert ctx.repositories_hostname == "https://toolshed.example.com"
def test_strips_trailing_slash_from_config_url(self, shed_app_with_url):
"""Config URL trailing slashes are stripped."""
app = shed_app_with_url("https://toolshed.example.com/")
request = MockRequest()
response = MockResponse()
ctx = SessionRequestContextImpl(app, request, response)
assert ctx.repositories_hostname == "https://toolshed.example.com"
def test_falls_back_to_request_base_when_not_configured(self, shed_app_with_url):
"""When tool_shed_url is not set, fall back to request.base."""
app = shed_app_with_url(None)
request = MockRequest("http://from-request.example.com/")
response = MockResponse()
ctx = SessionRequestContextImpl(app, request, response)
assert ctx.repositories_hostname == "http://from-request.example.com"
def test_strips_trailing_slash_from_request_base(self, shed_app_with_url):
"""Request base trailing slashes are stripped in fallback."""
app = shed_app_with_url(None)
request = MockRequest("http://from-request.example.com/")
response = MockResponse()
ctx = SessionRequestContextImpl(app, request, response)
assert ctx.repositories_hostname == "http://from-request.example.com"
def test_empty_string_config_falls_back_to_request(self, shed_app_with_url):
"""Empty string config is falsy, falls back to request."""
app = shed_app_with_url("")
request = MockRequest("http://fallback.example.com/")
response = MockResponse()
ctx = SessionRequestContextImpl(app, request, response)
assert ctx.repositories_hostname == "http://fallback.example.com"
def test_config_url_with_port(self, shed_app_with_url):
"""Config URL with port number works correctly."""
app = shed_app_with_url("https://toolshed.example.com:8080")
request = MockRequest()
response = MockResponse()
ctx = SessionRequestContextImpl(app, request, response)
assert ctx.repositories_hostname == "https://toolshed.example.com:8080"
class TestToolShedGalaxyWebTransactionRepositoriesHostname:
"""Tests for ToolShedGalaxyWebTransaction.repositories_hostname."""
def test_returns_config_url_when_set(self, shed_app_with_url):
"""When tool_shed_url is configured, use it instead of url_for."""
app = shed_app_with_url("https://toolshed.example.com")
# Create a minimal mock transaction
trans = mock.Mock(spec=ToolShedGalaxyWebTransaction)
trans.app = app
# Call the actual property implementation
result = ToolShedGalaxyWebTransaction.repositories_hostname.fget(trans)
assert result == "https://toolshed.example.com"
def test_strips_trailing_slash_from_config_url(self, shed_app_with_url):
"""Config URL trailing slashes are stripped."""
app = shed_app_with_url("https://toolshed.example.com/")
trans = mock.Mock(spec=ToolShedGalaxyWebTransaction)
trans.app = app
result = ToolShedGalaxyWebTransaction.repositories_hostname.fget(trans)
assert result == "https://toolshed.example.com"
@mock.patch("tool_shed.webapp.buildapp.url_for")
def test_falls_back_to_url_for_when_not_configured(self, mock_url_for, shed_app_with_url):
"""When tool_shed_url is not set, fall back to url_for."""
app = shed_app_with_url(None)
mock_url_for.return_value = "http://url-for-result.example.com/"
trans = mock.Mock(spec=ToolShedGalaxyWebTransaction)
trans.app = app
result = ToolShedGalaxyWebTransaction.repositories_hostname.fget(trans)
assert result == "http://url-for-result.example.com"
mock_url_for.assert_called_once_with("/", qualified=True)
@mock.patch("tool_shed.webapp.buildapp.url_for")
def test_strips_trailing_slash_from_url_for(self, mock_url_for, shed_app_with_url):
"""url_for result trailing slashes are stripped."""
app = shed_app_with_url(None)
mock_url_for.return_value = "http://url-for-result.example.com/"
trans = mock.Mock(spec=ToolShedGalaxyWebTransaction)
trans.app = app
result = ToolShedGalaxyWebTransaction.repositories_hostname.fget(trans)
assert result == "http://url-for-result.example.com"
@mock.patch("tool_shed.webapp.buildapp.url_for")
def test_empty_string_config_falls_back_to_url_for(self, mock_url_for, shed_app_with_url):
"""Empty string config is falsy, falls back to url_for."""
app = shed_app_with_url("")
mock_url_for.return_value = "http://fallback.example.com/"
trans = mock.Mock(spec=ToolShedGalaxyWebTransaction)
trans.app = app
result = ToolShedGalaxyWebTransaction.repositories_hostname.fget(trans)
assert result == "http://fallback.example.com"
mock_url_for.assert_called_once()
def test_config_url_with_port(self, shed_app_with_url):
"""Config URL with port number works correctly."""
app = shed_app_with_url("https://toolshed.example.com:8080")
trans = mock.Mock(spec=ToolShedGalaxyWebTransaction)
trans.app = app
result = ToolShedGalaxyWebTransaction.repositories_hostname.fget(trans)
assert result == "https://toolshed.example.com:8080"
@mock.patch("tool_shed.webapp.buildapp.url_for")
def test_url_for_not_called_when_config_set(self, mock_url_for, shed_app_with_url):
"""When config is set, url_for should not be called."""
app = shed_app_with_url("https://configured.example.com")
trans = mock.Mock(spec=ToolShedGalaxyWebTransaction)
trans.app = app
ToolShedGalaxyWebTransaction.repositories_hostname.fget(trans)
mock_url_for.assert_not_called()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment