Skip to content

Bring in a "CDN mode" #59

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Nov 19, 2022
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
1 change: 0 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ jobs:
matrix:
include:
- { python: "3.10", os: "ubuntu-latest", session: "pre-commit" }
- { python: "3.10", os: "ubuntu-latest", session: "safety" }
- { python: "3.10", os: "ubuntu-latest", session: "mypy" }
- { python: "3.10", os: "ubuntu-latest", session: "tests" }
- { python: "3.10", os: "windows-latest", session: "tests" }
Expand Down
13 changes: 12 additions & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,20 @@

## Now

- CDN mode

## Soon

- Start documentation for example writers
- Explain that we own the `src` in `py-config`
- Remove the hack in noxfile to have tests only run the "fast" ones
- Playright files get the example index.html directly
- And thus, don't have the py-config src re-pointed to cdn
- When run in nox, there are no local files and need to do CDN

## Eventually

- Get nox to work with downloaded pyodide/pyscript
- Get numpy, pandas, etc. downloaded into local dir
- Get rid of Poetry

## Done
Expand Down
93 changes: 93 additions & 0 deletions docs/developers/cdn_mode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# CDN Mode

Add a way to get PyScript/Pyodide locally sometimes, but from CDN other times.

## Why

When running and testing locally, the developers (and example writers) want fast turnaround.
They don't want to keep going out on a possibly-slow network -- or even no-network, if offline.
In "production", though, we want people browsing the examples to get the CDN version.

Other times are harder to decide.
GitHub Actions would like a nice speedup.
But it will take some investigation to learn how to cache artifacts.

## When

There are several contexts where this decision needs to be made.

## Standalone Example

The Gallery examples are designed to allow people to preview an example's `index.html` without the app.
They have a `<script>` in `<head>` pointed at `pyscript.js`.
They also have a `<py-config>` in the body to point at a Pyodide runtime.

We encourage them to point at the locally-downloaded assets.
The build step then removes those nodes from the generated output, inserting the Gallery's decision on both.

### Local Web App

Some people will pip install the PSC and go through them locally.
Perhaps even hack on them.
Or, contributors writing an example will want a preview.
Both will fire up Starlette locally.

### Local Playwright

When running an "end-to-end" (E2E) test, you want fast turnaround.
You don't want to go out on the network, repeatedly.

### Local/GHA Nox

Nox is used to run our "machinery" in isolation.
We run it locally, as a last step before pushing.
We might also run it locally just for automation, e.g. the reloading Sphinx server.
But it also runs when GitHub Actions workflows call it.

In theory, you want local Nox to be exactly the same as GHA Nox.
Otherwise, you aren't recreating the build environment.

### Production Static Website

The GHA generates a static website.
This should be pointed at the CDN

## Where

The moral of the story: we should point at the CDN *unless* something says to point at local.
There are several places we make point at assets.

### `pyscript.js`

Two locations.
First, in an example's `index.html`, when it is viewed "standalone".
Second, in the `example.jinja2` template's `<head>`.

### `gallery/examples/pyconfig.toml`

This is also pointed to in two locations.
Again, from an example's `index.html`.
Here you'll have a `<py-config src="../pyconfig.toml">` node which might include some instructions in the content.

## Solution

During local (non-CDN) use, we'll change the `<py-config>` to use `src="pyconfig.local.toml`.

How?
We're already using BeautifulSoup to pick apart the `index.html`.
Once we make the local vs. CDN decision, we can easily change the `src` attribute to point either TOML file.

What is the local vs. CDN criteria?
We'll just look for the `src/pyscript` and `src/pyodide` directory.
If they exist, then someone downloaded the assets.
That's a good flag for whether to point at those directories.

What is the action to take?
The `py_config.local.toml` file has a `runtimes.src` entry pointing to the local `pyodide.js`.
The `py_config.cdn.toml` file points at the CDN version.

## Actions

- Remove the timeout
- Test/implementation that calls a `is_local` function to detect the correct mode
- Test/implementation which wires that into the HTML munger
12 changes: 12 additions & 0 deletions docs/developers/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Developers

Articles and notes for the implementers of the app itself.

```{toctree}
---
hidden:
maxdepth: 1
---

cdn_mode
```
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ maxdepth: 1

