Open
Conversation
Proof-of-concept, Shiny-inspired runner: "tethys run app.py" serves a single-file component app with no portal configuration, no database setup, no pip install, and no login. It generates an isolated TETHYS_HOME (~/.tethys/express/<pkg>_<hash>/) containing a portal config (single-app mode + open portal) and a throwaway SQLite database, grafts the app file into the tethysapp namespace via sys.modules, and launches the standard development server. - tethys_cli/run_commands.py: new "run" subcommand (-p/--port, --host, --no-browser, --no-reload, --clean) - tethys_apps/base/express.py: express loader + metadata synthesis (package/name/root_url/index derived from the file when omitted) - tethys_apps/harvester.py: include the express app during harvest - tethys_apps/base/component_base.py: __init_subclass__ hook for express metadata; fix auto nav links in single-app mode (/apps/<url>/ 404s when MULTIPLE_APP_MODE=False) - tethys_apps/utilities.py: catch sqlite OperationalError in get_configured_standalone_app (fresh single-app portals crashed during migrate when reactpy_django imports the URLconf) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- docs/tethys_cli/run.rst: full reference page (quick start, how it works, auto-generated arguments via sphinx-argparse, examples) - docs/tethys_cli.rst: add run to the CLI toctree - docs/whats_new.rst: release note entry for express mode - docs/tethys_sdk/components.rst: tip cross-referencing tethys run from the component app.py docs Docs build verified locally with sphinx (no warnings from these files). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot
AI
changed the title
[WIP] Polish PR #1287 for tethys run express mode
Polish Sep 11, 2026
tethys run express mode for safer defaults and clearer validation
…/polish-1287-tethys-run # Conflicts: # docs/whats_new.rst Co-authored-by: swainn <5123221+swainn@users.noreply.github.com>
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved package/path and host-exposure findings, plus encoding and regression-test gaps, block approval.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Polishes tethys run express mode for Component Apps with safer host handling, stronger package validation, robust loading, and focused documentation/tests.
Changes:
- Hardens express CLI setup and runtime safety.
- Improves synthetic app discovery, metadata, and routing.
- Adds focused tests and updates documentation and release notes.
File summaries
| File | Reviewed changes | Final review notes |
|---|---|---|
tethys_cli/run_commands.py |
Express CLI workflow and server startup | Critical (2 votes, line 103): validate explicit packages before path construction. Critical (3 votes, line 141): warn for non-loopback hosts. |
tethys_cli/__init__.py |
Registers the run command |
— |
tethys_apps/utilities.py |
Handles SQLite startup errors | Moderate (3 votes, line 685): add OperationalError regression coverage. |
tethys_apps/harvester.py |
Harvests express apps | — |
tethys_apps/base/express.py |
Loads and synthesizes express apps | Moderate (3 votes, lines 45/198): read source explicitly as UTF-8. Moderate (1 vote, line 77): validate normalized identifiers. Critical (1 vote, line 100): validate explicit package names. |
tethys_apps/base/component_base.py |
Adds metadata and root routing | Moderate (3 votes, line 69): test the MULTIPLE_APP_MODE=False navigation branch. |
tests/unit_tests/test_tethys_cli/test_run_commands.py |
CLI test coverage | — |
tests/unit_tests/test_tethys_apps/test_base/test_express.py |
Express loader test coverage | — |
docs/whats_new.rst |
Release-note entry | — |
docs/tethys_sdk/components.rst |
Express-mode guidance | — |
docs/tethys_cli/run.rst |
Run-command documentation | Nit (3 votes, line 73): warn about non-localhost exposure. Nit (3 votes, line 40): qualify the full-portal graduation claim. |
docs/tethys_cli.rst |
Includes run-command documentation | — |
Review details
Suppressed comments (2)
tethys_apps/base/express.py:199
spec_from_file_locationcan returnNonefor a file without a supported Python suffix, or a spec without a loader, but both are passed straight intomodule_from_spec/exec_module. Check these results first so unsupported inputs produce a controlled loader error instead of an AttributeError/TypeError during harvesting.
module_spec = spec_from_file_location(module_name, app_file)
module = module_from_spec(module_spec)
tethys_apps/base/express.py:78
- This normalization does not guarantee a valid Python identifier for Unicode names; for example,
².pyproducesapp_², for whichisidentifier()is false. That value is then used for the synthetic module and generated state path, so the claimed invalid-derived-name validation is still missing. Validate the final normalized name as nonempty andisidentifier()before returning it, and surface the failure through the CLI.
package = re.sub(r"\W", "_", _source_stem(app_file)).lower()
if package[0].isdigit():
- Files reviewed: 12/12 changed files
- Comments generated: 8
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| and isinstance(statement.value, ast.Constant) | ||
| and isinstance(statement.value.value, str) | ||
| ): | ||
| return statement.value.value |
| ) | ||
| exit(1) | ||
|
|
||
| package = get_express_package_name(app_file) |
Comment on lines
+141
to
+142
| url = f"http://{args.host}:{args.port}/" | ||
| write_success(f'Running "{app_file.name}" at {url} (CTRL+C to quit)') |
| key=lambda x: x.index if x.index is not None else 999, | ||
| ): | ||
| href = f"/apps/{self.root_url}/" | ||
| href = f"/apps/{self.root_url}/" if settings.MULTIPLE_APP_MODE else "/" |
| ast.ClassDef or None: the class definition node, or None if no such class is found. | ||
| """ | ||
| try: | ||
| tree = ast.parse(Path(app_file).read_text()) |
| else: | ||
| app = TethysApp.objects.first() | ||
| except (ProgrammingError, TethysApp.DoesNotExist): | ||
| except (OperationalError, ProgrammingError, TethysApp.DoesNotExist): |
|
|
||
| The first run initializes an isolated environment for the app (a few seconds); then your default browser opens directly to the running app. Edits to :file:`app.py` are picked up automatically while the server is running. | ||
|
|
||
| Note that the app class above only sets ``name`` — and even that is optional. In express mode, any required metadata that is not defined on the app class (``package``, ``name``, ``root_url``, ``index``) is derived automatically from the file name. The same file can later be dropped unchanged into the :file:`app.py` of a scaffolded component app project to install it in a full Tethys Portal (see :ref:`scaffold command <tethys_scaffold_cmd>` and the :ref:`Component App Basics tutorial <component_app_basics_tutorial>`). |
Comment on lines
+73
to
+74
| # Serve on all interfaces (e.g. to share on a local network) | ||
| tethys run --host 0.0.0.0 -p 8080 |
tethys run express mode for safer defaults and clearer validationtethys run express mode
Member
|
@shawncrawley and @gagelarsen I had co-pilot try to clean this up and tie up loose ends. Can you test it to make sure it still works? |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This is an AI-generated proof of concept. The design and implementation were produced with an AI assistant (Claude) working interactively with @gagelarsen, then verified end-to-end locally. It is a draft for discussion with the Tethys team — not a request to merge as-is. All suggestions, redesigns, and pushback are welcome.
Implements #1286.
What this does
Runs a bare, single-file component app — never pip-installed, no
pyproject.toml, no portal config, no database setup, no login — with the ergonomics ofshiny run/streamlit run:First run takes ~4s (silent SQLite migration); subsequent runs ~3s. Hot reload is on by default (Django's stock autoreloader — the app file is an imported module).
tethys runwith no argument looks forapp.pyin the cwd.How it works (design)
"Portal-in-a-box": the existing portal runtime runs unmodified, configured down via an ephemeral generated environment — chosen over (a) a purpose-built minimal runtime (dual-runtime drift risk) and (b) materializing the file into a generated package (codegen indirection). Because everything downstream is stock machinery, an express app is guaranteed to behave identically when later installed in a real portal — the same file drops verbatim into a scaffolded component app's
app.py(a futuretethys scaffold --from app.pycould automate graduation).tethys_cli/run_commands.py(new): validates the file (AST check for aComponentBasesubclass), creates~/.tethys/express/<pkg>_<hash-of-abs-path>/as an isolatedTETHYS_HOME, writes aportal_config.ymlinto it (MULTIPLE_APP_MODE: False,STANDALONE_APP: <pkg>,ENABLE_OPEN_PORTAL: True, persistedSECRET_KEY), runsmanage.py migrate --no-input(idempotent, every start), then execsmanage.py runserver. Configuration flows through env vars (TETHYS_HOME,TETHYS_EXPRESS_APP) so it survives autoreloader restarts. No changes tosettings.pywere needed.tethys_apps/base/express.py(new): whenTETHYS_EXPRESS_APPis set, loads the file astethysapp.<pkg>.appand pre-registers it (plus a synthetic parent package) insys.modules, so the untouched harvester/register_controllersmachinery — including itsimportlib.reload()— resolves it like an installed app. Missing metadata is synthesized:package/root_urlfrom the filename (parent dir for genericapp.py),nametitle-cased,index= first@App.pagefunction.tethys_apps/harvester.py(~10 lines): include the express app in the harvest dict.tethys_apps/base/component_base.py:__init_subclass__hook calls the metadata synthesis at class-definition time — required because@App.pagereadsapp.packageat decoration time (ComponentLibrary keying).Drive-by bug fixes (pre-existing, single-app mode)
Both were hit during testing and affect non-express portals too — happy to split them into a separate PR:
tethys_apps/utilities.py:get_configured_standalone_app()caught Postgres'sProgrammingErrorbut not SQLite'sOperationalError, so a fresh single-app portal withreactpy_djangoinstalled crashed duringmigrate(reactpy's app-ready hook imports the URLconf, which queriesTethysAppbefore tables exist).tethys_apps/base/component_base.py: auto nav links hard-coded/apps/<root_url>/, which 404s whenMULTIPLE_APP_MODE=False(apps are served at root). Now settings-aware.Verification performed
test_express.py,test_run_commands.py) — all passing; regression run of touched-module test files shows failure sets identical tomain(3 pre-existing, unrelated).flake8andblack --checkclean.Known limitations / open questions
ENABLE_OPEN_PORTAL); no auth flag yet.controllers.pyetc. are importable via the synthetic package path) but are untested/undocumented.~/.tethys/express/(per-app, keyed by file path;--cleanwipes one app's state). No global GC.requests/network access to esm.sh (React CDN) is required at page load, as with all component apps.tethys runvstethys express), the state-dir lifecycle, and whether portal-in-a-box is the right long-term architecture are all up for debate in Feature proposal:tethys run app.py— zero-config single-file app runner ("Tethys Express") #1286.🤖 Generated with Claude Code
Co-Pilot Additions
Description
This merge request hardens the existing
tethys runexpress-mode flow for Component Apps without expanding scope beyond the v1 design. It addresses the open review feedback, makes localhost-only serving the documented default, and closes edge cases that previously failed with confusing import/runtime errors.Changes Made to Code
CLI hardening
--hostis set to a non-loopback address because express mode runs withDEBUG=TrueandENABLE_OPEN_PORTAL=True.Express loader robustness
tethysappnamespace as a proper package and checks module spec/loader creation before execution.Docs
127.0.0.1as the default bind address.Focused test coverage
packagevalues, missing runtime dependency handling, loopback/non-loopback host detection, warning emission, UTF-8 source reads, and express loader spec failures.Example of the supported explicit override for filenames that would not derive a valid package cleanly:
Related PRs, Issues, and Discussions
tethys run— zero-config single-file app runner (express mode POC) #1287tethys run app.py— zero-config single-file app runner ("Tethys Express") #1286Additional Notes
Quality Checks