Skip to content

Commit

Permalink
Initial support for Python.
Browse files Browse the repository at this point in the history
  • Loading branch information
filipstachura committed Nov 7, 2024
1 parent 401b3ac commit ce589f7
Show file tree
Hide file tree
Showing 6 changed files with 683 additions and 1 deletion.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@
.RData
.Ruserdata
/docs/
.pyc
__pycache__/
*.egg-info
12 changes: 11 additions & 1 deletion inst/www/shiny.router.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,22 @@ window.shinyrouter = function() {
}();

var switchUI = function(message) {
// In Shiny for R message can be string and path is send as string.
// In Shiny for Python meesage has to be an object. Path is send as field.
const path = typeof message === "string" ? message : message?.path;

var routes = $("#router-page-wrapper").find(".router");
var active_route = routes.filter(function() {
return $(this).data("path") == message;
return $(this).data("path") == path;
});
routes.addClass('router-hidden');
active_route.removeClass('router-hidden');
};

Shiny.addCustomMessageHandler("switch-ui", switchUI);

$(window).on("hashchange", function (e) {
Shiny.setInputValue("_clientdata_url_hash", window.location.hash);
return;
e;
});
10 changes: 10 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[project]
name = "shiny-router"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"pandas",
"shiny",
]
4 changes: 4 additions & 0 deletions src/shiny_router/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from .router import route_link, router_ui, route, router_server

def hello() -> str:
return "Hello from shiny-router!"
162 changes: 162 additions & 0 deletions src/shiny_router/router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
from shiny import ui, reactive
from htmltools import HTMLDependency
from pathlib import PurePath

log_msg = print
PAGE_404_ROUTE = "404"

def page404(page=None, message404=None):
if page is None:
# Return a default "Not found" message or a custom 404 message
return ui.div(
ui.h1(message404 if message404 else "Not found")
)
else:
# Return the provided page if available
return page


def cleanup_hashpath(hashpath):
# Check if already formatted correctly
if hashpath.startswith("#!/"):
return hashpath

# Remove any leading # or / characters
hashpath = hashpath.lstrip("#/")

# Add the correct hashbang format
return f"#!/{hashpath}"

def create_router_callback(root, routes=None):
def router_callback(input, output, session=None, **kwargs):
log_msg("Creating current_page reactive...")

input.shiny_router_page = reactive.value(dict(
path = root,
query = None,
unparsed = root
))
@reactive.effect
@reactive.event(input._clientdata_url_hash)
def _():
requested_path = input._clientdata_url_hash()
clean_path = requested_path[3:] if requested_path.startswith("#!/") else requested_path
# TODO: Below simplifies and is incorrect:
input.shiny_router_page.set(dict(
path = clean_path,
query = None,
unparsed = requested_path,
))

@reactive.effect
@reactive.event(input.shiny_router_page)
async def _():
page_path = input.shiny_router_page()
print("shiny router page changed")
print(page_path)
log_msg("shiny.router main output. path: ", page_path["path"])
await session.send_custom_message("switch-ui", page_path)

return router_callback


def router_server(input, output, session, root_page="/"):
# Create the router callback
router = create_router_callback(root_page)

# Invoke the router with the necessary environment inputs
router(input, output, session)


def route_link(path):
return f"./{cleanup_hashpath(path)}"

def attach_attribs(ui_element, path):
if isinstance(ui_element, ui.Tag):
ui_element.attrs['data-path'] = path
ui_element.attrs['class'] = f"router router-hidden {ui_element.attrs.get('class', '')}"
else:
container = ui.div(*ui_element)
container.attrs['data-path'] = path
container.attrs['class'] = "router router-hidden"
ui_element = container
return ui_element

def callback_mapping(path, ui, server=None):
if not callable(server):
server = lambda input, output, session, *args: None
# R version does wrapping here, we're trying to skip this in python.
# At least in the initial PoC.
ui = attach_attribs(ui, path)
return {'ui': ui, 'server': server}

def route(path, ui, server=None):
if server is not None:
print("Warning: 'server' argument in 'route' is deprecated.")
return {"path": path, "logic": callback_mapping(path, ui, server)}

def router_ui(default, *args, page_404=None):
routes = {**default, **{arg: callback_mapping(arg, ui) for arg, ui in args}}
root = list(default.keys())[0]
if '404' not in routes:
routes['404'] = route('404', page_404)
return {'root': root, 'routes': routes}


def router_ui_internal(router):
# Define paths to JavaScript and CSS files
js_file = "shiny.router.js"
css_file = "shiny.router.css"

pkg_dependency = HTMLDependency("shiny_router", "0.0.1",
source={
"package": "shiny_router",
"subdir": str(PurePath(__file__).parent.parent.parent / "inst" / "www"),
},
script={"src": js_file, "type": "module"},
stylesheet={"href": css_file}
)

# Create UI elements
return ui.TagList(
pkg_dependency,
ui.div(
[route["logic"]["ui"] for route in router["routes"]],
id="router-page-wrapper"
)
)

def router_ui(default, *args, page_404=None, env=None):
# Initialize page_404 if not provided
if page_404 is None:
page_404 = page404()

# Collect dynamic routes and ensure they are unnamed
paths = list(args)

# Set up routes by merging default and additional paths
routes = [default] + paths
root = default.get('path') if isinstance(default, dict) else default

# Add 404 route if not already defined
if PAGE_404_ROUTE not in [route.get('path') for route in routes]:
routes.append(route(PAGE_404_ROUTE, page_404))

router = {"root": root, "routes": routes}

# Determine input ID for routes
routes_input_id = "routes"
if env and 'ns' in env:
routes_input_id = env['ns'](routes_input_id)

# Prepare JavaScript to set routes dynamically
routes_names = ", ".join([f"'{route['path']}'" for route in routes if 'path' in route])

# Create HTML and script tags (similar to Shiny's tagList)
html_output = ui.TagList(
ui.tags.script(f"$(document).on('shiny:connected', function() {{Shiny.setInputValue('{routes_input_id}', [{routes_names}]);}});"),
router_ui_internal(router)
)

# Return HTML list for the UI (modify as per your web framework if needed)
return html_output
Loading

0 comments on commit ce589f7

Please sign in to comment.