contributing
building/index
developers/index
Code of Conduct <codeofconduct>
License <license>
Changelog <https://github.com/pyscript/pyscript-collective/releases>
Expand Down
22 changes: 11 additions & 11 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
nox.needs_version = ">= 2021.6.6"
nox.options.sessions = (
"pre-commit",
"safety",
"mypy",
"tests",
# "typeguard",
Expand Down Expand Up @@ -138,18 +137,10 @@ def precommit(session: Session) -> None:
activate_virtualenv_in_precommit_hooks(session)


@session(python=python_versions[0])
def safety(session: Session) -> None:
"""Scan dependencies for insecure packages."""
requirements = session.poetry.export_requirements()
session.install("safety")
session.run("safety", "check", "--full-report", f"--file={requirements}")


@session(python=python_versions)
def mypy(session: Session) -> None:
"""Type-check using mypy."""
args = session.posargs or ["src", "tests", "docs/conf.py"]
args = session.posargs or ["--exclude=examples", "src", "tests", "docs/conf.py"]
session.install(".")
session.install(
"mypy",
Expand Down Expand Up @@ -185,7 +176,16 @@ def tests(session: Session) -> None:
"python-frontmatter",
)
try:
session.run("coverage", "run", "--parallel", "-m", "pytest", *session.posargs)
session.run(
"coverage",
"run",
"--parallel",
"-m",
"pytest",
"-m",
"not full",
*session.posargs,
)
finally:
if session.interactive:
session.notify("coverage", posargs=[])
Expand Down
45 changes: 1 addition & 44 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ documentation = "https://psc.readthedocs.io"
classifiers = [
"Development Status :: 1 - Planning",
]
exclude = [
"src/psc/pyodide/",
"src/psc/pyscript/",
]

[tool.poetry.urls]
Changelog = "https://github.com/pauleveritt/psc/releases"
Expand Down Expand Up @@ -42,7 +46,6 @@ pre-commit = ">=2.16.0"
pre-commit-hooks = ">=4.1.0"
pytest = ">=6.2.5"
pyupgrade = ">=2.29.1"
safety = ">=1.10.3"
sphinx = ">=4.3.2"
sphinx-autobuild = ">=2021.3.14"
sphinx-click = ">=3.0.2"
Expand Down
3 changes: 2 additions & 1 deletion src/psc/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,10 @@ async def content_page(request: Request) -> _TemplateResponse:
Route("/pages/{page_name}.html", content_page),
Mount("/gallery", StaticFiles(directory=HERE / "gallery")),
Mount("/static", StaticFiles(directory=HERE / "static")),
]
if PYODIDE.exists():
Mount("/pyscript", StaticFiles(directory=PYSCRIPT)),
Mount("/pyodide", StaticFiles(directory=PYODIDE)),
]


@contextlib.asynccontextmanager # type: ignore
Expand Down
45 changes: 31 additions & 14 deletions src/psc/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,6 @@ def _route_handler(route: Route) -> None:
# fake server and run through the interceptor instead.
page.route("**", _route_handler)

# Don't spend 30 seconds on timeout
page.set_default_timeout(12000)
return page


Expand All @@ -190,6 +188,7 @@ class FakeDocument:

values: dict[str, str] = field(default_factory=dict)
log: list[str] = field(default_factory=list)
nodes: dict[str, FakeElement] = field(default_factory=dict)


@pytest.fixture
Expand All @@ -199,39 +198,57 @@ def fake_document() -> Iterable[FakeDocument]:


@dataclass
class ElementNode:
"""An element node."""
class FakeElementNode:
"""Fake for PyScript's ``element`` accessor that gets DOM node."""

log: list[str] = field(default_factory=list)

def removeAttribute(self, name: str) -> None: # noqa
"""Pretend to remove an attribute from this node."""
self.log.append(f"Removed {name}")


@dataclass
class FakeElement:
"""A fake for PyScript's Element global."""

value: str
document: FakeDocument
element: FakeElementNode = FakeElementNode()

def write(self, value: str) -> None:
"""Collect anything that is written to the node."""
self.document.log.append(value)

def removeAttribute(self, name: str) -> None: # noqa
"""Pretend to remove an attribute from this node."""
pass


@dataclass
class ElementCallable:
"""A callable that returns an ElementNode."""
"""A callable that registers and returns an ElementNode."""

document: FakeDocument

def __call__(self, key: str) -> ElementNode:
def __call__(self, key: str) -> FakeElement:
"""Return an ElementNode."""
value = self.document.values[key]
node = ElementNode(value, self.document)
node = FakeElement(value, self.document)
self.document.nodes[key] = node
return node

def removeAttribute(self, attr: str) -> None: # noqa: N802
"""Fake the remove attribute call."""
pass

def write(self, content: str) -> None:
"""Fake the write call."""
pass


@pytest.fixture
def fake_element(fake_document: FakeDocument) -> None: # type: ignore [misc]
def fake_element(fake_document: FakeDocument) -> ElementCallable: # type: ignore [misc]
"""Install the stateful Element into builtins."""
try:
builtins.Element = ElementCallable(fake_document) #type: ignore [attr-defined]
yield
this_element = ElementCallable(fake_document)
builtins.Element = this_element # type: ignore [attr-defined]
yield this_element
finally:
delattr(builtins, "Element")
2 changes: 1 addition & 1 deletion src/psc/gallery/examples/altair/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<script defer src="../../../pyscript/pyscript.js"></script>
</head>
<body>
<py-config src="../py_config.toml">
<py-config src="../py_config.local.toml">
packages=[
"altair",
"pandas",
Expand Down
Loading